Repository navigation
feat(plugin-auth): a forced password change requires a password different from the current one - #27440
Conversation
…rent from the current one Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
…sword Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
…ge flag Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
…from the temporary one Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0daffe1d5cc46cfea449e1684fc7111be1f91259 && git checkout 0daffe1d5cc46cfea449e1684fc7111be1f91259
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a552e8699255db754446256aabac02c81ebd73a8 44d99fe72416bcac825e75e860e6453deb0a2eff && git checkout -B drift-repro a552e8699255db754446256aabac02c81ebd73a8 && git merge --no-ff 44d99fe72416bcac825e75e860e6453deb0a2eff
node scripts/docs-audit/affected-docs.mjs --json a552e8699255db754446256aabac02c81ebd73a8
|
…ssword is confirmed Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
…tion Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Face: narrowing. The diff touches no Security-hardening ruling: this record names positions and refusals; it carries no reproduction. ① Derived judgmentsThe accept set and the served surface the diff changes, read off the code on this head:
② Semver levelDeclared: The arm is missing. A request every holder of a forced-change flag could send on What is right and stays: ③ Boundary flags
④ Design judgmentOn the narrowing face, against the published spec on
Ablation evidence in the body (forced refusal off, claim skipped, Console check off; each restored to the HEAD blob) is consistent with the pins read here and was taken on Must-fix
Should-fix
Implemented-by: VERDICT: FAIL The refusal covers both completion doors, the claim-first order closes an oracle, the ledger and the published surface do not move, and the Console keeps the server as the authority; the FAIL is the declaration and the body that no longer describes the head (must-fixes 1 and 2). A head that carries the corrected declaration and body and nothing else owes a delta review by this reviewer, not a full round. |
…he changeset declares the narrowing The sys_user must_change_password read in isForcedRotationPending still fails open, but now reports through logDurabilityDegradation, the channel stampPasswordChangedAt reports its write through. The docblock and the before-hook comment say the current-password claim gate sits above the history ring as well as the forced refusal. The changeset declares Clause-② no (narrowing) with the BREAKING banner, FROM / TO and its ADR-0087 disposition. Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk {
"issue": null,
"status": "done",
"branch": "claude/harden-forced-password-change",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27440",
"session": "session_017xRBcVy4VXmkQjn2MGeaWk (subagent of the PM session; parent's id)",
"premise_still_valid": true,
"summary": "Rework of #27440 against contract review 6501172187, one commit 44d99fe72 on top of 2a467e5fc, pushed to the same branch with no force and no merge of origin/main (GitHub reports it mergeable, merge base c5d463417). Must-fix 1: the changeset now declares `Clause-②: no (narrowing)` and keeps `Metadata-protocol: none` and the three `patch` grades. It carries the `**BREAKING** (an accept-set narrowing)` banner with FROM / TO, which names the changed answer: a wrong current password with a recycled new one at history depth > 0 now gets INVALID_PASSWORD, not PASSWORD_REUSE. It has exactly one ADR-0087 marker, `not-required (no-migration-prescription)`. Must-fix 2 and the PR-body half of must-fix 1 are in the staged replacement body /tmp/claude-0/-home-user-objectstack/bdda939c-7424-5594-af38-0e6fe5d7eeed/scratchpad/pr-27440/pr-body.md: the three declaration lines including `Skills-docs: none — REASON` (git grep over skills/ for seven forced-change terms found 0 hits each; control defineStack found 22 files), What changes, the three history-on test cases, and Verification read on 44d99fe72. The PR body was NOT patched; the seat writes it. Should-fix 1: `isForcedRotationPending` still fails open and now reports through `logDurabilityDegradation`, the channel `stampPasswordChangedAt` uses. A new unit case pins this. Should-fix 2: the `assertPasswordNotReused` docblock in auth-manager.ts and the before-hook comment now say the claim gate sits above the history ring as well. PR stays draft. Deviations: the body keeps the create-time session-URL footer, with this run's session named in Verification prose. The failed-flag-read log fires once per affected request, the same as the stamp beside it, not once per boot; recorded in Acceptance notes.",
"tests": "All on 44d99fe72. `pnpm --filter @objectstack/plugin-auth test`: Test Files 203 passed (203), Tests 3852 passed | 10 skipped (3862). Targeted `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/auth-manager.test.ts src/forced-password-change-new-password.test.ts`: 2 files, 313/313. `pnpm --filter @objectstack/plugin-auth build && ... typecheck`: exit 0, check:test-typecheck OK (10 files / 88 errors / 23 pins held). The first typecheck attempt exited 2 only because the examples program reads the package's own unbuilt dist. From ui/: `pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/console/__tests__/RemediationOverlay`: Test Files 3 passed (3), Tests 12 passed (12). Ablation of the new log call via scripts/ablation-replace.mjs: anchor `host.logDurabilityDegradation(` went 1 to 0, blob 93a97d4a2ae2 to 2127a2c857e4. auth-manager.test.ts went 1 failed | 304 passed; the red case was the new failed-flag-read case. Restored blob equals HEAD blob 93a97d4a2ae2 and `git diff HEAD` is empty. The suite imports src directly, so no dist leg was needed.",
"gates": "Local, exit codes captured before any pipe. `pnpm check:adr-0087-registration` exit 0: 1 declared-breaking changeset, [BREAKING+clause-②-narrowing] not-required (no-migration-prescription). `node scripts/check-changeset-no-major.mjs` exit 0 (no major; level axis NOT APPLICABLE locally). With `--base origin/main --event` and a pull_request payload carrying the staged body (GITHUB_EVENT_NAME=pull_request): exit 0, 'LEVEL AXIS: this PR declares clause-② `no (narrowing)` and metadata-protocol `none` ... a non-protocol change at `patch`'. `check:nul-bytes` exit 0. `dispatch-gates.mjs --commands` derived 67 families, all run. 64 read exit 0 on the first pass. Three first hit build-state prerequisites: check:dual-build-cjs-loads exit 3, check:type-check-debt exit 3 (lock queue), and `pnpm --dir ui exec vitest run scripts/` exit 1 on an unbuilt @objectstack/client entry. All three exit 0 after the full package build: debt re-measure under the lock OK; dual-build 112 entry points across 69 packages load; ui/scripts 169 files passed, 2 skipped. `--ran` with exit codes recorded: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). Caveat: the derivation warned the tree is 53 commits behind origin/main and 4 family-defining files changed, so the list is the branch tree's own. CI on 44d99fe72: all 63 check-runs completed, 55 success and 8 skipped (opt-in or path-scheduled), 0 failures. The successes include Lint & Repo Gates, TypeScript Type Check, Test Core (8/8 shards), Build Core, Governed Surface Queue Guard, Lint UI, Type Check UI, Test UI (16/16), Build & E2E UI, Live E2E UI, Check Changeset and Dogfood Regression Gate. Check Changeset ran against the current, unpatched body; it re-reads on the seat's body edit.",
"dogfood_battery": "44d99fe72 · closure built (`pnpm --filter '@objectstack/dogfood^...' build`, BUILD lock VERDICT command-exit 0) · `pnpm --filter @objectstack/dogfood test` under the lock: VERDICT command-exit 0 · Test Files 348 passed | 3 skipped (351) · Tests 2806 passed | 20 skipped (2826) · no failures",
"mcp_calls": "0",
"api_writes": "1 — POST issues/27440/comments (this os-dev-report, via scripts/pm/post-stamped.mjs); plus 1 git push (not REST). No PR body PATCH, no label write, no ready flip.",
"open_questions": [],
"out_of_scope_findings": []
} |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Face: narrowing, now declared. Delta review of record 6501172187 (FAIL on the declaration and the body, head Security-hardening ruling: this record names positions and refusals; it carries no reproduction. ① Derived judgments
② Semver levelDeclared on the changeset and the PR body, both line-initial: ③ Boundary flags
④ Design judgmentUnchanged from 6501172187 on the narrowing itself: the change makes the Implemented-by: VERDICT: PASS Delta review complete: the declaration is corrected in the #22646 shape, the body describes the head, the fail-open read is reported through the existing durability channel and pinned, and the two comments state the gate's real extent. The seat reads the second |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk ACCEPT → head 维护者速读
席位意见:收。增量契约复核在本 head 上 PASS(6503923468,判的是 6501172187 之后的增量);dogfood 全套 348 个文件、0 失败。 席位核对
Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no
skills/**file teaches the forced password change,passwordHistoryCountor the change-password door (git grep -ioverskills/formust_change_password,mustChangePassword,passwordHistoryCount,change-password,reset-password,PASSWORD_REUSE,PASSWORD_RESET_REQUIRED: 0 hits each; controldefineStack: 22 files); the hand-written authentication guide says the user is held "until they complete a password change", which stays true.维护者速读
/change-password与/reset-password两个能清除该标记的入口都拒绝相同密码(400PASSWORD_REUSE),控制台强制改密页用用户语言提示同一句话。另外/change-password上任何"密码重复"的拒绝(强制改密的和passwordHistoryCount历史环的)都只在当前密码验证通过之后才给出,当前密码错误一律回INVALID_PASSWORD。passwordHistoryCount的设置。这是一次接受集收窄(BREAKING,按 patch 发布)。passwordHistoryCount大于 0 时,当前密码错误且新密码命中历史的请求,答复从PASSWORD_REUSE变为INVALID_PASSWORD。读标记失败时放行并写一条降级日志。回滚即 revert 本 PR。packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts(assertPasswordNotReused里"先验当前密码再判新密码"那段与isForcedRotationPending的降级日志)、auth-manager-before-hook.ts(传入 currentPassword 的那几行)、.changeset/forced-password-change-new-password.md(声明行与 FROM/TO)。pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/forced-password-change-new-password.test.ts,预期 8/8 通过。What changes
patch. The metadata protocol does not move (Metadata-protocol: none), so all three packages staypatchand the changeset carries the**BREAKING**banner, a FROM / TO paragraph and its ADR-0087 disposition,not-required (no-migration-prescription): no metadata key, spelling, export or stored shape moves; what narrows is a credential door's accept set.sys_user.must_change_password) is outstanding,assertPasswordNotReusedrefuses a new password that verifies against the CURRENT hash with400 PASSWORD_REUSEand the message "Choose a new password that is different from your temporary password.", whateverpasswordHistoryCountis (the default 0 included). The flag is cleared only bystampPasswordChangedAt, which the after-hook runs on a successful/change-passwordor/reset-password; both doors pass through this check in the before-hook first, so every user-facing door that clears the flag is covered. Sign-up (a new user) and the administrator's own Set Password with the requirement switched off are the other writers of the flag and are not user completion doors.passwordHistoryCountring. On/change-passwordbetter-auth verifiescurrentPasswordonly after the before-hook, soassertPasswordNotReusedfirst verifies the request'scurrentPasswordagainst the current hash, and judges the new password (forced refusal, then the history ring) only when that holds. A wrong current password is left to the endpoint's ownINVALID_PASSWORD, whatever the new password is. The changed answer: withpasswordHistoryCountabove 0, a wrong current password with a recycled new one answeredPASSWORD_REUSEand now answersINVALID_PASSWORD; a confirmed request reusing the current or a recent password still answersPASSWORD_REUSE./reset-passwordcarries no such claim (its token is the authority) and is judged directly.PASSWORD_REUSE. TO: refused400 PASSWORD_REUSEuntil a different password is chosen; a wrong current password answersINVALID_PASSWORDwhatever the new one is.isForcedRotationPendingstill fails open (treated as not set) on asys_userlookup error, as every read inassertPasswordNotReuseddoes, and now reports that the forced-change check did not run throughlogDurabilityDegradation, the channelstampPasswordChangedAtreports its write through. TheassertPasswordNotReuseddocblock inauth-manager.tsand the before-hook comment say the claim gate sits above the history ring as well as the forced refusal.passwordHistoryCountexactly as before (depth 0 still accepts the same password). The code is the existingPASSWORD_REUSE(the condition is reuse of the current password), so the error-code ledger andpackages/specdo not move. Cost: onesys_userflag read per change or reset, and on/change-passwordone extra password verification.PASSWORD_RESET_REQUIRED) showsauth.remediation.password.sameAsTemporaryin the user's language when the new password equals the one typed as current, and sends no request. The key is added to all ten locale packs (the key-parity test requires it). An expiry (PASSWORD_EXPIRED) screen is unchanged; the server judges reuse there..changeset/forced-password-change-new-password.md:@objectstack/plugin-auth,@object-ui/app-shell,@object-ui/i18npatch, with the two declaration lines, the banner, FROM / TO and the ADR-0087 marker.Tests
packages/plugins/plugin-auth/src/forced-password-change-new-password.test.ts, end to end throughAuthManager.handleRequestover a realObjectQLengine (better-sqlite3, the plugin's own manifest, so theinternal: truestrip ofsys_account.passwordis live). Forced change, history off: refused on/change-passwordwith the flag set (status, code, message; flag still set; current password still signs in); a different password completes and clears the flag; a wrong current-password claim getsINVALID_PASSWORD, notPASSWORD_REUSE;/reset-passwordrefused then completed; control: no flag and depth 0 still accepts the same password.PASSWORD_REUSE; a confirmed current password reused as the new one answersPASSWORD_REUSE; a wrong current password answers the endpoint's ownINVALID_PASSWORD, whatever the new password is.auth-manager.test.ts: the depth-0 no-op case pins that only thesys_userflag is read andsys_accountis not; a new case pins that a failedsys_userflag read fails open (depth 0 accepts) and reports exactly once through the logger with{ object: 'sys_user', fields: ['must_change_password'] }.ui/packages/app-shell/src/console/__tests__/RemediationOverlay.forcedResetNewPassword.test.tsx: en and zh text shown and no request sent; a different password is sent; an expiry screen sends the same password on.Acceptance notes
assertPasswordNotReusedalready does; the failure is now logged at the durability channel's level (warn, the one levelAuthManagerOptions.loggerguarantees), once per affected request.Verification
Every reading below is on head
44d99fe72(this PR's head;origin/mainnot merged again: GitHub reports the branch mergeable, merge basec5d463417), taken in the rework run ofsession_017xRBcVy4VXmkQjn2MGeaWk. The readings an earlier body cited onf1c03b281are superseded by these.Package tests and typecheck
pnpm --filter @objectstack/plugin-auth test: Test Files 203 passed (203), Tests 3852 passed, 10 skipped (3862).pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/auth-manager.test.ts src/forced-password-change-new-password.test.ts: Test Files 2 passed (2), Tests 313 passed (313).pnpm --filter @objectstack/plugin-auth build && pnpm --filter @objectstack/plugin-auth typecheck: exit 0;check:test-typecheck: OK(10 files / 88 errors / 23 pinned signatures held, unchanged).ui/:pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/console/__tests__/RemediationOverlay: Test Files 3 passed (3), Tests 12 passed (12); the forced-reset file is one of the three.pnpm --filter '@objectstack/dogfood^...' build, then under the verify lockpnpm --filter @objectstack/dogfood test(no shard, no path):VERDICT command-exit 0, Test Files 348 passed, 3 skipped (351), Tests 2806 passed, 20 skipped (2826).Ablation of the new log call (landed by `scripts/ablation-replace.mjs`, restored to the HEAD blob, `git diff HEAD` empty)
host.logDurabilityDegradation(replaced with a no-op call (anchor 1 to 0, blob93a97d4a2ae2to2127a2c857e4):auth-manager.test.ts1 failed, 304 passed, the one red being the new failed-flag-read case. Restored: blob after restore equals the HEAD blob93a97d4a2ae2.f1c03b281and are not re-run here; those lines are unchanged on this head.Declaration gates
pnpm check:adr-0087-registration: exit 0; "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition",.changeset/forced-password-change-new-password.md [BREAKING+clause-②-narrowing] not-required (no-migration-prescription).node scripts/check-changeset-no-major.mjs: exit 0; "This diff introduces nomajorbump"; the level axis reads NOT APPLICABLE locally (no payload).node scripts/check-changeset-no-major.mjs --event EVENT_JSON, with apull_requestpayload whose body is this body (GITHUB_EVENT_NAME=pull_request,--base origin/main): exit 0; "LEVEL AXIS: this PR declares clause-②no (narrowing)and metadata-protocolnone, and it introduces no changeset entry gradedminoror above and moves no metadata-protocol baseline".Derived gate families
node scripts/pm/dispatch-gates.mjs --commandsat44d99fe72: 67 commands, each run locally with its exit code captured before any pipe. 64 exit 0 on the first pass; three first read as build-state prerequisites (exit 3PREREQUISITE NOT METforcheck:dual-build-cjs-loadsandcheck:type-check-debt, andpnpm --dir ui exec vitest run scripts/red only on an unbuilt@objectstack/cliententry), and each exits 0 once re-run after the full package build:check:type-check-debtunder the verify lock (re-measure OK, 0 raw errors),check:dual-build-cjs-loads(112 entry points across 69 packages load),ui/scripts(Test Files 169 passed, 2 skipped; Tests 4901 passed, 2 skipped).--ranreconciliation with exit codes recorded: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. The derivation warned that the tree is 53 commits behindorigin/mainand four family-defining files changed across that range, so its list is the branch tree's own; repo-wide scans (pnpm lint, the fullcheck:*battery) are left to CI.Generated by Claude Code