Skip to content

Commit 4981e64

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23974-package-doors-convert-forward
# Conflicts: # packages/cloud-connection/src/marketplace-install-local-plugin.ts # packages/metadata-protocol/src/protocol.ts # packages/services/service-package/src/index.ts
2 parents cbec0ea + e246f06 commit 4981e64

187 files changed

Lines changed: 7814 additions & 613 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): an `os serve` boot that composes no HTTP server now stays up until it is stopped, as its banner's `Press Ctrl+C to stop` says
6+
7+
Clause-②: no
8+
9+
- **Before.** `os serve --no-server --preset minimal`, in production with no storage driver (no `OS_DATABASE_URL`), printed the ready banner ending `Press Ctrl+C to stop` and then exited 0 by itself, about 50 ms later, with no signal and without the kernel shutting down. The same command stayed up whenever the storage driver kept a connection pool (`--dev --no-server`, or `--no-server` with `OS_DATABASE_URL` set). Measured, that pool's reaping interval was the only thing keeping those boots alive, so whether a kernel-only boot stayed up depended on the driver.
10+
- **After, for an operator or supervisor of a boot with no HTTP server composed** (`--no-server` with no config-mounted transport, or a transport plugin that did not load). The process stays up after its banner, whatever the driver, until SIGINT, SIGTERM or SIGQUIT. The kernel's own signal handling then drains it and exits with the kernel's code, 0 on a clean shutdown, as on a server boot. No signal listener is added.
11+
- **Why "stay up" and not "run and exit".** The banner says `Press Ctrl+C to stop`. The kernel is built to own the process: a signal drains it, then exits it. The platform's background loops never keep a process alive by themselves, so the host decides the lifetime. The run-and-exit kernel boot already exists, and is explicit: `OS_MIGRATE_AND_EXIT=1`. Every in-repo kernel-only boot that is meant to exit sets it, and every other one is stopped by a signal.
12+
- **Unchanged.** A boot with an HTTP server composed, including a `--no-server` boot whose config mounts its own `HonoServerPlugin`. `OS_MIGRATE_AND_EXIT=1`, which still shuts the kernel down and exits 0 before any banner. The IPC messages and the runtime state file.
13+
- **If you relied on the exit.** A script that ran `os serve --no-server` and waited for the process to end now has to set `OS_MIGRATE_AND_EXIT=1` (bootstrap once, then exit 0), or stop the process with a signal.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `@objectstack/spec/shared` no longer exports `normalizeVisibleWhen` or `VISIBILITY_ALIAS_KEYS`; read `visibleWhen` directly
6+
7+
Clause-②: yes (narrowing)
8+
9+
**BREAKING (TS/API surface):** two exports of `@objectstack/spec/shared` are gone — the function `normalizeVisibleWhen`, which folded the retired `visibleOn` / `visibility` keys onto the canonical `visibleWhen`, and the const `VISIBILITY_ALIAS_KEYS`, which listed those two keys. Use `visibleWhen` directly instead: it is the one conditional-visibility key, and since 18 every schema that had the two old keys (the form field, the form section and the page component) refuses them by name. No schema used the fold after those keys retired, and no caller of either export was found in this repository or in the pinned renderer.
10+
11+
## FROM → TO
12+
13+
| you wrote | write instead |
14+
| --- | --- |
15+
| `import { normalizeVisibleWhen } from '@objectstack/spec/shared'`, then `normalizeVisibleWhen(node)` | read and write `node.visibleWhen`; there is no alias left to fold |
16+
| `import { VISIBILITY_ALIAS_KEYS } from '@objectstack/spec/shared'` | nothing — the two keys it named are refused by the schemas; to reject them yourself, parse with the schema |
17+
18+
**The one-line fix:** delete the import and the call, and read `visibleWhen`.
19+
20+
This supersedes the sentence in this release's `visibleOn` / `visibility` retirement note that says the two exports "stay exported": they leave in the same release.
21+
22+
**What an author who still imports them sees:** `tsc` fails with TS2305 (`Module '"@objectstack/spec/shared"' has no exported member …`); an ESM named import fails at link time with a `SyntaxError` (the module does not provide an export of that name); a CommonJS `require` reads the member as `undefined`.
23+
24+
**Nothing to migrate in metadata.** No authored document ever carried either export, so `os migrate meta` has nothing to visit. The stored-row conversions `view-visibleOn-to-visibleWhen` and `page-component-visibility-to-visibleWhen` are unchanged: a pre-18 `sys_metadata` view or page row that still carries an old key keeps loading with `visibleWhen`.
25+
26+
<!-- adr-0087: registered visibility-alias-fold-unexported -->
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@objectstack/plugin-approvals': minor
3+
---
4+
5+
Clause-②: yes (widening)
6+
7+
feat(plugin-approvals): the approval lists show engine-written display labels, which are the step and flow labels, whom a pending request waits on, the record an action is about, and the slot it was taken as (#23889)
8+
9+
The Requests and Action History lists are generic list views. They show stored columns only, so admins used to read node ids (`manager_review`), `flow:` api names, slot addresses and raw user ids there. The approval engine now writes display columns beside the machine ones, and the list views show those columns. Nothing is removed or renamed. The machine columns (`process_name`, `current_step`, `pending_approvers`, `step_name`, `acted_as`) keep their values, and every query and decision still reads them.
10+
11+
**`sys_approval_request`**, written by `ApprovalService`:
12+
13+
- `process_label`: the flow's label, and `current_step_label`: the Approval node's label. Both are snapshots taken when the request opens, from the labels the flow engine already hands to `openNodeRequest`. Renaming the flow later does not rewrite them.
14+
- `pending_approver_labels`: whom the request waits on, one label per pending slot, joined with `, `. A slot that names a person (a user id, or an email an account carries) gets that account's name. Any other slot (a `position:` or `team:` literal) shows its address. The column is rewritten whenever the slate is written (open, a partial tally, reassign, SLA reassign) and cleared with the slate on every terminal write.
15+
- The list views: `process_label` replaces `process_name` in every view. `current_step_label` replaces `current_step` in `all_requests`, `my_pending` and `submitted_by_me`. `pending_approver_labels` is added to `all_requests` and `my_pending`.
16+
17+
**`sys_approval_action`**: each new row copies these columns from its request:
18+
19+
- `step_label`: the request's step label.
20+
- `object_name` + `record_id`: the request's record pointer (`record_id` declares `referenceVia: 'object_name'`), so a list can name the record.
21+
- `acted_as_label`: `acted_as` labelled the same way as the waiting-on column. It is null when `acted_as` is null, and also while a person slot's name could not be read (see below).
22+
- The list views: `step_label` replaces `step_name` in `recent`, `all_actions` and `by_actor`. `recent` and `all_actions` add the record pointer and show `acted_as_label` in place of `acted_as`.
23+
24+
The inbox door's `process_label` / `step_label` now read the new columns first. They fall back to the config snapshot for rows opened earlier, so what they serve is unchanged.
25+
26+
**Read security.** Every new value is either a column of the request the row's readers can already read, or a name the approvals door already serves those readers for that request: `pending_approver_names`, and the timeline's names. The names come from the same resolver, under the same system read. The generic data door serves both objects only to the callers the approvals door would serve.
27+
28+
**Rows written before this release are filled at boot.** A new boot-time pass runs on `kernel:ready`, after the action-slot repair. It fills the new columns on rows that hold null. Each request is re-read just before it is written, and the write names only the columns that are still null on that re-read row. Whom a request waits on is written only while the request is still pending on the slate its names were read for. A value a live writer stored, such as the boot sweep or another node, is therefore kept. The only window left is the single engine call between the re-read and the write. Every row it fills keeps its own `updated_at`: the write passes the stored value back under `preserveAudit`, so the request lists, which sort on `updated_at`, keep their order. The action-slot repair's writes keep it the same way. Where `plugin-audit` is composed, the first boot after the upgrade writes one system entry in the audit log and one in the record's activity timeline for each row it fills. A second boot writes nothing.
29+
30+
- On every request, the flow and step labels come from the chain the approvals door serves: the authored labels the config snapshot carries, else the humanized machine name. The list and the inbox therefore show the same label for the same row, and no request is left blank.
31+
- On a pending request, whom it waits on is filled.
32+
- On every action, the step label and the record pointer are copied from its request, and its acting-as label is filled. An action whose request no longer exists keeps a null pointer.
33+
- The action-slot repair now labels every `acted_as` it writes.
34+
35+
**Two details.**
36+
37+
- An empty authored label (the spec allows `label: ''`) is stored as the humanized machine name, never `''`.
38+
- A name that cannot be read because the `sys_user` read failed is stored as null, never as the raw slot address. The failure is logged once at `warn`, and the boot pass fills it on the next boot. A slot that names no person still shows its address.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): the SQLite "does not look like a file path" refusal names what the operator can change, instead of telling an operator who set `OS_DATABASE_DRIVER` to set it explicitly
6+
7+
Clause-②: no
8+
9+
`sqliteFilenameFromUrl` raises this refusal, and both hosts print it: the runtime's standalone host with its `[StandaloneStack]` tag, and the CLI's legacy `os serve` path, untagged, as the fatal `UnsupportedDriverError`. The first clause is unchanged and only the advice after it changes. The same URLs are refused, with the same error class and the same exit, and the message still names the URL's scheme and never the URL. Nothing that resolved before resolves differently.
10+
11+
- **A URL with a scheme.** This is how an explicit `sqlite` or `sqlite-wasm` selection (`OS_DATABASE_DRIVER`, `--database-driver`, or the `databaseDriver` option) meets a server URL, an opaque scheme such as `jdbc:`, or a `<scheme>://` nothing recognises.
12+
- FROM: `… (its scheme is "jdbc:"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.` The operator who reads this has already set the driver explicitly: that selection is what led here.
13+
- TO: `… (its scheme is "jdbc:"). Use file:/path/to/db.sqlite to keep the sqlite driver, or use a driver that matches the URL: unset the driver selection (OS_DATABASE_DRIVER, --database-driver or the databaseDriver option) so the URL selects it, or set the selection to the URL's driver.` With the selection unset, a scheme no driver claims, such as `jdbc:`, gets the unsupported-scheme refusal, which lists the supported schemes.
14+
- **A `file:` that names no file** (`OS_DATABASE_URL=file:`, whether the URL or an explicit `sqlite` selection chose the driver).
15+
- FROM: `… (it names no file after "file:"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
16+
- TO: `… (it names no file after "file:"). Name the file after "file:", as in file:/path/to/db.sqlite.` A `file:` URL is a SQLite URL whichever way the driver was chosen, so changing the driver never fixed it.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): the standalone server hands every `/api/v1/keys` verb to the `keys` domain, so an anonymous caller is answered `401 UNAUTHENTICATED` whatever the verb
6+
7+
Clause-②: no
8+
9+
The v18 text for this door reads "`/keys`: `401 UNAUTHENTICATED` for every verb". The `keys` domain takes that decision first, but the standalone server (the one `os dev` and `os serve` run) mounted `/api/v1/keys` for `POST` only. Any other verb never reached the domain: the HTTP adapter answered it itself, with a `405` that named `POST` as the allowed method.
10+
11+
What a `GET`, `PUT`, `PATCH` or `DELETE` call to `/api/v1/keys` is answered on the standalone server:
12+
13+
- **Anonymous caller.** Before: `405 METHOD_NOT_ALLOWED` with `Allow: POST`, from the adapter. Now: `401 UNAUTHENTICATED` in the dispatcher's standard envelope, `{ success: false, error: { code: 'UNAUTHENTICATED', message, httpStatus: 401 } }`.
14+
- **Signed-in caller.** Before: `405 METHOD_NOT_ALLOWED` with `Allow: POST`, from the adapter. Now: still `405 METHOD_NOT_ALLOWED`, answered by the `keys` domain in the same standard envelope (`error.httpStatus: 405`, no `error.details`), and without an `Allow` header.
15+
16+
`POST /api/v1/keys` is unchanged, and so is every answer on a host that reaches the dispatcher through `@objectstack/hono`'s catch-all, which already answered this way. A `HEAD` request follows `GET`: `401` anonymous, `405` signed in. A sub-path such as `/api/v1/keys/<anything>` is still not mounted on the standalone server and is answered `404`.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/cloud-connection': minor
3+
---
4+
5+
fix(cloud-connection)!: the cloud-connection and install-local routes refuse a declared JSON body that does not parse with `400 INVALID_REQUEST`, instead of answering as if no body was sent
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, no stored row is rewritten or deleted, and no exported declaration is added, removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the accept set of four of this package's own routes for one input: a body declared as JSON that does not parse, which used to be read as no body. The remedy is in the requests a client sends, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
`POST /api/v1/cloud-connection/bind/start`, `/bind/poll`, `/install` and `POST /api/v1/marketplace/install-local` read their bodies as `try { body = await c.req.json() } catch { body = {} }`, so a truncated body was answered as if the client had sent none: `/bind/poll` answered `400 "device_code is required"` for a body that carried `device_code` and lacked a closing brace.
14+
15+
What changes for an integrator. A body sent as `application/json` (matched case-insensitively) that is not blank and does not parse: FROM the route's answer for an empty body, TO `400` with `{ "success": false, "error": { "code": "INVALID_REQUEST", "message": "The request body could not be parsed as JSON (it was sent as application/json). It was refused rather than read as an empty body: send a well-formed JSON document, or no body at all." } }`. Install-local refuses it after its sign-in and capability checks and before anything is fetched, registered or written.
16+
17+
**Remedy, in one line.** Send a JSON body as well-formed JSON with `Content-Type: application/json`, or send no body.
18+
19+
**Unchanged.** An empty or whitespace-only JSON body is still no body, and every route answers it as before (for `/bind/poll`, `400 "device_code is required"`). A body that parses, and a body that does not declare JSON, are read as before.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/hono': minor
3+
---
4+
5+
fix(hono)!: `createHonoApp` refuses a declared JSON body that does not parse with `400 INVALID_REQUEST` when the dispatched domain reads it, instead of dispatching it as `{}`
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, no stored row is rewritten or deleted, and no exported declaration is added, removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the accept set of the `createHonoApp` catch-all and its `/auth/*` fallback for one input: a body declared as JSON that does not parse, which used to be dispatched as `{}` and is now refused when a domain reads it. The remedy is in the requests a client sends, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
The `${prefix}/*` catch-all handed `dispatch()` `{}` for a body that did not parse, and the data domain read that as an empty query or an empty update: a truncated `POST /data/OBJECT/query` answered every row the caller could read, and a truncated `PATCH` answered `200` for an update that changed nothing.
14+
15+
What changes for an integrator, on a host that serves `createHonoApp`:
16+
17+
- A `POST`, `PUT` or `PATCH` body sent as `application/json` (matched case-insensitively) that is not blank and does not parse, to a domain that reads the parsed body (the data, metadata and every other domain that takes `body`): FROM that domain's answer for `{}`, TO `400` with `{ "success": false, "error": { "code": "INVALID_REQUEST", "message": "The request body could not be parsed as JSON (it was sent as application/json). It was refused rather than read as an empty body: send a well-formed JSON document, or no body at all." } }`. The domain acts on nothing past its read of the body, and the dispatcher's own gates still answer first. This is the same status and body `@objectstack/plugin-hono-server` answers for the same request.
18+
- The same applies to the `/auth/*` legacy fallback that runs when no auth service is registered, for every method it hands a body (all but `GET` and `HEAD`).
19+
20+
**Remedy, in one line.** Send a JSON body as well-formed JSON with `Content-Type: application/json`, or send no body.
21+
22+
**Unchanged.**
23+
24+
- An empty or whitespace-only JSON body is dispatched as `{}` (an empty query), as before.
25+
- A body that does not declare JSON is read as before: parsed when it is JSON, `{}` when it is not.
26+
- The request handed on beside the body still arrives unread, in every case. A domain that forwards it and reads its own bytes answers exactly as before, a malformed JSON body included: `/webhooks/redeliver` and the inbound automation hooks give their own refusal after their own sign-in or signature check, and the `/approvals/act` form `POST` reads its form.
27+
- The catch-all dispatches `GET`, `HEAD` and `DELETE` with no body, and the `/auth/*` fallback hands `GET` and `HEAD` `{}`, as before.

0 commit comments

Comments
 (0)