You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Allowed IP ranges: an administrator can exclude their own address unwarned, and clearing the field does not take effect until a restart #30203
From a real-user walkthrough of Setup, filed on the maintainer's word: 「以上都同意,立卡」 (2026-11-05). Measured on mainb7075d9908, with the console in dev mode, unless a line says otherwise. Test app scaffolded with [email protected].
What a customer sees
A company wants sign-in only from the office. The administrator, working from home, enters the office range and saves. Nothing says their own address is outside it. They find out on the next sign-in. If they still have a page open and clear the field, the page says saved, and sign-in keeps being refused until someone restarts the server. A business administrator cannot do that and has no way to tell why it did not work.
Measured
allowed_ip_ranges = 203.0.113.0/24 from ::1: toast 「设置已保存」, no warning, the current address is not shown. POST /api/v1/auth/sign-in/email then answers 403 IP_NOT_ALLOWED.
The open session keeps working, so the field can still be edited.
Field cleared and saved: toast 「设置已保存」, the stored value is empty, and sign-in still answers 403 on three tries over 12 seconds. /api/v1/auth/two-factor/enable was refused the same way.
After a server restart: sign-in 200.
Likely cause, from reading the code
packages/plugins/plugin-auth/src/auth-plugin.ts:2062 applies a setting to the running config only when isExplicit(key), and :2292 does so for this key. A cleared value appears to read as default, so the old list stays in force. If so, other settings with an empty or restored-default state may behave the same; 恢复默认 was not measured.
Expected
The field shows the caller's current address. A list that excludes it is refused or asks for explicit confirmation.
Clearing applies at once, as saving a value does.
The documentation says how to recover when locked out. The seat found no environment variable for this key in content/docs/deployment/environment-variables.mdx.
Read fresh. Allowed IP ranges can exclude the saving administrator's own address with no warning. Clearing the field does not take effect until a restart. A self-lockout with a recovery path through other admins or the environment, so p2.
Fix site: plugin-auth (auth-plugin.ts): read the setting live, and warn or refuse when the caller's address falls outside. The warning text is the domain:ui half.
Claim: PM loop round 3
Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6Ca3N2EsTAqg7jhabeL
Account: os-bill
Branch: claude/project-thread-2etnqx (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30203
Domain: domain:server
Seat: domain:server#2
File surface: packages/plugins/plugin-auth/src/ (auth-plugin.ts settings apply, the IP gate helper if reused, tests), possibly manifest.ts (help text), one changeset .changeset/30203-allowed-ip-ranges-live.md. ⛔ packages/spec/**, ui/**, apps/**, skills/**, docs/adr/**, content/docs/**, .claude/**. (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: high
Clause-②: no — a fix to an existing settings apply plus a refused self-excluding save; the dev re-declares no (narrowing) if a list an admin could save before is now refused (then CONTRACT_REVIEW_TIER review), yes for a new published code or settings key
Metadata-protocol: none
Responsibility: applySettings in auth-plugin.ts skips a default-sourced (cleared) allowed_ip_ranges, so the running list survives until restart; the save door does not check the caller's own address | the IP gate itself enforces the running list | every administrator who sets or clears allowed IP ranges
Thread-read: 6648444250
Premise: holds — at bb86ff96isExplicit (:2062) treats a default source as not explicit and allowed_ip_ranges (:2292) is applied only when explicit; the cleared-value source is the card's reading, re-measured by the dev
Serial constraints cleared: this seat's #30202 edits the same applySettings at email_password_enabled (:2112) and #30201 may edit near mfa_required (:2210); disjoint branches, a change to isExplicit is announced to the seat first, each pulls main before PR and before landing. No sibling seat's card names plugin-auth.
Order: project file server-seat2/dispatch-30203.md, delivered as the card thread's opening message.
{
"issue": 30203,
"status": "done",
"branch": "claude/project-thread-2etnqx",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30309",
"session": "session_01Utp6Ca3N2EsTAqg7jhabeL",
"premise_still_valid": true,
"summary": "Premise measured on the real SettingsService with the shipped auth manifest: the console sends an emptied field as null, and after that save (and after resetNamespace) allowed_ip_ranges resolves {value: null, source: 'default'}, which applySettings skipped, so the stored list stayed in force until a restart. Now a default-sourced value hands the gate back to the deployment's own declared allowedIpRanges (none, or the plugin options' list) in the same pass; isExplicit is unchanged. A console save whose list leaves out the saving request's address (resolved by AuthManager.resolveClientAddress under trustProxy, matched by ipMatchesRange) is refused by a settings write predicate: 400 SETTINGS_VALIDATION, invalid_value, constraint.clientAddress, localized message naming the address and the recovery (OS_AUTH_ALLOWED_IP_RANGES set empty, an existing env override, measured in force). service-settings carries the request to write predicates on setMany's options (SettingsWriteOptions.transport), not on the caller's context, so the door's identity pins hold. Scope choice: this key only; the clear-skips family is reported below.",
"tests": "allowed-ip-ranges.pin.test.ts 9/9 (real AuthPlugin + real SettingsService + real settings routes). plugin-auth test: Test Files 221 passed (221), Tests 3885 passed | 10 skipped; service-settings test: Test Files 67 passed (67), Tests 1159 passed; both typecheck exit 0 (head 4a971127; the main merge 89ff3aa9 touched neither package). First service-settings run red on two door pins asserting toBe identity of ctx while transport rode the context; moved to options, both green unchanged. Ablations via scripts/ablation-replace.mjs (tests resolve both packages from src through the vitest alias, so no build in between): A clear branch off -> 3 failed | 6 passed; B predicate registration off -> 3 failed; C route transport renamed away -> 3 failed; D service forwarding of transport deleted -> 3 failed; each mutation landed (anchor 1 -> 0, blob changed), each restore blob equals HEAD, git diff HEAD empty after.",
"gates": "head 89ff3aa9: dispatch-gates --commands derived 65; each run with exit captured before any pipe; --ran: '65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED'. NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3 (35 packages without dist; needs a whole-workspace build), left to CI. check-plugin-teardown-shape --self-test exit 3 on the shallow checkout, exit 0 ('48 cases pass') after fetching 621a4876. pnpm lint exit 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "89ff3aa9 · VERDICT command-exit 1 · Test Files 1 failed | 418 passed | 3 skipped (422) · Tests 1 failed | 3491 passed | 36 skipped (3528) · failure: test/permission-set-clone-door.dogfood.test.ts 'a platform set ... copies into its shipping package's pair' answers 422 INVALID_METADATA (member_default clone grants on sys_comment / sys_activity 'not defined in objects'); not this change (no permission, metadata or clone code in the diff); the grants arrived with #30222 (490d136f2)",
"line_budget": "8 files, +507 / -12 vs merge base (dispatch-gates count); under the 3000 human-merge threshold",
"files_changed": [
".changeset/30203-allowed-ip-ranges-live.md",
"packages/plugins/plugin-auth/src/allowed-ip-ranges.ts",
"packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/services/service-settings/src/index.ts",
"packages/services/service-settings/src/settings-routes.ts",
"packages/services/service-settings/src/settings-service.ts",
"packages/services/service-settings/src/settings-service.types.ts"
],
"deviations": [
"service-settings (4 files) is outside the order's expected landing: the save door had no way to carry the caller's address; it now carries the request on setMany's options. No packages/spec change, no new error code, no new settings key, no new env var.",
"manifest help text (service-settings auth.manifest.ts and its three translations) not changed: the refusal text carries the explanation; changing the description would move the translation bundles.",
"The full dogfood is red on one file from main (#30222), reported, not fixed here."
],
"mcp_calls": "2 — mcp__github__issue_read (get, get_comments); no write tool",
"api_writes": "2 relay writes — pr_create #30309 (fleet-write run 41975771563), os-dev-report comment on #30203 (post-stamped); plus git pushes to the designated branch",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: PUT /api/settings/auth with a key set to null (the console's clear) or the namespace reset answers saved, while the running value keeps the last stored one until a restart; measured on the real AuthPlugin + SettingsService: lockout_threshold 3 and max_concurrent_sessions_per_user 2 stay in force after both are saved null · evidence: every if (isExplicit(key)) branch in auth-plugin.ts applySettings without a reset; the general fix must restore each key's deployment-declared value, across the refusable emailAndPassword pieces #30201/#30202 are editing · dedupe words: auth settings cleared default restart, restore default not applied, isExplicit default source",
"class: a · reach: POST permission-set clone of member_default on a booted showcase answers 422 INVALID_METADATA ('grants on object sys_comment / sys_activity which is not defined in objects') where 200 is expected; packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts red on main since #30222 (490d136f2) · dedupe words: member_default clone sys_comment, permission set clone door 422, sys_activity not defined",
"carrier: seat (domain:ui) · the console field does not show the caller's current address before saving; the refusal carries it in constraint.clientAddress for the console to show · noted, not filed",
"carrier: seat (docs) · content/docs/permissions/authentication.mdx does not state the save refusal or the OS_AUTH_ALLOWED_IP_RANGES recovery; content/docs is outside this card's surface · noted, not filed"
]
}
Claim correction (supersedes the Clause-② line and widens the file surface of claim 6649086146; every other line stands)
Clause-②: no (narrowing) — measured by the dev (report 6650755695): a save of allowed_ip_ranges that leaves the saving caller's own address outside the list is now refused, a value an admin could save before; clearing the list now takes effect at once (falls back to the deployment's declared list). Review owed: CONTRACT_REVIEW_TIER over the non-prose diff (narrowing face), which also judges whether the new optional SettingsWriteOptions.transport reachable from the @objectstack/service-settings exports is a published widening (then the line becomes yes (narrowing)).
File surface (added): packages/services/service-settings/src/index.ts, settings-routes.ts, settings-service.ts, settings-service.types.ts (the route hands the caller's transport to the write predicate). Accepted by the seat, subject to the contract review. PR #30298 (card #30202) adds a batch field to the same predicate input in the same two files; whichever lands second merges main first.
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-05T02:57Z⛔ Not a claim.
plugin-auth(auth-plugin.ts): read the setting live, and warn or refuse when the caller's address falls outside. The warning text is thedomain:uihalf.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
Claim: PM loop round 3
Session:
session_01Utp6Cbvcmv8YWFhFJwzjKL(seat) · dev threadsession_01Utp6Ca3N2EsTAqg7jhabeLAccount:
os-billBranch:
claude/project-thread-2etnqx(harness-pinned thread branch; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30203Domain:
domain:serverSeat:
domain:server#2File surface:
packages/plugins/plugin-auth/src/(auth-plugin.tssettings apply, the IP gate helper if reused, tests), possiblymanifest.ts(help text), one changeset.changeset/30203-allowed-ip-ranges-live.md. ⛔packages/spec/**,ui/**,apps/**,skills/**,docs/adr/**,content/docs/**,.claude/**. (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier (opus) — dispatch-gates --tier: no path-derived mandate,effort: highClause-②: no — a fix to an existing settings apply plus a refused self-excluding save; the dev re-declares
no (narrowing)if a list an admin could save before is now refused (thenCONTRACT_REVIEW_TIERreview),yesfor a new published code or settings keyMetadata-protocol: none
Responsibility:
applySettingsinauth-plugin.tsskips a default-sourced (cleared)allowed_ip_ranges, so the running list survives until restart; the save door does not check the caller's own address | the IP gate itself enforces the running list | every administrator who sets or clears allowed IP rangesThread-read: 6648444250
Premise: holds — at
bb86ff96isExplicit(:2062) treats a:2292) is applied only when explicit; the cleared-value source is the card's reading, re-measured by the devdefaultsource as not explicit andallowed_ip_ranges(Serial constraints cleared: this seat's #30202 edits the same
applySettingsatemail_password_enabled(:2112) and #30201 may edit near:2210); disjoint branches, a change tomfa_required(isExplicitis announced to the seat first, each pullsmainbefore PR and before landing. No sibling seat's card namesplugin-auth.Order: project file
server-seat2/dispatch-30203.md, delivered as the card thread's opening message.objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Ca3N2EsTAqg7jhabeL
os-dev-report
{ "issue": 30203, "status": "done", "branch": "claude/project-thread-2etnqx", "pr": "https://github.com/objectstack-ai/objectstack/pull/30309", "session": "session_01Utp6Ca3N2EsTAqg7jhabeL", "premise_still_valid": true, "summary": "Premise measured on the real SettingsService with the shipped auth manifest: the console sends an emptied field as null, and after that save (and after resetNamespace) allowed_ip_ranges resolves {value: null, source: 'default'}, which applySettings skipped, so the stored list stayed in force until a restart. Now a default-sourced value hands the gate back to the deployment's own declared allowedIpRanges (none, or the plugin options' list) in the same pass; isExplicit is unchanged. A console save whose list leaves out the saving request's address (resolved by AuthManager.resolveClientAddress under trustProxy, matched by ipMatchesRange) is refused by a settings write predicate: 400 SETTINGS_VALIDATION, invalid_value, constraint.clientAddress, localized message naming the address and the recovery (OS_AUTH_ALLOWED_IP_RANGES set empty, an existing env override, measured in force). service-settings carries the request to write predicates on setMany's options (SettingsWriteOptions.transport), not on the caller's context, so the door's identity pins hold. Scope choice: this key only; the clear-skips family is reported below.", "tests": "allowed-ip-ranges.pin.test.ts 9/9 (real AuthPlugin + real SettingsService + real settings routes). plugin-auth test: Test Files 221 passed (221), Tests 3885 passed | 10 skipped; service-settings test: Test Files 67 passed (67), Tests 1159 passed; both typecheck exit 0 (head 4a971127; the main merge 89ff3aa9 touched neither package). First service-settings run red on two door pins asserting toBe identity of ctx while transport rode the context; moved to options, both green unchanged. Ablations via scripts/ablation-replace.mjs (tests resolve both packages from src through the vitest alias, so no build in between): A clear branch off -> 3 failed | 6 passed; B predicate registration off -> 3 failed; C route transport renamed away -> 3 failed; D service forwarding of transport deleted -> 3 failed; each mutation landed (anchor 1 -> 0, blob changed), each restore blob equals HEAD, git diff HEAD empty after.", "gates": "head 89ff3aa9: dispatch-gates --commands derived 65; each run with exit captured before any pipe; --ran: '65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED'. NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3 (35 packages without dist; needs a whole-workspace build), left to CI. check-plugin-teardown-shape --self-test exit 3 on the shallow checkout, exit 0 ('48 cases pass') after fetching 621a4876. pnpm lint exit 0. CI check-runs: in_progress at report time.", "dogfood_battery": "89ff3aa9 · VERDICT command-exit 1 · Test Files 1 failed | 418 passed | 3 skipped (422) · Tests 1 failed | 3491 passed | 36 skipped (3528) · failure: test/permission-set-clone-door.dogfood.test.ts 'a platform set ... copies into its shipping package's pair' answers 422 INVALID_METADATA (member_default clone grants on sys_comment / sys_activity 'not defined in objects'); not this change (no permission, metadata or clone code in the diff); the grants arrived with #30222 (490d136f2)", "line_budget": "8 files, +507 / -12 vs merge base (dispatch-gates count); under the 3000 human-merge threshold", "files_changed": [ ".changeset/30203-allowed-ip-ranges-live.md", "packages/plugins/plugin-auth/src/allowed-ip-ranges.ts", "packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts", "packages/plugins/plugin-auth/src/auth-plugin.ts", "packages/services/service-settings/src/index.ts", "packages/services/service-settings/src/settings-routes.ts", "packages/services/service-settings/src/settings-service.ts", "packages/services/service-settings/src/settings-service.types.ts" ], "deviations": [ "service-settings (4 files) is outside the order's expected landing: the save door had no way to carry the caller's address; it now carries the request on setMany's options. No packages/spec change, no new error code, no new settings key, no new env var.", "manifest help text (service-settings auth.manifest.ts and its three translations) not changed: the refusal text carries the explanation; changing the description would move the translation bundles.", "The full dogfood is red on one file from main (#30222), reported, not fixed here." ], "mcp_calls": "2 — mcp__github__issue_read (get, get_comments); no write tool", "api_writes": "2 relay writes — pr_create #30309 (fleet-write run 41975771563), os-dev-report comment on #30203 (post-stamped); plus git pushes to the designated branch", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: PUT /api/settings/auth with a key set to null (the console's clear) or the namespace reset answers saved, while the running value keeps the last stored one until a restart; measured on the real AuthPlugin + SettingsService: lockout_threshold 3 and max_concurrent_sessions_per_user 2 stay in force after both are saved null · evidence: every if (isExplicit(key)) branch in auth-plugin.ts applySettings without a reset; the general fix must restore each key's deployment-declared value, across the refusable emailAndPassword pieces #30201/#30202 are editing · dedupe words: auth settings cleared default restart, restore default not applied, isExplicit default source", "class: a · reach: POST permission-set clone of member_default on a booted showcase answers 422 INVALID_METADATA ('grants on object sys_comment / sys_activity which is not defined in objects') where 200 is expected; packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts red on main since #30222 (490d136f2) · dedupe words: member_default clone sys_comment, permission set clone door 422, sys_activity not defined", "carrier: seat (domain:ui) · the console field does not show the caller's current address before saving; the refusal carries it in constraint.clientAddress for the console to show · noted, not filed", "carrier: seat (docs) · content/docs/permissions/authentication.mdx does not state the save refusal or the OS_AUTH_ALLOWED_IP_RANGES recovery; content/docs is outside this card's surface · noted, not filed" ] }Report taken 2026-11-05T05:44Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
Claim correction (supersedes the Clause-② line and widens the file surface of claim 6649086146; every other line stands)
Clause-②: no (narrowing) — measured by the dev (report 6650755695): a save of
allowed_ip_rangesthat leaves the saving caller's own address outside the list is now refused, a value an admin could save before; clearing the list now takes effect at once (falls back to the deployment's declared list). Review owed:CONTRACT_REVIEW_TIERover the non-prose diff (narrowing face), which also judges whether the new optionalSettingsWriteOptions.transportreachable from the@objectstack/service-settingsexports is a published widening (then the line becomesyes (narrowing)).File surface (added):
packages/services/service-settings/src/index.ts,settings-routes.ts,settings-service.ts,settings-service.types.ts(the route hands the caller's transport to the write predicate). Accepted by the seat, subject to the contract review. PR #30298 (card #30202) adds a batch field to the same predicate input in the same two files; whichever lands second mergesmainfirst.objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
Landed · 2026-11-05T07:29Z
d1d90b70, reviewed head8767d6aa(seat ACCEPT 6651982979)CONTRACT_REVIEW_TIER, narrowing face: FAIL 6650936040 (89ff3aa9, declaration), delta PASS 6651963356 (8767d6aa)Fixes #30203@objectstack/plugin-authand@objectstack/service-settingspatch,Clause-②: yes (narrowing), BREAKING banner and ADR-0087 marker; next releaseState change:
pm:dispatchedremoved. Follow-ups: #30312 (clearing other auth keys needs a restart), #30313 (console shows the caller's address), #30314 (docs), #30320 (IPv6 CIDR never matches).