HALO Knowledge Docs
Generated knowledge spine · research/lanes/VERIFIED-BY-MAIN-AGENT.md

02 · Verified findings

Findings read directly from source by the main agent, not delegated.

Generated from research/lanes/VERIFIED-BY-MAIN-AGENT.md · regenerate with /opt/homebrew/opt/node@24/bin/node site/scripts/generate-docs.mjs

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-507socialMediaLogins stores password: optionalString.

Plaintext. No encryption, no envelope key, no ciphertext prefix.

  • convex/socialMediaLogins.ts:20listByCreatorEmail is a public query.
  • convex/socialMediaLogins.ts — grep for getUserIdentity|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:6activePasswordHash is a **public query returning the

gate's password hash** to any caller.

  • convex/secureArea.ts:17savePasswordHash is 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:84resetAndSeed = mutation({ args: {}, handler }); the

handler immediately iterates allTables and deletes every document. No identity check, no argument, no guard.

  • Same public mutation exposure: seed.ts:6 bootstrap,

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 matches ctx.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-10sha256Hex via crypto.subtle.digest("SHA-256", …).
  • convex/haloAuth.ts:28sha256Hex(${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 is v.number() (float64): amount at

: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|toFixed across 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 use crypto.getRandomValues over 20

bytes (160 bits). Unguessable; correct primitive.

  • VITE_RUNPOD_API_KEY appears in .env.example but is never read in src/, 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.ts taxonomy-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).

Canonical source remains research/lanes/VERIFIED-BY-MAIN-AGENT.md. This HTML is a generated projection; edit the source, then run generate-docs.mjs.