Repository navigation
Closed
Labels
Description
Environment fingerprint
- framework:
5b14b7f3ad8e2e0786d228772f473d07b36b2477(objectstack-ai/objectstack main, detached, not pulled) - console:
.objectui-sha=4016370af3c9c9dafec2bc5299afd8e8079701b8, built withpnpm objectui:build; dist stamp matched the pin - app: examples/app-showcase,
OS_PORT=P objectstack dev --ui --seed-admin -p P -d file:/tmp/qa18/RUN/data.db— every runner its own port (472xx) and file DB; per-item extra env recorded in the run scratch - seed:
--seed-admin(admin@objectos.ai) + showcase demo personas; members minted via admin create-user / invite-then-signup - date: 2026-10-19 (UTC); runner: checklist-test card of the 18.0 release QA wave (dispatch session session_01CfRDy7SuTfUqaTu2AEH2Hi); items driven by parallel runner sub-agents, each on its own port + DB + browser context (Playwright, chromium via /opt/pw-browsers/chromium)
- plan: maintainer-approved option B — Tier 1 (pinned, vitest batch) + Tier 2 (unpinned P0/P1, driven live); Tier 3 (unpinned P2) deferred
Scope
Selector area:identity-auth → 28 runnable items (node scripts/checklist-select.mjs area:identity-auth --json); planned items: none; blocked items excluded by default.
| item | priority | tier | revision | item verdict | port |
|---|---|---|---|---|---|
| identity-auth.sso-enforced-first-paint | P2 | 3 | 3 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.phone-signin-surfaces | P2 | 3 | 3 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.auth-method-matrix | P1 | 1 | 8 | partial | -p 47241 |
| identity-auth.invitation-scope-gates | P1 | 1 | 4 | pass | -p 47241 |
| identity-auth.admin-lifecycle-operations | P1 | 1 | 8 | pass | -p 47241 |
| identity-auth.session-list-revoke | P1 | 2 | 3 | pass | -p 47271 |
| identity-auth.api-key-ui-lifecycle | P1 | 2 | 3 | pass | -p 47271 |
| identity-auth.self-service-profile-password | P1 | 2 | 1 | pass | -p 47271 |
| identity-auth.org-membership-team-management | P1 | 2 | 4 | pass | -p 47231 |
| identity-auth.teams-bu-membership | P2 | 3 | 2 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.self-service-password-reset | P1 | 2 | 2 | pass | -p 47211 |
| identity-auth.oauth-app-consent-loop | P2 | 3 | 3 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.linked-accounts-social | P2 | 3 | 5 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.identity-import-wizard | P2 | 3 | 2 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.two-factor-enrollment-reveal | P1 | 2 | 3 | pass | -p 47211 |
| identity-auth.two-factor-verify-to-activate | P1 | 2 | 2 | pass | -p 47211 |
| identity-auth.two-factor-backup-codes | P1 | 2 | 3 | pass | -p 47211 |
| identity-auth.two-factor-disable-lifecycle | P1 | 2 | 1 | pass | -p 47211 |
| identity-auth.first-run-owner-bootstrap | P1 | 2 | 2 | pass | -p 47232 |
| identity-auth.self-signup-gate | P1 | 2 | 4 | pass | -p 47231 |
| identity-auth.email-verification-loop | P2 | 3 | 4 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.workspace-org-switch | P2 | 3 | 4 | not-run | Tier 3 deferred by the 18.0 release-sweep plan |
| identity-auth.implicit-account-linking-ownership | P1 | 1 | 3 | partial | -p 47241 |
| identity-auth.auth-gate-held-session-raw-host-families | P1 | 1 | 1 | pass | -p 47241 |
| identity-auth.api-key-expiry-lifecycle | P1 | 1 | 1 | pass | -p 47241 |
| identity-auth.cookie-session-renewal-in-process | P2 | 1 | 1 | pass | -p 47241 |
| identity-auth.default-organization-boot-invariant | P0 | 1 | 1 | pass | -p 47241 |
| identity-auth.import-invitation-and-sso-domain-answers | P2 | 1 | 1 | pass | -p 47241 |
Per-clause verdicts
Tier 1 clauses carry [pin] when the pinned test asserts them (evidence = the vitest batch below), [live] when driven against the boot.
| item | clause | verdict | oracle evidence (text) |
|---|---|---|---|
| identity-auth.auth-method-matrix | 0 | blocked(environment: device-authorization has no switch on os dev, @better-auth/sso half … [live] | proven variants only — OFF boot 47241: socialProviders [], phoneNumber false, phoneNumberOtp false, twoFactor false, sso false, deviceAuthorization false, magicLink/passkeys keys ABSENT; ON boot 47242 (OS_AUTH_PHONE_NUMBER_ENABLED, OS_AUTH_TWO_FACTOR, GOOGLE_… |
| identity-auth.auth-method-matrix | 1 | pass [live] | sign-in 200 + better-auth.session_token cookie; get-session 200 mx.ep@example.com; sign-out 200 {success:true} clears session_token/session_data; replayed cookie get-session 401 UNAUTHENTICATED user null, protected data read 401 (sign-up/email w/o invite 403 … |
| identity-auth.auth-method-matrix | 2 | pass [live] | ON boot: POST /sign-in/phone-number +8613800138000/showcase123 → 200; get-session 200 phone.demo@example.com id usr_showcase_phone_demo; wrong password 401 INVALID_PHONE_NUMBER_OR_PASSWORD |
| identity-auth.auth-method-matrix | 3 | pass [live] | NODE_ENV=production boot + OS_AUTH_PHONE_NUMBER_ENABLED: send-otp 400 application/json code SMS_SERVICE_REQUIRED (x2, ~100-190ms, named message); /auth/config phoneNumberOtp false; dev-boot control send-otp 200 'code sent' |
| identity-auth.auth-method-matrix | 4 | blocked(dependency: magic-link/passkeys have no login UI — objectui#4179 closed not_plann… [live] | screenshots of proven variants (console 4016370): OFF login = Email + Password + Forgot password only; ON login = 'Continue with Google', 'Email or phone number' field, 'Sign in with verification code' — matching each boot's /auth/config |
| identity-auth.auth-method-matrix | 5 | pass [live] | stock boot, anonymous forged: /sign-in/phone-number 404, /phone-number/send-otp 404, /two-factor/enable 404, /two-factor/verify-totp 404, /device/code 404, /magic-link/send 404, /sign-in/magic-link 404, /sign-in/social google 404 PROVIDER_NOT_FOUND, /sign-in/… |
| identity-auth.auth-method-matrix | 6 | pass [live] | ON boot (OS_AUTH_TWO_FACTOR=true): enable 200 + verify-totp activate 200; password sign-in 200 {twoFactorRedirect:true,twoFactorMethods:[totp]} with better-auth.two_factor cookie; get-session before challenge 401 UNAUTHENTICATED user null; protected read 401;… |
| identity-auth.auth-method-matrix | 7 | pass [pin] | pin: packages/qa/dogfood/test/oidc-authorization-code-flow.dogfood.test.ts › authorize: skip_consent client gets an immediate code redirect / token exchange succeeds and persists the access-token row / userinfo answers with the signed-in subject (3 tests) ✓ (… |
| identity-auth.auth-method-matrix | 8 | pass [live] | GET /.well-known/openid-configuration 200 JSON and /.well-known/oauth-authorization-server 200 JSON, issuer http://localhost:47241/api/v1/auth, endpoints /api/v1/auth/oauth2/{authorize,token,userinfo} mounted (authorize 200, token 415 on JSON body = mounted, … |
| identity-auth.auth-method-matrix | 9 | pass [live] | change-email authed 200 {status:true} (address not flipped: still mx.ep@example.com), anon 401 UNAUTHORIZED; delete-user authed 404 empty body, anon 401 UNAUTHORIZED, GET /delete-user/callback 404 NOT_FOUND, account survives (get-session 200) |
| identity-auth.invitation-scope-gates | 0 | pass [live+pin] | pin: delegated-admin-invite.dogfood.test.ts › 'a delegated_admin CAN issue an invitation' ✓ (log t1log-identity-auth-packages_qa_dogfood.log; asserts 200 + 1 row role member, not status); live: delegate (set via owner POST /organization/update-member-role → d… |
| identity-auth.invitation-scope-gates | 1 | pass [pin] | pin: delegated-admin-invite.dogfood.test.ts › 'the same delegate issuing admin is REFUSED, with no invitation row left behind' ✓ (log t1log-identity-auth-packages_qa_dogfood.log); live corroboration 403 INVITE_ROLE_ABOVE_ISSUER |
| identity-auth.invitation-scope-gates | 2 | pass [pin] | pin: same test asserts 0 rows for the admin invitee ✓; live: 0 rows for inv.admin.target, sys_invitation total 1→1 |
| identity-auth.invitation-scope-gates | 3 | pass [pin] | pin: delegated-admin-invite.dogfood.test.ts › 'a plain member still cannot invite at all — the ROLE is what opened the endpoint' ✓; live: 403 YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION, 0 rows |
| identity-auth.invitation-scope-gates | 4 | pass [live] | screenshots /_console/apps/com.objectstack.account/sys_invitation (form sign-in): delegated_admin header shows 'Invite User' button; plain member same page has NO Invite User button (admin control: present); server side proven by clause 3 |
| identity-auth.invitation-scope-gates | 5 | pass [live] | statuses observed pending/accepted/rejected/canceled ⊂ InvitationStatus enum; delegate cancel 403 YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION row stays pending; owner resend 200 same row pending, expires_at moved forward; owner cancel 200 → canceled; invite… |
| identity-auth.invitation-scope-gates | 6 | pass [live] | delegate 200 {isTenantAdmin:false,scopes:[],placeableBusinessUnitIds:[],assignablePositions:[]} (DelegableScope shape, no role field — under-reports, expected); plain member same empty shape; delegate ?userId=‹admin› ignored (still own empty scope, while admi… |
| identity-auth.admin-lifecycle-operations | 0 | pass [live+pin] | pin: admin-platform-admin-standing.dogfood.test.ts › 'C0 — ban persists and reverses for a platform admin, through the ObjectStack mounts' ✓ (storage read); live: ban-user 200 → sys_user banned=true ban_reason=qa-ban-reason; banned sign-in 403 BANNED_USER (x2… |
| identity-auth.admin-lifecycle-operations | 1 | pass [pin] | pin: packages/qa/dogfood/test/admin-credential-lifecycle.dogfood.test.ts › set-user-password rotates the credential: the new password signs in, the old is refused (old → 401 INVALID_EMAIL_OR_PASSWORD) ✓ (log t1log-identity-auth-packages_qa_dogfood.log, file ✓… |
| identity-auth.admin-lifecycle-operations | 2 | pass [pin] | pin: packages/qa/dogfood/test/admin-credential-lifecycle.dogfood.test.ts › create-user with BOTH an explicit password and generatePassword:true applies the EXPLICIT one (+ contrast 'a generated-password create is the contrast') ✓ (log t1log-identity-auth-pack… |
| identity-auth.admin-lifecycle-operations | 3 | pass [pin+live] | pin: admin-platform-admin-standing.dogfood.test.ts › 'the eight consumer-less admin routes refuse the platform admin BY DESIGN, each with its ruled code' ✓ (incl. no-effect role control); live: 403 YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE, sys_user.role stays… |
| identity-auth.admin-lifecycle-operations | 4 | pass [pin+live] | pin: same by-design test ✓ (status+code); live: target protected GET /data/showcase_task 200 → revoke-user-sessions 403 YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS → same protected read 200 |
| identity-auth.admin-lifecycle-operations | 5 | pass [pin+live] | live: owner lc.owner creates showcase_task (201, owner_id=owner); remove-user 403 YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS; sys_user 200 still, owned row 200 owner_id unchanged; pin (run by me): plugin-security security-plugin.test.ts › '[#3023] an engine referent… |
| identity-auth.admin-lifecycle-operations | 6 | pass [pin+live] | pin: admin-platform-admin-standing › 'C6 — the platform admin may impersonate, and the session records the attribution' ✓; live (cookie lane): impersonate 200; get-session = lc.target with session.impersonatedBy=admin id; sys_session.impersonated_by=admin; st… |
| identity-auth.admin-lifecycle-operations | 7 | pass [live] | screenshot after Setup → Users row ⋯ → Impersonate User → Continue: red banner 'You are impersonating lc.target — every action is recorded as them. Started by administrator ‹admin id›.' + 'Stop impersonating' button; in-page get-session = lc.target@example.com |
| identity-auth.admin-lifecycle-operations | 8 | pass [pin] | pin: packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts › POST /admin/create-user … / POST /admin/set-user-password … / POST /admin/import-users … (3 tests) ✓ (log t1log-identity-auth-packages_qa_dogfood.log, file ✓, RC 0, 0 skipped) |
| identity-auth.admin-lifecycle-operations | 9 | pass [pin+live] | pin: admin-route-nonadmin-refusal.dogfood.test.ts › 'no anonymous caller and no plain member ever gets a 2xx from an /admin/ route' ✓ (+4 per-bucket tests, 31 derived routes); live member: ban-user 403 PERMISSION_DENIED, list-users 403 YOU_ARE_NOT_ALLOWED_TO_… |
| identity-auth.session-list-revoke | 0 | pass | A (2 form sign-ins, distinct tokens): GET /auth/list-sessions 200 -› 2 rows both user_id=A (tokens match device-1/device-2); GET /data/sys_session as A 200 -› same 2 ids, revoked_at null; B's own read 200 -› 1 row id oBS8he... (user B); B's id absent from bot… |
| identity-auth.session-list-revoke | 1 | pass | screenshot /_console/apps/com.objectstack.account/sys_session/view/mine: 'My Sessions' tab, 2 rows (IP Address '—', Active Organization, Created At, Expires At), footer '2 records' = API count 2; ip_address empty for local headless clients |
| identity-auth.session-list-revoke | 2 | pass | POST /auth/revoke-session {token:‹device-2, len 32›} 200 {status:true}; device-2 GET /data/sys_session -› 401, get-session -› 401 UNAUTHENTICATED; device-1 GET /data/sys_session -› 200, get-session still usera |
| identity-auth.session-list-revoke | 3 | pass | device-3 signed in (200 before); list-sessions 2 -› POST /auth/revoke-other-sessions from device-1 200 -› list-sessions 1 row (token == device-1); device-3 -› 401; device-1 -› 200; user B unaffected 200 |
| identity-auth.session-list-revoke | 4 | pass | screenshot Setup Sessions all_sessions: 5 live rows with User column (Dev Admin x3, QA userb, QA usera), no tombstones; Revoked tab: 2 'QA usera' rows with Revoked At + Revoke Reason 'user_revoked'; admin GET /data/sys_session: both A tombstones revoked_at se… |
| identity-auth.session-list-revoke | 5 | pass | admin PATCH /data/sys_session/{id} -› 405 OBJECT_API_METHOD_NOT_ALLOWED allowed [get,list,aggregate,export]; admin DELETE -› 405 same; member A PATCH -› 405 same |
| identity-auth.api-key-ui-lifecycle | 0 | pass | UI Generate key -› POST /api/v1/keys 201 data{id,name,prefix,key(len 47, starts with prefix osk_UMSDCWO2),...}; GET /data/sys_api_key/{id} 200 fields id..prefix,revoked (no key), raw absent; owner list raw absent; mine-view page HTML lacks raw, has prefix |
| identity-auth.api-key-ui-lifecycle | 1 | pass | GET /data/sys_session with x-api-key (no cookie/bearer) 200: 2 rows, all user_id=owner, id set == owner's session read (2); admin session read of same route 11 rows across 4 users |
| identity-auth.api-key-ui-lifecycle | 2 | pass | owner PATCH /data/sys_api_key/{id} {revoked:true} 200 record.revoked=true; next x-api-key call 403 body {error:{code:API_KEY_REVOKED,...}} no records; control: unknown key -› 401 UNAUTHENTICATED |
| identity-auth.api-key-ui-lifecycle | 3 | pass | PATCH {revoked:false} 200 revoked=false; x-api-key GET /data/sys_session 200, 2 rows |
| identity-auth.api-key-ui-lifecycle | 4 | pass | 2nd member keyother: list 200 [] excludes owner key, GET by id 404; keyother minted own key; owner list excludes it; admin GET /data/sys_api_key lists both ids with distinct user_ids; Setup API Keys all_keys screenshot shows row with Owner column |
| identity-auth.api-key-ui-lifecycle | 5 | pass | screenshot: dialog 'Your new API key' — 'Copy this now — for your security it will not be shown again.' with key + Copy button; Account API Keys My Keys screenshot: row qa18-key, Prefix osk_UMSDCWO2, no secret |
| identity-auth.self-service-profile-password | 0 | pass | Save Changes -› POST /auth/update-user 200 {status:true}; get-session name='QA Profile Renamed'; after reload #profile-name value same, screenshot card header shows new name |
| identity-auth.self-service-profile-password | 1 | pass | form sign-in; setInputFiles -› POST /storage/upload/presigned 200 (fileId 9584434f...), PUT /storage/_local/raw/‹signed› 200, POST /storage/upload/complete 200 (8551 B image/png), POST /auth/update-user 200; get-session image=/api/v1/storage/files/9584434f...… |
| identity-auth.self-service-profile-password | 2 | pass | profile-avatar-remove-btn -› POST /auth/update-user 200; get-session image null; after reload no ‹img›, screenshot shows 'QP' initials and Upload button |
| identity-auth.self-service-profile-password | 3 | pass | UI change-password -› POST /auth/change-password 200; sign-out 200; sign-in new pw 200; sign-in old pw 401 INVALID_EMAIL_OR_PASSWORD |
| identity-auth.self-service-profile-password | 4 | pass | UI with wrong current -› POST /auth/change-password 400 INVALID_PASSWORD, card shows 'Invalid password'; old pw sign-in still 200, new pw 401 |
| identity-auth.self-service-profile-password | 5 | pass | credential-less user via admin import-users passwordPolicy none; email+pw sign-in before 401; session via admin impersonate-user; Profile card 'Set Local Password', no #current-password (only new/confirm), list-accounts []; POST /auth/set-initial-password 200… |
| identity-auth.org-membership-team-management | 0 | pass | admin form sign-in; Setup nav 'Organization' href=/_console/apps/com.objectstack.setup/sys_organization/record/org_mvf97wck43euyjub = session.activeOrganizationId and get-full-organization id; screenshot: record page 'QA18 Renamed Org' with tabs Members 6 / I… |
| identity-auth.org-membership-team-management | 1 | pass | m1 member: invite-member 403 YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION; update-member-role admin 200, list-members shows m1 admin; m1 invite-member 200; demote to member 200 -› m1 invite-member 403 again; delegated_admin on m3 200 (sys_member r… |
| identity-auth.org-membership-team-management | 2 | pass | update-member-role guest/contributor(position)/showcase_contributor(permission set) -› 400 {code:ROLE_NOT_FOUND}; invite-member guest/contributor -› 400 {message:'ROLE_NOT_FOUND: guest'}; follow-up sys_member 7 rows roles {delegated_admin,member,owner}, m2 st… |
| identity-auth.org-membership-team-management | 3 | pass | get-full-organization name 'Default Organization' -› POST organization/update {name:'QA18 Renamed Org'} 200 -› get-full-organization name 'QA18 Renamed Org' slug default; org record page header shows the new name (nav item label is the static 'Organization') |
| identity-auth.org-membership-team-management | 4 | pass | invited1 fresh session: get-full-organization?organizationId=ORG 200, set-active 200; admin remove-member 200; old session get-session 401 UNAUTHENTICATED; fresh sign-in (activeOrg null): get-full-organization 403 and set-active 403 USER_IS_NOT_A_MEMBER_OF_TH… |
| identity-auth.org-membership-team-management | 5 | pass | create-team 'QA18 Team' 200; add-team-member m1, m2 each 200; re-add m1 returned the same row id (no duplicate); GET /api/v1/data/sys_team_member total 2: (team t6mQ..., m2 userId) and (team t6mQ..., m1 userId) |
| identity-auth.org-membership-team-management | 6 | pass | m2 (get-session=m2, role member): update-member-role x2 403 YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER, remove-member 403 ..._DELETE_THIS_MEMBER, update 403 ..._UPDATE_THIS_ORGANIZATION, create-team 403 ..._CREATE_TEAMS..., add-team-member 403 YOU_ARE_NOT_ALLO… |
| identity-auth.self-service-password-reset | 0 | pass | anon POST /auth/request-password-reset 200 {status:true,message:"If this email exists..."}; sys_email row to "QA Reset ‹qa.reset.member@example.com›", subject "Reset your ObjectStack password", status sent, body carries /api/v1/auth/reset-password/‹token len … |
| identity-auth.self-service-password-reset | 1 | pass | POST /auth/reset-password {token,newPassword} 200 {status:true}; sign-in NEW 200; sign-in OLD 401 INVALID_EMAIL_OR_PASSWORD |
| identity-auth.self-service-password-reset | 2 | pass | second reset same token 400 INVALID_TOKEN; that password 401, current still 200 |
| identity-auth.self-service-password-reset | 3 | pass | garbage 400 INVALID_TOKEN; tampered fresh token 400 INVALID_TOKEN; fresh token with sys_verification.expires_at forced to 2020 -› 400 INVALID_TOKEN; credential unchanged (current pw 200, attempted pw 401) |
| identity-auth.self-service-password-reset | 4 | pass | known and unknown email both 200 with byte-identical body; no sys_email row for the unknown address |
| identity-auth.two-factor-enrollment-reveal | 0 | pass | GET /auth/config features.twoFactor=true; anon POST /two-factor/enable {} -› 400 VALIDATION_ERROR (not 404) |
| identity-auth.two-factor-enrollment-reveal | 1 | pass | enable 200 keys [method,totpURI,backupCodes]; otpauth://totp label "ObjectStack:‹user email›", issuer=ObjectStack, digits=6, period=30, algorithm absent; secret ‹len 52› base32-decodes to 32 bytes |
| identity-auth.two-factor-enrollment-reveal | 2 | pass | backupCodes: string[10], all strings, each 11 chars shape xxxxx-xxxxx (values redacted) |
| identity-auth.two-factor-enrollment-reveal | 3 | pass | meta sys_two_factor.enable_two_factor resultDialog fields totpURI(qrcode)-›string, backupCodes(code-list)-›string[10]; successMessage absent |
| identity-auth.two-factor-enrollment-reveal | 4 | pass | pre-verify: no pw 400 VALIDATION_ERROR, wrong pw 400 INVALID_PASSWORD, right pw 200 SAME secret, anon 401; post-verify: no pw 400, wrong pw 400, right pw 200 SAME secret. Password-gated re-reveal, no ungated path |
| identity-auth.two-factor-enrollment-reveal | 5 | pass | meta sys_user.enable_two_factor resultDialog fields [{totpURI,qrcode},{backupCodes,code-list}] acknowledge "I have saved my backup codes", no successMessage; totpURI-›string, backupCodes-›string[10] |
| identity-auth.two-factor-verify-to-activate | 0 | pass | features.twoFactor=true; enable probe 400 (non-404) |
| identity-auth.two-factor-verify-to-activate | 1 | pass | post-enable pre-verify sign-in 200 with token, no twoFactorRedirect, only better-auth.session_token cookie (no two_factor cookie); session usable (get-session 200); sys_user.two_factor_enabled=0. TOTP_NOT_ENABLED branch cited from lockout pin, not driven |
| identity-auth.two-factor-verify-to-activate | 2 | pass | row read immediately after enable: verified=0 (integer) — healthy reading; note column DB default is '1' (pragma dflt_value), so better-auth writes 0 explicitly |
| identity-auth.two-factor-verify-to-activate | 3 | pass | POST /two-factor/verify-totp (bearer) 200 keys [token,user] + set-auth-token (rotated); row verified=1, sys_user.two_factor_enabled=1, failed_verification_count 0 |
| identity-auth.two-factor-verify-to-activate | 4 | pass | fresh sign-in 200 twoFactorRedirect:true, no token, cookies session_token/session_data/two_factor; get-session with challenge cookies 401 UNAUTHENTICATED; cookie-lane verify-totp 200 with token; that token get-session 200 |
| identity-auth.two-factor-backup-codes | 0 | pass | features.twoFactor=true; verify-to-activate walk completed on this boot (row verified=1, challenge twoFactorRedirect:true) |
| identity-auth.two-factor-backup-codes | 1 | pass | challenge -› POST /two-factor/verify-backup-code via two_factor cookie 200 + token; get-session with it 200 (user matches) |
| identity-auth.two-factor-backup-codes | 2 | pass | same code at fresh challenge 401 INVALID_BACKUP_CODE; different retained code at next challenge 200 + usable session |
| identity-auth.two-factor-backup-codes | 3 | pass | generate-backup-codes 200 {status,backupCodes[10]}, 0 overlap with old; old unspent code at fresh challenge 401 INVALID_BACKUP_CODE; new code 200 + usable session |
| identity-auth.two-factor-backup-codes | 4 | pass | sys_two_factor.backup_codes: 334-char opaque base64/hex-like blob, not JSON-parseable, contains none of the 20 issued plaintext codes (encrypted); secret column 144 chars, not the plaintext secret |
| identity-auth.two-factor-backup-codes | 5 | pass | meta sys_user.generate_backup_codes resultDialog fields [{backupCodes,code-list}], acknowledge, no successMessage, refreshAfter:false; regenerate response.backupCodes -› string[10] |
| identity-auth.two-factor-disable-lifecycle | 0 | pass | features.twoFactor=true; BEFORE sign-in twoFactorRedirect:true + two_factor cookie |
| identity-auth.two-factor-disable-lifecycle | 1 | pass | disable wrong pw 400 INVALID_PASSWORD (no pw 400 VALIDATION_ERROR, anon 401); row still present verified=1, two_factor_enabled=1; fresh sign-in still twoFactorRedirect:true |
| identity-auth.two-factor-disable-lifecycle | 2 | pass | disable 200 {"status":true}; sys_two_factor find for user -› 0 rows; sys_user.two_factor_enabled=0 |
| identity-auth.two-factor-disable-lifecycle | 3 | pass | sign-in 200 with token, no twoFactorRedirect, only session_token cookie; get-session 200 |
| identity-auth.two-factor-disable-lifecycle | 4 | pass | post-disable verify-backup-code: no cookie 401 INVALID_TWO_FACTOR_COOKIE, with session 400 BACKUP_CODES_NOT_ENABLED; re-enable: new secret differs, 0 code overlap; old-secret TOTP 401 INVALID_CODE (session lane) and at new challenge 401; prev-set and original… |
| identity-auth.two-factor-disable-lifecycle | 5 | pass | PUT /api/settings/auth mfa_required=true grace=7 (200, same boot); disable 200 (not refused); stamp null before, lazily stamped 12:46:47Z on first request -› meta/app 200 (in grace); stamp aged to 2026-10-01 -› 403 MFA_REQUIRED; while gated /two-factor/enable… |
| identity-auth.first-run-owner-bootstrap | 0 | pass | anonymous GET /api/v1/auth/bootstrap-status on fresh --no-seed-admin boots: 47233 {hasOwner:false} 200, 47232 {hasOwner:false} 200; after owner creation both answer {hasOwner:true} 200 (47232 read in-page after wizard) |
| identity-auth.first-run-owner-bootstrap | 1 | pass | /auth/config disableSignUp:true audiencePosture invite_only on both boots; 47233 direct POST sign-up/email 200 -› get-session owner-c@qa18.test (positions platform_admin,org_owner); 47232 wizard POST sign-up/email 200 -› in-page get-session owner-b@qa18.test |
| identity-auth.first-run-owner-bootstrap | 2 | pass | wizard org field '测试组织': network POST organization/update {organizationId:org_mvf9ev61..., data:{name:'测试组织'}} (no slug sent) 200; organization/list -› exactly 1 org {name:'测试组织', slug:'default'}; no create call observed |
| identity-auth.first-run-owner-bootstrap | 3 | pass | main-frame document requests GET /_console/ after submit (window marker cleared = new document), landed /_console/home; screenshot: 'Good afternoon, Owner B', Your apps 1 (Showcase), activity 'Owner B Updated Organization 测试组织'; authed /api/v1/meta/app names … |
| identity-auth.first-run-owner-bootstrap | 4 | pass | door 1 (OS_DISABLE_SIGNUP boots): invited address (owner invite-member 200) -› 400 EMAIL_PASSWORD_SIGN_UP_DISABLED on 47233 (invitee-c@) and 47232 (invitee-b@); uninvited -› 403 SELF_REGISTRATION_CLOSED (audience gate, recorded not scored); sys_user stays 1 r… |
| identity-auth.first-run-owner-bootstrap | 5 | pass | post-bootstrap 47232: fresh anon context /_console/setup -› /_console/login?redirect=%2Fsetup (screenshot: Welcome back sign-in card, no wizard, #orgName absent); owner /_console/setup -› /_console/apps/com.objectstack.setup/dashboard/system_overview (screens… |
| identity-auth.self-signup-gate | 0 | pass | stock 47231: data.features.audiencePosture 'invite_only' + emailPassword.disableSignUp false; OS_DISABLE_SIGNUP boots 47232/47233: audiencePosture 'invite_only' + disableSignUp true |
| identity-auth.self-signup-gate | 1 | pass | POST sign-up/email uninvited1@qa18.test -› 403 {code:SELF_REGISTRATION_CLOSED}; admin sys_user full list (3 rows: auditor.demo, phone.demo, admin) and email filter (0) show no row |
| identity-auth.self-signup-gate | 2 | pass | invite-member invited1@qa18.test 200 (pending) -› sign-up/email 200 -› sign-in + get-session identifies invited1@qa18.test with activeOrganizationId set (also m1-m3 same path) |
| identity-auth.self-signup-gate | 3 | pass | 47232 anon /_console/register?redirect=%2Fapps%2Fcom.objectstack.setup -› /_console/login?redirect=%2Fapps%2Fcom.objectstack.setup (screenshot: sign-in card only); login links = Forgot password/Privacy/Terms, no Sign up |
| identity-auth.self-signup-gate | 4 | pass | invited (owner invite 200) invitee-b@ on 47232 and invitee-c@ on 47233 -› 400 EMAIL_PASSWORD_SIGN_UP_DISABLED; uninvited anon-b@/second-anon@ -› 403 SELF_REGISTRATION_CLOSED (audience gate); sys_user 1 row (owner) on each boot |
| identity-auth.implicit-account-linking-ownership | 0 | pass [pin] | pin: packages/plugins/plugin-auth/src/implicit-account-linking.test.ts › implicit link on external sign-in, end to end › refuses an unverified local row, writes no link and leaves the row unverified (asserts error=IMPLICIT_LINK_REFUSED, 0 sys_account, email_v… |
| identity-auth.implicit-account-linking-ownership | 1 | blocked(fixture: local OIDC IdP runtime fixture (scratch app plugin + applyConfigPatch) n… [pin] | pin: packages/plugins/plugin-auth/src/implicit-account-linking.test.ts › 'links a verified local row and signs the caller in' ✓ asserts redirect == callbackURL with no error and 1 sys_account row, but never reads get-session — 'the session is U's' unproven |
| identity-auth.implicit-account-linking-ownership | 2 | pass [pin] | pin: packages/plugins/plugin-auth/src/implicit-account-linking.test.ts › after an unlink, an implicit sign-in is refused; an explicit link is allowed and lifts the refusal (unlink 200, row gone, refused redirect error=IMPLICIT_LINK_REFUSED, 0 rows) ✓ (log t1l… |
| identity-auth.implicit-account-linking-ownership | 3 | pass [pin] | pin: packages/plugins/plugin-auth/src/implicit-account-linking.test.ts › after an unlink … explicit link is allowed and lifts the refusal (+ next implicit sign-in no error) AND › allows an explicit link-social for an unverified user, without marking the email… |
| identity-auth.implicit-account-linking-ownership | 4 | pass [pin] | pin: packages/plugins/plugin-auth/src/implicit-account-linking.test.ts › exempts the platform identity provider from the local-verification precondition / binds the platform exception to the OAuth sign-in method, not the provider id alone / keeps the platform… |
| identity-auth.auth-gate-held-session-raw-host-families | 0 | pass [pin] | packages/services/service-storage/src/storage-routes.auth-gate.pin.test.ts ✓ (42 tests, log t1log-identity-auth-packages_services_service-storage.log); packages/services/service-settings/src/settings-routes.auth-gate.pin.test.ts ✓ (22 tests, log t1log-identit… |
| identity-auth.auth-gate-held-session-raw-host-families | 1 | pass [live] | member created by admin create-user (mustChangePassword) signed in, get-session user.authGate=PASSWORD_RESET_REQUIRED; cookie lane HELD/ADMIN/ANON: GET /api/settings/auth 403 PASSWORD_RESET_REQUIRED/200/401; GET /api/v1/datasources 403/200/401; GET /api/v1/sh… |
| identity-auth.auth-gate-held-session-raw-host-families | 2 | pass [live] | admin create-user (mustChangePassword:true) → first held call error.code PASSWORD_RESET_REQUIRED, message 'You must change your password before continuing.' |
| identity-auth.auth-gate-held-session-raw-host-families | 3 | pass [live] | member m3 changed password, minted key (201), key read 200; org require_mfa=true (PATCH sys_organization) + scratch-DB mfa_required_at 10d ago → x-api-key GET /data/showcase_task 403 MFA_REQUIRED (after ~70s org-MFA cache TTL); then require_mfa=false, setting… |
| identity-auth.api-key-expiry-lifecycle | 0 | pass [pin] | packages/rest/src/rest-api-key-lifecycle.pin.test.ts ✓ (4 tests, log t1log-identity-auth-packages_rest.log); packages/runtime/src/api-key-lifecycle.pin.test.ts ✓ (14 tests, log t1log-identity-auth-packages_runtime.log); packages/qa/dogfood/test/api-key-lifecy… |
| identity-auth.api-key-expiry-lifecycle | 1 | pass [live] | POST /api/v1/keys 201, expires_at 2027-01-17T12:54:25.458Z vs server Date 19 Oct 2026 12:54:25 GMT = 90.0000 days |
| identity-auth.api-key-expiry-lifecycle | 2 | pass [live] | 400 {error.code API_KEY_EXPIRY_TOO_LONG, latest allowed 2027-10-19}; sys_api_key count 1→1 |
| identity-auth.api-key-expiry-lifecycle | 3 | pass [live] | revoked via revoke_api_key action shape (PATCH /data/sys_api_key/{id} {revoked:true} 200): before 200, after 403 API_KEY_REVOKED; key minted with expires_at now+8s: before lapse 200, after 403 API_KEY_EXPIRED (message names the date) |
| identity-auth.cookie-session-renewal-in-process | 0 | pass [pin] | packages/runtime/src/in-process-session-renewal.pin.test.ts ✓ (11 tests, log t1log-identity-auth-packages_runtime.log); packages/rest/src/in-process-session-read.pin.test.ts ✓ (3 tests, log t1log-identity-auth-packages_rest.log) — RC 0, 0 skipped |
| identity-auth.cookie-session-renewal-in-process | 1 | pass [live] | session aged by setting sys_session.expires_at = now+5d (expiresIn 7d, updateAge 1d) in scratch DB → 2026-10-24T12:54:00.429Z; cookie-only GET /data/showcase_task 200 and GET /meta/object/showcase_task 200, each Set-Cookie [] and expires_at unchanged 2026-10-… |
| identity-auth.cookie-session-renewal-in-process | 2 | pass [live] | cookie GET /auth/get-session 200 → Set-Cookie better-auth.session_token=‹redacted›; Max-Age=604800; HttpOnly; SameSite=Lax; expires_at 2026-10-24T12:54:00Z → 2026-10-26T12:54:01Z |
| identity-auth.default-organization-boot-invariant | 0 | pass [pin] | packages/plugins/plugin-auth/src/ensure-default-organization.test.ts ✓ (55 tests, log t1log-identity-auth-packages_plugins_plugin-auth.log); packages/runtime/src/identity-objects-default-organization.pin.test.ts ✓ (4 tests incl. 'holds exactly the Default Org… |
| identity-auth.default-organization-boot-invariant | 1 | pass [live] | sys_organization single row id org_mvf98n1h9g413t70 slug default created_at 12:52:54.840Z; first user admin@objectos.ai (--seed-admin) created_at 12:53:03.508Z (8.7s later); owner sys_member 12:53:03.558Z |
| identity-auth.default-organization-boot-invariant | 2 | pass [live] | sys_member: admin role owner, m2.deforg (admin create-user) role member, both organization_id org_mvf98n1h9g413t70; get-session session.activeOrganizationId org_mvf98n1h9g413t70 for both (create-user response organizationId same) |
| identity-auth.default-organization-boot-invariant | 3 | pass [live] | all 5 seeded BUs (Acme Corporation, East Coast, Field Operations, HQ Finance, West Coast) organization_id=org_mvf98n1h9g413t70, none NULL |
| identity-auth.import-invitation-and-sso-domain-answers | 0 | pass [pin] | packages/plugins/plugin-auth/src/sso-domain-verification-unknown-provider.pin.test.ts ✓ (25 tests), packages/plugins/plugin-auth/src/admin-import-users-invite-link.test.ts ✓ (2 tests, '① through the mounted import, lands on the Console set-password page…'), l… |
| identity-auth.import-invitation-and-sso-domain-answers | 1 | pass [live] | sys_email (dev transport, status sent): import → subject 'Dev Admin invited you to ObjectStack' (= auth.account_invitation subject '{{inviter.name}} invited you to {{appName}}'), body names inviter (admin@objectos.ai), address, 'Set your password (link expire… |
| identity-auth.import-invitation-and-sso-domain-answers | 2 | pass [live] | GET mailed /api/v1/auth/reset-password/‹token›?callbackURL=…/_console/reset-password → 302 /_console/reset-password?token=‹24 chars›, no INVALID_TOKEN; POST /reset-password with it 200; invitee signs in with new password 200 |
| identity-auth.import-invitation-and-sso-domain-answers | 3 | pass [live] | boot OS_SSO_ENABLED=true OS_SSO_DOMAIN_VERIFICATION=true (47242): request-domain-verification 404 RESOURCE_NOT_FOUND 'SSO provider "qa-unknown-provider-xyz" was not found.' (verify-domain same); boot with =false: both 400 DOMAIN_VERIFICATION_DISABLED |
Failures and reproduction rules
None — 0 FAIL.
Checklist-accuracy findings / fixture gaps
For the wave anchor card (not extracted, not edited here).
Checklist accuracy
- identity-auth.auth-method-matrix: discovery documents are ALSO answered under the /api/v1/auth prefix (GET /api/v1/auth/.well-known/openid-configuration 200) — harmless, item only requires the root mount
- identity-auth.invitation-scope-gates: fixtures.requires says 'a multi-org boot'; the stock single-tenancy showcase (multiOrgEnabled:false, features.organization:true) is sufficient — the pin itself boots single
- identity-auth.invitation-scope-gates: pin's first case asserts row count + role, not status pending; status pending was proven live
- identity-auth.admin-lifecycle-operations: UX note (not scored): the impersonation banner names the starting administrator by raw user id ('Started by administrator c9g6…'), not by name/email
- identity-auth.api-key-ui-lifecycle: source[2] says sys_api_key apiMethods ['get','list']; live declaration is ['get','list','update'] (sys-api-key.object.ts:469, userActions.edit + D2 column whitelist) — which is what makes the revoke/restore PATCH answer 200. Citation stale, behavior consistent.
- identity-auth.org-membership-team-management: step 'nav label follows' the rename: nav_organization's label is the static string 'Organization'; what follows the rename is the record page header (and get-full-organization). Suggest rewording to 'the org record page title follows'
- identity-auth.self-service-password-reset: acceptance[0].oracle is "log" but the verify/evidence now read the sys_email row (an api read) — oracle label could be "api".
- identity-auth.two-factor-enrollment-reveal: Clause 0/steps[1] expect the anonymous empty-body probe to answer 400/401; live answers 400 VALIDATION_ERROR (body validation precedes the auth check) — consistent with the clause, just noting the anon probe never reaches 401.
- identity-auth.two-factor-verify-to-activate: fixtures.requires names an in-process ObjectQL system read; an HTTP-booted run can read the same rows from the -d SQLite file directly — worth naming as an accepted alternative rig.
- identity-auth.two-factor-backup-codes: fixtures.requires names an in-process ObjectQL system read; an HTTP-booted run can read the same rows from the -d SQLite file directly — worth naming as an accepted alternative rig.
- identity-auth.two-factor-disable-lifecycle: fixtures.requires names an in-process ObjectQL system read; an HTTP-booted run can read the same rows from the -d SQLite file directly — worth naming as an accepted alternative rig.
- identity-auth.self-signup-gate: step 1 quotes the by-invitation notice as 'Self-registration is not open on this environment…'; the pinned console renders 'Self-registration is not open on this system. Ask an administrator for an invitation.' (data-testid register-by-invitation present, no form) — wording drift only
- identity-auth.auth-gate-held-session-raw-host-families: fixtures say
pnpm dev -- --fresh; canonical os dev boot with a fresh -d DB used - identity-auth.auth-gate-held-session-raw-host-families: MFA hold took ~70s to appear after require_mfa flipped (org-MFA cache TTL) — a runner probing immediately sees 200; poll
- identity-auth.default-organization-boot-invariant: the default-level boot log prints no Default-Organization line, so step 1's 'read the boot log ordering' has nothing to read on os dev; ordering taken from created_at timestamps
Fixture gaps
- identity-auth.auth-method-matrix: clause 0/4: generic-OIDC variant needs the scratch-plugin localhost IdP fixture (not provisioned this run); device authorization needs a custom host; @better-auth/sso half needs a registered provider
- identity-auth.self-service-profile-password: knownGaps says set-initial branch needs an IdP; a passwordless user is provisionable on stock showcase: POST /api/v1/auth/admin/import-users {format:'json',rows:[{email,name}],passwordPolicy:'none'} then POST /api/v1/auth/admin/impersonate-user {userId} for a session (inject session_token cookie + localStorage auth-session-token). Recipe could replace the knownGap.
- identity-auth.self-service-password-reset: Natural expiry (1h window) not waited out; expiry was forced by back-dating sys_verification.expires_at.
- identity-auth.two-factor-disable-lifecycle: The elapsed-stamp branch of clause 5 needs an aged sys_user.mfa_required_at; there is no API to set it, so this run back-dated it in SQLite. mfa_grace_period_days=0 would also do it but gates the (unenrolled) admin too.
- identity-auth.implicit-account-linking-ownership: the live lane needs the local OIDC IdP runtime fixture (QA run · follow-up: 6 previously unrun or published-only items (6/6) · 316be321 + 53021e3a · 2026-10-05 · 4 PASS / 0 PARTIAL / 2 FAIL / 0 BLOCKED / 0 NOT-RUN #21845 recipe: scratch app plugin calling applyConfigPatch({oidcProviders:[…]}) before the first auth request, from a scratch copy of the app); not provisioned in this Tier-1 run — clauses 0/2/3 rest on the pin's end-to-end cases, clause 1's session-identity half is unproven
Handover — items without a verdict
- identity-auth.sso-enforced-first-paint (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.phone-signin-surfaces (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.teams-bu-membership (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.oauth-app-consent-loop (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.linked-accounts-social (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.identity-import-wizard (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.email-verification-loop (P2) — Tier 3 deferred by the 18.0 release-sweep plan
- identity-auth.workspace-org-switch (P2) — Tier 3 deferred by the 18.0 release-sweep plan
Tier 1 evidence — vitest batch (RUNNER rule 6, one command per package, run once for the area)
| package | files | result | wall |
|---|---|---|---|
| packages/qa/dogfood | 7 (7 passed (7)) | 34 passed (34), RC 0 | 207s |
| packages/plugins/plugin-auth | 4 (4 passed (4)) | 105 passed (105), RC 0 | 53s |
| packages/services/service-storage | 1 (1 passed (1)) | 42 passed (42), RC 0 | 21s |
| packages/services/service-settings | 1 (1 passed (1)) | 22 passed (22), RC 0 | 20s |
| packages/plugins/plugin-sharing | 1 (1 passed (1)) | 33 passed (33), RC 0 | 30s |
| packages/plugins/plugin-webhooks | 1 (1 passed (1)) | 8 passed (8), RC 0 | 27s |
| packages/services/service-datasource | 1 (1 passed (1)) | 45 passed (45), RC 0 | 24s |
| packages/cloud-connection | 1 (1 passed (1)) | 12 passed (12), RC 0 | 29s |
| packages/runtime | 5 (5 passed (5)) | 43 passed (43), RC 0 | 45s |
| packages/verify | 1 (1 passed (1)) | 28 passed (28), RC 0 | 64s |
| packages/rest | 3 (3 passed (3)) | 12 passed (12), RC 0 | 22s |
| packages/core | 1 (1 passed (1)) | 19 passed (19), RC 0 | 19s |
| packages/plugins/plugin-email | 1 (1 passed (1)) | 28 passed (28), RC 0 | 14s |
All files: 0 failed, 0 skipped, 0 todo (grep of each log).
Also run live by the Tier 1 runner (not in the area map): packages/plugins/plugin-security [#3023] exemption test — 1 passed (supports admin-lifecycle-operations clause 5).
Observations (not scored, no card)
invite-memberanswersROLE_NOT_FOUNDwith amessageonly and nocodefield, whileupdate-member-rolecarries the code (org-membership-team-management clause 2 passes on status + no-row read-back). Relevant to ADR-0112's assert-the-code rule; left for the anchor card.- After an invited address signs up it is joined to the org, but its
sys_invitationrow stayspending(no item in this area asserts the accept transition on that path). - Impersonation banner names the target by raw user id, not display name (UX note).
- An authorization-adjacent observation on an admin-only path was recorded in the runner session and reported to the maintainer channel; it grants no capability an admin lacks and is not scored. Detail withheld pending maintainer.
Run notes
- Authentication / authorization findings: none (0 FAIL in this area).
- Execution: parallel runner sub-agents (5 for this area), each with its own port, file DB and browser context; browser concurrency kept at 3 or fewer.
- Env time for the whole card (install 48 s, framework build ~13 min incl. one self-inflicted concurrent-build race, console build ~16 min) is reported in the wave hand-back.
Activity
[-]QA run · area:identity-auth (20/28) · 5b14b7f3 · 2026-10-19 · 18 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 8 NOT-RUN[/-][+]QA run · identity-auth (20/28) · 5b14b7f3 · 2026-10-19 · 18 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 8 NOT-RUN[/+]objectstack-fleet commentedon Oct 19, 2026
Parked-on: #24579 · this record's checklist-accuracy findings, fixture gaps, partial coverage gaps and not-run handover (wave anchor §2–§4)
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Closure complete (checklist-test §5): every FAIL in this record has a disposition (the
Extracted:/Parked-on:lines above), and its partial coverage gaps and not-run handover are parked on #24579.Closed by: the 18.0 QA wave dispatch session
session_01CfRDy7SuTfUqaTu2AEH2Hi, on the maintainer's instruction there on 2026-10-19, verbatim 「qa-run的卡片处理完的是不是可以关闭」.Generated by Claude Code
docs(qa): correct eight identity-auth checklist items the 18.0 sweep …