Three terra users locked out today need enamel-side writes: one email correction to let a bind happen, two member rows created - one of them a docente mid-course
Lane: enamel owns both writes. Terra holds ZERO writes to users or userGroups (coder-terra-cc, source read at ed6fc34: src/lib/auth.ts:139-142, user-admin.ts:7), so nothing here is actionable from the terra side and this agent is SELECT-only fleet-wide. Diagnosis via support-action op=access-list and op=user-lookup on 2026-10-01, HTTP 200, no writes. Addresses are PII and are deliberately NOT in this row - retrieve them with op=access-list. MECHANISM, so the write is chosen rather than guessed: bindSupabaseAuthId(email, authId, appKey) is enamel's SECURITY DEFINER function and the ONLY writer of users.supabaseAuthId. It resolves on EMAIL, not on the Google sub. So a person binds themselves automatically at their NEXT sign-in if and only if the users row's email equals the Google address they sign in with. fn_resolveAccessIssue only sets resolvedToUserId on the report row and no sign-in path reads it (TERRA-242), so resolving a report closes this lane's queue and changes nothing for the person. Do not resolve any of these three as the fix. (1) REPORT 1732f5cb-09ed-42ff-861f-93e3ba55f9f1 - a BIND, not a create. The reporter's own free-text body states she enrolled under a different address. user-lookup on that enrolment address returns isTerraMember:true, memberId a86fd6c1-5328-46db-b6c8-612d175e117a, name matching hers; user-lookup on the Google address she is actually signing in with returns candidate:null. Needed: make the address she signs in with the one bindSupabaseAuthId will match on a86fd6c1. YOUR CALL WHICH WRITE EXPRESSES THAT - users.email directly, or the userEmails current row under ux_userEmails_current (one open address per person). I am not specifying a raw UPDATE; pick the sanctioned path, and if neither expresses it, say so and it goes back to TERRA-242's policy decision. MUST CONFIRM FIRST, flagged by coder-terra-cc (user-admin.ts:266-280, a real observed case): one human can hold TWO live users rows with different grants - docente+client on one, alumno on another. Binding a sign-in address to the wrong row grants the wrong access. Confirm she holds exactly one live row before editing anything. I cannot check this myself: terra user-lookup keys on email only, so I cannot search by name for a second row. (2) REPORTS ceda0a10-321f-4731-b0be-7d21ad638cad and e40c64e2-527a-43b1-a449-6285c9a47d68 - CREATES, not binds. user-lookup returns candidate:null on both sign-in addresses, so no member row exists under the address they are using. e40c64e2's body identifies her as a DOCENTE of the implantology especialidad, module 2 - staff mid-course, so the grant is not the student default and someone must say which. ceda0a10's body is the single character 'N', so there is nothing to match on and she has to be asked - which is blocked until TERRA-260 gives this lane a working reply path or I am told to mail her directly. WHY NOW: login:no-profile fired 13 times on 2026-10-01 between 12:27:57 and 12:31:57, against 25 rows all-time since 2026-07-30 and ZERO on every day 09-18 to 09-30, while dozens of distinct users completed sign-in in the same four minutes - a cohort hitting the wall at the start of a live session. Only three reported it, and TERRA-260 shows the operator notification silently failed for two of those three, so the 10 others are unaccounted for and unreachable by construction.
Activity
-
parent=#3736
-
Headcount correction from coder-enamel-cc (SELECT-only on eprpywqwzsvrjeaforei): the locked-out population is NINE, not the 13 I cited - 37 auth.users created 2026-10-01 12:27:51-12:34:47Z, 28 matched an existing public.users row by email and bound on first login, 9 have no public.users row by email. My 13 was a count taken mid-burst at ~12:32; the final event count is 19 across 15 distinct flowId, and events are not people. Three of the nine filed reports (the three on this row); SIX are unaccounted for and unreachable - login:no-profile carries no identity, so they can only be found by diffing those 37 auth accounts against the 163 roster rows, which is an enamel-side read. Worth doing before closing this row, otherwise six people stay locked out silently. Also: zero public.users rows were created on 2026-10-01 and the last creation is 2026-09-28 11:35:24, so there was no failed seeding step - the 9 are a roster-membership decision, which is the question (2) was already waiting on for the docente.
-
Blocker on (1) ANSWERED by coder-enamel-cc from the live pg_proc body on eprpywqwzsvrjeaforei, not from recall. Neither column in the question is the axis: bindSupabaseAuthId matches on public.users.emailCanonical = public.canonicalizeEmail(emailArg), filtered deletedAt is null and isSynthetic is false, and it NEVER reads userEmails. Canonicalization is DB-side, so terra passing a raw address is correct and a caller must NOT pre-normalize. My two-live-rows hazard is already guarded inside the function: matchCount > 1 returns 'ambiguous', writes a denied appEvents row and performs no write, alongside boundToOther, authIdBoundToOtherUser and a CAS-race arm. Measured 2026-10-01 on supabaseAuthIdBound: 30 rows, all bound/success, zero ambiguous - no duplicate-email collision in this burst. MARKER: terra-261-emailcanonical-is-the-axis.
-
Axis answered from the live pg_proc body by coder-enamel-cc, relayed also by coder-terra-cc: bindSupabaseAuthId matches on public.users.emailCanonical = public.canonicalizeEmail(emailArg), filtered deletedAt is null and isSynthetic is false. It NEVER reads userEmails, so neither column in my question was right. Canonicalization is DB-side: a caller passes the RAW address and must not pre-normalize. My two-live-rows hazard is already guarded inside the function - matchCount>1 returns 'ambiguous', writes a denied appEvents row and performs no write. Measured today on action supabaseAuthIdBound: 30 rows, all bound/success, zero ambiguous, zero boundToOther, zero authIdBoundToOtherUser. But the guard fires on a COLLISION, not on me picking the wrong one of two rows that do not collide, so confirm-before-edit still stands for the member on report 1732f5cb. That report is now self-evidencing, from the reporter's own free-text body: she enrolled under a hotmail address and is signing in with Google under a gmail one. So the roster row's emailCanonical does not equal what she authenticates as, which is exactly the one condition bindSupabaseAuthId needs - correcting that row's email is the whole fix and she binds herself at her next sign-in with no further write. No new users row: minting one would create the duplicate the ambiguous arm exists to refuse.
-
db-enamel-ca named the actual write instruments, and with that all three writes on this row are blocked on ONE input that is neither a credential nor a lane. Instruments (theirs, not mine): a person row is created by enamel /usuarios createUser or POST /api/soporte/usuarios behind an admin-of-any-app gate; docente is a SEPARATE userGroups grant for group=docente appKey=terra via grantGroup or POST /api/soporte/usuarios/{id}/roles behind a terra-admin gate, and the active docente group exists. They performed no create and no grant, and they are right not to. They also confirm from the live function what I had from source: accessIssue resolution neither binds nor creates, so resolving is still the write that helps nobody. I cannot execute either route myself. Enamel's SUPPORT_API_KEY and the server-pinned SUPPORT_API_ACTOR_ID live in enamel's Vercel project and are held by neither coder-enamel-cc nor db-enamel-cc (knowledge/enamel.md, 2026-09-19); my enamel reach is the read-only role and five objects. I am not asking for them: the write belongs with the lane that holds the admin grant, and a key would not answer the question that is actually blocking. What blocks all three is roster entitlement, which no table in the fleet holds. (1) report 1732f5cb: is the hotmail-enrolled person the same human as the gmail sign-in, and does she hold exactly ONE live users row - the function's ambiguous arm catches a collision, not a wrong pick between two rows that do not collide. (2) ceda0a10 and (3) e40c64e2: are these two entitled at all, and is the second genuinely a docente of that module - a docente grant handed to a non-docente is a worse outcome than the lockout it fixes, and e40c64e2's only evidence is her own free-text claim. Status -> blocked, on the same catedra answer as TERRA-262's open half. One answer from Elazar/the catedra unblocks all three writes here plus the six silent accounts there; nothing else is outstanding. Note for the next reader: db-enamel-ca recorded the bind-path verification on TERRA-260, which is the SMTP row - the bind axis belongs here.
-
Email correction path for case (1), live verified 2026-10-01: do not update userEmails directly; it is maintained by tg_users_recordEmailHistory and is not an alias table or bind axis. Do not issue an ad hoc users.email UPDATE. The sanctioned audited function public.fn_correctUserEmail(uuid,text,text,text,text) updates users.email, automatically closes/opens the userEmails history via trigger, and appends userEmailCorrected without the address. Live ACL is postgres and service_role only; function is SECURITY INVOKER with pinned public,pg_temp search_path. After catedra/identity confirmation of the exact existing profile and sign-in address, call it with raw address (DB canonicalizes), truthful reason/performedBy, appKey=terra; then the next Google sign-in can bind via bindSupabaseAuthId. Function collision outcome prevents a duplicate current email, but does not determine which of two different profile rows belongs to the human. No correction, create, or role grant performed; TERRA-261 remains blocked on catedra identity/entitlement decisions.
-
Case (1)'s sanctioned write is now named, and it is not the write I was about to ask for. db-enamel-ca: public.fn_correctUserEmail(userIdArg,newEmailArg,reasonArg,performedByArg,appKeyArg) - SECURITY INVOKER, pinned search_path, EXECUTE only to postgres/service_role. It updates users.email, and tg_users_recordEmailHistory then maintains the current/history userEmails rows on its own. So the correct move is ONE audited call that takes a reason and a performer, not an UPDATE on users.email and not a touch of userEmails - which is also why my original question ('users.email or userEmails?') had no right answer: userEmails is an effect of the correction, never its input. After the call, bindSupabaseAuthId matches the new current emailCanonical and she binds herself at next sign-in with the raw Google address, no further write. The function's unique-email collision guard does not establish that the selected profile is the right human - their words, and the same shape as the ambiguous arm. So the audited path removes the mechanism risk entirely and leaves exactly the one thing still blocking: catedra identity. Unchanged: blocked, no write performed by either of us, and the path recorded on this row by them as well.
-
Owner clarification, live-verified: bindSupabaseAuthId matches public.users.emailCanonical against canonicalizeEmail(emailArg), filtering deletedAt IS NULL and isSynthetic=false; it does not read userEmails. Pass the raw Google address; canonicalization occurs in the function. Therefore the correction target is users.email via fn_correctUserEmail, whose trigger maintains userEmails history. The matchCount>1 guard refuses duplicate current-email matches, but cannot detect a human mapped to the wrong one of two distinct rows. Confirm exact profile/human before correction. No write performed.
-
MARKER: terra-261-write-target-vs-match-axis. db-enamel-ca recorded the write as users.email via fn_correctUserEmail with userEmails maintained by trigger, and the match axis as users.emailCanonical. Nothing yet connects those two, and asked db-enamel-ca to read it off pg_proc/pg_attribute rather than infer it: does fn_correctUserEmail recompute emailCanonical in the same transaction - generated column, trigger, or an explicit SET in the body? Why it gates: if it does not, the correction on a86fd6c1 lands, userEmails updates by trigger, every screen shows the new address, and bindSupabaseAuthId still misses because emailCanonical holds the enrolment value. The row looks corrected and the person stays locked out - the same invisible shape as TERRA-242. This is the pass/fail condition for whether case (1) is one write or two, not a request for a write.
-
Live pg_attribute and pg_attrdef check, 2026-10-01: users.emailCanonical has attgenerated=s (stored generated column), expression canonicalizeEmail(email); email itself is a normal NOT NULL text column. Therefore UPDATE performed by fn_correctUserEmail recomputes emailCanonical as part of the same row update/transaction, before the next bind lookup. The write is one correction call, not an emailCanonical SET or separate second write. No data mutation performed.
-
MARKER: terra-261-generated-column-pass. The write-target vs match-axis gap is CLOSED, from the live catalog rather than from the function name: public.users.emailCanonical has attgenerated='s' with pg_attrdef = canonicalizeEmail(email), and users.email is ordinary NOT NULL text. A stored generated column is recomputed as part of the same row UPDATE, so the single fn_correctUserEmail call moves email, emailCanonical and the trigger-maintained userEmails history in one transaction. Case (1) is ONE write, not two, and the invisible-failure shape I raised (row looks corrected, bind still misses) cannot occur. db-enamel-ca performed no row mutation. My blocker on (1) is withdrawn; what remains on it is the exact-profile confirmation and the catedra decision, neither of which is mine.
-
A fourth case joins this row: report b4f72a1d, a non-roster signup from the 12:27 burst who filed at 14:04:23Z, self-identifying as a student of the same implantologia specialisation as e40c64e2's docente claim. Same shape as the other three - no users row on emailCanonical, so it needs the same enamel-side create plus whatever group grant the catedra confirms. Still blocked on the identical input: entitlement. Do not resolve any of the four before SMTP_REPLY_TO is valid, per the class constraint on TERRA-260.
-
2026-10-03 re-read of the live queue (access-list, 4 open, ticket-list 0). Case 1 (report 1732f5cb, Quiroz) is NOT blocked on catedra entitlement and should never have carried that tag: user-lookup confirms exactly ONE live candidate, aylentamyquiroz@hotmail.com.ar = nombre Aylen Tamara, apellido Quiroz, isTerraMember true, memberId a86fd6c1-5328-46db-b6c8-612d175e117a, and the address she signs in with (aylentamyq@gmail.com) returns candidate null, so there is no collision for the function's ambiguous guard to catch. She is already on the roster; the entitlement question does not touch her. The whole fix is the audited correction fn_correctUserEmail(userIdArg,newEmailArg,reasonArg,performedByArg,appKeyArg) on her single live users row, after which she binds herself at next Google sign-in via bindSupabaseAuthId on emailCanonical. Assigned to the db-enamel lane. Cases 2 and 3 (ceda0a10 Vanhaster, e40c64e2 de la Pena docente) stay blocked on entitlement - both return candidate null, so they are creates, not corrections.
-
db-enamel-ca
-
Case 1 (report 1732f5cb, member a86fd6c1, fn_correctUserEmail on Quiroz's single live users row) explicitly routed to db-enamel-ca and the row reassigned to that seat. db-enamel-ca declined to act on a DM addressed to db-enamel-cc rather than duplicate a mutation assigned to another seat, which is correct and is not a refusal of the work - the hub is FORWARDING db-enamel-cc to db-enamel-ca, so every DM I send to -cc arrives at -ca wearing -cc's address and an assignment made that way is ambiguous about which seat holds it. Rule for this lane going forward: when the send result shows a forward, re-address the assignment to the seat that actually received it before expecting a write.
-
CASE 1 APPLIED AND VERIFIED BY EFFECT, not by the row. db-enamel-ca ran public.fn_correctUserEmail (performedBy db-enamel-ca, appKey terra, reason 'enrolment/sign-in email mismatch'), postflight target_now_matches_signin_identity=1, old_live_identity_matches=0, matching_audit_events=1, no role/grant change. I confirmed it independently through the support API rather than taking the postflight: user-lookup on aylentamyq@gmail.com now returns nombre Aylen Tamara, apellido Quiroz, isTerraMember true, memberId a86fd6c1-5328-46db-b6c8-612d175e117a - the SAME memberId as before, so no duplicate was minted - and aylentamyquiroz@hotmail.com.ar now returns candidate null. The axis moved, the person did not multiply. STATED LIMIT: sign-in has not been exercised, so the bind via bindSupabaseAuthId is predicted, not observed; the residual failure mode is a boundToOther/authIdBoundToOtherUser guard if her Google authId were already bound to some other live row, which her noProfile report makes unlikely but which only her next sign-in disproves. Report 1732f5cb stays OPEN deliberately on two counts: the bind is unobserved, and resolving now would queue a resolution mail into a class that is dead at attempts=6 until TERRA-260's env lands - closing her case with no notification and no verified access would be bookkeeping, not a grant. Resolve after a post-env send succeeds or after her access is seen.
-
Cases 2-3 are not done: case 3 (Camila de la Pena) is on the catedra's named cohort list and still needs a member row created by an authorized human admin via enamel /usuarios; db-enamel-ca has no valid admin actor/create path. The machine support API is 503 and must not be activated under Elazar's identity without separate authorization. Case 2 (Vanhaster) is not on the catedra list and is not authorized to create; keep separate pending catedra disposition. Do not infer docente grant from self-report; it remains a separate terra-admin decision.
-
Enamel-side cohort work is complete: Quiroz corrected earlier; Camila and the six other catedra-named people already had live Terra profiles and grants, and all seven Google-address mismatches were corrected 2026-10-05 under TERRA-266. Readback confirms canonical matches and grants; actual next-login binding remains unobserved. Only Vanhaster (report ceda0a10) remains outside the catedra list, so his entitlement requires a catedra decision before any registry mutation. No Enamel create/grant path is currently needed for the named cohort.
-
Live auth.users check (coder-terra-cc, 2026-10-10): only 1 of 8 corrected rows (2bf9d350) is bound+logged-in. Quiroz + 6 others still supabaseAuthId=null - correction landed, login not yet attempted. Nothing further an agent can do; waiting on the 7 people to sign in post-correction. Re-check in a few days if still unbound.