Skip to content

Commit dc3548d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-7afd2e
2 parents 878ecc0 + c24203f commit dc3548d

26 files changed

Lines changed: 1492 additions & 284 deletions

‎.changeset/24583-7b3-organizationless-counter-retired.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ Clause-②: yes (narrowing)
1515

1616
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D9, stage 8d).
1717

18-
- **`os serve`: a production server boot whose auth did not compose for a missing secret is refused.** With the `auth` tier on and no `OS_AUTH_SECRET` (outside `--dev`), `serve` used to skip AuthPlugin with a warning and serve anyway. That boot had no security model: anonymous object data is always denied, so its data API was unusable. It also had no organization object, so its inline seed wrote rows with `organization_id` NULL and drew their autonumbers from the `__global__` counter. Measured on `examples/app-showcase` before this change: health 200, two NULL-organization rows and a `__global__` counter row on a fresh install. The boot now stops before the kernel starts, so nothing is written, and the message names both fixes. Unchanged: `--no-server` (kernel only) still boots with the warning, a development boot still uses its fallback secret, and the cloud host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's) still only warns. `os start` never reaches the refusal: it always supplies a secret to the `serve` child it spawns.
18+
- **`os serve`: a production server boot whose auth did not compose for a missing secret is refused.** With the `auth` tier on and no `OS_AUTH_SECRET` (outside `--dev`), `serve` used to skip AuthPlugin with a warning and serve anyway. That boot had no security model: anonymous object data is always denied, so its data API was unusable. It also had no organization object, so its inline seed wrote rows with `organization_id` NULL and drew their autonumbers from the `__global__` counter. Measured on `examples/app-showcase` before this change: health 200, two NULL-organization rows and a `__global__` counter row on a fresh install. The boot now stops before the kernel starts, so nothing is written, and the message names both fixes. A kernel-only boot (`--no-server`) is refused the same way: it runs the same seed, so it would write the same ownerless rows. Unchanged: a development boot still uses its fallback secret, and the cloud host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's) still only warns. `os start` never reaches the refusal: it always supplies a secret to the `serve` child it spawns.
1919
- **`@objectstack/driver-sql` (and `@objectstack/driver-turso`'s remote face, which calls the same rule): the counter refuses an organization-less row on a tenant table.** Wherever the driver registered the organization object (`sys_organization`), a `create` / `bulkCreate` / `upsert` on an object with a tenant column, whose row carries no organization and whose options carry no `tenantId`, is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500), the code the engine already answers for the same write. It is refused before any counter row is read or written, and nothing is written. New exports: `OrganizationlessCounterRefusal`, `ORGANIZATIONLESS_COUNTER_REFUSAL_CODE`, `ORGANIZATIONLESS_COUNTER_REFUSAL_STATUS`, `renderOrganizationlessCounterRefusal`. Unchanged: `__global__` stays the counter key of an object with no tenant column, and of a composition that registers no organization object on the driver (a lean `@objectstack/objectql/core` embedding), where no organization can exist. Counter rows a 17.x install filed under `__global__` for a tenant table stay in `_objectstack_sequences`; the counter no longer reads them, and `os migrate duplicates` still reports them.
2020
- **`@objectstack/objectql`: `GLOBAL_TENANT` is no longer exported**, and `SystemWriteOrganizationRequiredError`'s message no longer describes the `__global__` counter as where the write would land. The code, status and fields are unchanged.
2121
- **Texts.** The lint explanation of `unique/legacy-organization-composite`, the `UniqueScopeSchema` TSDoc and the error-code ledger now say that a NULL-organization row is residue an earlier release wrote, not something a new write produces.
2222

2323
**The remedy.**
24-
- A production boot refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes), or run kernel-only with `--no-server`.
24+
- A production boot refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes), or compose auth by other means (mount `AuthPlugin` in `plugins`). Running kernel-only with `--no-server` does not avoid the refusal.
2525
- A direct driver write refused by the counter: carry the organization — `driver.create(object, { ...row, organization_id: orgId })` or `driver.create(object, row, { tenantId: orgId })` — or write through the engine, which derives it under `single`.
2626

