Repository navigation
fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable - #30448
objectstack-fleet[bot] wants to merge 5 commits into
Conversation
…ored value beneath the pin An env override in force refused every write, clears included, so the documented allowed-IP recovery (OS_AUTH_ALLOWED_IP_RANGES set empty) could not clear the stored list and removing the variable locked the caller out again. A clear (null/undefined) now goes through the pin; every other write stays 409 SETTINGS_LOCKED. The refusal copy names the full recovery. Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i Co-authored-by: Claude <noreply@anthropic.com>
…d to end Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i Co-authored-by: Claude <noreply@anthropic.com>
…very; changeset Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i Co-authored-by: Claude <noreply@anthropic.com>
# Conflicts: # packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 26 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 716531ac25b81ac59bc4dbcc20cf860c295d54c8 && git checkout 716531ac25b81ac59bc4dbcc20cf860c295d54c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0892a94d8e190b7547408d1a37e49ba087ddd635 173cc44ee855da37932e879685f181625e6102da && git checkout -B drift-repro 0892a94d8e190b7547408d1a37e49ba087ddd635 && git merge --no-ff 173cc44ee855da37932e879685f181625e6102da
node scripts/docs-audit/affected-docs.mjs --json 0892a94d8e190b7547408d1a37e49ba087ddd635
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6CZjAtpVuoYZBzB9n5i
Fixes #30385
Clause-②: yes (widening) —
SettingsService.setMany/set(andPUT /api/settings/:namespace) accept a clear (null/undefined) of a key anOS_*override in force pins, where every write to such a key was refused409 SETTINGS_LOCKED; every other write is still refused. No export, type or error code changes.Metadata-protocol: none
维护者速读
OS_AUTH_ALLOWED_IP_RANGES)现在可以“清除”:清除只删掉它下面保存的旧值,当前生效的仍是环境变量;其他任何写入照旧 409。IP 白名单的拒绝提示改写成完整的四步救援。packages/services/service-settings/src/settings-service.ts的setMany预检里 env 锁那一处分支;packages/plugins/plugin-auth/src/allowed-ip-ranges.ts四种语言的恢复句;packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts末尾的端到端用例。pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts,预期 1 个文件、10 条全绿(最后一条是 env 置空 → 清除 → 去掉变量重启后仍能进)。What changes
The rule: an
OS_*override in force refuses every write to the key it pins except a clear, and a clear clears the value stored beneath it.@objectstack/service-settings,setMany's pre-flight). It threwSettingsLockedError(locked-by-env) whenevereffectiveEnvOverridewas in force, whatever the patch value. A clear (null/undefined, the spelling every clear in this service uses, never a value such as''that a key would store) now goes on to the judgments any clear meets: the Phase 2 row locks (lockThatRefuses), the platform-rung rule, the write predicates. The write then nulls the stored row as every clear does (the audit records areset), so the key falls back to its default once the override is removed. Nothing in force changes: the answer still reads the env value withsource: 'env'andlocked: true. No change toeffectiveEnvOverride,auditEnvOverrides,validatePatch, the routes or any export.auth.allowed_ip_rangesthat made the documented lockout recovery hold only while the variable stayed set.@objectstack/plugin-auth,allowed-ip-ranges.ts). The save refusal's recovery sentence, in en, zh-CN, ja-JP and es-ES, now names the four steps that work: start the server withOS_AUTH_ALLOWED_IP_RANGESset empty, sign in, clear the stored list with a settings request that setsallowed_ip_rangestonull(PUT /api/settings/auth), then remove the variable. It names the settings API, not Setup, because the console renders a pinned field read-only and offers no reset on it.docs/HARDENING.md(the Network row gains the recovery) andcontent/docs/protocol/kernel/config-resolution.mdx(the write-error table: an override in force refuses a write "and the write is not a clear"; added to the claimed surface by the seat). The changeset names the earlier allowed-IP entry's recovery sentence it supersedes.Tests
packages/services/service-settings/src/settings-service.test.ts, new top-level describe (5 cases), on the realSettingsService:{ value: env, source: 'env', locked: true }, aresetchange event fires, and once the pin is lifted the key reads its default, not the old value;undefinedis a clear too, beside an ordinary write in the same batch;SETTINGS_LOCKED/locked-by-envas before,''included, and a batch carrying one stores nothing;locked-by-global);locked-by-global), and the platform administrator's clear goes through.packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts, new top-level describe (1 case), on the realAuthPlugin, the realSettingsServicewith the shipped auth manifest and the real settings routes: a stored list that refuses::1; restart withOS_AUTH_ALLOWED_IP_RANGES=''admits it;''throughPUT /api/settings/authis409 SETTINGS_LOCKED(detailsexact);nullis200with the value read from env, locked, and the gate stays open; remove the variable, restart:::1is admitted.Suites and typecheck (head 173cc44)
pnpm --filter @objectstack/service-settings test: Test Files 68 passed (68), Tests 1161 passed (1161);typecheck: exit 0.pnpm --filter @objectstack/plugin-auth test: Test Files 224 passed (224), Tests 3955 passed | 10 skipped (3965);typecheck(tsc, the examples project andcheck:test-typecheck): exit 0.Measured before the fix (base 0e4dfbe, real AuthPlugin + SettingsService + settings routes)
203.0.113.0/24, no override:::1refused at the gate.OS_AUTH_ALLOWED_IP_RANGES='', restart:::1admitted; the key answers{ value: '', source: 'env', locked: true }.PUT /api/settings/auth:''andnullboth409 SETTINGS_LOCKED(locked-by-env);resetNamespace('auth')returns without clearing it (the key resolvesenv, so the reset skips it).source: 'global'); restart:::1refused.Ablations (each through `scripts/ablation-replace.mjs` on `settings-service.ts`: anchor 1 → 0, blob changed, restore blob == HEAD blob, `git diff HEAD` empty; both test files resolve `service-settings` from source, so no build sits between)
if (/* ABLATION-30385-A */ this.effectiveEnvOverride(…))): service-settings 4 failed | 1 passed (the clear,undefined, and both lock cases, which now answerlocked-by-envinstead oflocked-by-global); plugin-auth pin 1 failed | 9 passed (the end-to-end recovery).if (false /* ABLATION-30385-B */ && …)): service-settings 1 failed | 4 passed (every other write refused); plugin-auth pin 1 failed | 9 passed (the409step).if (clears) continue; // ABLATION-30385-Cafter the env branch): service-settings 2 failed | 3 passed (both lock cases); plugin-auth pin 10 passed.Acceptance notes
locked-by-env", and its REST table "409 if any key locked-by-env". This change lets a clear through, so D6 needs its amendment (AGENTS.md Prime Directive 13).docs/adr/**is Tier H and outside this change; the seat has put the amendment to the maintainer, and this PR does not land before it. The code comment cites ADR-0007 D6.SettingsFielddisables a locked field and hides "Reset to default" on it, so the clear is a settings API request for now; the console half is the seat's to file. The env answer also lists no stored value beneath the pin, so the console cannot tell whether there is one to clear.content/docs/permissions/authentication.mdxuntouched here. PR docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 (open, another lane) adds its "Saving, clearing and lockout recovery" subsection; its last bullet ("a save to it is refused with409 SETTINGS_LOCKED. Remove the variable, and the stored list applies again") is true onmainand false after this change. Seat ruling: whichever lands second rewrites that bullet to the four steps.resetNamespace(theresetaction) still skips a pinned key. It builds its patch from keys whose resolved source is a stored rung, and a pinned key resolvesenv, so a row stored beneath a pin survives a namespace reset while the action answers "environment/default configuration is back in effect". Unchanged here (outside the env-lock branch); reported to the seat.Verification
Gates, lint and the full dogfood on head
173cc44e(origin/mainmerged in; its one conflict was the tail ofallowed-ip-ranges.pin.test.ts, where PR #30392 appended its own describe; both are kept).Derived gates: 92 derived, 92 exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 92 commands for the changed paths; each ran with its exit code captured before any pipe.--ranreconciliation: "92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED".check:dual-build-cjs-loadsandcheck-plugin-teardown-shape --self-testexited 3 on the first round (no whole-workspace build; shallow checkout); after building the dogfood closure and fetching621a4876, both exit 0 on this head.pnpm lint(eslint . --no-inline-config): exit 0.Full dogfood (asked by the order; head 173cc44)
pnpm --filter @objectstack/dogfood testunder the verify lock:VERDICT command-exit 0, Test Files 419 passed | 3 skipped (422), Tests 3492 passed | 36 skipped (3528).Human Minimum
packages/specholds no runtime logic (untouched).pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i