Skip to content

Commit 2445cb4

Browse files
fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable (#30448)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgE9CDELX3sPuAkeE2cdR3ph)_ domain:server#2 · os-bill · session_01Utp6CZjAtpVuoYZBzB9n5i Fixes #30385 Clause-②: yes (widening) — `SettingsService.setMany` / `set` (and `PUT /api/settings/:namespace`) accept a clear (`null` / `undefined`) of a key an `OS_*` override in force pins, where every write to such a key was refused `409 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 白名单的拒绝提示改写成完整的四步救援。 - 为什么改:卡上实测,管理员被自己的 IP 白名单锁在外面后,按提示把变量设空能进来,但存着的旧列表清不掉(保存一律 409),一去掉变量重启又被锁,文档写的救援办法只管一次。 - 风险与回滚:放宽,只放行“清除”这一种写入;行锁、平台档、写入谓词照旧拦。依赖维护者对 ADR-0007 第 6 条(env 锁定拒绝一切写入)的修订,修订未定前席位不落地。回滚即撤本 PR。 - 先看:`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` 末尾的端到端用例;`content/docs/permissions/authentication.mdx` 的锁定恢复条目(已改成四步)。 - 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts`,预期 1 个文件、10 条全绿(最后一条是 env 置空 → 清除 → 去掉变量重启后仍能进)。 - 需要你做的:裁定 ADR-0007 第 6 条的修订(席位已提交),之后席位落地。 ## 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.** - **The env-lock branch (`@objectstack/service-settings`, `setMany`'s pre-flight).** It threw `SettingsLockedError` (`locked-by-env`) whenever `effectiveEnvOverride` was 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 a `reset`), so the key falls back to its default once the override is removed. Nothing in force changes: the answer still reads the env value with `source: 'env'` and `locked: true`. No change to `effectiveEnvOverride`, `auditEnvOverrides`, `validatePatch`, the routes or any export. - **Why the stored value needed a door.** The override hides the stored row from every read and refused every write to it, so an operator who pinned a key to recover from a bad stored value got that value back the moment the pin was lifted. For `auth.allowed_ip_ranges` that made the documented lockout recovery hold only while the variable stayed set. - **The recovery copy (`@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 with `OS_AUTH_ALLOWED_IP_RANGES` set empty, sign in, clear the stored list with a settings request that sets `allowed_ip_ranges` to `null` (`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.** `docs/HARDENING.md` (the Network row gains the recovery); `content/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); `packages/services/service-settings/README.md` (Env override: every write but a clear (`null`) fails with HTTP 409, and a clear removes the value stored beneath the override and changes nothing in force); `content/docs/permissions/authentication.mdx` (the "If you are already locked out" bullet now walks the four steps, and says that removing the variable without the clear brings the stored list back). 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 real `SettingsService`: - a clear under a pin set over a stored value: the answer reads `{ value: env, source: 'env', locked: true }`, a `reset` change event fires, and once the pin is lifted the key reads its default, not the old value; - `undefined` is a clear too, beside an ordinary write in the same batch; - every other write is refused `SETTINGS_LOCKED` / `locked-by-env` as before, `''` included, and a batch carrying one stores nothing; - a locked row at an upper rung still refuses a clear below it (`locked-by-global`); - a locked row at the write's own rung refuses a non-platform clear (`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 real `AuthPlugin`, the real `SettingsService` with the shipped auth manifest and the real settings routes: a stored list that refuses `::1`; restart with `OS_AUTH_ALLOWED_IP_RANGES=''` admits it; `''` through `PUT /api/settings/auth` is `409 SETTINGS_LOCKED` (`details` exact); `null` is `200` with the value read from env, locked, and the gate stays open; remove the variable, restart: `::1` is admitted. <details><summary>Suites and typecheck (head 173cc44)</summary> - `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 and `check:test-typecheck`): exit 0. </details> <details><summary>Measured before the fix (base 0e4dfbe, real AuthPlugin + SettingsService + settings routes)</summary> 1. Stored list `203.0.113.0/24`, no override: `::1` refused at the gate. 2. `OS_AUTH_ALLOWED_IP_RANGES=''`, restart: `::1` admitted; the key answers `{ value: '', source: 'env', locked: true }`. 3. Through `PUT /api/settings/auth`: `''` and `null` both `409 SETTINGS_LOCKED` (`locked-by-env`); `resetNamespace('auth')` returns without clearing it (the key resolves `env`, so the reset skips it). 4. Remove the override: the key answers the stored list again (`source: 'global'`); restart: `::1` refused. </details> <details><summary>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)</summary> - A. the clear exemption removed (`if (/* ABLATION-30385-A */ this.effectiveEnvOverride(…))`): service-settings 4 failed | 1 passed (the clear, `undefined`, and both lock cases, which now answer `locked-by-env` instead of `locked-by-global`); plugin-auth pin 1 failed | 9 passed (the end-to-end recovery). - B. every write let through the pin (`if (false /* ABLATION-30385-B */ && …)`): service-settings 1 failed | 4 passed (every other write refused); plugin-auth pin 1 failed | 9 passed (the `409` step). - C. a clear skipping the Phase 2 row locks (`if (clears) continue; // ABLATION-30385-C` after the env branch): service-settings 2 failed | 3 passed (both lock cases); plugin-auth pin 10 passed. </details> ## Acceptance notes - **ADR-0007 D6 dependency.** ADR-0007 (Accepted) D6 reads "Writes from UI are rejected with HTTP 409 + reason `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. - **Setup cannot issue the clear yet.** `SettingsField` disables 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.mdx` rewritten here.** PR #30382 merged first (2026-11-05T10:10Z, `7d7babb1`), adding the "Saving, clearing and lockout recovery" subsection. Its last bullet ("a save to it is refused with `409 SETTINGS_LOCKED`. Remove the variable, and the stored list applies again") is false after this change. Seat ruling: whichever lands second rewrites that bullet, so this PR does. It now names the four steps: start with `OS_AUTH_ALLOWED_IP_RANGES` empty, sign in, `PUT /api/settings/auth` with `allowed_ip_ranges: null`, remove the variable. - **`resetNamespace` (the `reset` action) still skips a pinned key.** It builds its patch from keys whose resolved source is a stored rung, and a pinned key resolves `env`, 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 Suites, typecheck, lint and the full dogfood on head `173cc44e` (`origin/main` merged in; its one conflict was the tail of `allowed-ip-ranges.pin.test.ts`, where PR #30392 appended its own describe; both are kept). Head `5c0cbbfb` adds one docs commit for the contract review's two must-fixes (`packages/services/service-settings/README.md`, `content/docs/permissions/authentication.mdx`) and changes no code, so those were not re-run on it. The derived gates were re-run on `5c0cbbfb`. <details><summary>Derived gates: 93 derived, 93 exit 0 (head 5c0cbbf)</summary> - `5c0cbbfb`: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 93 commands for the 10 changed paths (one more than round 1: `pnpm check:merge-driver`); each ran with its exit code captured before any pipe. `--ran` reconciliation: "93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED". They ran on commit `b4b507a9`; the pushed `5c0cbbfb` carries the same tree (`af8723c4`), its message reworded only because the pre-push `check:commit-card-trailers` refused a card trailer. - `173cc44e`: 92 derived, 92 exit 0, `--ran` "92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED". `check:dual-build-cjs-loads` and `check-plugin-teardown-shape --self-test` exited 3 on the first round (no whole-workspace build; shallow checkout); after building the dogfood closure and fetching `621a4876`, both exit 0. - `pnpm lint` (`eslint . --no-inline-config`): exit 0. </details> <details><summary>Full dogfood (asked by the order; head 173cc44)</summary> - Built the closure, then `pnpm --filter @objectstack/dogfood test` under the verify lock: `VERDICT command-exit 0`, Test Files 419 passed | 3 skipped (422), Tests 3492 passed | 36 skipped (3528). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic (untouched). - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is Chinese by the Communication rule). <!-- signed: domain:server#2 · os-bill · session_01Utp6CZjAtpVuoYZBzB9n5i --> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
1 parent ad6826c commit 2445cb4

10 files changed

Lines changed: 238 additions & 24 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
'@objectstack/plugin-auth': patch
4+
---
5+
6+
fix(service-settings): a clear of an env-pinned setting clears the stored value beneath the pin, so the allowed-IP lockout recovery holds once the variable is removed
7+
8+
Clause-②: yes (widening) — `SettingsService.setMany` / `set` (and `PUT /api/settings/:namespace`) accept a clear (`null` / `undefined`) of a key an `OS_*` override in force pins, where every write to such a key was refused `409 SETTINGS_LOCKED`; every other write is still refused. No export, type or error code changes.
9+
Metadata-protocol: none
10+
11+
- **A clear goes through an env pin.** An `OS_*` override in force decides the value in force and still refuses every write but a clear. A clear (`null` / `undefined`, never a value such as `''` that a key would store) clears the value stored beneath the pin, so the key falls back to its default once the override is removed. It changes nothing in force: the answer still reads the env value with `source: 'env'` and `locked: true`. It meets every judgment a clear meets without a pin: a locked row at an upper scope, or a locked row at the write's own scope for a caller that is not the platform, still refuses it `SETTINGS_LOCKED` (`locked-by-<scope>`), and the key's write predicates still run.
12+
- **Why.** The override hides the stored value from every read, and used to refuse every write to it, so an operator who pinned a key to recover from a bad stored value got the same value back the moment the pin was lifted. For `auth.allowed_ip_ranges` that meant the documented recovery (`OS_AUTH_ALLOWED_IP_RANGES` set empty) opened the gate only until the variable was removed, and the next restart locked the administrator out again.
13+
- **Recovery when locked out by `allowed_ip_ranges` (`@objectstack/plugin-auth`).** Start the server with `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value, sign in, clear the stored list with a settings request that sets `allowed_ip_ranges` to `null` (`PUT /api/settings/auth`), then remove the variable. The save refusal's message names these four steps in all four of its languages. This supersedes the recovery sentence of the earlier entry for the allowed-IP save refusal ("remove it once the list is fixed"): removing the variable without the clear brings the stored list back at the next restart. The Setup console still shows a pinned field read-only, so the clear is a settings API request for now.

‎content/docs/permissions/authentication.mdx‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ These rules apply to `allowed_ip_ranges`.
662662
API save, `PUT /api/settings/auth`, which **Setup → Authentication** calls, so any
663663
HTTP client gets the same answer. The message names your address, says the list
664664
would refuse your own next sign-in, and asks you to add your address or its range,
665-
or to save from an address the list matches. It ends with the recovery step below.
665+
or to save from an address the list matches. It ends with the recovery steps below.
666666
It is written in your language (English, Chinese, Japanese or Spanish). The field
667667
error in `details.fields` also carries your address as `constraint.clientAddress`.
668668
If the server cannot establish your address at all, any non-empty list is refused,
@@ -681,15 +681,24 @@ These rules apply to `allowed_ip_ranges`.
681681
the gate goes back to the list the deployment itself declares (`allowedIpRanges` in
682682
its auth configuration, for example set by an `auth:configure` handler), or to no
683683
restriction when it declares none. A clear is never refused.
684-
- **If you are already locked out**, start the server with
685-
`OS_AUTH_ALLOWED_IP_RANGES` set to an empty value (`OS_AUTH_ALLOWED_IP_RANGES=`).
686-
Environment variables are read at startup, so a restart is needed. An empty value
687-
is not the same as an unset variable: any value that is set, the empty string
688-
included, is an env override in force. It wins over the stored list, and an empty
689-
list means no restriction. While the variable is set, the stored list is kept but
690-
not used, and the field is locked: a save to it is refused with
691-
`409 SETTINGS_LOCKED`. Remove the variable, and the stored list applies again at
692-
the next restart.
684+
- **If you are already locked out**, recover in four steps:
685+
1. Start the server with `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value
686+
(`OS_AUTH_ALLOWED_IP_RANGES=`). Environment variables are read at startup, so a
687+
restart is needed. An empty value is not the same as an unset variable: any
688+
value that is set, the empty string included, is an env override in force. It
689+
wins over the stored list, and an empty list means no restriction.
690+
2. Sign in.
691+
3. Clear the stored list: send `PUT /api/settings/auth` with
692+
`{ "allowed_ip_ranges": null }`. While the variable is set the field is locked,
693+
so **Setup → Authentication** shows it read-only and any other save to it is
694+
refused with `409 SETTINGS_LOCKED`; a clear (`null`) is the one write the lock
695+
lets through. It clears the stored value and changes nothing in force: the
696+
answer still reads the empty value from the environment.
697+
4. Remove the variable and restart. With the stored list cleared, nothing brings
698+
it back: the gate applies the list the deployment declares, or no restriction.
699+
700+
Removing the variable without step 3 brings the stored list back at the next
701+
restart, and locks you out again.
693702

694703
---
695704

‎content/docs/protocol/kernel/config-resolution.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ Writes fail loudly rather than silently degrading:
218218

219219
| Condition | Thrown | Code |
220220
| :--- | :--- | :--- |
221-
| An `OS_*` override is in force for the key | `SettingsLockedError` | `SETTINGS_LOCKED` |
221+
| An `OS_*` override is in force for the key, and the write is not a clear | `SettingsLockedError` | `SETTINGS_LOCKED` |
222222
| A row at an upper scope has `locked=true`, or a row at the write's own scope has it and the caller is not the platform | `SettingsLockedError` | `SETTINGS_LOCKED` (`locked-by-<scope>`) |
223223
| Key not declared by the manifest | `UnknownKeyError` | `SETTINGS_UNKNOWN_KEY` |
224224
| Namespace has no manifest | `UnknownNamespaceError` | `SETTINGS_UNKNOWN_NAMESPACE` |

‎docs/HARDENING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ production deployment, enable at least:
241241
| Anti-abuse | `lockout_threshold: 5`, `lockout_duration_minutes: 15`, `rate_limit_max` / `rate_limit_window_seconds` |
242242
| MFA | `mfa_required: true` (or per-org `require_mfa`, which needs two-factor enrollment on), `mfa_grace_period_days: 7`; neither saves while `OS_AUTH_TWO_FACTOR=false` |
243243
| Sessions | `session_idle_timeout_minutes: 30–60`, `session_absolute_max_hours: 8–12`, `max_concurrent_sessions_per_user` |
244-
| Network | `allowed_ip_ranges` (IPv4 CIDR) for tenant- or office-scoped access |
244+
| Network | `allowed_ip_ranges` (IPv4 CIDR) for tenant- or office-scoped access; locked out, restart with `OS_AUTH_ALLOWED_IP_RANGES=` (empty), sign in, clear the stored list (`PUT /api/settings/auth` with `allowed_ip_ranges: null`), then remove the variable |
245245

246246
Password expiry and enforced MFA share a *session-validation gate*: a
247247
non-compliant session can authenticate but is blocked from data access (auth +

‎packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
* the deployment declares `trustProxy`. A list that includes it saves.
2222
* - The recovery the refusal names: `OS_AUTH_ALLOWED_IP_RANGES` set empty
2323
* opens the gate on the next boot over a stored list.
24+
* - [#30385] The rest of that recovery: while the override is in force a
25+
* clear (`null`) goes through the settings door and clears the stored
26+
* list, so removing the override does not bring it back; any other value
27+
* is still refused `409 SETTINGS_LOCKED`.
2428
* - [#30398] The read an administrator asks before saving,
2529
* `GET /api/v1/auth/admin/client-address`, answers the address that same
2630
* save would be judged by: the plugin's real auth routes on a real Hono app,
@@ -385,3 +389,36 @@ describe('#30398 the address read serves the address the save check judges', ()
385389
expect(asked).not.toHaveBeenCalled();
386390
});
387391
});
392+
393+
describe('#30385 the env recovery clears the stored list, so lifting the override does not lock the caller out again', () => {
394+
it('set the override empty, sign in, clear the stored list through the settings door, remove the override: the caller is admitted', async () => {
395+
const env: Record<string, string | undefined> = {};
396+
const svc = new SettingsService({ env });
397+
svc.registerManifest(authManifest);
398+
await svc.set('auth', 'allowed_ip_ranges', OFFICE, {});
399+
expect((await boot({ svc })).manager.isClientIpAllowed('::1')).toBe(false);
400+
401+
// Start the server with the override set empty: the gate opens.
402+
env.OS_AUTH_ALLOWED_IP_RANGES = '';
403+
const recovered = await boot({ svc });
404+
expect(recovered.manager.isClientIpAllowed('::1')).toBe(true);
405+
406+
// A value is still refused while the override is in force, the empty string included.
407+
const refused = await recovered.save({ allowed_ip_ranges: '' }, { remoteAddress: '::1' });
408+
expect(refused.status).toBe(409);
409+
expect(refused.body.error.code).toBe('SETTINGS_LOCKED');
410+
expect(refused.body.error.details).toEqual({ namespace: 'auth', key: 'allowed_ip_ranges', reason: 'locked-by-env' });
411+
412+
// The clear goes through; the answer still reads the override, locked, and the gate stays open.
413+
const cleared = await recovered.save({ allowed_ip_ranges: null }, { remoteAddress: '::1' });
414+
expect(cleared.status).toBe(200);
415+
expect(cleared.body.data.values.allowed_ip_ranges).toMatchObject({ value: '', source: 'env', locked: true });
416+
expect(recovered.manager.isClientIpAllowed('::1')).toBe(true);
417+
418+
// Remove the override and restart: no stored list comes back.
419+
delete env.OS_AUTH_ALLOWED_IP_RANGES;
420+
expect((await svc.get('auth', 'allowed_ip_ranges', {})).source).toBe('default');
421+
const restarted = await boot({ svc });
422+
expect(restarted.manager.isClientIpAllowed('::1')).toBe(true);
423+
});
424+
});

‎packages/plugins/plugin-auth/src/allowed-ip-ranges.ts‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@
3030
* lock out, and it is let through.
3131
*
3232
* The way back when locked out anyway — a list saved from another address, or
33-
* an address that changed — is the settings env override every auth key has:
34-
* `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value, then a restart. The
35-
* refusal names it.
33+
* an address that changed — is the settings env override every auth key has,
34+
* in four steps, and the refusal names them: start the server with
35+
* `OS_AUTH_ALLOWED_IP_RANGES` set to an empty value (the override wins over
36+
* the stored list, so the gate is open), sign in, clear the stored list (a
37+
* settings write of `null` to the key, which the override lets through,
38+
* #30385), then remove the variable. Removing it without the clear brings the
39+
* stored list back at the next restart.
3640
*/
3741

3842
import { resolveLocale } from '@objectstack/core';
@@ -132,7 +136,10 @@ export const ALLOWED_IP_RANGES_REFUSAL_COPY: Record<SettingsCopyLocale, AllowedI
132136
'The server cannot establish the address you are connected from, and while any list is set it refuses every '
133137
+ 'such request, your own next sign-in included. Save the list from a connection whose address the server can '
134138
+ 'establish.',
135-
recovery: ` If you are locked out, start the server with ${ALLOWED_IP_RANGES_ENV} set to an empty value.`,
139+
recovery:
140+
` If you are locked out, start the server with ${ALLOWED_IP_RANGES_ENV} set to an empty value, sign in, `
141+
+ 'clear the stored list with a settings request that sets allowed_ip_ranges to null '
142+
+ '(PUT /api/settings/auth), then remove the variable.',
136143
},
137144
'zh-CN': {
138145
outside: (address) =>
@@ -141,7 +148,9 @@ export const ALLOWED_IP_RANGES_REFUSAL_COPY: Record<SettingsCopyLocale, AllowedI
141148
undetermined:
142149
'服务器无法确定你当前连接所用的地址;只要设置了列表,这样的请求都会被拒绝,你自己下一次登录也不例外。'
143150
+ '请在服务器能确定地址的连接上保存。',
144-
recovery: `如果已被锁在外面,可将环境变量 ${ALLOWED_IP_RANGES_ENV} 设为空值后重启服务器。`,
151+
recovery:
152+
`如果已被锁在外面:将环境变量 ${ALLOWED_IP_RANGES_ENV} 设为空值后重启服务器并登录,`
153+
+ '用一次把 allowed_ip_ranges 设为 null 的设置请求(PUT /api/settings/auth)清除已保存的列表,再去掉该环境变量。',
145154
},
146155
'ja-JP': {
147156
outside: (address) =>
@@ -150,7 +159,10 @@ export const ALLOWED_IP_RANGES_REFUSAL_COPY: Record<SettingsCopyLocale, AllowedI
150159
undetermined:
151160
'サーバーは現在の接続のアドレスを確定できません。リストが設定されている間はそのようなリクエストはすべて拒否され、'
152161
+ 'ご自身の次回のサインインも同様です。サーバーがアドレスを確定できる接続から保存してください。',
153-
recovery: `締め出された場合は、環境変数 ${ALLOWED_IP_RANGES_ENV} を空の値にしてサーバーを再起動してください。`,
162+
recovery:
163+
`締め出された場合は、環境変数 ${ALLOWED_IP_RANGES_ENV} を空の値にしてサーバーを再起動してサインインし、`
164+
+ 'allowed_ip_ranges を null にする設定リクエスト(PUT /api/settings/auth)で保存済みのリストを消去してから、'
165+
+ '環境変数を削除してください。',
154166
},
155167
'es-ES': {
156168
outside: (address) =>
@@ -161,7 +173,10 @@ export const ALLOWED_IP_RANGES_REFUSAL_COPY: Record<SettingsCopyLocale, AllowedI
161173
'El servidor no puede determinar la dirección desde la que está conectado y, mientras haya una lista, rechaza '
162174
+ 'todas esas solicitudes, incluido su propio próximo inicio de sesión. Guarde la lista desde una conexión cuya '
163175
+ 'dirección el servidor pueda determinar.',
164-
recovery: ` Si queda bloqueado, inicie el servidor con ${ALLOWED_IP_RANGES_ENV} con un valor vacío.`,
176+
recovery:
177+
` Si queda bloqueado, inicie el servidor con ${ALLOWED_IP_RANGES_ENV} con un valor vacío, inicie sesión, `
178+
+ 'borre la lista guardada con una solicitud de ajustes que ponga allowed_ip_ranges a null '
179+
+ '(PUT /api/settings/auth) y después quite la variable.',
165180
},
166181
'zh-Hant': {
167182
outside: (address) =>

‎packages/services/service-settings/README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ a resolver that layers `OS_* env > Tenant > User > Default`.
3030
to underscores) takes precedence over any stored value. For example,
3131
`mail.smtp_host` resolves from `OS_MAIL_SMTP_HOST`, and
3232
`ai.log_prompts` resolves from `OS_AI_LOG_PROMPTS`.
33-
Such fields are returned with `source: 'env', locked: true` and writes
34-
(service or REST) fail with HTTP 409.
33+
Such fields are returned with `source: 'env', locked: true`. Every write
34+
(service or REST) but a clear (`null`) fails with HTTP 409. A clear removes
35+
the value stored beneath the override and changes nothing in force: the key
36+
still answers the env value, and falls back to its default once the variable
37+
is removed.
3538

3639
## Encryption
3740

0 commit comments

Comments
 (0)