2727
The retired export:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os lint` and `os i18n check` ask for the label of a navigation item another package contributes into an app (#24993)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A package can add items to an app it does not own through `manifest.navigationContributions`. The runtime merges those items into the app's navigation and shows each one's label from `apps.APP.navigation.ID.label`, where `APP` is the app the item is contributed to. `os validate` already accepted that key. But the i18n extractor only read each app's own `navigation`, so a contributed item's key was never expected: deleting its translation left `os lint --strict` and `os i18n check` green. The same deletion on an item the app declares itself was reported.
11+
12+
Now a contributed item is expected under its target app, from either place a contribution is declared: the stack's own `manifest`, or a package's manifest inside a `composeStacks(…, { manifest: 'preserve' })` artifact. Its source text is its own `label`. A contributed group's children are expected as an app's own group children are. An item the app also declares keeps the app's entry, and a contribution read from two places is expected once.
13+
14+
**What you may see.** A project that contributes navigation and declares a locale beyond its default now gets an `i18n/missing-navigation` finding for each contributed item that locale's pack does not translate. It is a warning by default, which `os lint --strict` fails on. Under `os lint --i18n-strict` or `os i18n check --strict` it is an error, like every other missing translation. To clear them, translate the key under the target app in that locale's pack, for example:
15+
16+
```ts
17+
apps: {
18+
crm_enterprise: {
19+
navigation: { nav_case: { label: '工单' } },
20+
},
21+
},
22+
```
23+
24+
`os i18n extract` now writes these slots too, and keeps the strings a pack already translates. No key, schema or export changes: the key was already part of the translation contract.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
---
4+
5+
fix(service-messaging): the heading of a notification digest of two or more items is rendered in its recipient's language, not as the English "You have N notifications" for everyone
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** A recipient whose notification preference digests `daily` or `weekly` received each window's digest headed "You have N notifications" in English, whatever their language, even when every item under it was rendered in their language.
11+
- **Now.** The heading is keyed (`messages.notificationDigestTitle`, with a `{{count}}` hole) in `@objectstack/service-messaging`'s own translation bundle, in en, zh-CN, ja-JP and es-ES. It is rendered at delivery by the same keyed-text renderer as the digest's items (`EmitInput.keyedText`), in the same locale read: the recipient's `sys_user.locale`, then the deployment default. A zh-CN recipient reads "您有 3 条通知".
12+
- **Unchanged.** The English sentence stays the literal fallback, byte for byte: with no `i18n` service, with no renderer composed, or when neither the recipient's language nor the deployment default has a pack. A one-item digest is still titled by its item. No export, option or type changes.
13+
- **Cost.** With an `i18n` service registered, each digest of two or more items now reads its recipient's locale once, even when none of its items is keyed, because its heading always is.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli)!: a production `os serve` that composes no organization object is refused whether or not `--server`, so `--no-server` no longer writes ownerless rows
6+
7+
Clause-②: yes (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) A boot-time refusal in `os serve`: no key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. No export, type, flag or error code is removed or renamed; `--no-server` keeps its meaning (no HTTP server plugin) and only stops being a way round the auth refusal. The remedy is a deployment one, stated below. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this boot (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
11+
12+
**BREAKING** boot-refusal narrowing (ADR-0131 D1, "NULL is not a state", and D9). It changes which production boots start, not what a metadata author can write, so it ships as `patch` with this banner.
13+
14+
- FROM: a production `os serve --no-server` (no `--dev`; `os start` has no `--no-server` flag) booted the kernel when the stack's `auth` tier was on and no auth secret resolved, or when the stack mounted no auth at all (the `auth` tier off, `--preset minimal` included, with no `AuthPlugin` of its own), warning `⚠ AuthPlugin skipped — set OS_AUTH_SECRET …`. Neither boot composes an organization object, so every row it wrote, its inline seed first, landed with no organization. Measured on an 18.0.0-born store: a deleted seed row was written back with `organization_id` NULL. On a fresh store the seed failed instead, every write refused by the NOT NULL tenant column. Both refusal texts named "run with --no-server" as a remedy.
15+
- TO: both boots are refused whether or not `--server`, before any plugin starts. That includes a production `os serve --preset minimal`: the minimal preset composes no `auth` tier, so unless the stack mounts `AuthPlugin` itself it is refused, kernel-only or not (with `--server` it already was), so nothing is written and the database is never opened. That covers a `--no-server` migrate-and-exit job (`OS_MIGRATE_AND_EXIT=1`), which runs the same kernel bootstrap. Neither refusal text offers `--no-server` any more: each says a kernel-only boot does not avoid it.
16+
17+
**Unchanged:** `os dev` and `--dev` (a development boot resolves its fallback secret, and a no-auth development boot is still refused only when it serves); an embedded kernel (`createStandaloneStack`, `@objectstack/objectql/core`), which never runs this command; and the cloud runtime's host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's), which keeps its warn-only path. `os start` always hands its `serve` child an auth secret, so it reaches the no-auth case only.
18+
19+
**The remedy.**
20+
- Refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes, e.g. `OS_AUTH_SECRET=$(openssl rand -hex 32)`).
21+
- Refused because the stack mounts no auth: enable auth (add the `auth` tier, or mount `AuthPlugin` in `plugins`) and set `OS_AUTH_SECRET`. Declaring the stack's own objects `tenancy: { enabled: false }` does not lift this refusal: the platform's own system objects are tenant-scoped, so a production stack with no organization object would still write ownerless rows.
22+
- A cloud migrate-and-exit job (`OS_MIGRATE_AND_EXIT=1`) must carry `OS_AUTH_SECRET` in production; without it the job is now refused, naming the variable.
23+
24+
**Release-notes correction.** The 18.0 release page says a production `os serve` with no auth secret can "serve without the data API (`--no-server`)". That remedy is withdrawn: `--no-server` is refused the same way. The pending changeset `24583-7b3-organizationless-counter-retired` is corrected in this change to match.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: a `controlled_by_parent` detail whose master the engine cannot compose a read over now refuses the boot, instead of booting and failing at read time
6+
7+
Clause-②: yes (narrowing)
8+
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) A boot refusal of one deployment composition: no key of any metadata schema is added, removed, renamed or re-shaped, and `ObjectSchema.parse` accepts exactly what it accepted before, so `objectstack migrate meta` has nothing to rewrite and no tombstone is owed. Both facts the refusal reads are deployment facts (datasource mapping, a package's default datasource, lifecycle routing, the middleware registrations), and the remedy is the author's choice among three, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this composition (not already-registered); and nothing exported is removed or narrowed, one engine method is added (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
**BREAKING** (a narrowing of what boots). A stack declaring a `controlled_by_parent` detail over either class of master below booted before this change. It is now refused at `kernel:bootstrapped`, before `kernel:listening` opens the socket. It ships as `patch` because no metadata protocol shape moves.
14+
15+
ADR-0139 D5.2 and D5.3 record two classes of master the engine composes no `ReadableIds` leaf over, and refuses by name at read time:
16+
17+
- **(a)** a master bound to another datasource than the detail. No driver evaluates a subquery across two datasources.
18+
- **(b)** a master whose rows an object-scoped read middleware narrows (`registerMiddleware(fn, { object })`), for example `sys_attachment`, `sys_comment`, `sys_activity`, `sys_audit_log`, `sys_mail_message` or `sys_approval_request`. That WHERE is built at read time and is no part of the master's read scope, so a leaf over it would be wider than a read of the master.
19+
20+
A `controlled_by_parent` detail derives its access from its one required master (ADR-0055 §1). A detail over such a master is a declaration the runtime cannot honour, so under ADR-0049 it is no longer admitted. `ObjectQLPlugin` refuses the boot with `INVALID_METADATA` / 422, naming each detail, its master field, the master and the class, and the remedies below.
21+
22+
- The check is judged at `kernel:bootstrapped`, not at `kernel:ready` or `registerApp`. Several platform plugins (mailbox, audit, attachment storage) register their object-scoped read middleware from their own `kernel:ready` handler, which runs after the engine's, so only then are both facts final.
23+
- It reuses the read path's own predicate (`hasObjectMiddleware` and the datasource binding resolution), so the boot rule and the read rule cannot drift. The read-time refusal is unchanged.
24+
- New engine method: `ObjectQL#findUnservableControlledByParentMasters()` lists the offending details.
25+
26+
No producer is known to be affected. HotCRM's six `controlled_by_parent` objects all sit on one datasource over masters with no object-scoped read middleware, and no in-repo declaration has either shape.
27+
28+
### Remedy
29+
30+
Pick one, for each detail the refusal names:
31+
32+
- bind the detail and its master to one datasource (the same `datasource`, mapping rule or package default);
33+
- or choose a master that no object-scoped read middleware narrows;
34+
- or declare the detail with a sharing model other than `controlled_by_parent`, and grant its access directly.

‎content/docs/deployment/cli.mdx‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -354,20 +354,28 @@ os serve # Default: port 3000
354354
os serve -p 4000 # Custom port
355355
os serve --dev # Development mode (pretty logs, devPlugins)
356356
os serve --dev --ui # Dev mode with Console UI
357-
os serve --no-server # Skip HTTP server (kernel only)
358-
os serve --preset minimal # Skip auto-loaded auth/i18n/ui plugins
357+
os serve --no-server # Skip HTTP server (kernel only; production still needs OS_AUTH_SECRET)
358+
os serve --preset minimal # Skip auto-loaded auth/i18n/ui plugins (production: refused unless the stack mounts AuthPlugin)
359359
```
360360
361361
**Options:**
362362
- `-p, --port <port>` — Server port (default: env `OS_PORT` or `3000`)
363363
- `--dev` — Development mode (loads devPlugins, pretty logging)
364364
- `--ui / --no-ui` — Toggle Console UI at `/_console/` (default `on`)
365-
- `--server / --no-server` — Toggle HTTP server plugin
365+
- `--server / --no-server` — Toggle HTTP server plugin. `--no-server` serves no
366+
data API but still boots the kernel and writes its seed, so a production boot
367+
(no `--dev`) is held to the same auth rule as a server boot: a stack that
368+
enables auth needs `OS_AUTH_SECRET`, and a stack that mounts no auth is
369+
refused. Both are refused before any plugin starts, and so is a
370+
`--no-server` migrate-and-exit job (`OS_MIGRATE_AND_EXIT=1`) without the
371+
secret.
366372
- `--prebuilt` — Skip esbuild / `bundle-require` and load the config as
367373
native ESM (use this in production builds where the config is already
368374
pre-compiled)
369375
- `--preset minimal | default | full` — Override the auto-registration
370-
tier (see below)
376+
tier (see below). `minimal` composes no `auth` tier, so a production boot
377+
(no `--dev`) with it is refused, with or without `--server`, unless the
378+
stack mounts `AuthPlugin` in its own `plugins`.
371379
372380
**Probes.** A served process exposes `GET /api/v1/health` (liveness — process
373381
only, and deliberately blind to configuration and credentials, so a
@@ -386,14 +394,18 @@ only gate the **automatic** registration of optional plugins.
386394
387395
| Preset | Tiers | Auto-loaded optional plugins |
388396
|:---|:---|:---|
389-
| `minimal` | `core` | none |
397+
| `minimal` | `core` | none — in production, refused unless the stack mounts `AuthPlugin` itself (see below) |
390398
| `default` *(default)* | `core`, `i18n`, `ui`, `ai`, `auth` | i18n service, Console UI, AI service, Auth + Security + Audit |
391399
| `full` | `core`, `i18n`, `ui`, `ai`, `auth` | currently an alias of `default` — same tiers, no additional plugins |
392400
393-
The `auth` tier requires `OS_AUTH_SECRET` to be set; otherwise `AuthPlugin`
394-
is skipped with a yellow warning and the `/api/v1/auth/*` endpoints will
395-
return 404. (In `--dev` mode the CLI falls back to an insecure local secret so
396-
login works out of the box.) To take full control, set `tiers` on the stack config:
401+
The `auth` tier requires `OS_AUTH_SECRET` in production: without it the boot
402+
is refused before any plugin starts, with or without `--server`, because a boot
403+
with no `AuthPlugin` has no organization and every row it wrote would have no
404+
owner. A production boot whose tiers leave `auth` out (`--preset minimal`
405+
included) is refused the same way unless the stack mounts `AuthPlugin` itself.
406+
In `--dev` mode the CLI falls back to an insecure local secret, so login works
407+
out of the box. (The cloud runtime's host kernel, where auth is each project's,
408+
only warns.) To take full control, set `tiers` on the stack config:
397409
398410
```typescript
399411
import { defineStack } from '@objectstack/spec';

0 commit comments

Comments
 (0)