Findings verified directly by the main agent (not delegated)
Observed 2026-08-29 from source at clients/halocrm/repo, commit 347329a. Every claim below was read in the file cited, not inferred.
S1 — Creator platform credentials stored in plaintext, publicly readable
convex/schema.ts:497-507—socialMediaLoginsstorespassword: optionalString.
Plaintext. No encryption, no envelope key, no ciphertext prefix.
convex/socialMediaLogins.ts:20—listByCreatorEmailis a publicquery.convex/socialMediaLogins.ts— grep forgetUserIdentity|ctx.auth|requireAdmin|role
returns 0 matches across the module. No caller identity check anywhere.
- Convex functions are callable by anyone holding the deployment URL. The UI is not a
boundary.
The "secure area" gate in front of this UI is cosmetic:
convex/secureArea.ts:6—activePasswordHashis a **public query returning the
gate's password hash** to any caller.
convex/secureArea.ts:17—savePasswordHashis a public mutation; any caller
can overwrite the gate password.
Impact: third-party account credentials belonging to creators are retrievable by an unauthenticated party. Highest-severity finding in the review.
S2 — Every destructive reset is a public mutation with no auth check
convex/resetHalo.ts:84—resetAndSeed = mutation({ args: {}, handler }); the
handler immediately iterates allTables and deletes every document. No identity check, no argument, no guard.
- Same public
mutationexposure:seed.ts:6bootstrap,
demoSeed.ts:121 resetAndSeedFullDemo, demoFixes.ts:36 applyLatestDemoFixes, gamificationSeed.ts:57 seedGame, referralDemo.ts:23 seedDemoMonth, referralDemo.ts:236 clearDemoMonth, resetHalo.ts:180 renameKishanToKeyshawn.
Exposure ratio across convex/, counting exported declarations (^export const X = query( etc., re-verified 2026-08-29): 221 public (73 query, 132 mutation, 16 action) vs 22 internal (8 internalQuery, 9 internalMutation, 5 internalAction). 91% of the function surface is public.
Correction: an earlier count of "456 public" in this session was wrong — it counted the substring
query(, which also matchesctx.db.query(database calls. Use 221. The security lane caught this; the direction of the finding is unchanged.
Reinforcing this: ctx.auth / getUserIdentity appears 0 times across all 37 Convex modules — there is no server-side authentication anywhere, so "public" means genuinely unauthenticated.
S3 — Password hashing is single-round SHA-256
convex/haloAuth.ts:4-10—sha256Hexviacrypto.subtle.digest("SHA-256", …).convex/haloAuth.ts:28—sha256Hex(${authUser.passwordSalt}:${args.password}).
Salted, but unstretched: no iterations, no bcrypt/scrypt/argon2. Commodity GPUs do billions of SHA-256/sec, so a leaked authUsers table is effectively plaintext for any non-random password. Session is asserted client-side from localStorage (haloLocalAuthSession).
M1 — All money is float, with essentially no rounding
convex/schema.ts— every monetary field isv.number()(float64):amountat
:153, :456, :703, :714, :785, :849; commissionPercent at :132, :152, :262; earnings at :393; FX rate at :103. No integer minor units, no decimal type.
- Float commission arithmetic:
convex/invoicing.ts:34
entry.netSales * ((entry.percentage ?? 50) / 100); convex/referrals.ts:94 commission += payment?.amount ?? (monthGross * percent) / 100; also referrals.ts:270, :534, :546.
- Rounding discipline: 2 occurrences of
Math.round|toFixedacross all 37 modules
(11,274 LOC) — one in referralDemo.ts, one in paymentLog.ts. Zero in invoicing.ts, referrals.ts, referralInvoicing.ts, payroll.ts.
Error accumulates via += across a reconciliation period, then is multiplied by a float FX rate. This is an app whose core function is splitting other people's revenue.
M2 — No audit trail
No append-only history table exists in the 57-table schema. customStatusHistory is the only history-shaped table and is scoped to the customs tracker. There is no record of who changed a payment, invoice, or commission rate.
Correctly done — credit where due
convex/contracts.ts:6-10— signing tokens usecrypto.getRandomValuesover 20
bytes (160 bits). Unguessable; correct primitive.
VITE_RUNPOD_API_KEYappears in.env.examplebut is never read insrc/, so it
is not compiled into the public bundle. Only VITE_CONVEX_URL, VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, VITE_DISABLE_SUPABASE, VITE_AUTH0_DOMAIN, VITE_AUTH0_CLIENT_ID are read client-side.
- No secrets in any commit in history (scanned all blobs across all refs before mirroring).
convex/driveStructure.tstaxonomy-as-source-of-truth, idempotent Drive provisioning,
and the markUsed move-before-delete ordering are genuinely well-reasoned.
Scale (verified)
622 tracked files · src/ 68,706 LOC · convex/ 11,274 LOC · 57 tables · 39 pages · 37 Convex modules · 57 stock shadcn components · 39 files still importing the Supabase client (12 of them in components/payroll — the densest remaining migration target).