Skip to content

fix(auth): treat PASSWORD_RESET_REQUIRED as a remediation gate (objectui#12365) - #12368

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12365-password-reset-required-gate
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12365-password-reset-required-gate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12365
Clause-②: yes

What

The backend answers an administrator-forced password change with 403 PASSWORD_RESET_REQUIRED, its own code, distinct from PASSWORD_EXPIRED (objectstack-ai/objectstack#23287 ruling C′, carried by objectstack-ai/objectstack#23440). Before this PR the client ignored it, so a force-reset user would get raw 403s and no overlay.

  • packages/auth/src/auth-gate-events.ts: detectAuthGate accepts PASSWORD_RESET_REQUIRED beside MFA_REQUIRED and PASSWORD_EXPIRED. The three codes now sit in one module-private set. AuthGateInfo.code names the new literal.
  • packages/app-shell/src/console/RemediationOverlay.tsx: the overlay routes PASSWORD_RESET_REQUIRED to the change-password form, the same form PASSWORD_EXPIRED gets (renamed ExpiredPasswordForm to ChangePasswordForm).
  • Copy: the form's title was "Your password has expired". That is false for a forced reset, and the backend always sends a message, so the title is what the user reads first. A forced reset now gets auth.remediation.password.resetTitle ("You must change your password"), added to all ten built-in packs. PASSWORD_EXPIRED keeps its title. The fallback message ("Please set a new password to continue.") is true for both and is unchanged.

Harmless before the backend emits the code: nothing else raises it.

Spec derivation (mechanism assumption A2): measured, hand-written literal

The installed @objectstack/spec 18.0.0-next.0 has no AuthGateCodeSchema and no PASSWORD_RESET_REQUIRED anywhere in its dist. objectstack origin/main does not have them either: git grep for AuthGateCodeSchema or PASSWORD_RESET_REQUIRED under packages returns nothing. They exist only on the branch of objectstack-ai/objectstack#23440 (packages/spec/src/kernel/execution-context.zod.ts: AuthGateCodeSchema = z.enum(['PASSWORD_EXPIRED', 'PASSWORD_RESET_REQUIRED', 'MFA_REQUIRED'])). So the literal is hand-written, and a comment beside the set says to derive it from the spec export once the spec this package builds against has it. The hand-written set has the same three members as that enum.

Published surface (Clause-②)

  • Behaviour widens: createAuthenticatedFetch now emits the gate for one more code, so AuthProvider sets remediationRequired for it.
  • Types: no type reachable through the package exports changes. detectAuthGate and AuthGateInfo are not exported from @object-ui/auth's entry (src/index.ts has no auth-gate-events export). remediationRequired stays typed { code: string; message: string }. The only type text that changes is AuthGateInfo.code in dist/auth-gate-events.d.ts. That file ships under files: ["dist"], but no exports path reaches it.
  • Changeset .changeset/12365-password-reset-required-gate.md: minor for @object-ui/auth, @object-ui/app-shell and @object-ui/i18n (new pack key).

Tests

New pins (on HEAD edc7f7a; 7af6488 only adds the changeset):

  • packages/auth/src/__tests__/authGatePasswordResetRequired-12365.test.tsx: a 403 PASSWORD_RESET_REQUIRED response through createAuthenticatedFetch raises the event with the server's code and message. detectAuthGate accepts all three codes, and refuses PERMISSION_DENIED and a non-403.
  • packages/app-shell/src/console/__tests__/RemediationOverlay.passwordResetRequired-12365.test.tsx: for PASSWORD_RESET_REQUIRED the overlay shows the change-password form (current, new and confirm fields) under the reset title, not the expiry title and not the MFA form. PASSWORD_EXPIRED keeps the expiry title.

pnpm exec vitest run on the two pins plus authRemediation-locale-parity.test.ts and createAuthenticatedFetch.test.tsx: Test Files 4 passed (4), Tests 25 passed (25).

Ablation (one-time, on HEAD 7af6488; mutated with ablation-replace.mjs, which checks the anchor count on disk and restores to HEAD): both test files resolve @object-ui/* to src through the root vitest aliases, so no build sits between the mutation and the run.

  • Removing PASSWORD_RESET_REQUIRED from the accepted set: both auth pins go red (expected [] to deeply equal, expected null to deeply equal). Restored: the blob equals HEAD and git diff HEAD is empty.
  • Routing only PASSWORD_EXPIRED to the form: the forced-reset overlay pin goes red and the expiry pin stays green. Restored the same way.

Gates (all run on HEAD 7af6488, every exit code recorded):

  • After turbo run build --filter='@object-ui/app-shell^...' exited 0: type-check exits 0 for @object-ui/auth, @object-ui/i18n and @object-ui/app-shell.
  • These checks exit 0: check:phantom-deps, check:self-import, check:unreferenced-sources, check:doc-example-readers, check:handler-key-reads, check:side-effects-array, check:element-data-source-declaration, check:spec-symbols, check:action-forward-parity, check:designer-field-key-parity, check:icon-record-names, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check-changeset-fixed.mjs and check-changeset-presence.mjs.
  • check:spec-floors exits 1 on main for an unrelated reason (objectui#12334): "The scan collapsed: 0 package(s) declare @objectstack/spec in a consumer-facing field". Recorded, not fixed here.
  • Lint, narrowed: eslint --no-inline-config --format json over the 14 changed .ts/.tsx files reports 14 files, 0 errors and 0 warnings. eslint.config.js sets no parserOptions.project and no projectService, so the linting is not type-aware and this diff cannot change the verdict on any file it does not touch. The full pnpm lint is left to CI.

Acceptance notes

  • The card and the dispatch call detectAuthGate "published". Measured: it is shipped in dist but not exported from the package entry. The published face that widens is the behaviour of createAuthenticatedFetch and AuthProvider.

Generated by Claude Code

…tui#12365)

The backend answers an administrator-forced password change with
403 PASSWORD_RESET_REQUIRED (objectstack#23287 ruling C'). detectAuthGate
now recognises it, and RemediationOverlay routes it to the change-password
form under its own title, since "your password has expired" is false for a
forced reset.

Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 72 chunks) 3054.8 KB 3119.4 KB
Main entry chunk (gzip) 73.8 KB 350 KB
Entry file index-dIcBcwmW.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 20.69KB 7.62KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 592.14KB 142.83KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.41KB 69.48KB
fields (index.js) 283.00KB 72.12KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 143.63KB 38.79KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 259.78KB 68.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 178.88KB 46.47KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 258.97KB 72.23KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 123.35KB 31.60KB
plugin-map (index.js) 27.14KB 8.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.99KB 23.23KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7af64885822a1b5d399d5f1c5a3a261b4511f536
Local-runs: none

Inputs read: card #12365 body, the triage grade (6166805143), the claim (6166921786), the os-dev-report (6167481283); PR #12368 body, its 15-file list and net diff; the head's 33 check-runs (filter=latest: every one success or skipped, the skips are the coverage matrix and dependabot); package.json and src/index.ts of @object-ui/auth, @object-ui/app-shell, @object-ui/i18n at head and at base ccd7e94 (byte-identical at both refs for all three); createAuthenticatedFetch.ts, AuthProvider.tsx and the i18n locales/index.ts and en.ts at head; objectstack origin/main (e4ec6ec) and the objectstack#23440 branch head 5b59be6 for the spec enum.

① Derived judgments

  • Accept set of detectAuthGate: MFA_REQUIRED | PASSWORD_EXPIRED becomes MFA_REQUIRED | PASSWORD_EXPIRED | PASSWORD_RESET_REQUIRED. Right. The diff replaces the two-literal || test with a module-private AUTH_GATE_CODES set of exactly those three strings, guarded by typeof code === 'string'; the 403-only and error.code shape checks are untouched. The objectstack#23440 branch's AuthGateCodeSchema = z.enum(['PASSWORD_EXPIRED', 'PASSWORD_RESET_REQUIRED', 'MFA_REQUIRED']) has the same three members, so the set equals the producer's vocabulary.
  • detectAuthGate and AuthGateInfo are NOT on the published face. Verified, the dev's measurement holds. @object-ui/auth exports is the single "." entry (./dist/index.js / ./dist/index.d.ts) at head and base; src/index.ts has no auth-gate-events export at either ref. dist/auth-gate-events.d.ts ships under files: ["dist"] but an exports map blocks deep specifiers, so no consumer reaches it. The claim's sentence "the published detectAuthGate" was wrong on mechanism; the dev corrected it in the PR body and the report. The yes it carried still stands, by the next item.
  • The widening that IS published is behavioural: createAuthenticatedFetch (exported) now emits the gate for one more 403 code, so AuthProvider.remediationRequired (exported through useAuth) is set for it. Right. createAuthenticatedFetch.ts calls detectAuthGate on every isApiCall && status === 403 response and emits; AuthProvider.tsx subscribes and sets remediationRequired (prev ?? gate). Its type stays { code: string; message: string } | null, so a consumer's switch over code compiles unchanged and sees one more runtime value.
  • AuthGateInfo.code literal union gains 'PASSWORD_RESET_REQUIRED'. Correct and type-inert: the union already ends in | string, so it collapses to string before and after; only documentation changes, and the type is unreachable anyway.
  • RemediationOverlay (exported from @object-ui/app-shell's entry, unchanged export line) routes PASSWORD_RESET_REQUIRED to the change-password form, the same branch PASSWORD_EXPIRED takes; the MFA form stays the else. Right. ExpiredPasswordForm renamed to ChangePasswordForm with a forcedReset prop is module-private, not exported, so no public symbol moves.
  • Copy: a forced reset shows auth.remediation.password.resetTitle ("You must change your password"); PASSWORD_EXPIRED keeps auth.remediation.password.title; the fallback message is shared and unchanged. Right, and the A3 reasoning is sound: "Your password has expired" is false for an administrator-forced change.
  • @object-ui/i18n: one new key under auth.remediation.password in all ten packs. Right on both counts I checked: TranslationKeys = typeof en is published from the entry, so the type gains one property (additive); the ten-pack parity and drift gates (check:i18n-keys, check:i18n-drift, check:i18n-designer-parity) are inside Repo Gates, green on this head.
  • The hand-written literal. Judged acceptable, not a second-source defect, on three facts: (a) the installed spec ^18.0.0-next.0 carries neither AuthGateCodeSchema nor the code, and objectstack origin/main at e4ec6ec has neither under packages (I re-ran the dev's git grep: zero hits); the enum exists only on the #23440 branch, so there is nothing to derive from in a package that builds against the published spec. (b) The two codes already accepted at base were already hand-written literals in this file, so the PR introduces no new class of second source, it consolidates the existing one into one named set. (c) The set carries a comment naming the spec symbol to derive from. What is missing is a gate, not a derivation: nothing will fail when the spec bump lands and the set is still hand-written. See ③.
  • Pins. Both new tests exercise the published face (a real Response through createAuthenticatedFetch, the overlay rendered under I18nProvider), and the dev's two ablations each turned the right pin red. Test (shard 1..12/12), Test (dist pins) and Type Check are green on this head.

② Semver level

  • Changeset .changeset/12365-password-reset-required-gate.md: minor for @object-ui/auth, @object-ui/app-shell, @object-ui/i18n. Right. Each is an additive widening of a published face (one more accepted backend code on the fetch wrapper's behaviour; one more routed code on the overlay; one more key on the pack type and data). Nothing is removed or renamed on any exports path, and no package.json contract field moves, so nothing is breaking; under objectui AGENTS.md §9 even a breaking change would be minor, never major, and Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check are green on the head.
  • Clause-②: yes, as the claim and the PR body say. The reason the claim gave (a published detectAuthGate) was the wrong mechanism; the published faces that widen are createAuthenticatedFetch / AuthProvider.remediationRequired and the @object-ui/i18n pack. The verdict on the clause does not change.

③ Boundary flags

  • Deviation (file surface): the ten locale packs plus the changeset, outside the claim's two files. Answered: the dispatch put A3 (the title copy) in scope and a new key must land in all ten packs or the parity gates fail; the changeset is mandatory for any src/ change in the fixed group. No breach.
  • Deviation: ExpiredPasswordForm renamed ChangePasswordForm. Answered: module-private, no export, no consumer.
  • check:spec-floors exit 1. Answered: pre-existing on main (objectui#12334, "0 package(s) declare @objectstack/spec in a consumer-facing field"); Repo Gates is green on this head, so CI does not read it as this PR's.
  • Out-of-scope finding: AUTH_GATE_CODES should be derived from AuthGateCodeSchema once the spec exports it, "noted, not filed". Escalated: the carrier is the domain:devx hop that bumps @objectstack/spec past objectstack#23440's landing, and today only a code comment names the debt, no gate and no card. Recommend the seat attach it to objectstack#23287's landing order (the same order that holds #23440 until .objectui-sha carries this fix) or file it on the objectui side, so the two-way dependency is written down somewhere a reader opens.
  • Order dependency. objectstack#23440 is still open (head 5b59be6); if its enum spelling changes before merge, this set drifts silently. The card names the literal, so this PR is right as filed; the previous flag covers the rest.
  • Open questions: none in the report, none raised here.

Implemented-by: claude/issue-12365-password-reset-required-gate
Reviewed-by: session_01XVp19E1MYUTFM3dnkwz8gB

VERDICT: PASS


Generated by Claude Code

1 similar comment
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7af64885822a1b5d399d5f1c5a3a261b4511f536
Local-runs: none

Inputs read: card #12365 body, the triage grade (6166805143), the claim (6166921786), the os-dev-report (6167481283); PR #12368 body, its 15-file list and net diff; the head's 33 check-runs (filter=latest: every one success or skipped, the skips are the coverage matrix and dependabot); package.json and src/index.ts of @object-ui/auth, @object-ui/app-shell, @object-ui/i18n at head and at base ccd7e94 (byte-identical at both refs for all three); createAuthenticatedFetch.ts, AuthProvider.tsx and the i18n locales/index.ts and en.ts at head; objectstack origin/main (e4ec6ec) and the objectstack#23440 branch head 5b59be6 for the spec enum.

① Derived judgments

  • Accept set of detectAuthGate: MFA_REQUIRED | PASSWORD_EXPIRED becomes MFA_REQUIRED | PASSWORD_EXPIRED | PASSWORD_RESET_REQUIRED. Right. The diff replaces the two-literal || test with a module-private AUTH_GATE_CODES set of exactly those three strings, guarded by typeof code === 'string'; the 403-only and error.code shape checks are untouched. The objectstack#23440 branch's AuthGateCodeSchema = z.enum(['PASSWORD_EXPIRED', 'PASSWORD_RESET_REQUIRED', 'MFA_REQUIRED']) has the same three members, so the set equals the producer's vocabulary.
  • detectAuthGate and AuthGateInfo are NOT on the published face. Verified, the dev's measurement holds. @object-ui/auth exports is the single "." entry (./dist/index.js / ./dist/index.d.ts) at head and base; src/index.ts has no auth-gate-events export at either ref. dist/auth-gate-events.d.ts ships under files: ["dist"] but an exports map blocks deep specifiers, so no consumer reaches it. The claim's sentence "the published detectAuthGate" was wrong on mechanism; the dev corrected it in the PR body and the report. The yes it carried still stands, by the next item.
  • The widening that IS published is behavioural: createAuthenticatedFetch (exported) now emits the gate for one more 403 code, so AuthProvider.remediationRequired (exported through useAuth) is set for it. Right. createAuthenticatedFetch.ts calls detectAuthGate on every isApiCall && status === 403 response and emits; AuthProvider.tsx subscribes and sets remediationRequired (prev ?? gate). Its type stays { code: string; message: string } | null, so a consumer's switch over code compiles unchanged and sees one more runtime value.
  • AuthGateInfo.code literal union gains 'PASSWORD_RESET_REQUIRED'. Correct and type-inert: the union already ends in | string, so it collapses to string before and after; only documentation changes, and the type is unreachable anyway.
  • RemediationOverlay (exported from @object-ui/app-shell's entry, unchanged export line) routes PASSWORD_RESET_REQUIRED to the change-password form, the same branch PASSWORD_EXPIRED takes; the MFA form stays the else. Right. ExpiredPasswordForm renamed to ChangePasswordForm with a forcedReset prop is module-private, not exported, so no public symbol moves.
  • Copy: a forced reset shows auth.remediation.password.resetTitle ("You must change your password"); PASSWORD_EXPIRED keeps auth.remediation.password.title; the fallback message is shared and unchanged. Right, and the A3 reasoning is sound: "Your password has expired" is false for an administrator-forced change.
  • @object-ui/i18n: one new key under auth.remediation.password in all ten packs. Right on both counts I checked: TranslationKeys = typeof en is published from the entry, so the type gains one property (additive); the ten-pack parity and drift gates (check:i18n-keys, check:i18n-drift, check:i18n-designer-parity) are inside Repo Gates, green on this head.
  • The hand-written literal. Judged acceptable, not a second-source defect, on three facts: (a) the installed spec ^18.0.0-next.0 carries neither AuthGateCodeSchema nor the code, and objectstack origin/main at e4ec6ec has neither under packages (I re-ran the dev's git grep: zero hits); the enum exists only on the #23440 branch, so there is nothing to derive from in a package that builds against the published spec. (b) The two codes already accepted at base were already hand-written literals in this file, so the PR introduces no new class of second source, it consolidates the existing one into one named set. (c) The set carries a comment naming the spec symbol to derive from. What is missing is a gate, not a derivation: nothing will fail when the spec bump lands and the set is still hand-written. See ③.
  • Pins. Both new tests exercise the published face (a real Response through createAuthenticatedFetch, the overlay rendered under I18nProvider), and the dev's two ablations each turned the right pin red. Test (shard 1..12/12), Test (dist pins) and Type Check are green on this head.

② Semver level

  • Changeset .changeset/12365-password-reset-required-gate.md: minor for @object-ui/auth, @object-ui/app-shell, @object-ui/i18n. Right. Each is an additive widening of a published face (one more accepted backend code on the fetch wrapper's behaviour; one more routed code on the overlay; one more key on the pack type and data). Nothing is removed or renamed on any exports path, and no package.json contract field moves, so nothing is breaking; under objectui AGENTS.md §9 even a breaking change would be minor, never major, and Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check are green on the head.
  • Clause-②: yes, as the claim and the PR body say. The reason the claim gave (a published detectAuthGate) was the wrong mechanism; the published faces that widen are createAuthenticatedFetch / AuthProvider.remediationRequired and the @object-ui/i18n pack. The verdict on the clause does not change.

③ Boundary flags

  • Deviation (file surface): the ten locale packs plus the changeset, outside the claim's two files. Answered: the dispatch put A3 (the title copy) in scope and a new key must land in all ten packs or the parity gates fail; the changeset is mandatory for any src/ change in the fixed group. No breach.
  • Deviation: ExpiredPasswordForm renamed ChangePasswordForm. Answered: module-private, no export, no consumer.
  • check:spec-floors exit 1. Answered: pre-existing on main (objectui#12334, "0 package(s) declare @objectstack/spec in a consumer-facing field"); Repo Gates is green on this head, so CI does not read it as this PR's.
  • Out-of-scope finding: AUTH_GATE_CODES should be derived from AuthGateCodeSchema once the spec exports it, "noted, not filed". Escalated: the carrier is the domain:devx hop that bumps @objectstack/spec past objectstack#23440's landing, and today only a code comment names the debt, no gate and no card. Recommend the seat attach it to objectstack#23287's landing order (the same order that holds #23440 until .objectui-sha carries this fix) or file it on the objectui side, so the two-way dependency is written down somewhere a reader opens.
  • Order dependency. objectstack#23440 is still open (head 5b59be6); if its enum spelling changes before merge, this set drifts silently. The card names the literal, so this PR is right as filed; the previous flag covers the rest.
  • Open questions: none in the report, none raised here.

Implemented-by: claude/issue-12365-password-reset-required-gate
Reviewed-by: session_01XVp19E1MYUTFM3dnkwz8gB

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 11:58
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 11:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 37db09f Oct 14, 2026
34 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12365-password-reset-required-gate branch October 14, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants