Repository navigation
QA run · area:identity-auth (23/23) · 2034975a · 2026-10-16 · 18 PASS / 5 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN #23943
Description
QA run record for the area:identity-auth slice of wave #23878 (cloud runner for session session_01K3qUoyh1xFGoWzAACPMbXJ). Text only, per checklist-test §4. Selector resolved with node scripts/checklist-select.mjs area:identity-auth --json.
Environment fingerprint
- Framework:
objectstack-ai/objectstackmain @2034975a47193f04db4791614f4ac9331ff211d3.pnpm install --frozen-lockfile && pnpm build: 75/75 tasks, 6m52s. All servers booted from a second linked worktree at the same sha (../os-qa) holding copies of every builtdist. - Console:
.objectui-sha=daeb77fc84dd824e74c95b87cdede85afdb248e7.pnpm objectui:buildfails on this sha (Console pin does not build on main: objectui daeb77fc imports checkPageComponentViewData, deleted from spec by #23784 #23908, not re-filed), so the console was built in a worktree ata35f7a67^(34b740f) throughscripts/build-console.sh, then copied in.pnpm check:console-shareadsConsole dist matches the objectui pin (objectui@daeb77fc84dd). Browser verdicts ran on objectui daeb77fc built against speca35f7a67^. - Boots:
OS_PORT=P pnpm exec objectstack dev --ui --seed-admin -p P -d file:DIR/data.dbfromexamples/app-showcase. Each runner group used its own ports and file DBs (47110–47161), with fresh DBs per variant. Variant env, always exported before boot:OS_AUTH_TWO_FACTOR=true- runtime
PUT /api/settings/auth {mfa_required} OS_AUTH_REQUIRE_EMAIL_VERIFICATION=trueOS_AUTH_AUDIENCE_POSTURE=open+OS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SET=showcase_contributorOS_DISABLE_SIGNUP=trueon a--no-seed-adminzero-user DBOS_AUTH_PHONE_NUMBER_ENABLED=trueNODE_ENV=production+ scratchOS_AUTH_SECRET+OS_OIDC_PROVIDER_ENABLED=falseOS_AUTH_SSO_ONLY=trueOS_TENANCY_POSTURE=group/isolatedOS_AUTH_URL(custom issuer)
- Scratch fixtures (never committed, no product change):
- A local authorization-code + PKCE (S256) OIDC provider: discovery, authorize, token, RS256 id_token with nonce, userinfo, jwks.
- It is registered through
auth.applyConfigPatch({ oidcProviders })from an env-gated plugin in a scratch copy of the showcase. No env switch foroidcProvidersexists, and a config file beside the showcase config is not git-ignored. - Walled-tenancy boots ran from a scratch showcase copy that adds
@objectstack/organizations.
- Seed:
--seed-admin(admin@objectos.ai) plus the showcase demo personas. Disposable users were minted per item through invite → sign-up oradmin/create-user; no demo persona was banned or removed. - Mail capture: the persisted
sys_emailrow (adminGET /api/v1/data/sys_email). See the checklist-accuracy notes on the close-out. - Browser: Playwright with
chromium.launch({executablePath:'/opt/pw-browsers/chromium'}). Each persona had its own context and signed in through the console form; screenshot first, then DOM. Identity was proven withGET /api/v1/auth/get-sessionbefore each cell. - Execution: six runner subagents in parallel groups, each with its own ports and DBs, at most 4 groups at once. Automated pins ran afterwards one vitest at a time (
--maxWorkers=1), with no server up. - Date: 2026-10-16, about 12:15–13:25 UTC.
Scope
Selector area:identity-auth → 23 runnable items. Planned: none. Blocked-hidden: none (--include-blocked returns the same 23).
Derived item verdicts
18 PASS / 5 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN. Item verdicts are derived strictly: any clause that is not pass, a skipped negative included, makes the item partial.
| item | rev | prio | surface | verdict | clauses pass | non-pass clauses |
|---|---|---|---|---|---|---|
identity-auth.sso-enforced-first-paint |
2 | P2 | browser | PASS | 6/6 | — |
identity-auth.phone-signin-surfaces |
3 | P2 | browser | PASS | 6/6 | — |
identity-auth.auth-method-matrix |
7 | P1 | mixed | PARTIAL | 13/15 | A0 blocked(environment); A4 blocked(environment) |
identity-auth.invitation-scope-gates |
3 | P1 | mixed | PASS | 10/10 | — |
identity-auth.admin-lifecycle-operations |
7 | P1 | mixed | PARTIAL | 13/14 | N2 skipped |
identity-auth.session-list-revoke |
2 | P1 | mixed | PASS | 10/10 | — |
identity-auth.api-key-ui-lifecycle |
1 | P1 | mixed | PASS | 10/10 | — |
identity-auth.self-service-profile-password |
1 | P1 | mixed | PARTIAL | 8/10 | A5 blocked(fixture); N3 skipped |
identity-auth.org-membership-team-management |
3 | P1 | mixed | PASS | 11/11 | — |
identity-auth.teams-bu-membership |
2 | P2 | mixed | PARTIAL | 7/8 | N2 skipped |
identity-auth.self-service-password-reset |
1 | P1 | api | PASS | 9/9 | — |
identity-auth.oauth-app-consent-loop |
2 | P2 | mixed | PASS | 10/10 | — |
identity-auth.linked-accounts-social |
3 | P2 | mixed | PASS | 9/9 | — |
identity-auth.identity-import-wizard |
2 | P2 | mixed | PASS | 10/10 | — |
identity-auth.two-factor-enrollment-reveal |
2 | P1 | api | PASS | 9/9 | — |
identity-auth.two-factor-verify-to-activate |
1 | P1 | api | PASS | 8/8 | — |
identity-auth.two-factor-backup-codes |
2 | P1 | api | PASS | 10/10 | — |
identity-auth.two-factor-disable-lifecycle |
1 | P1 | api | PASS | 10/10 | — |
identity-auth.first-run-owner-bootstrap |
1 | P1 | mixed | PASS | 11/11 | — |
identity-auth.self-signup-gate |
3 | P1 | mixed | PASS | 10/10 | — |
identity-auth.email-verification-loop |
3 | P2 | mixed | PASS | 11/11 | — |
identity-auth.workspace-org-switch |
3 | P2 | mixed | PARTIAL | 10/11 | A5 blocked(fixture) |
identity-auth.implicit-account-linking-ownership |
2 | P1 | mixed | PASS | 12/12 | — |
Automated pins run (RUNNER rule 6)
All seven ran on os-qa @ 2034975a (vitest --maxWorkers=1) and all passed:
packages/qa/dogfood/test/oidc-authorization-code-flow.dogfood.test.ts: 3/3delegated-admin-invite.dogfood.test.ts: 4/4admin-identity-audit-trail.dogfood.test.ts: 3/3admin-credential-lifecycle.dogfood.test.ts: 5/5admin-route-nonadmin-refusal.dogfood.test.ts: 7/7admin-platform-admin-standing.dogfood.test.ts: 7/7packages/plugins/plugin-auth/src/implicit-account-linking.test.ts: 23/23
Reproduction rules
No clause scored fail, so none are owed. No authentication or authorization hole was found; nothing on this record is withheld under the RUNNER rule-2 carve-out.
Non-pass clauses: reasons
auth-method-matrixA0, A4: blocked(environment). Every variant that could be booted advertised and rendered exactly its config. That covers stock; phone + 2FA; production with phone on, OTP undeliverable and the OIDC provider off; a generic-OIDC fixture; and a custom issuer. The on-sides ofdeviceAuthorizationand the@better-auth/ssoplugin cannot be booted onos dev, so the "every variant" claim is not fully proven.self-service-profile-passwordA5: blocked(fixture). N3: skipped. The passwordless (SSO/social-only) set-initial-password branch needs a provisioned passwordless user. Contrast proven: set-initial on a user with a password → 409PASSWORD_ALREADY_SET.workspace-org-switchA5: blocked(fixture). The server half holds: a forgedset-activeto a non-member org → 403USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, and the active org becomes none, never the foreign org. The "UI surfaces an error" half has no UI gesture that can trigger it.admin-lifecycle-operationsN2: skipped. Moot whilerevoke-user-sessionsstays refused by design (finding: seven better-auth/admin/routes refuse platform admins and have no ObjectStack consumer — decide whether they are capability or just vendor surface #9969); the platform admin's 403 is recorded under A4.teams-bu-membershipN2: skipped. No intermediate-depth BU scope was authored; the depth matrix belongs toaccess-security.scope-depth-asymmetry.
Fixture gaps
- No provisionable passwordless user on stock showcase. This blocks
self-service-profile-passwordA5/N3. - Device authorization and the enterprise-SSO plugin have no
os devboot switch. This blocksauth-method-matrixA0/A4 on-sides. - Generic OIDC (
oidcProviders) has no env door. The local-IdP fixture needs a scratch app copy plusapplyConfigPatch; the recipe is held by this runner.
Checklist-accuracy findings and fixture gaps close out on #23878. Extracted defect cards are listed in the close-out comment.
Per-clause verdicts (text evidence)
A = acceptance index and N = negative index, both 0-based as in the item JSON. Evidence is trimmed to one line; secrets, tokens and backup codes are redacted.
| item | clause | verdict | evidence |
|---|---|---|---|
sso-enforced-first-paint |
A0 | pass | cold-started b2 (OS_AUTH_SSO_ONLY=true + qa-oidc fixture), first request before any browser: GET /api/v1/auth/config → features.ssoEnforced true, features.sso false, socialProviders [{id:'qa-oidc',na… |
sso-enforced-first-paint |
A1 | pass | 3 fresh contexts (2 plain, 1 with /auth/config delayed 3 s) using an init-script MutationObserver on every DOM change: input[type=password] count 0 at every recorded state (t=0 empty → ~0.8–4.0 s bre… |
sso-enforced-first-paint |
A2 | pass | enf1-settled.png: 'Sign in with your organization's single sign-on', SSO button, no sign-up link (0 sign-up matches), 'Use a password instead' present; clicking it shows 1 password input and admin@ob… |
sso-enforced-first-paint |
A3 | pass | hung sign-in simulated by holding POST /api/v1/auth/sign-in/social unanswered (Playwright route never fulfilled): at +1.5 s still pending; at ~+20 s (wd-2-after-window.png) the inline error 'Sign-in … |
sso-enforced-first-paint |
A4 | pass | without enforcement: b1 (same fixture, no SSO flag) /auth/config ssoEnforced false + qa-oidc listed, first paint has visible email+password inputs at 894 ms next to the OIDC button (unenf-settled.png… |
sso-enforced-first-paint |
N0 | pass | no password input appeared in any DOM state on 3 enforced loads (2 plain, 1 with the config fetch delayed 3 s), so the hydration-race trap was ruled out (the spinner holds until the config resolves). |
phone-signin-surfaces |
A0 | pass | Both A and B: GET /api/v1/data/sys_user?filter=[[phone_number,=,+8613800138000]] as admin → 1 row: id usr_showcase_phone_demo, name 'Mei Phone (demo)', email phone.demo@example.com, phone_number +861… |
phone-signin-surfaces |
A1 | pass | B, through console screenshots. The All Users list (/apps/com.objectstack.setup/sys_user/view/all_users) has a Phone Number column: Mei row +8613800138000, Ph One +15551230001. Mei's record detail hi… |
phone-signin-surfaces |
A2 | pass | B: the console Create User posted POST /api/v1/auth/admin/create-user {email:ph2@example.test, phoneNumber:+15551230002, generatePassword:true} → 200. A fresh read, GET /api/v1/data/sys_user/jt9QOe8c… |
phone-signin-surfaces |
A3 | pass | A (default): /auth/config features.phoneNumber:false; the Create User dialog (screenshot first) has inputs email/name/password only, no phoneNumber; the server refuses a forged create-user with phone… |
phone-signin-surfaces |
N0 | pass | The seeded persona is present on both boots. |
phone-signin-surfaces |
N1 | pass | With the flag false (A) the dialog offered no phoneNumber param. |
auth-method-matrix |
A0 | blocked(environment) | Every variant I could boot advertised exactly what it booted. phoneNumber false/true/true on A/B/C. twoFactor false on A, true on B. oidcProvider true on A, false on C. phoneNumberOtp false on A, tru… |
auth-method-matrix |
A1 | pass | A: uninvited sign-up/email → 403 SELF_REGISTRATION_CLOSED; invite-member 200 then sign-up 200; sign-in/email 200 + better-auth.session_token cookie; get-session 200 user ep1@example.test; sign-out 20… |
auth-method-matrix |
A2 | pass | B: admin/create-user {phoneNumber:+15551230001} 200. POST /sign-in/phone-number with the right password → 200 + token; get-session names ph1@example.test, phoneNumber +15551230001. Wrong password → 4… |
auth-method-matrix |
A3 | pass | C (NODE_ENV=production, phone on, no SMS provider): POST /phone-number/send-otp → 400 {code:SMS_SERVICE_REQUIRED, message names Setup → Settings → SMS Delivery}. /auth/config on the same boot: phoneN… |
auth-method-matrix |
A4 | blocked(environment) | Login screenshots matched each booted config. A: an 'Email' field only, no OTP link, no provider button. B: 'Email or phone number' + 'Sign in with verification code'. C: 'Email or phone number', no … |
auth-method-matrix |
A5 | pass | A (all off): POST /sign-in/phone-number 404; /phone-number/send-otp 404; /two-factor/enable (authed) 404; /device/code 404; /sign-in/sso 404; /sign-in/magic-link 404; GET /passkey/generate-register-o… |
auth-method-matrix |
A6 | pass | B: enable 2FA for ph1 (two-factor/enable + verify-totp, sys_user.two_factor_enabled=true). Then sign-in/email → 200 {twoFactorRedirect:true, twoFactorMethods:[totp]} with only a better-auth.two_facto… |
auth-method-matrix |
A7 | pass | test: oidc-authorization-code-flow.dogfood.test.ts 3/3 passed; plus live generic-OIDC round trip on a local IdP fixture (sign-in/social 200 → authorize 302 → callback 302 → /_console/home, get-sessio… |
auth-method-matrix |
A8 | pass | A: both GET /.well-known/openid-configuration and /.well-known/oauth-authorization-server (app root) → 200 JSON with issuer http://localhost:47160/api/v1/auth. authorize/token/userinfo/jwks/register/… |
auth-method-matrix |
A9 | pass | A: change-email (authed) 200 {status:true}; the session email stays ep1@example.test (unflipped); sys_email holds a 'Verify your ObjectStack email address' mail to ep1-new@ plus a security notice. An… |
auth-method-matrix |
N0 | pass | No disabled method answered 2xx (see acceptance 5). |
auth-method-matrix |
N1 | pass | With SMS genuinely undeliverable (production boot C) the answer is a loud 400 SMS_SERVICE_REQUIRED, not a hang or a 500. The dev-boot 200 is deliberate (log transport counts as deliverable outside pr… |
auth-method-matrix |
N2 | pass | Magic-link and passkey browser lanes were not ticked: blocked(dependency, objectui#4179 closed not_planned). Their server routes 404 on stock boot A. |
auth-method-matrix |
N3 | pass | No magicLink or passkeys key appears in /auth/config on boots A, B, C or D. |
auth-method-matrix |
N4 | pass | Discovery issuer/endpoints resolve on the live server for both the default and the bespoke-issuer boot (acceptance 8). |
invitation-scope-gates |
A0 | pass | live-REST plus browser. g3deleg (positions delegated_admin) POST /auth/organization/invite-member {role:'member'} -› 200 status=pending. Console: Account app › Invitations › 'Invite User' dialog › Co… |
invitation-scope-gates |
A1 | pass | The same principal with role:'admin' -› 403 '[Security] Access denied: you may not invite a member as 'admin' — an invitation can never confer a role above the issuer's own (ADR-0090 D12...)'. This w… |
invitation-scope-gates |
A2 | pass | sys_invitation re-read right after each refused admin/owner attempt (REST and UI) holds only the member invitations. There is no row for g3invitee2, g3invitee3 or g3invitee8. |
invitation-scope-gates |
A3 | pass | g3forger (org_member) invite-member {role:'member'} -› 403 YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION. A forged POST /data/sys_invitation -› 405 OBJECT_API_METHOD_NOT_ALLOWED. The invit… |
invitation-scope-gates |
A4 | pass | Screenshots (Account app, /_console/apps/com.objectstack.account/sys_invitation and /sys_user). g3deleg's toolbar shows 'Invite User' on both lists. g3forger's has no Invite User: the Invitations pag… |
invitation-scope-gates |
A5 | pass | Every status read is in the spec InvitationStatus enum {pending,accepted,rejected,expired,canceled}. Resend (invite-member resend:true) -› 200, same row id, expires_at moved from 12:59:30 to 12:59:54… |
invitation-scope-gates |
A6 | pass | GET /security/my-delegable-scope. g3deleg -› 200 {isTenantAdmin:false, scopes:[], placeableBusinessUnitIds:[], assignablePositions:[]}, and adding ?userId=‹admin› changes nothing (no target param). g… |
invitation-scope-gates |
N0 | pass | The admin and owner invitations were refused and left no row behind. |
invitation-scope-gates |
N1 | pass | The member's affordance is hidden AND the forged request is refused server-side (403/405). |
invitation-scope-gates |
N2 | pass | my-delegable-scope did not over-report: it was empty for both the delegate and the member. |
admin-lifecycle-operations |
A0 | pass | live-REST: admin POST /auth/admin/ban-user {userId,banReason:'g3 qa ban'} -› 200. sys_user re-read shows banned=true, ban_reason='g3 qa ban'. Target sign-in/email -› 403 BANNED_USER, and the target's… |
admin-lifecycle-operations |
A1 | pass | live-REST: admin POST /auth/admin/set-user-password -› 200 {mustChangePassword:true, revoked.sessions:1}. Sign-in with the NEW password -› 200. Sign-in with the OLD password -› 401 INVALID_EMAIL_OR_P… |
admin-lifecycle-operations |
A2 | pass | live-REST: POST /auth/admin/create-user {password:'Explicit#Pass1', generatePassword:true} -› 200, and the response carries no generated password. Sign-in with the EXPLICIT password -› 200. GET /data… |
admin-lifecycle-operations |
A3 | pass | live-REST: admin POST /auth/admin/set-role {role:'admin'} -› 403 YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE. The sys_user re-read shows role='user' before and after, and the target's get-session positi… |
admin-lifecycle-operations |
A4 | pass | live-REST: the target's protected GET /data/sys_user read 200 just before. Admin POST /auth/admin/revoke-user-sessions -› 403 YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS. The same protected read rig… |
admin-lifecycle-operations |
A5 | pass | live-REST: throwaway g3owner created showcase_inquiry UG2_X6nONucrmVB8 (owner_id=g3owner). Admin POST /auth/admin/remove-user -› 403 YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS. Re-reads show the user still … |
admin-lifecycle-operations |
A6 | pass | live-REST: admin POST /auth/admin/impersonate-user -› 200. get-session then returns user=g3target with session.impersonatedBy=‹admin id›, and the sys_session row has impersonated_by=‹admin id›. POST … |
admin-lifecycle-operations |
A7 | pass | screenshot (STILL-MANUAL clause): Setup › Users › row menu (G3 Target) › 'Impersonate User' › confirm dialog › Continue. A red banner then sits across the top of every page: 'You are impersonating G3… |
admin-lifecycle-operations |
A8 | pass | test: admin-identity-audit-trail.dogfood.test.ts 3/3 passed (vitest, os-qa 2034975) — create-user / set-user-password / import-users audit shapes; live corroboration: sys_audit_log rows attributed t… |
admin-lifecycle-operations |
A9 | pass | live-REST, g3forger (positions org_member,everyone). ban-user 403 PERMISSION_DENIED. list-users 403 YOU_ARE_NOT_ALLOWED_TO_LIST_USERS. create-user 403 PERMISSION_DENIED. set-role 403 YOU_ARE_NOT_ALLO… |
admin-lifecycle-operations |
N0 | pass | No forged non-admin admin call succeeded: 8 routes plus 1 anonymous call were all refused, and the state was unchanged. |
admin-lifecycle-operations |
N1 | pass | The ban is enforced at sign-in (403 BANNED_USER), not only as a list filter. |
admin-lifecycle-operations |
N2 | skipped | Moot while revoke-user-sessions stays ruled (#9969). The route refused the platform admin, as recorded under clause 4. |
admin-lifecycle-operations |
N3 | pass | Partial live check: the explicit password signed in, so there is no #3031 regression on that path. The owner_id cascade half is left to the plugin-security pin. |
session-list-revoke |
A0 | pass | A had 2 live sessions (device-1 nLwRN9r1…, device-2 RF3pqsfy…, both from browser contexts) + B 1 (zp6Bu96d…). A GET /auth/list-sessions 200 -› exactly those 2 ids; A GET /data/sys_session 200 -› only… |
session-list-revoke |
A1 | pass | Screenshot of Account › Active Sessions (sys_session/view/mine) as A: 'My Sessions' tab, 2 rows with IP Address/Active Organization/Created At/Expires At columns, footer '2 records' = API count of li… |
session-list-revoke |
A2 | pass | device-1 POST /auth/revoke-session {token:‹device-2 token›} 200 {status:true}; device-2 GET /data/sys_session -› 401 UNAUTHENTICATED and get-session 401; device-1 GET /data/sys_session -› 200. Unenti… |
session-list-revoke |
A3 | pass | Added device-3 (TaCewA4M…); list-sessions = 2; device-1 POST /auth/revoke-other-sessions 200; list-sessions -› 1 row [nLwRN9r1… = device-1 sid]; device-3 GET /data/sys_session -› 401; device-1 -› 200… |
session-list-revoke |
A4 | pass | Admin Setup › Sessions all_sessions screenshot: cross-user rows (Dev Admin, Mei Phone (demo), Ada Auditor (demo)) with User column, 5 records. Revoke metadata is on the sibling 'Revoked' tab (view re… |
session-list-revoke |
A5 | pass | As A: PATCH/DELETE/POST /data/sys_session[/id] -› 405 OBJECT_API_METHOD_NOT_ALLOWED (allowed get,list,aggregate,export); PUT -› 405 METHOD_NOT_ALLOWED. As admin: PATCH/DELETE on B's session -› 405 sa… |
session-list-revoke |
N0 | pass | No B row in A's list-sessions or data read. |
session-list-revoke |
N1 | pass | device-1 stayed 200 after revoking device-2. |
session-list-revoke |
N2 | pass | revoke-other-sessions kept device-1 (200) and device-3 got 401. |
session-list-revoke |
N3 | pass | Revoked sessions answered 401 on the very next request. |
api-key-ui-lifecycle |
A0 | pass | UI Generate key on Account › Developer › Integrations (/_console/apps/com.objectstack.account/developer/integrations) fired POST /api/v1/keys 201 {data:{id:Zy20EaCpp1VD-udR,name,prefix:osk_2PFDqPdb,k… |
api-key-ui-lifecycle |
A1 | pass | GET /data/showcase_task with x-api-key (no cookie/bearer) 200, 10 rows, id set identical to owner's session read. Not a superuser: x-api-key GET /data/sys_session returns only user_id=usr_showcase_au… |
api-key-ui-lifecycle |
A2 | pass | Owner PATCH /data/sys_api_key/{id} {revoked:true} 200 -› next x-api-key call refused 403 API_KEY_REVOKED (reproduced on 3 cycles). Clause text says 401; 403 is the deliberate #23367 ruling (packages/… |
api-key-ui-lifecycle |
A3 | pass | PATCH {revoked:false} 200 -› x-api-key GET showcase_task 200 (10 rows), reproduced 3 cycles. |
api-key-ui-lifecycle |
A4 | pass | Second member phone.demo: GET /data/sys_api_key -› 0 rows, GET by id -› 404 RECORD_NOT_FOUND, mine-view screenshot 'No identity records'; forged PATCH {revoked:true} on A's key -› 404 and key still 2… |
api-key-ui-lifecycle |
A5 | pass | Dialog screenshot: 'Your new API key — Copy this now — for your security it will not be shown again.' with the osk_ secret + Copy button; re-opened Account › API Keys (mine) screenshot: one row qa-g4… |
api-key-ui-lifecycle |
N0 | pass | Raw secret absent from get-by-id, list, mine grid, admin list. |
api-key-ui-lifecycle |
N1 | pass | Revoked key -› 403 API_KEY_REVOKED, never 200. |
api-key-ui-lifecycle |
N2 | pass | Key read of sys_session = owner rows only vs admin 3 users. |
api-key-ui-lifecycle |
N3 | pass | phone.demo list 0 rows / get 404. |
self-service-profile-password |
A0 | pass | qa.prof (form sign-in) edited Name -› 'QA Prof Renamed' + Save Changes: POST /auth/update-user 200 {status:true}; get-session user.name='QA Prof Renamed'; after page.reload the Name input and identit… |
self-service-profile-password |
A1 | pass | setInputFiles on [data-testid=profile-avatar-file] (64x64 PNG): POST /storage/upload/presigned 200 -› PUT /storage/_local/raw/‹signed› 200 -› POST /storage/upload/complete 200 {fileId,path:user/‹id›.… |
self-service-profile-password |
A2 | pass | Click profile-avatar-remove-btn -› POST /auth/update-user 200; get-session image=null; screenshot shows 'QP' initials fallback and Upload button again (2 runs). |
self-service-profile-password |
A3 | pass | PasswordCard (#current-password/#new-password/#confirm-password, 'Change password') with correct current -› POST /auth/change-password 200; API sign-in with OLD -› 401 INVALID_EMAIL_OR_PASSWORD, with… |
self-service-profile-password |
A4 | pass | UI with wrong current -› POST /auth/change-password 400 INVALID_PASSWORD, card shows 'Invalid password' (screenshot); old password still signs in 200, new one 401. Direct forged API change-password w… |
self-service-profile-password |
A5 | blocked(fixture) | see ref |
self-service-profile-password |
N0 | pass | Name survived reload; get-session confirms server write. |
self-service-profile-password |
N1 | pass | image is /api/v1/storage/files/‹id›, a committed storage object, not data:. |
self-service-profile-password |
N2 | pass | Wrong current -› 400 INVALID_PASSWORD (UI and forged API). |
self-service-profile-password |
N3 | skipped | Needs the passwordless persona (blocked fixture). |
org-membership-team-management |
A0 | pass | Screenshot: Setup › People & Organization › Organization opens /_console/apps/com.objectstack.setup/sys_organization/record/org_mvayuz49hd2evoo1, the record page (not the list), with tabs Members 10 … |
org-membership-team-management |
A1 | pass | g3m2 as member: create-team -› 403 YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION. Admin update-member-role to 'admin' -› 200, list-members shows admin and positions become ['org_admin','ev… |
org-membership-team-management |
A2 | pass | update-member-role {role:'guest'} -› 400 {code:ROLE_NOT_FOUND}. {role:'contributor'} (a showcase position) -› 400 ROLE_NOT_FOUND. invite-member with guest/contributor -› 400 'ROLE_NOT_FOUND: …'. The … |
org-membership-team-management |
A3 | pass | POST /auth/organization/update {data:{name:'G3 Renamed Org'}} -› 200. get-full-organization.name went from 'Default Organization' to 'G3 Renamed Org', sys_organization agrees, and the console record … |
org-membership-team-management |
A4 | pass | Before: g3m3 had get-full-organization 200 and list-members 200 (10). Admin remove-member -› 200 and m3 is gone from the roster. m3's live session is killed at once: get-session and data reads -› 401… |
org-membership-team-management |
A5 | pass | create-team g3-team-A -› 200. add-team-member for m2 and m3 -› 200 each. GET /data/sys_team_member returns exactly two (team_id,user_id) rows. A duplicate add returns the existing row id and the row … |
org-membership-team-management |
A6 | pass | g3forger (member) AND g3deleg (delegated_admin) each called update-member-role, update-member-role to owner on self, remove-member, update, create-team, add-team-member and remove-team-member. All go… |
org-membership-team-management |
N0 | pass | Hidden affordance with a forged success: not observed. Every forged call was 403. |
org-membership-team-management |
N1 | pass | remove-member changed authorization: the session was killed, org_member was dropped, and org endpoints were refused. |
org-membership-team-management |
N2 | pass | No role outside the 4-name vocabulary was stored. guest and contributor -› 400 ROLE_NOT_FOUND, and the role change bites. |
org-membership-team-management |
N3 | pass | The nav resolved to the record page for the active org, not the list. |
teams-bu-membership |
A0 | pass | sys_team_member for g3-team-A: [m2, m3] (one row each). A duplicate add-team-member returns the existing row and creates no second one. Add forger -› 3 rows. remove-team-member {teamId,userId:forger}… |
teams-bu-membership |
A1 | pass | POST /data/sys_business_unit {name:'G3 Child Dept', kind:'department', parent_business_unit_id:'bu_acme'} -› 201, and the re-read shows parent bu_acme. Org Chart screenshot (Setup › Business Units, O… |
teams-bu-membership |
A2 | pass | POST /data/sys_business_unit_member -› 201. The read shows business_unit_id=‹child›, user_id=g3m2, function_in_business_unit='lead', is_primary=true. |
teams-bu-membership |
A3 | pass | Scoped persona g3m2 (plain member). Before any placement: showcase_inquiry 0 rows (admin sees 4: 2 status=new). Placed in G3 Child Dept under bu_acme (outside the bu_field_ops subtree): still 0. Chil… |
teams-bu-membership |
A4 | pass | g3forger: POST/PATCH/DELETE sys_business_unit and POST/PATCH sys_business_unit_member -› 403 PERMISSION_DENIED each. create-team -› 403 YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION. add-t… |
teams-bu-membership |
N0 | pass | add-team-member created a row and did not duplicate it. |
teams-bu-membership |
N1 | pass | The placement plus re-parent moved the scoped read (0 -› 2 -› 0 -› 2). |
teams-bu-membership |
N2 | skipped | No intermediate-depth BU scope was authored in this run. The rule exercised is unit_and_subordinates and it enforced exactly. The depth matrix belongs to access-security.scope-depth-asymmetry. |
self-service-password-reset |
A0 | pass | POST /auth/request-password-reset {email:reset1@g2.test} -› 200 {status:true,message:'If this email exists...'}; LogTransport-sent sys_email row (message_id ‹dev-...@objectstack.local›, subject 'Rese… |
self-service-password-reset |
A1 | pass | POST /auth/reset-password {token,newPassword:NewPass#24680} -› 200 {status:true}; sign-in new pw -› 200; sign-in old pw -› 401 INVALID_EMAIL_OR_PASSWORD |
self-service-password-reset |
A2 | pass | second POST /auth/reset-password with the same token -› 400 {code:INVALID_TOKEN}; the pw it tried to set -› sign-in 401 |
self-service-password-reset |
A3 | pass | garbage token -› 400 INVALID_TOKEN, and a fresh real token with its last char flipped -› 400 INVALID_TOKEN; the current pw still signs in (200) and the garbage pw is refused (401). Natural expiry not… |
self-service-password-reset |
A4 | pass | known (reset1@g2.test) and unknown (nobody-here@g2.test) requests both -› 200 with a byte-identical body {status:true,message:'If this email exists in our system, check your email for the reset link'… |
self-service-password-reset |
N0 | pass | reset 2xx, then new pw 200 and old pw 401 |
self-service-password-reset |
N1 | pass | reused token -› 400 INVALID_TOKEN, pw unchanged |
self-service-password-reset |
N2 | pass | identical status and body for known and unknown |
self-service-password-reset |
N3 | pass | garbage token -› 400 INVALID_TOKEN, not a silent success |
oauth-app-consent-loop |
A0 | pass | Runner env had no OS_MCP_SERVER_ENABLED or OS_OIDC_PROVIDER_ENABLED. On stock A: /auth/config oidcProvider:true; GET /.well-known/oauth-authorization-server 200 with authorization_endpoint .../api/v1… |
oauth-app-consent-loop |
A1 | pass | A, admin: POST /oauth2/create-client {client_name 'QA G5b App', redirect_uris [https://qa-rp.example.test/cb], client_secret_post} → 201 carrying client_id + client_secret. Later GET /oauth2/get-clie… |
oauth-app-consent-loop |
A2 | pass | The resource owner ep1 signed in through the console form. GET /oauth2/authorize (scope 'openid profile email', PKCE S256) → 302 to /_console/oauth/consent. Screenshot: 'QA G5b App wants to access yo… |
oauth-app-consent-loop |
A3 | pass | Authorize → POST /oauth2/consent 200 → redirect https://qa-rp.example.test/cb?code=…&state=st-t1&iss=… . POST /oauth2/token (code + code_verifier + client_secret_post) → 200 access_token, id_token, s… |
oauth-app-consent-loop |
A4 | pass | A repeat authorize for the same client and scopes as ep1 → 302 straight to https://qa-rp.example.test/cb?code=rrL94…&state=st-t2. The consent page never loaded, and that code exchanged for a token (2… |
oauth-app-consent-loop |
A5 | pass | Authorize with the added offline_access scope → consent page → Deny → redirect cb?error=access_denied&error_description=User+denied+access&state=st-t3, with no code. Afterwards sys_oauth_access_token… |
oauth-app-consent-loop |
A6 | pass | ep1 get-consents → only userId IaOa… (ep1). Admin, after granting their own consent, sees only userId kcRN… with scopes [openid, email]; before that grant, admin saw []. Admin GET /oauth2/get-consent… |
oauth-app-consent-loop |
N0 | pass | Neither get-client nor the data API returns the secret (acceptance 1). |
oauth-app-consent-loop |
N1 | pass | Approve recorded a consent (and the screen was skipped on repeat); deny minted no tokens. |
oauth-app-consent-loop |
N2 | pass | No cross-user consent visibility (acceptance 6). |
linked-accounts-social |
A0 | pass | [local OIDC fixture] member L (invite+sign-up, signed in through the console FORM; get-session ll1@example.test) → in-page POST /api/v1/auth/link-social {provider:'qa-oidc', callbackURL:‹origin›/_con… |
linked-accounts-social |
A1 | pass | screenshots la-2-linked-after.png (All tab) and lb-1-mylinks.png (nav 'Linked Accounts' → /apps/com.objectstack.account/sys_account/view/mine) show only User L's 2 rows (credential + qa-oidc); U's an… |
linked-accounts-social |
A2 | pass | row menu 'Unlink Account' → confirm → network POST /api/v1/auth/unlink-account {accountId:'e9qpJ8tozG19FsiGw2JHlHqKYE3izXvR' (the ROW id), organizationId} 200 {status:true}; view re-renders '1 record… |
linked-accounts-social |
A3 | pass | provider-less stock boot b3 (/auth/config socialProviders []): Linked Accounts (pl-1-linked.png) has no link affordance — header '…' menu = 'Add to favorites' only, row menu = 'Unlink Account' only, … |
linked-accounts-social |
A4 | pass | as member L and as admin: POST /api/v1/data/sys_account → 405 OBJECT_API_METHOD_NOT_ALLOWED; DELETE /data/sys_account/‹id› → 405; PATCH → 405; body allowed:['get','list','aggregate','export']; the ta… |
linked-accounts-social |
N0 | pass | every link that appeared in the UI had a durable sys_account row in the server-side admin read (L e9qp…, U NXsop…, W FDaLG…). |
linked-accounts-social |
N1 | pass | after unlink the row is gone server-side (admin 404) AND a fresh-context implicit sign-in as L through qa-oidc is refused: final redirect /_console/login?error=account_not_linked ('This sign-in metho… |
linked-accounts-social |
N2 | pass | L's mine/All views and data-API list never contained U's or W's rows; cross-user probe: L POST unlink-account {accountId: W's row id} → 400 ACCOUNT_NOT_FOUND, W's row intact. |
linked-accounts-social |
N3 | pass | provider-less boot offers no link affordance at all (see acceptance 3); nothing navigates to a dead endpoint. |
identity-import-wizard |
A0 | pass | Console wizard (Setup › Users › Import › upload scratch CSV › Mapping auto-matched email+name › Preview 'Automatic (recommended)' › Import 2 Rows). POST /auth/admin/import-users {passwordPolicy:'auto… |
identity-import-wizard |
A1 | pass | Wizard with 'Temporary passwords': import-users {passwordPolicy:'temporary'} -› 200, delivery {temporary:2, emailInvite:0}, and every row carries temporaryPassword. The result step shows 'Temporary p… |
identity-import-wizard |
A2 | pass | Temporary row: sign-in/email g3imp3 with the revealed password -› 200. A protected read is then 403 PASSWORD_RESET_REQUIRED until change-password, after which sign-in with the chosen password -› 200.… |
identity-import-wizard |
A3 | pass | Wizard re-run of the same auto CSV with 'Update if matched, else create' + Match on Email: import-users {mode:'upsert', matchBy:'email'} -› 200 {created:0, updated:2}. REST upsert of the temporary CS… |
identity-import-wizard |
A4 | pass | Neither temporary password string appears in /data reads of sys_user, sys_account, sys_session, sys_audit_log, sys_email or sys_notification (limit 500), in boot.log, or in a raw byte grep of data.db… |
identity-import-wizard |
A5 | pass | g3forger (member) and g3deleg (delegated_admin): import-users (insert and upsert) -› 403 PERMISSION_DENIED 'Admin role required'. Anonymous -› 401 UNAUTHENTICATED. sys_user is unchanged: no g3forgedi… |
identity-import-wizard |
N0 | pass | Every temporary row carried a one-time credential. |
identity-import-wizard |
N1 | pass | The one-time password was not found in any server-side read, log or DB file. |
identity-import-wizard |
N2 | pass | Upsert updated and did not duplicate (the set stays 4). |
identity-import-wizard |
N3 | pass | Non-admin import refused, 403. |
two-factor-enrollment-reveal |
A0 | pass | GET /api/v1/auth/config -› features.twoFactor true; anon POST /auth/two-factor/enable {} -› 400 VALIDATION_ERROR (not 404). Contrast unarmed boot: false + 404 |
two-factor-enrollment-reveal |
A1 | pass | enable {password} -› 200 {method,totpURI,backupCodes}; totpURI otpauth://totp/ObjectStack:admin@objectos.ai, issuer=ObjectStack, algorithm absent, digits=6, period=30, secret 52 base32 chars -› base3… |
two-factor-enrollment-reveal |
A2 | pass | backupCodes: array of 10 strings, each 11 chars, alnum with '-' (values redacted) |
two-factor-enrollment-reveal |
A3 | pass | meta sys_two_factor.enable_two_factor resultDialog.fields = [totpURI/qrcode, backupCodes/code-list]; response.totpURI string, response.backupCodes string[10] — both resolve |
two-factor-enrollment-reveal |
A4 | pass | POST /two-factor/get-totp-uri: pre-verify {} -›400 VALIDATION_ERROR, wrong pw -›400 INVALID_PASSWORD, correct pw -›200 SAME secret as enable, anon -›401; post-verify identical (400/400/200 same secre… |
two-factor-enrollment-reveal |
A5 | pass | GET /api/v1/meta/object/sys_user item.actions enable_two_factor: resultDialog fields [totpURI qrcode, backupCodes code-list], acknowledge 'I have saved my backup codes', successMessage null, target /… |
two-factor-enrollment-reveal |
N0 | pass | wrong-password enable -› 400 INVALID_PASSWORD with 0 sys_two_factor rows after; anon enable {password} -› 401 UNAUTHORIZED |
two-factor-enrollment-reveal |
N1 | pass | guard scored first on armed boot; unarmed contrast confirmed 404 |
two-factor-enrollment-reveal |
N2 | pass | get-totp-uri refuses without/with wrong password; no unauthenticated re-reveal |
two-factor-verify-to-activate |
A0 | pass | same guard as enrollment-reveal on this boot: features.twoFactor true, enable probe 400 |
two-factor-verify-to-activate |
A1 | pass | Diverges from the expected wording, recorded as observed: between enable and verify, POST /auth/sign-in/email -› 200 with session token, NO twoFactorRedirect and NO better-auth.two_factor cookie (get… |
two-factor-verify-to-activate |
A2 | pass | sqlite read-only row read ~2ms after enable 200: sys_two_factor.verified = 0 (healthy). Note: the physical column DDL default is '1' (sqlite dflt_value), i.e. the declared defaultValue:true reached t… |
two-factor-verify-to-activate |
A3 | pass | session-lane POST /two-factor/verify-totp {correct} -› 200 {token,user} + rotated better-auth.session_token; row verified 0-›1, failed_verification_count 0; sys_user.two_factor_enabled 0-›1; get-sess… |
two-factor-verify-to-activate |
A4 | pass | post-activation sign-in -› 200 twoFactorRedirect:true, no token, only better-auth.two_factor cookie; get-session with that jar -› 401; cookie-lane verify-totp correct -› 200 hasToken + Set-Cookie ses… |
two-factor-verify-to-activate |
N0 | pass | no never-verified enrollment satisfied a challenge (no challenge issued for it at all) |
two-factor-verify-to-activate |
N1 | pass | session-lane wrong code -› 401 INVALID_CODE; failed_verification_count stayed 0, verified stayed 0, two_factor_enabled 0; subsequent correct code activated |
two-factor-verify-to-activate |
N2 | pass | armed boot guard confirmed |
two-factor-backup-codes |
A0 | pass | guard as above + verified enrollment (verified=1, two_factor_enabled=1) and challenge trace twoFactorRedirect:true from verify-to-activate on this boot |
two-factor-backup-codes |
A1 | pass | challenge -› POST /two-factor/verify-backup-code {code[0]} via two_factor cookie -› 200 hasToken + session_token cookie; get-session 200 admin |
two-factor-backup-codes |
A2 | pass | fresh challenge, code[0] again -› 401 INVALID_BACKUP_CODE (get-session 401); fresh challenge code[1] -› 200 session; reproduced: code[1] re-spend at a further challenge -› 401 INVALID_BACKUP_CODE |
two-factor-backup-codes |
A3 | pass | generate-backup-codes {password} -› 200 {status,backupCodes[10]}, 0 overlap with old; old unspent code[3] -› 401 INVALID_BACKUP_CODE, old code[4] -› 401 (twice); new code[0] -› 200 session |
two-factor-backup-codes |
A4 | pass | sqlite read: backup_codes is a 362-char hex string (non-JSON), changes on regenerate, shrinks to 334 after a spend; no plaintext code (with or without dashes) is a substring — encrypted blob, healthy… |
two-factor-backup-codes |
A5 | pass | meta sys_user.generate_backup_codes: resultDialog fields [backupCodes code-list], acknowledge 'I have saved the new codes', successMessage null, refreshAfter false; regenerate response.backupCodes st… |
two-factor-backup-codes |
N0 | pass | no code spendable twice (two codes tested) |
two-factor-backup-codes |
N1 | pass | two old-set codes refused after regeneration |
two-factor-backup-codes |
N2 | pass | wrong-pw generate -› 400 INVALID_PASSWORD (no codes returned); anon -› 401 UNAUTHORIZED |
two-factor-backup-codes |
N3 | pass | after refused wrong-pw regenerate, old code[2] still spent -› 200 session (refusal was a no-op) |
two-factor-disable-lifecycle |
A0 | pass | features.twoFactor true; BEFORE trace: sign-in twoFactorRedirect:true + two_factor cookie with active (verified=1) enrollment |
two-factor-disable-lifecycle |
A1 | pass | disable {wrong pw} via a full session -› 400 INVALID_PASSWORD; row still present verified=1, two_factor_enabled=1; fresh sign-in still twoFactorRedirect:true. anon disable -› 401 |
two-factor-disable-lifecycle |
A2 | pass | disable {correct} -› 200 {status:true} (+ rotated session cookie); sqlite: 0 sys_two_factor rows for user; sys_user.two_factor_enabled 0 |
two-factor-disable-lifecycle |
A3 | pass | post-disable sign-in -› 200 with token, twoFactorRedirect absent, only better-auth.session_token cookie (no two_factor); get-session 200 twoFactorEnabled false |
two-factor-disable-lifecycle |
A4 | pass | retained unspent code post-disable: no cookie -› 401 INVALID_TWO_FACTOR_COOKIE; with session -› 400 BACKUP_CODES_NOT_ENABLED; with leftover pre-disable 2FA cookie -› 400 BACKUP_CODES_NOT_ENABLED. Re-… |
two-factor-disable-lifecycle |
A5 | pass | 47111 mfa_required:true grace 0 (patch also flipped features.twoFactor false-›true, enable 404-›400): first authed GET /api/v1/data/sys_user -› 403 MFA_REQUIRED, stamp mfa_required_at=12:39:24.484Z w… |
two-factor-disable-lifecycle |
N0 | pass | wrong-pw disable refused; row-gone and challenge-gone agree |
two-factor-disable-lifecycle |
N1 | pass | old TOTP / old backup codes refused at the re-enrolled challenge |
two-factor-disable-lifecycle |
N2 | pass | gated user's /two-factor/enable answered 400/200, never 404 |
two-factor-disable-lifecycle |
N3 | pass | immediate-bite recorded as the retained-stamp branch, not filed |
first-run-owner-bootstrap |
A0 | pass | zero-user OS_DISABLE_SIGNUP=true --no-seed-admin fresh DB: anonymous GET /auth/bootstrap-status -› 200 {hasOwner:false}; after the wizard, anonymous -› 200 {hasOwner:true} |
first-run-owner-bootstrap |
A1 | pass | /auth/config disableSignUp:true on that boot; the wizard's POST /auth/sign-up/email (owner@g2.test) -› 200 with a token; get-session -› owner@g2.test, positions [platform_admin,org_owner,everyone], i… |
first-run-owner-bootstrap |
A2 | pass | CJK-only org name '测试公司': the network shows organization/list polls then POST organization/update -› 200 {name:'测试公司',slug:'default',id:org_mvaytwl5yk2g2tzr}, and no create call; organization/list af… |
first-run-owner-bootstrap |
A3 | pass | after set-active, document requests for /_console/ fire and a window marker set before submit is gone, so this was a full-document navigation. Landed on /_console/home: the screenshot shows 'Good aft… |
first-run-owner-bootstrap |
A4 | pass | disabled boot after bootstrap: two anonymous forges -› 403 SELF_REGISTRATION_CLOSED (audience door); an invited address -› 400 EMAIL_PASSWORD_SIGN_UP_DISABLED (toggle door); sys_user still [owner@g2.… |
first-run-owner-bootstrap |
A5 | pass | hasOwner:true. Anonymous fresh context /_console/setup lands on /_console/login?redirect=%2Fsetup (screenshot: sign-in card, no wizard). Owner context (session 200 owner@g2.test) /_console/setup land… |
first-run-owner-bootstrap |
N0 | pass | the wizard does not render once hasOwner:true, for anonymous or owner |
first-run-owner-bootstrap |
N1 | pass | every post-bootstrap anonymous sign-up refused on both boots; no new rows |
first-run-owner-bootstrap |
N2 | pass | full-document exit, landed console shows the owner's apps |
first-run-owner-bootstrap |
N3 | pass | CJK-only name applied as the display name, slug 'default' kept |
first-run-owner-bootstrap |
N4 | pass | one org only, renamed through organization/update |
self-signup-gate |
A0 | pass | /auth/config per boot: stock {audiencePosture:invite_only, disableSignUp:false}; OS_DISABLE_SIGNUP=true boot {invite_only, disableSignUp:true}; OS_AUTH_AUDIENCE_POSTURE=open boot {open, disableSignUp… |
self-signup-gate |
A1 | pass | stock POST /auth/sign-up/email uninvited@g2.test -› 403 {code:SELF_REGISTRATION_CLOSED}; admin GET /api/v1/data/sys_user has no uninvited row (users: reset1, admin, invitee, auditor.demo, phone.demo) |
self-signup-gate |
A2 | pass | invite-member invitee@g2.test -› 200 status pending; sign-up with that exact email -› 200 with a token; get-session -› invitee@g2.test, positions [org_member,...]. Console: /register?redirect=/accept… |
self-signup-gate |
A3 | pass | disabled boot: /_console/register?redirect=%2Fapps%2Fshowcase_app lands on /_console/login?redirect=%2Fapps%2Fshowcase_app; the screenshot shows the sign-in card, and the DOM has only email/password/… |
self-signup-gate |
A4 | pass | disabled boot after an owner exists: uninvited forge -› 403 SELF_REGISTRATION_CLOSED (audience door answers first); INVITED email (invite 200) -› 400 {code:EMAIL_PASSWORD_SIGN_UP_DISABLED} (the disab… |
self-signup-gate |
N0 | pass | uninvited -› 403 code, no synthetic 200 |
self-signup-gate |
N1 | pass | no row created for uninvited@g2.test |
self-signup-gate |
N2 | pass | endpoint refuses on the disabled boot (403 or 400 per door), so this is not UI-only |
self-signup-gate |
N3 | pass | invited registrant admitted 200 on stock invite_only |
self-signup-gate |
N4 | pass | stock (hasOwner:true) /login has no Sign up link (DOM: dev-admin-hint, email, Forgot password?, password, Sign In); /register renders data-testid=register-by-invitation 'Self-registration is not open… |
email-verification-loop |
A0 | pass | (a)-style boot OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, invited verify1/2/3: unverified POST /auth/sign-in/email -› 403 {code:EMAIL_NOT_VERIFIED}; anonymous get-session -› 401. Console: the login for… |
email-verification-loop |
A1 | pass | sign-up -› 200 {token:null}; sys_email row (LogTransport id ‹dev-...›, status sent) subject 'Verify your ObjectStack email address' carries .../api/v1/auth/verify-email?token=‹JWT›&callbackURL=...; t… |
email-verification-loop |
A2 | pass | verify2: the sign-up mail's token, GET /auth/verify-email?token= (no callbackURL) -› 200 {status:true,user:null}; sign-in -› 200; get-session -› verify2@g2.test emailVerified:true. POST /verify-email… |
email-verification-loop |
A3 | pass | API resend POST /send-verification-email -› 200 {status:true}, and the console prompt's 'Resend verification email' button -› POST 200, UI 'Verification email sent!'. A second sys_email capture with … |
email-verification-loop |
A4 | pass | GET /auth/verify-email?token=garbage.tok.en -› 401 {code:INVALID_TOKEN}; sign-in afterwards still 403 EMAIL_NOT_VERIFIED; the console /verify-email?token=garbage screenshot shows the 'Verification fa… |
email-verification-loop |
A5 | pass | stock /auth/config emailPassword.requireEmailVerification:false, and an invited stock registrant (emailVerified:false) signs in 200 with a token, no challenge. Forced boot (posture open via env, no o… |
email-verification-loop |
N0 | pass | unverified sign-in refused server-side 403 |
email-verification-loop |
N1 | pass | every runtime send landed a sys_email row (status sent). The only failure, the seed-time TEMPLATE_NOT_FOUND, was thrown into the log, not swallowed |
email-verification-loop |
N2 | pass | open boot advertises true |
email-verification-loop |
N3 | pass | stock boot does not challenge |
email-verification-loop |
N4 | pass | garbage token -› 401 and the error card; the account stays unverified |
workspace-org-switch |
A0 | pass | STOCK /auth/config tenancyPosture=single multiOrgEnabled=false: admin and A top bar = no workspace-switcher, no current-organization-indicator (screenshots + DOM 0/0); a 2nd membership is unreachable… |
workspace-org-switch |
A1 | pass | Both walled boots: dropdown click -› POST /auth/organization/set-active {organizationId:‹Second Org›} 200; post-reload GET /auth/organization/get-active-member -› organizationId = Second Org id. |
workspace-org-switch |
A2 | pass | After set-active the browser issued a document request for /_console/ (resourceType document), then main-frame navs /_console/ -› /_console/apps/showcase_app/…; a window.__marker set before the click… |
workspace-org-switch |
A3 | pass | ISOLATED: pre-switch GET /data/showcase_task 10 rows all organization_id=Default, sentinel 7zaJjuUtkNIzQbc5 present; post-switch 10 rows all organization_id=Second Org, sentinel absent, GET /data/sho… |
workspace-org-switch |
A4 | pass | GROUP dropdown screenshot: header 'Working organization', hint 'New records are created here. Views show data from all your organizations.' (data-testid workspace-switcher-group-hint count 1). ISOLAT… |
workspace-org-switch |
A5 | blocked(fixture) | see ref |
workspace-org-switch |
N0 | pass | single: no indicator, no switcher (admin and member). |
workspace-org-switch |
N1 | pass | isolated reads moved to the new org after switch; meta unchanged. |
workspace-org-switch |
N2 | pass | 1 membership: no dropdown; indicator has no click target/menu. |
workspace-org-switch |
N3 | pass | non-member set-active 403; foreign org never active. |
workspace-org-switch |
N4 | pass | switch exits via document reload. |
implicit-account-linking-ownership |
A0 | pass | [local OIDC fixture, IdP answers email=uu1@example.test email_verified:true, signed id_token] premise: U (invite+sign-up) get-session emailVerified false. Fresh jar: POST /auth/sign-in/social {provid… |
implicit-account-linking-ownership |
A1 | pass | verified U through the product: POST /auth/send-verification-email → captured link in the sys_email row 'Verify your ObjectStack email address' → GET /auth/verify-email?token=… 200 {status:true}; ema… |
implicit-account-linking-ownership |
A2 | pass | U (password session, get-session uu1) POST /auth/unlink-account {accountId:'vfnJRf10…' row id} 200 {status:true}; admin read 0 rows; signed out; fresh-jar implicit sign-in → 302 /_console/login?error… |
implicit-account-linking-ownership |
A3 | pass | U explicit: POST /auth/link-social with U's session → round-trip → 302 /_console/apps/account/sys_account; (U, qa-oidc) row NXsopcb5… back; next fresh-jar implicit sign-in → 302 /_console/ no error, … |
implicit-account-linking-ownership |
A4 | pass | test: plugin-auth implicit-account-linking.test.ts 23/23 passed — incl. "exempts the platform identity provider…", "binds the platform exception to the OAuth sign-in method, not the provider id alone… |
implicit-account-linking-ownership |
N0 | pass | unverified U: no sys_account row and no session after the implicit sign-in (0 rows, get-session 401). |
implicit-account-linking-ownership |
N1 | pass | after the refusal U's email_verified re-read as false; W's stayed false after the explicit link. |
implicit-account-linking-ownership |
N2 | pass | post-unlink implicit sign-ins for U (and for L in the sibling item) were refused and re-created no row. |
implicit-account-linking-ownership |
N3 | pass | explicit signed-in links for U (verified) and W (unverified) both succeeded. |
implicit-account-linking-ownership |
N4 | pass | test: same pin — "binds the platform exception to the OAuth sign-in method, not the provider id alone" passed (SSO under the same id gets no exception) |
implicit-account-linking-ownership |
N5 | pass | the fixture answered email_verified:true in both id_token and userinfo, and the refusal text names the LOCAL row's unverified state, so the run measured this gate, not the provider-side one. |
implicit-account-linking-ownership |
N6 | pass | every implicit sign-in used a newly created empty cookie jar (anon1..anon5), and get-session was read on it before/after. |
Activity
objectstack-fleet commentedon Oct 16, 2026
Close-out (
checklist-test§5) for this record. No clause scoredfail, so no four-way disposition is owed.The defects below were found during the run and extracted. Each was confirmed on two boots or loads and passed the automation self-check. None is an authentication or authorization hole.
objectstack-fleet commentedon Oct 16, 2026
Close-out (
checklist-test§5) for this record. No clause scoredfail, so no four-way disposition is owed.The defects below were found during the run and extracted. Each was confirmed on two boots or loads and passed the automation self-check. None is an authentication or authorization hole.
objectstack-fleet commentedon Oct 16, 2026
Closing at 2026-10-16T15:22Z: this record's §5 close-out is posted — comment 6216600094 (repeated verbatim in 6216608645) — with 18 pass · 5 partial · 0 fail of 23. Each partial has its
Parked-on: #23878line there, and the four extractions are #23944, #23945, #23946 and objectstack-ai/objectui#12682. The slice's three unfiled objectui observations were filed on the maintainer's pointer: objectstack-ai/objectui#12697 (import wizardupdatemode), objectstack-ai/objectui#12698 (stale organization header), #23957 (invite role options).Closed by the
domain:devxseat 1 PM (seat post #6023) ·os-sales·session_014eFvbvGK4FjXvscggDNTD4, as part of the paced close-out of the #23878 wave.Generated by Claude Code