You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Full identity-auth area run of the checklist-test skill — all 14 items (12 runnable + 2 ledger-blocked) driven against a live showcase across 3 opus subagents, each on its own port + file DB. Text-only per RUNNER.md.
Result: 5 PASS · 4 PARTIAL · 3 FAIL · 2 BLOCKED.
Environment — framework 92f26f75/f268b2b2 (branch claude/platform-test-checklist-ocwugl) · console bundle 09987b680 (packages/console/dist/.objectui-sha) · showcase app · isolated port + file DB per batch · personas provisioned as real sign-ups and real in-product role writes, not stubs.
Environment fact that shaped this whole area, stated up front: the seeded dev admin is promoted to the ObjectStack platform admin, but its better-auth sys_user.role stays 'user' — so the entire better-auth-native admin family (list-users, ban/unban, set-role, remove-user, revoke-user-sessions, impersonate-user) 403s out of the box. Framework-overridden endpoints (create-user, set-user-password, import-users) work regardless. Rather than record six blocked, the runner provisioned a genuine admin with one direct write to its own throwaway DB — the same system-context write the pinned dogfood harness performs — and judged the clauses on merit.
✅ PASS — 5 items
self-service-password-reset (5/5 + all four negatives) — known and unknown addresses return byte-identical bodies (asserted by string comparison, not by eye) andsys_email holds 1 artifact for the known address / 0 for the unknown, so nothing is sent for a non-account while the HTTP response stays constant. Token reuse → 400 and the password it tried to set never became valid.
self-service-profile-password (6/6 + four negatives) — name and avatar persist across a full sign-out/sign-in into a new browser context; the avatar is a genuine presign/PUT/commit cycle, not a data: URL. The item's knownGap was closed by provisioning a passwordless persona (deleting the credential sys_account row), proving the set-initial-password branch works and 409s in both directions.
sso-enforced-first-paint (5/5) — no password flash: 64 samples across two independent fresh contexts (24 @100 ms from domcontentloaded, 40 @50 ms from commit) all showed input[type=password] = 0, with a CDP screencast confirming the page was genuinely painting. The sampling is proven sensitive because the unenforced boot shows the password field at the very first sample. The hung-IdP timeout was driven by intercepting the handshake and never fulfilling it — button disabled to ~20 s, then re-enabled with the running bundle's own watchdog message. Caveat: exercises ssoEnforced with a social (Google) provider; features.sso stays false because no oidcProviders[] lever exists on the dev path.
org-membership-team-management (6/6 + four negatives) — {current_org_id} resolves to the active org id (the nav href is the record route, not the raw list); role changes bite in both directions; team membership keys on the (team,user)pair (a duplicate add returns the same row id, count unchanged); remove-member flips org-scoped reads 200→403 on both the live token and a fresh sign-in. All seven forged management calls refused server-side with a byte-identical before/after state snapshot.
🔴 FAIL — 3 items
1. admin-lifecycle-operations — remove-user is non-atomic and can never succeed
Reproduction rule — as a genuine better-auth admin: POST /auth/admin/create-user {email:…} → 200; create a record owned by that user; then POST /api/v1/auth/admin/remove-user {userId}. Expected 2xx, sys_user gone, the owned record surviving with owner_id null (engine FK clear). ActualHTTP 500 with an empty body; sys_user still readable; the record still carries owner_id; and the user's credential sys_account rows have already been deleted — so that email can no longer sign in (401) while still occupying the org roster. Reproduced twice; verified across three victims (accounts=0 for all three vs accounts=1 for untouched users).
Server log gives the real cause: Cannot delete sys_user (<id>): 1 dependent sys_member record(s) reference it via user_id (user_id is required, so it cannot be cleared)… set deleteBehavior:cascade on sys_member.user_id with {code:DELETE_RESTRICTED, status:409} — a 409 leaking to the client as a bodyless 500.
Three compounding problems: (i) every user is auto-bound to the default org at sign-up/create, so remove-user can never succeed on this deployment; (ii) the operator cannot clear the blocker either — DELETE /data/sys_member → 405 with allowed:[get,list,aggregate,history,export]; (iii) the account/session deletions are not rolled back when the sys_user delete is refused, leaving an orphaned, un-authenticatable identity on the roster.
This is not the #3023 owner-anchor regression — the veto comes from the engine's referential restrict on sys_member.user_id, not from the owner-anchor transfer guard.
Also failing in this item — impersonation is invisible in the console. With an impersonation server-confirmed in that very browser context (an in-page get-session returns the target's email and session.impersonatedBy=<admin id>), /_console/home renders "Good morning, Dev Admin." with avatar DA — the admin's identity — and the DOM contains no banner, no "Stop impersonating" control, and the impersonated user's name nowhere. Reproduced twice in fresh contexts with hard reload + networkidle + 3 s settle. The console also offers no Impersonate entry point at all. Support sessions are silent in this build. Not compared against objectui origin/main, so this may already be fixed upstream.
Everything else in this item passes: ban/unban is reversible and enforced at sign-in; set-role is authoritative in both directions (probed as the target, bracketing the write); revoke-user-sessions kills the live session immediately, disproving the "only stops new logins" negative; create-user's explicit password wins over the generated one (#3031/#3033 holds); all 8 forged non-admin operations 403 with state byte-identical.
2. invitation-scope-gates — pending → accepted is unreachable
The delegated-admin scope itself is correct: a real in-product delegated_admin can invite a member (200, exactly one row attributed to the delegate) but cannot invite an admin (403 with the ADR-0090 D12 message) and leaves zero orphan rows; a plain member is refused entirely.
(a) accept-invitation → HTTP 500 with an empty body, and the row stays pending forever. Reproduced twice with fresh emails. Server log: insert into sys_member … UNIQUE constraint failed: sys_member.organization_id, sys_member.user_id — acceptance re-inserts a membership the sign-up reconciler already created. Suspected fix: acceptance should upsert/adopt the existing sys_member row (or skip the insert when the invitee is already a member of the target org). Same bodyless-500 statusCode-leak class as the remove-user failure above.
(b) cancel-invitation writes status: 'canceled', a value absent from the spec enum.InvitationStatus = z.enum(['pending','accepted','rejected','expired']) (organization.zod.ts:105) is what this item's source names as the authority, while the platform object declares a divergent 5-value select including canceled (sys-invitation.object.ts:214). Anything validating an invitation against InvitationSchema rejects a canceled row.
Resend behaves correctly (expiry extended, still exactly one row, a second sys_email artifact). Clause 4 is blocked(fixture): no org-role persona can reach the Setup app at all ("App not available", 0 nav entries) because of the everyone-anchor baseline, so the entitled and unentitled UI halves cannot be discriminated.
3. api-key-ui-lifecycle — API keys cannot be revoked through any product route
Reproduction rule — POST /api/v1/keys {name} → 201, then PATCH /api/v1/data/sys_api_key/<id> {revoked:true}as admin. Expected 200 and the key stops authenticating. Actual405OBJECT_API_METHOD_NOT_ALLOWED, allowed:[get,list,aggregate,history,export]. Same 405 for {revoked:false} (restore). Reproduced 3×, including through the real UI — Setup → API Keys → Open menu → Revoke API Key → Continue produces an error toast, the key keeps authenticating, and the row still reads revoked=false.
Root cause.packages/platform-objects/src/identity/sys-api-key.object.ts declares actions revoke_api_key / restore_api_key as PATCH /api/v1/data/sys_api_key/{id} (lines 41-72) while the same file sets enable.apiMethods = ['get','list'] (line 219) with the comment "#1591 — reads only: writes are refused… HTTP answers 405 before the 403". The two halves of the file contradict each other. No alternative route exists: route-ledger.ts carries only POST /keys; /api/v1/auth/api-key/* and DELETE /api/v1/keys/{id} all 404.
Enforcement of the flag is fine — setting revoked=1 out-of-band makes the very next x-api-key call 401 UNAUTHENTICATED — so the fix is either to allow update scoped to the revoked column, or to give revoke/restore a dedicated auth route.
⚠️Security consequence worth naming: a leaked API key cannot be revoked without direct database access. Filed publicly because this is a dead feature rather than a bypassable gate — knowing about it grants an attacker nothing they don't already have — but say the word if you'd prefer it moved to the private channel.
Nothing pins this today: the unit tests (http-dispatcher.keys.test.ts, resolve-execution-context.test.ts) exercise key resolution with a pre-revoked row and never call the PATCH route the action declares.
The rest of the item is clean: show-once holds (get-by-id, list and a reloaded DOM never carry the raw secret), and a key carries exactly its owner's permissions — an org_member-owned key reproduces its owner's 403, and get-session via x-api-key returns null. Secondary finding: the key column (a SHA-256 hash) does serialize over the data API on both get-by-id and list, despite being declared Field.text({hidden:true, description:'Hashed API key value - never exposed to clients'}). The spec defines hidden as "Hidden from default UI", so this is not a hidden-contract violation — but the item's parenthetical claim is false on this build. Admin-readable only on this app.
🟡 PARTIAL — 4 items
teams-bu-membership — the BU tree is load-bearing, but revocation is lazy
⚠️Security-relevant. Subtree expansion resolves correctly through three levels and is symmetric (0→1 on placement inside the shared subtree, 1→0 on re-parenting out), so the tree is genuinely load-bearing. The qualification is timing: the placement or re-parent alone does not move the read — the share only materializes when the shared record is next written.
The risky direction: after a business unit is moved out of a shared subtree, its members keep read access until the record is next written.
Root cause (located). The sharing-rule rebind triggers are bound to data changes on the rule's object (the boot log binds them for showcase_project, showcase_inquiry, showcase_task) but not to sys_business_unit_member or sys_business_unit changes — so tree and membership edits never enqueue a recompute for rules with unit_and_subordinates recipients.
The item's knownGap can be retired: it anticipated that no showcase geometry might consume BU membership, but share_new_inquiries_with_field_ops does, so the clause ran. Seed note for the next sweep: a fresh boot seeds the BU tree but zerosys_business_unit_member rows, so the rule materializes nothing until a tester places someone — that is the seed-data-thin trap for this item.
auth-method-matrix
Every flag with a boot switch matched the config that booted it; 13 forged disabled-method endpoints all 404 across two boots with no silent 200; the 2FA challenge interrupts before a session is granted (the accompanying token is provably unusable); both .well-known documents are byte-identical and every advertised endpoint was probed and resolves.
Three partials: (i) /auth/config still advertises passkeys:false/magicLink:false — a stale prebuiltplugin-auth/dist/index.js, not a regression (src dropped them in 86f7a20e); (ii) the phone-OTP NOT_SUPPORTED branch is unreachable because isPhoneOtpDeliverable() admits LogSmsTransport outside production — proven not a silent success (the code appears in the transport log, a resend is 429, and the captured code completes a real verify); (iii) change-email and delete-user are booked as mounted in auth-route-ledger.ts (lines 118/120) but plugin-auth never configures better-auth's user.changeEmail/user.deleteUser — 0 grep hits in both src and dist — so they answer 400 CHANGE_EMAIL_DISABLED / 404 with no switch to enable them.
session-list-revoke
Revocation is immediate, token-scoped and server-enforced on both bearer and cookie transports (401 on a protected route, not merely absence from the list); revoke-other-sessions leaves exactly the caller alive and never touches another user; sys_session writes are refused as a method gate (405, same for admin — not a permission accident).
Two gaps: (i) no ordinary persona can read sys_session at all (403 for [org_member, everyone]), so raw-table RLS for a non-admin is unobserved — the admin mine view (12 of 50 rows) was substituted; (ii) sys_session.revoked_at/revoke_reason are declared readonly and "System-managed" but have no writer anywhere in the tree and appear in no listView — better-auth deletes the row rather than tombstoning — so the ADR-0069 D4 revoke-audit trail is inert, an enforce-or-remove candidate. Also: Setup → Sessions lands on mine rather than all_sessions because nav_sessions carries no viewName, so an admin sees only their own sessions until they notice the "All" tab.
identity-import-wizard
Both invite channels proven in one batch (delivery {emailInvite:1, smsInvite:1}, the reset link captured from sys_email and the invite SMS from the transport log); the temporary policy gives every row a distinct one-time password via API and through the real wizard; both credential routes then authenticate; upsert is idempotent on the same ids with no duplicates; non-admin 403 / anonymous 401 with zero users created.
Strongest oracle of the run: a byte-level grep for all four plaintext passwords across data.db, data.db-wal, data.telemetry.db, data.telemetry.db-wal and boot.log returned 0 matches — against a positive control that matched 25 times, proving the search reached the stored bytes.
The one unproven half is the auto policy's temporary-fallback branch, which is structurally unreachable on objectstack dev: both transports always register, and dev.ts hard-sets NODE_ENV='development' so isPhoneOtpDeliverable() cannot be made false. Suggest recording this as a fixture note on the item so the next sweep does not re-derive it.
⛔ BLOCKED — 2 items
oauth-app-consent-loop and linked-accounts-social — both carry a ledgered blocked:{by,ref} and remain unrunnable on stock fixtures (no outbound egress for a real IdP round trip).
Checklist maintenance falling out of this run
org-membership-team-management clause 1 and its negative name the wrong vocabulary. The enforced builtin set is {owner, admin, delegated_admin, member} per ADR-0108 (BUILTIN_MEMBERSHIP_ROLE_OPTIONS, "nothing widens these at boot any more") — not{owner, admin, member, guest}. guest is actually rejected (400 ROLE_NOT_FOUND), and delegated_admin is legitimate. The spec doc-comment on MemberSchema.role (organization.zod.ts:84-87, "Common roles: owner, admin, member, guest") is itself stale and is probably where the checklist text came from — worth fixing both.
invitation-scope-gates clause 6 conflates two axes.DelegableScope models ObjectStack positions / permission sets / business units and has no field that could carry a better-auth org invitation role, so it can never "contain member". The delegate demonstrably can mint a member invitation while the endpoint reports an empty assignable set — it under-reports, never over-reports, which is the safe direction. Needs a revision, not a product fix.
admin-lifecycle-operations clause 4 should name the authed-request oracle, not get-session's status code — better-auth's no-session convention is 200-with-null-body, so the clause's literal 401 expectation misdescribes a correct implementation. The session was provably gone.
teams-bu-membership — retire the knownGap (a BU-consuming geometry does ship) and add the zero-seeded-placements note.
identity-import-wizard — add the fixture note that the auto temporary-fallback branch cannot occur on objectstack dev.
Full
identity-autharea run of thechecklist-testskill — all 14 items (12 runnable + 2 ledger-blocked) driven against a live showcase across 3 opus subagents, each on its own port + file DB. Text-only per RUNNER.md.Result: 5 PASS · 4 PARTIAL · 3 FAIL · 2 BLOCKED.
Environment — framework
92f26f75/f268b2b2(branchclaude/platform-test-checklist-ocwugl) · console bundle09987b680(packages/console/dist/.objectui-sha) · showcase app · isolated port + file DB per batch · personas provisioned as real sign-ups and real in-product role writes, not stubs.✅ PASS — 5 items
self-service-password-reset(5/5 + all four negatives) — known and unknown addresses return byte-identical bodies (asserted by string comparison, not by eye) andsys_emailholds 1 artifact for the known address / 0 for the unknown, so nothing is sent for a non-account while the HTTP response stays constant. Token reuse → 400 and the password it tried to set never became valid.self-service-profile-password(6/6 + four negatives) — name and avatar persist across a full sign-out/sign-in into a new browser context; the avatar is a genuine presign/PUT/commit cycle, not adata:URL. The item'sknownGapwas closed by provisioning a passwordless persona (deleting the credentialsys_accountrow), proving theset-initial-passwordbranch works and 409s in both directions.sso-enforced-first-paint(5/5) — no password flash: 64 samples across two independent fresh contexts (24 @100 ms fromdomcontentloaded, 40 @50 ms fromcommit) all showedinput[type=password]= 0, with a CDP screencast confirming the page was genuinely painting. The sampling is proven sensitive because the unenforced boot shows the password field at the very first sample. The hung-IdP timeout was driven by intercepting the handshake and never fulfilling it — button disabled to ~20 s, then re-enabled with the running bundle's own watchdog message. Caveat: exercisesssoEnforcedwith a social (Google) provider;features.ssostays false because nooidcProviders[]lever exists on the dev path.phone-signin-surfaces(4/4 + both negatives) — the fix(showcase): stop passing a non-existent org column when seeding the phone demo user #3408 seed regression has not returned (checked by API before the browser). The create-user dialog is not a client echo — the network shows thephoneNumbergoing out and an independent re-read confirms it persisted. The feat(spec,platform-objects): action param 'visible' predicate; gate create-user phone on features.phoneNumber #2871 gate was proven both ways by cold-booting twice: withfeatures.phoneNumber:falsethe dialog has no phone field at all, and/meta/object/sys_usershows the param carriesvisible:{dialect:'cel', source:'features.phoneNumber == true'}— so the console is evaluating the CEL gate against the served flag.org-membership-team-management(6/6 + four negatives) —{current_org_id}resolves to the active org id (the nav href is the record route, not the raw list); role changes bite in both directions; team membership keys on the(team,user)pair (a duplicate add returns the same row id, count unchanged);remove-memberflips org-scoped reads 200→403 on both the live token and a fresh sign-in. All seven forged management calls refused server-side with a byte-identical before/after state snapshot.🔴 FAIL — 3 items
1.
admin-lifecycle-operations—remove-useris non-atomic and can never succeedReproduction rule — as a genuine better-auth admin:
POST /auth/admin/create-user {email:…}→ 200; create a record owned by that user; thenPOST /api/v1/auth/admin/remove-user {userId}.Expected 2xx,
sys_usergone, the owned record surviving withowner_idnull (engine FK clear).Actual HTTP 500 with an empty body;
sys_userstill readable; the record still carriesowner_id; and the user's credentialsys_accountrows have already been deleted — so that email can no longer sign in (401) while still occupying the org roster. Reproduced twice; verified across three victims (accounts=0for all three vsaccounts=1for untouched users).Server log gives the real cause:
Cannot delete sys_user (<id>): 1 dependent sys_member record(s) reference it via user_id (user_id is required, so it cannot be cleared)… set deleteBehavior:cascade on sys_member.user_idwith{code:DELETE_RESTRICTED, status:409}— a 409 leaking to the client as a bodyless 500.Three compounding problems: (i) every user is auto-bound to the default org at sign-up/create, so
remove-usercan never succeed on this deployment; (ii) the operator cannot clear the blocker either —DELETE /data/sys_member→ 405 withallowed:[get,list,aggregate,history,export]; (iii) the account/session deletions are not rolled back when thesys_userdelete is refused, leaving an orphaned, un-authenticatable identity on the roster.This is not the #3023 owner-anchor regression — the veto comes from the engine's referential restrict on
sys_member.user_id, not from the owner-anchor transfer guard.Also failing in this item — impersonation is invisible in the console. With an impersonation server-confirmed in that very browser context (an in-page
get-sessionreturns the target's email andsession.impersonatedBy=<admin id>),/_console/homerenders "Good morning, Dev Admin." with avatarDA— the admin's identity — and the DOM contains no banner, no "Stop impersonating" control, and the impersonated user's name nowhere. Reproduced twice in fresh contexts with hard reload +networkidle+ 3 s settle. The console also offers no Impersonate entry point at all. Support sessions are silent in this build. Not compared against objectuiorigin/main, so this may already be fixed upstream.Everything else in this item passes: ban/unban is reversible and enforced at sign-in;
set-roleis authoritative in both directions (probed as the target, bracketing the write);revoke-user-sessionskills the live session immediately, disproving the "only stops new logins" negative;create-user's explicit password wins over the generated one (#3031/#3033 holds); all 8 forged non-admin operations 403 with state byte-identical.2.
invitation-scope-gates—pending → acceptedis unreachableThe delegated-admin scope itself is correct: a real in-product
delegated_admincan invite a member (200, exactly one row attributed to the delegate) but cannot invite an admin (403 with the ADR-0090 D12 message) and leaves zero orphan rows; a plain member is refused entirely.(a)
accept-invitation→ HTTP 500 with an empty body, and the row stayspendingforever. Reproduced twice with fresh emails. Server log:insert into sys_member … UNIQUE constraint failed: sys_member.organization_id, sys_member.user_id— acceptance re-inserts a membership the sign-up reconciler already created. Suspected fix: acceptance should upsert/adopt the existingsys_memberrow (or skip the insert when the invitee is already a member of the target org). Same bodyless-500 statusCode-leak class as theremove-userfailure above.(b)
cancel-invitationwritesstatus: 'canceled', a value absent from the spec enum.InvitationStatus = z.enum(['pending','accepted','rejected','expired'])(organization.zod.ts:105) is what this item'ssourcenames as the authority, while the platform object declares a divergent 5-value select includingcanceled(sys-invitation.object.ts:214). Anything validating an invitation againstInvitationSchemarejects a canceled row.Resend behaves correctly (expiry extended, still exactly one row, a second
sys_emailartifact). Clause 4 isblocked(fixture): no org-role persona can reach the Setup app at all ("App not available", 0 nav entries) because of the everyone-anchor baseline, so the entitled and unentitled UI halves cannot be discriminated.3.
api-key-ui-lifecycle— API keys cannot be revoked through any product routeReproduction rule —
POST /api/v1/keys {name}→ 201, thenPATCH /api/v1/data/sys_api_key/<id> {revoked:true}as admin.Expected 200 and the key stops authenticating. Actual 405
OBJECT_API_METHOD_NOT_ALLOWED,allowed:[get,list,aggregate,history,export]. Same 405 for{revoked:false}(restore). Reproduced 3×, including through the real UI — Setup → API Keys → Open menu → Revoke API Key → Continue produces an error toast, the key keeps authenticating, and the row still readsrevoked=false.Root cause.
packages/platform-objects/src/identity/sys-api-key.object.tsdeclares actionsrevoke_api_key/restore_api_keyasPATCH /api/v1/data/sys_api_key/{id}(lines 41-72) while the same file setsenable.apiMethods = ['get','list'](line 219) with the comment "#1591 — reads only: writes are refused… HTTP answers 405 before the 403". The two halves of the file contradict each other. No alternative route exists:route-ledger.tscarries onlyPOST /keys;/api/v1/auth/api-key/*andDELETE /api/v1/keys/{id}all 404.Enforcement of the flag is fine — setting
revoked=1out-of-band makes the very nextx-api-keycall 401UNAUTHENTICATED— so the fix is either to allowupdatescoped to therevokedcolumn, or to give revoke/restore a dedicated auth route.Nothing pins this today: the unit tests (
http-dispatcher.keys.test.ts,resolve-execution-context.test.ts) exercise key resolution with a pre-revoked row and never call the PATCH route the action declares.The rest of the item is clean: show-once holds (get-by-id, list and a reloaded DOM never carry the raw secret), and a key carries exactly its owner's permissions — an
org_member-owned key reproduces its owner's 403, andget-sessionviax-api-keyreturns null. Secondary finding: thekeycolumn (a SHA-256 hash) does serialize over the data API on both get-by-id and list, despite being declaredField.text({hidden:true, description:'Hashed API key value - never exposed to clients'}). The spec defineshiddenas "Hidden from default UI", so this is not ahidden-contract violation — but the item's parenthetical claim is false on this build. Admin-readable only on this app.🟡 PARTIAL — 4 items
teams-bu-membership— the BU tree is load-bearing, but revocation is lazyMeasured matrix:
inSubtree=1,reparentedOut(no touch)=1,reparentedOut(touched)=0,restored(no touch)=0,restored(touched)=1.The risky direction: after a business unit is moved out of a shared subtree, its members keep read access until the record is next written.
Root cause (located). The sharing-rule rebind triggers are bound to data changes on the rule's object (the boot log binds them for
showcase_project,showcase_inquiry,showcase_task) but not tosys_business_unit_memberorsys_business_unitchanges — so tree and membership edits never enqueue a recompute for rules withunit_and_subordinatesrecipients.The item's
knownGapcan be retired: it anticipated that no showcase geometry might consume BU membership, butshare_new_inquiries_with_field_opsdoes, so the clause ran. Seed note for the next sweep: a fresh boot seeds the BU tree but zerosys_business_unit_memberrows, so the rule materializes nothing until a tester places someone — that is theseed-data-thintrap for this item.auth-method-matrixEvery flag with a boot switch matched the config that booted it; 13 forged disabled-method endpoints all 404 across two boots with no silent 200; the 2FA challenge interrupts before a session is granted (the accompanying token is provably unusable); both
.well-knowndocuments are byte-identical and every advertised endpoint was probed and resolves.Three partials: (i)
/auth/configstill advertisespasskeys:false/magicLink:false— a stale prebuiltplugin-auth/dist/index.js, not a regression (src dropped them in86f7a20e); (ii) the phone-OTPNOT_SUPPORTEDbranch is unreachable becauseisPhoneOtpDeliverable()admitsLogSmsTransportoutside production — proven not a silent success (the code appears in the transport log, a resend is 429, and the captured code completes a real verify); (iii)change-emailanddelete-userare booked as mounted inauth-route-ledger.ts(lines 118/120) butplugin-authnever configures better-auth'suser.changeEmail/user.deleteUser— 0 grep hits in bothsrcanddist— so they answer 400CHANGE_EMAIL_DISABLED/ 404 with no switch to enable them.session-list-revokeRevocation is immediate, token-scoped and server-enforced on both bearer and cookie transports (401 on a protected route, not merely absence from the list);
revoke-other-sessionsleaves exactly the caller alive and never touches another user;sys_sessionwrites are refused as a method gate (405, same for admin — not a permission accident).Two gaps: (i) no ordinary persona can read
sys_sessionat all (403 for[org_member, everyone]), so raw-table RLS for a non-admin is unobserved — the adminmineview (12 of 50 rows) was substituted; (ii)sys_session.revoked_at/revoke_reasonare declared readonly and "System-managed" but have no writer anywhere in the tree and appear in no listView — better-auth deletes the row rather than tombstoning — so the ADR-0069 D4 revoke-audit trail is inert, an enforce-or-remove candidate. Also: Setup → Sessions lands onminerather thanall_sessionsbecausenav_sessionscarries noviewName, so an admin sees only their own sessions until they notice the "All" tab.identity-import-wizardBoth invite channels proven in one batch (
delivery {emailInvite:1, smsInvite:1}, the reset link captured fromsys_emailand the invite SMS from the transport log); thetemporarypolicy gives every row a distinct one-time password via API and through the real wizard; both credential routes then authenticate; upsert is idempotent on the same ids with no duplicates; non-admin 403 / anonymous 401 with zero users created.Strongest oracle of the run: a byte-level grep for all four plaintext passwords across
data.db,data.db-wal,data.telemetry.db,data.telemetry.db-walandboot.logreturned 0 matches — against a positive control that matched 25 times, proving the search reached the stored bytes.The one unproven half is the
autopolicy's temporary-fallback branch, which is structurally unreachable onobjectstack dev: both transports always register, anddev.tshard-setsNODE_ENV='development'soisPhoneOtpDeliverable()cannot be made false. Suggest recording this as a fixture note on the item so the next sweep does not re-derive it.⛔ BLOCKED — 2 items
oauth-app-consent-loopandlinked-accounts-social— both carry a ledgeredblocked:{by,ref}and remain unrunnable on stock fixtures (no outbound egress for a real IdP round trip).Checklist maintenance falling out of this run
org-membership-team-managementclause 1 and its negative name the wrong vocabulary. The enforced builtin set is{owner, admin, delegated_admin, member}per ADR-0108 (BUILTIN_MEMBERSHIP_ROLE_OPTIONS, "nothing widens these at boot any more") — not{owner, admin, member, guest}.guestis actually rejected (400ROLE_NOT_FOUND), anddelegated_adminis legitimate. The spec doc-comment onMemberSchema.role(organization.zod.ts:84-87, "Common roles: owner, admin, member, guest") is itself stale and is probably where the checklist text came from — worth fixing both.invitation-scope-gatesclause 6 conflates two axes.DelegableScopemodels ObjectStack positions / permission sets / business units and has no field that could carry a better-auth org invitation role, so it can never "contain member". The delegate demonstrably can mint a member invitation while the endpoint reports an empty assignable set — it under-reports, never over-reports, which is the safe direction. Needs a revision, not a product fix.admin-lifecycle-operationsclause 4 should name the authed-request oracle, notget-session's status code — better-auth's no-session convention is 200-with-null-body, so the clause's literal 401 expectation misdescribes a correct implementation. The session was provably gone.teams-bu-membership— retire theknownGap(a BU-consuming geometry does ship) and add the zero-seeded-placements note.identity-import-wizard— add the fixture note that theautotemporary-fallback branch cannot occur onobjectstack dev.