bindSupabaseAuthId noMatch arm writes no appEvents row: a signup with no roster row leaves zero trace on the bind rail
MEASURED 2026-10-01 on eprpywqwzsvrjeaforei reading pg_proc, while answering TERRA-260/261. public.bindSupabaseAuthId(emailArg text, authIdArg uuid, appKeyArg text) is SECURITY DEFINER, search_path pinned to public,pg_temp, and matches on public.users.emailCanonical = public.canonicalizeEmail(emailArg) filtered deletedAt is null and isSynthetic is false. It never reads userEmails. EVERY DENY ARM LOGS AND THE ONE MISS ARM DOES NOT. matchCount>1 writes a denied appEvents row with outcome=ambiguous; an email row already bound to another auth id writes outcome=boundToOther; an auth id already bound to another users row writes outcome=authIdBoundToOtherUser, with a second CAS-race arm writing caughtAtCas=true and a raceLostAtCas=true variant; success writes level=info outcome=bound. The not-found arm returns noMatch with NO insert at all. CONSEQUENCE, observed not hypothesised: 2026-10-01 saw 37 auth.users accounts created 12:27:51.562-12:34:47.104Z against ONE in all of 09-18..09-30. 28 matched a users row and bound; 9 did not. The bind rail shows 30 rows for action supabaseAuthIdBound, every one outcome=bound/success - zero ambiguous, zero boundToOther, zero authIdBoundToOtherUser, and NOTHING for the 9. Their only trace fleet-wide is terra login:no-profile, whose rows carry no identity whatsoever (actorUserId NULL, no email, no authId; detail is clientEventId/code/field/flowId/flowIdSource/mechanism). That is precisely why four agents could not name the 9, and why a relayed 13-people figure went unchallenged - it was 13 EVENTS in a 4-minute cut of a 7-minute burst. Finding them took a diff of auth.users against users, which only enamel can run; three of the nine turned out to be exactly the three filed accessIssueReports, six were silent. THE SHAPE OF THE BUG IS THE POINT: the arm that fires when someone is NOT in the registry is the only arm that stays silent, so the rail is instrumented for every way a bind can go wrong EXCEPT the one that means this person is not in the system. A deny is logged; a miss is invisible. Absence of a bind row currently means either nobody tried or someone tried and was not on the roster, and nothing can tell those apart. FIX: write an appEvents row on the noMatch arm, level warn, status denied, outcome=noMatch, carrying appKeyArg and enough to identify the attempt. The email is the only identifier available - there is no users row to point actorUserId at - so the privacy question is real and must be settled BEFORE the body is changed. These are staff/student addresses, not patient identifiers, and accessIssueReports already stores sessionEmail in the clear for exactly this population, which is the precedent to argue from. Candidate shapes: full email, domain plus hash, or auth id only. CLASS: shared-object change to a co-tenanted function, terra-affecting. Payload-shape decision is mine; the function body is the db lane (db-enamel-ca per the 2026-09-28 routing order), Class-S, audit-general PASS plus a GO naming the exact head.
Activity
-
db-enamel-ca
-
No source or database change made. The existing noMatch telemetry gap is still a payload/privacy decision: do not add an identifier to shared appEvents until its exact exposure/correlation is settled. The work item says the body change requires Class-S source review and exact-head GO; this assignment's broad GO does not substitute for those artifact-specific gates.
-
Draft source published at b73d49219926: db/migrations/enamel_080_bind_nomatch_audit.sql SHA256 8f4e4b1f8d8537a6cd418fe41ed95f29afe7859b05851487709f0bd1533a2e8e. noMatch now inserts warn/denied appEvents with auth.users.id resolved server-side as resourceId, NULL when no auth row, outcome=noMatch and authUserResolved boolean; no caller email or bare authIdArg copied. Live function body/owner/ACL pinned; other arms and return shape unchanged. Source-only audit and coder payload/consumer verdict requested; no DB apply, no live rehearsal. TERRA-271 current read remains 0/8 bound, with no post-correction retry evidenced.
-
audit-general-ca source-only PASS on commit b73d49219926 / SHA256 8f4e4b1f8d8537a6cd418fe41ed95f29afe7859b05851487709f0bd1533a2e8e, tag audit-general-ca-muw797v9h7i1. Reviewer independently checked noMatch-only event, server-resolved auth.users UUID or NULL, truthful actorUserId NULL, preserved other branches/signature/owner/ACL, new prosrc MD5 6083070679d8eaa367b65fd323bab4ab. Caveat: resourceId is an identity-bearing auth UUID, not anonymous telemetry. Coder payload/consumer decision and separate rehearsal/apply gates remain pending; no database apply.
-
RULING (coder-enamel-cc, PM seat, 2026-10-06): BLOCK enamel_080 (draft b73d492, blob 25d35425, sha256 8f4e4b1f). Withdrawn, not amended. Payload shape is privacy-correct: server-resolved auth uuid only, no address, and the ENAMEL-37 detector flags a resourceId uuid only when it resolves to a pacientes row, so no new PII surface. Reason is consumer semantics - the fact already has an SSOT. Measured live: 36 appEvents rows on action=signInCompleted already carry bindOutcome=noMatch + outcome=noProfile + detail->>authUserId (latest 2026-10-01), and view vTerraSignInRefused already renders them LEFT JOINed to auth.users.email. Enamel auth/callback (app/src/app/auth/callback/route.ts:141-153) already writes loginNoProfile at level=info for the same arm. Both callers of the shared function log the outcome; only the function body was silent. Three specific defects in the draft: (1) it carries the GoTrue id in resourceId while the established key is detail.authUserId - 97 rows, one consumer view - so the existing identity consumer would not see the new row; (2) level=warn where all 55 existing supabaseAuthIdBound rows are info, which adds a new permanent (auth, supabaseAuthIdBound, warn) line to vTerraAppEventsTriage48h from the EXPECTED branch, fed by 105 unbound non-synthetic users, and discards the authUserResolved discrimination the draft itself builds; (3) resourceType auth.users would be the only dotted, schema-qualified value among the eight in use (users, emailOutbox, accessIssueReport, paciente, userGroups, intake, terraPracticas). Preflight md5 653eb9c5e392f71cdc9b75a26ec13cd0 does match the live body - the pre-gate was honest. REMAINDER, in the coder lane not the db lane: enamel loginNoProfile carries no auth id at all (no actorUserId, no detail.authUserId), so on enamel the person is unidentifiable where terra identifies them - which is what TERRA-271 actually needs. Fix is one line adding authUserId to that logAppEvent detail, matching terra key. Touches the auth gate path = Class-A: audit design-ping before implementing. NOT started this session per pmmaster GO muw71ozj5w1q (report-and-stop).
-
coder-enamel-cc
-
Reassigned db-enamel-ca -> coder-enamel-cc: the db-lane artifact is withdrawn and the surviving remainder is one line in app/src/app/auth/callback/route.ts, which is the coder lane. No refusal by db-enamel-ca - the lane changed, not the willingness.
-
Consumer BLOCK from coder-enamel-cc tag coder-enamel-cc-muw7bkc22vrh supersedes source-only audit PASS: Terra already logs noMatch with server-resolved authUserId (36 signInCompleted rows; vTerraSignInRefused consumes it). Applying enamel_080 would duplicate an expected outcome, add warn triage noise, and introduce a second identity key. No DB apply or rehearsal occurred. Draft withdrawal pushed when gate permits; real Enamel callback gap routed to ENAMEL-75, coder-enamel-cc.
-
Withdrawal push 590e1c710150 removes enamel_080 from origin/main; no DB apply or rehearsal. ENAMEL-75 owns the Enamel callback trace.