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
Commit 0b3e83c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): a dev server that refuses a drifted Console answers `503` at `/_console/` naming the rebuild, instead of a bare `404` (#22420)
6
+
7
+
Clause-②: no
8
+
9
+
A refused Console mount answers `503` naming the remedy instead of the router's bare `404` on the same routes, as the not-built position already does. No accepted input, key or export changes.
10
+
11
+
`os dev` refuses to mount a Console whose built `dist/.objectui-sha` differs from the repository's `.objectui-sha` pin. The terminal printed the refusal, but nothing was mounted, so `GET /_console/` and `GET /` answered `404 ENDPOINT_NOT_FOUND`, the same answer as a wrong URL. That boot now mounts the same routes as the not-built answer:
12
+
13
+
-`GET /_console/` and every path under it answer `503` (`text/plain`, `no-store`). The body is the refusal's own text: both objectui SHAs, why the stale build is refused, `pnpm objectui:build`, and the `OS_ALLOW_CONSOLE_DRIFT=1` escape hatch. The pin file is named without its absolute path.
14
+
-`GET /` and `GET /_console` redirect to `/_console/`, as they do when the Console is served.
15
+
16
+
The same answer now covers an install where `@objectstack/console` does not resolve at all. That boot used to print nothing and mount nothing. It now prints ``Console package `@objectstack/console` not found``, followed by the reinstall sentence an install with no `dist/` already gets. `/_console/` answers `503` saying the Console is not installed.
17
+
18
+
`--no-ui` and `--no-console` still mount nothing, so those routes answer `404`.
feat(service-storage)!: the `sys_file` scope option `public` is retired, and rows already stored with it are rewritten to `user` by a one-time operator sweep (#22443)
7
+
8
+
Clause-②: yes (narrowing)
9
+
10
+
<!-- adr-0087: not-required (already-registered storage-scope-public-retired) the retirement of the storage scope public is that step-18 entry; this changeset retires the same value from the stored sys_file vocabulary and rewrites its stored rows through an operator sweep -->
11
+
12
+
**BREAKING** — an accept-set narrowing on the `sys_file` object's `scope` select, shipped as `minor` under the launch-window convention for accept-set narrowings. It completes the retirement registered as `storage-scope-public-retired`: `StorageScopeSchema` and both upload doors already refuse `public`, and now the stored vocabulary does too.
13
+
14
+
### What changes
15
+
16
+
-**`sys_file.scope` no longer lists `public`.** Its options are `user`, `tenant`, `private`, `temp` and `attachments`. The engine refuses a write of `public` to the column as it refuses any undeclared option: `VALIDATION_FAILED`, `invalid_option` on `scope`. No access behaviour changes: the only scope value any code reads is `attachments`, and whether a file can be read before sign-in is decided by `acl: 'public_read'` alone.
17
+
-**Rows already stored with scope `public` are rewritten to `user`**, by a one-time sweep the operator runs (below), never at boot. `user` is the scope the upload doors and the field-reference copy path default to, and no reader tells it apart from `public`. One column moves: the storage key keeps its `public/` prefix, the acl and ownership columns are untouched, and no byte moves in the storage backend.
18
+
19
+
### FROM → TO
20
+
21
+
| before | write instead |
22
+
| --- | --- |
23
+
| a `sys_file` row written with `scope: 'public'`|`scope: 'user'`, or no scope; and `acl: 'public_read'` on the row if the file must be readable before sign-in |
24
+
| rows already stored with `scope: 'public'`| nothing by hand: the sweep below rewrites them to `user`|
25
+
26
+
**The one-line fix: stop writing scope `public` on `sys_file`, and run the sweep once on each deployment that stored it.**
27
+
28
+
### The operator step: run the sweep once after upgrading
29
+
30
+
Until the sweep has run on a deployment that stored `public` rows, a record write that names a `public` file another field already owns is refused. The field-reference copy path copies the file into a new row with the source row's scope, the engine refuses `public` there, and the write fails with `ERR_FILE_REFERENCE_COPY`. The data REST doors answer that `500 INTERNAL_ERROR` ("Internal server error"); the server log carries the full sentence, which ends `Scope must be one of: user, tenant, private, temp, attachments`. Reads, downloads and updates that do not write `scope` are unaffected.
31
+
32
+
The sweep ships in `@objectstack/service-storage` itself: `planSysFilePublicScopeBackfill`, `applySysFilePublicScopeBackfill`, `runSysFilePublicScopeBackfill` and `formatSysFilePublicScopeBackfillReport` are exported from the package, with their report types. It has the shape of the `sys_file` organization backfill: an operator step, never a boot hook. Import it from the package you upgraded to and run it server-side, from a context that holds the engine. It is a dry run first, and by default:
33
+
34
+
```ts
35
+
import {
36
+
planSysFilePublicScopeBackfill,
37
+
applySysFilePublicScopeBackfill,
38
+
formatSysFilePublicScopeBackfillReport,
39
+
} from'@objectstack/service-storage';
40
+
41
+
const plan =awaitplanSysFilePublicScopeBackfill(engine); // counts, writes nothing
const applied =awaitapplySysFilePublicScopeBackfill(engine, plan); // one scope write per row
44
+
console.log(formatSysFilePublicScopeBackfillReport(applied)); // keep it: it is the rollback list
45
+
```
46
+
47
+
It counts before it writes (`scanned`), writes nothing where the count is zero, and is idempotent: every write moves its row out of `scope = 'public'`, so a second run scans and writes zero. A row whose write fails is reported, never retried, and is picked up by the next run.
48
+
49
+
### Rollback
50
+
51
+
The inverse is `user` back to `public` on exactly the ids the applied report lists. This release cannot write it through the engine, which refuses `public` like any undeclared option. So the inverse goes with a code rollback: on the previous release, which still declares the option, write `scope = 'public'` back to those ids, through the engine there or as one raw driver statement against `sys_file` filtered to them.
52
+
53
+
### The step-18 entry says so too (`@objectstack/spec`)
54
+
55
+
The D3 entry `storage-scope-public-retired`, which the protocol upgrade guide is built from, said that files already stored with scope `public` are not touched. That no longer holds for the `scope` column: after the sweep they read `user`. Its reason now names the `sys_file` option's retirement and the operator sweep above, and says that the rewrite changes no access: the storage key and the bytes stay as they are, and those files still download exactly as before. Its acceptance criteria add the sweep's end state: a dry run on each deployment scans zero `sys_file` records with scope `public`.
feat(i18n): a screen field's option labels and a flow's terminal toasts have translation keys (`flows.<flow>.screens.<node_id>.fields.<field>.options.<value>`, `flows.<flow>.successMessage` / `.errorMessage`)
8
+
9
+
Clause-②: yes (narrowing)
10
+
11
+
<!-- adr-0087: not-required (no-migration-prescription) A value-domain narrowing on one metadata shape, ScreenFieldConfigSchema's options array. No spec key, export, option, config field or stored shape is removed, renamed or re-shaped, so there is no tombstone and no rename for `objectstack migrate meta` to apply. What narrows is which option-value combinations one screen field accepts: two options whose values read the same as text are now refused at parse. There is no lossless rewrite for such a pair (nothing in the metadata says which of the two values the author meant, or what the other should become), and the refusal itself names the pair and the fix at every door that parses a flow. Measured reach is 0: no flow in this repository's examples carries such a pair. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered); and no published interface or type is narrowed, the new members being additive (not runtime-interface-only / type-surface-only). -->
12
+
13
+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. Two options of one screen field whose values read the same as text — `1` and `"1"`, `true` and `"true"`, or a repeated value — are now refused when the flow is parsed (`FlowSchema`, `objectstack validate`, the metadata save door, `registerFlow`), at the second option's `value`. Neither the option's label translation (keyed by the value as text) nor the console's select can tell such a pair apart. To fix a refused flow, give each option of the field a value that stays distinct as text, or drop the duplicate. Measured reach in this repository: 0 — no flow in its examples carries such a pair.
14
+
15
+
-**What is new.** A fully translated screen flow no longer has to keep its select options and its completion or failure toast in the source language. Translate an option's label under `flows.<flow_name>.screens.<node_id>.fields.<field_name>.options.<value>`, and the flow's own toasts under `flows.<flow_name>.successMessage` and `flows.<flow_name>.errorMessage`.
16
+
-**How an option is addressed.** By its value read as text: `String(value)`, so the option `{ value: 1, label: 'Tier 1' }` is translated under `options.1` and `{ value: true, … }` under `options.true`. `flowScreenFieldOptionKey(value)` (`@objectstack/spec/automation`) spells the key. Not by position (reordering options would re-point every translation), and not by an object field's options (a screen field declares no such binding).
17
+
-**One new refusal**, the narrowing the BREAKING note above describes: option values of one screen field must stay distinct as text, because the label translation is keyed by them.
18
+
-**The toasts translate only where the flow authors one.** A bundle cannot add a toast to a flow that declares none; `objectstack validate` refuses such a key (`translation-target-unknown`, error).
19
+
-**Where it is read.**`translateFlow` (`@objectstack/spec/system`) overlays both, and `resolveFlowScreenFieldOptions(bundle, flowName, nodeId, field)` resolves a served screen field's options. The console's flow runner reads them in a following release; until then a translation is stored and the authored text is drawn.
20
+
-**`objectstack validate`** warns on an option key that names no declared option of the field, or names it by its label (`translation-option-key-unknown`).
21
+
-**`os i18n extract` and the coverage gate** now scaffold and demand each screen field's option labels, the failure toast of every flow that declares one, and the completion toast of a flow with a screen, in the `flow` bucket (`i18n/missing-flow`). A project that declares `supportedLocales` gets one missing-key issue per locale for each until it translates them.
22
+
-**Still not keyed: a screen's `description`.** It is a `{{ }}` template the server renders per run, so its translation has to be chosen before that render; a translated `description` is still refused by the schema with that reason.
Copy file name to clipboardExpand all lines: content/docs/permissions/tenant-audit-census.mdx
+18-18Lines changed: 18 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122
122
123
123
The same holds twice over for the context. An options argument spelled as a
124
124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125
-
forwarding shim cannot, and **53 of the 240 sites are spelled that way**. A
125
+
forwarding shim cannot, and **53 of the 241 sites are spelled that way**. A
126
126
context resolved from an inline literal or a local `const` can be tested for
127
127
`isSystem`; one arriving from a helper call cannot.
128
128
@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187
187
188
188
| carried figure | where it survives | this census |
189
189
| :--- | :--- | ---: |
190
-
| 175 write call sites | quoted in the merged changeset |**240**|
190
+
| 175 write call sites | quoted in the merged changeset |**241**|
191
191
| 24 carrying no tenant context | quoted in the merged changeset |**2** provable and tenancy-enabled; **32** more whose options argument is unreadable |
192
-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card |**159 of 240** decidable, **81** undecidable |
193
-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration**|**not reproduced**: 128 decidably elevated, 0 decidably not, 104 undecidable |
192
+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card |**160 of 241** decidable, **81** undecidable |
193
+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration**|**not reproduced**: 128 decidably elevated, 0 decidably not, 105 undecidable |
194
194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195
195
196
196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -201,17 +201,17 @@ at any commit.
201
201
202
202
Two structural facts do plausibly widen this reading against any hand or regex
203
203
one, and both are counted in the generated tables below: the 53 sites reached
204
-
through an erased (`any`) receiver, and the 52 that name their object through a
204
+
through an erased (`any`) receiver, and the 53 that name their object through a
205
205
`const` rather than inline. An instrument that read either the way a person does
206
206
would report a smaller number and would not say so.
207
207
208
208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209
209
figure has no surviving corroboration anywhere in the tree.** This census reads
210
-
128 of 240 (53%) as decidably elevated, with 104 more whose elevation is a
210
+
128 of 241 (53%) as decidably elevated, with 105 more whose elevation is a
211
211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212
212
answer is that a static reading cannot settle it.
213
213
214
-
⇒ **Cite `2 / 240`, and say what it is**: the sites whose options argument was
214
+
⇒ **Cite `2 / 241`, and say what it is**: the sites whose options argument was
215
215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216
216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217
217
without tenant context" — **32 further sites** have an options argument this
@@ -223,29 +223,29 @@ cannot read, and they are neither in nor out.
223
223
224
224
| what | count |
225
225
| :--- | ---: |
226
-
| write call sites on the application surface |**240**|
227
-
| …whose object name is statically decidable |159|
226
+
| write call sites on the application surface |**241**|
227
+
| …whose object name is statically decidable |160|
228
228
| …whose object name is chosen at run time | 81 |
229
-
| …against an object with tenancy ENABLED |158|
229
+
| …against an object with tenancy ENABLED |159|
230
230
| …against an object that declares tenancy off | 1 |
231
-
| threading a tenant context |179|
231
+
| threading a tenant context |180|
232
232
| PROVABLY carrying none (options read, no context key) |**8**|
233
233
| …of those, against a decidably tenancy-enabled object |**2**|
234
234
| options argument UNREADABLE — may or may not carry one | 53 |
235
235
| …of those, against a decidably tenancy-enabled object | 32 |
236
236
| threading a decidably ELEVATED (`isSystem`) context | 128 |
237
237
| threading a context that is decidably NOT elevated | 0 |
238
-
| threading a context whose elevation is a run-time fact |104|
238
+
| threading a context whose elevation is a run-time fact |105|
239
239
240
240
| how the instrument reached the site | count |
241
241
| :--- | ---: |
242
-
| receiver carried a readable engine type |187|
242
+
| receiver carried a readable engine type |188|
243
243
| receiver erased, placed by the object NAME | 33 |
244
244
| receiver erased, placed by an `object: string` PARAMETER | 15 |
245
245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246
246
247
247
| object name spelled inline | 107 |
248
-
| object name spelled through a `const`|52|
248
+
| object name spelled through a `const`|53|
249
249
| object name is an `object: string` parameter | 19 |
250
250
| object name is some other run-time expression | 62 |
251
251
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297
297
their values are not compared. The reasoning, and the measurement behind it,
298
298
are in `scripts/check-tenant-audit-census.mjs`.
299
299
300
-
Measured on 2026-10-08 at `715ba6f44`.
300
+
Measured on 2026-10-09 at `5551f8d8d`.
301
301
302
302
| corpus scale (not enforced) | count |
303
303
| :--- | ---: |
304
-
| tracked non-test sources scanned |623|
305
-
| engine-shaped types recognised |71|
306
-
| declared objects in the registry |117|
304
+
| tracked non-test sources scanned |625|
305
+
| engine-shaped types recognised |72|
306
+
| declared objects in the registry |116|
307
307
| same-named calls subtracted as non-engine | 162 |
0 commit comments