Skip to content

Commit ccc8d7e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24036-ledger-a
2 parents 0012c51 + be076da commit ccc8d7e

44 files changed

Lines changed: 2633 additions & 314 deletions

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/lint": patch
3+
---
4+
5+
fix(lint): the twelve flow anti-pattern rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 12 rule ids in `lint-flow-patterns.ts` now carries a `hint` (the CLI's `fix:` line, the runtime finding's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (lengths are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after: `flow-loop-body-uncontained` (800 → 174), `flow-multi-write-unfiltered` (695 → 73), `flow-try-catch-without-catch` (594 → 137), `flow-decision-inclusive-overlap` (437 → 159), `flow-approval-revise-target-not-service-owned` (412 → 76), `flow-time-relative-antipattern` (397 → 163), `flow-date-equality-filter` (379 → 123), `flow-runas-unscoped` (372 → 105), `flow-decision-mode-invalid` (295 → 134), `flow-decision-unconditional-branch` (286 → 149; its other arm, 142, is unchanged), `flow-error-label-not-fault` (239 → 81) and `flow-branch-label-unmatched` (224 → 122).
10+
- **Examples.** `flow-loop-body-uncontained` FROM ``Contain the failure per iteration: put a `try_catch` INSIDE the body and move this node into its `try` region — … A `catch` region cannot be empty … Measured: with it, all 5 items are processed … See content/docs/automation/flows.mdx §"Per-iteration containment".`` (800) TO ``Move this node into the `try` region of a `type: 'try_catch'` node inside the loop body, with `catch: { nodes: [ { id: 'handled', type: 'assignment', label: 'Handled' } ] }`.`` (174). `flow-multi-write-unfiltered` FROM ``Write the constraint you mean into `filter` (e.g. `{ status: 'closed' }` — see examples/app-showcase `showcase_inquiry_purge`, …). If emptying 'lead' really is the intent, keep it: … Distinct from the run-time erased-condition guard, …`` (684–695) TO ``Write the constraint you mean into `filter`, e.g. `{ status: 'closed' }`.`` (73). `flow-runas-unscoped` FROM ``Declare `runAs:'system'` to make the elevation explicit and intended (the run reads/writes every record). A schedule flow cannot scope to a user — … (ADR-0049, ADR-0073 D5)`` (362–372) TO ``Declare `runAs:'system'` to make the elevation explicit and intended (the run reads/writes every record).`` (105). `flow-time-relative-antipattern` used to spell its range query as ``BETWEEN {TODAY()} and {TODAY()+N}``, in the single-brace dialect flows no longer resolve; its line now names the declared `timeRelative` sweep, and its entry spells the range with `$gte` / `$lt`.
11+
- **`os explain RULE_ID` carries the rest.** Each of the 12 ids gains a `RULE_EXPLANATIONS` entry in `@objectstack/lint`, so its `rule:` line now carries the `os explain` pointer. What the fix lines stopped saying is there: which node types count as able to fail and how containment is judged; the two empty `catch` spellings the parse refuses, what the minimal handler may omit, and the measured result with it; when failing fast or retry-then-fail is a legitimate reading; the worked `showcase_inquiry_purge` example, keeping a meant whole-object write, and how the rule differs from the run-time erased-condition guard; when to keep `mode: 'inclusive'` and where `os migrate meta` left it on a flow authored against protocol major 17; what ends the revise window and why ADR-0044's amendment moved it off `wait`; the declared `timeRelative` sweep and the hand-written range-query alternative; why abutting one-day windows match each row once; why a user-less run is refused rather than run unscoped, and the anonymous-endpoint exception (ADR-0138 D2b); that `registerFlow` refuses an invalid `mode` with the same sentence; dropping `type: 'decision'` for a meant fan-out; which failures a `fault` edge routes and which labels read as an error path; and one branching mechanism per decision.
12+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), all 12 ids fire on a `flow` write, FROM → TO measured at the door. As 422 `issues[].hint`: `flow-approval-revise-target-not-service-owned` 412 → 76, `flow-runas-unscoped` 367 → 105 (schedule), 372 → 105 (time-relative) and 362 → 105 (api), `flow-decision-mode-invalid` 295 → 134, `flow-branch-label-unmatched` 224 → 122. As 2xx `advisories[].hint`: `flow-loop-body-uncontained` 800 → 174, `flow-multi-write-unfiltered` 695 → 73 and 684 → 73, `flow-try-catch-without-catch` 594 → 137, `flow-decision-inclusive-overlap` 437 → 159, `flow-time-relative-antipattern` 397 → 163, `flow-date-equality-filter` 379 → 123, `flow-decision-unconditional-branch` 286 → 149, `flow-error-label-not-fault` 239 → 81. No write changes channel.
13+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/service-package': minor
3+
'@objectstack/cloud-connection': minor
4+
---
5+
6+
Clause-②: yes (widening)
7+
8+
fix(service-package,cloud-connection): a stored or installed package body converts forward under ADR-0087 D2 at the `sys_packages` boot hydration and at the marketplace install, and one refused `sys_packages` record no longer stops the hydration of the rest
9+
10+
The registration door (`ObjectQL.convertPackageBodyForward`, `@objectstack/objectql`) already replays the versioned forward conversion for a package body that declares its `engines.protocol` range. Two more doors that load a body at rest now go through that same entry point, before the registry judges the body:
11+
12+
- **`sys_packages` boot hydration** (`PackageServicePlugin.start`). A stored record whose range admits this runtime, built before a retirement (for example a `type: 'home'` page with `isDefault: true`), now hydrates converted, with the engine's named warning, where the registry used to refuse it. The stored row is not rewritten, so it converts again at every boot.
13+
- A record the registry still refuses is reported **by name** at `error`, with the refusal attached, and the records after it keep hydrating. Before, the first refusal threw out of the whole loop to a `debug` line, and every durable package after it was absent from the registry with nothing saying so.
14+
- `Hydrated N package(s) from sys_packages into registry` counts the records the registry accepted in that pass.
15+
- The ADR-0087 D1 handshake still runs first: a record whose declared range excludes this runtime's major is refused by it, named, as before.
16+
- **The marketplace install** (`MarketplaceInstallLocalPlugin`, install and boot rehydrate). The body is converted once per load, and that one converted body is what `manifest.register`, the handler binder, the side effects (translations, seed datasets) and the rehydrate's sample-data heal read. Before, `register` converted its own copy while the side readers read the raw ledger entry. The ledger keeps the body as installed.
17+
18+
Nothing is narrowed. A body with no declared range (a manifest built in code) meets the registry's refusals exactly as before at both doors. The install primitive `protocol.installPackage` (`POST /api/v1/packages`) is authoring-side and stays strict: the registry judges the manifest as handed.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/plugin-approvals": patch
3+
---
4+
5+
Clause-②: no
6+
7+
fix(plugin-approvals): every approval notification body names its record by the object's label and the record id
8+
9+
- **What changed.** The reminder, SLA-breach (to approvers and to the submitter), escalation, hand-off, revision-limit, send-back (with no note) and both out-of-office notification bodies named the record as `<object_name>/<record_id>`, for example `crm_deal/D_123`. They now name it the way `approval.requested` already did: the object's label and the record id, for example `Deal D_123`. An object whose schema declares no label is named by its API name and the id, for example `crm_memo D_123`.
10+
- **No title.** No body carries the record's title, from the live record or from the submit snapshot. The body is stored and may be e-mailed, and it has no single reader whose field access it could be narrowed for.
11+
- **Unchanged.** Each sentence's other wording, every topic, title, `actionUrl`, action link and audience, and the `DUPLICATE_REQUEST` refusal text.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
---
4+
5+
Clause-②: no (narrowing)
6+
7+
fix(plugin-auth)!: sixteen of this package's own routes refuse a declared JSON body that does not parse with `400 INVALID_REQUEST`, instead of answering as if no body was sent
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 sixteen of this package's own routes, and of the exported bridges eleven of them mount, 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, every signature is unchanged (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+
These routes read a body that does not parse as no body, through `try { body = await request.json() } catch { body = {} }` or a module-private `parseJson` that answered `{}` from its `catch`, so a truncated body was answered as if the client had sent none:
14+
15+
- `POST /api/v1/auth/admin/oauth2/toggle-disabled`, `/admin/unlock-user` and `/sys-oauth-application/register`;
16+
- `POST /api/v1/auth/admin/import-users`;
17+
- `POST /api/v1/auth/admin/sso/register`, `/admin/sso/register-saml`, `/admin/sso/request-domain-verification` and `/admin/sso/verify-domain`;
18+
- `POST /api/v1/auth/send-verification-email`;
19+
- `POST /api/v1/auth/admin/ban-user` and `/admin/unban-user`;
20+
- `POST /api/v1/auth/admin/set-user-manager`;
21+
- `POST /api/v1/auth/admin/create-user` and `/admin/set-user-password`;
22+
- `POST /api/v1/auth/organization/add-member`;
23+
- `POST /api/v1/auth/set-initial-password`.
24+
25+
`/admin/unlock-user` answered `400 "userId is required"` for a body that carried `userId` and lacked its closing brace. `/send-verification-email` read the same input as the one-click resend, and sent the link to the caller's own address for a body that named another one.
26+
27+
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." } }`, the answer the HTTP transports give the same body. A declared JSON body whose stream cannot be read is refused with the same code and "The request body could not be read. It was refused rather than read as an empty body." Each route refuses where it reads its body. The import, the four SSO bridges, ban/unban, set-user-manager, create-user, set-user-password and add-member read it after their platform-admin gate (and after the admin-plugin or organization-plugin check where the route has one). toggle-disabled, unlock-user, sys-oauth-application/register, the resend and set-initial-password read it before anything else runs. Nothing is looked up, written, sent or forwarded to better-auth.
28+
29+
The exported bridges eleven of these routes mount (`runAdminImportUsers`, `runRegisterSsoProviderFromForm`, `runRegisterSamlProviderFromForm`, `runRequestDomainVerification`, `runVerifyDomain`, `runResendVerificationEmail`, `runSetInitialPassword`, `runAdminCreateUser`, `runAdminSetUserPassword`, `runAdminBanUser` and `runAdminUnbanUser`) answer the same refusal, so a host that mounts them itself refuses it too. Their signatures and result types are unchanged.
30+
31+
**Remedy, in one line.** Send a JSON body as well-formed JSON with `Content-Type: application/json`, or send no body.
32+
33+
**Unchanged.** An empty body (no body at all, a zero-byte one, or whitespace only) is still no body, and every route answers it as before: `/admin/unlock-user` with `400 "userId is required"`, and `/send-verification-email` as the one-click resend to the caller's own address. A body that parses is read as before, whatever its `Content-Type`, and a body that does not declare JSON and does not parse is still read as no body.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/hono': minor
3+
---
4+
5+
fix(hono)!: `createHonoApp` reads a dispatched body by `@objectstack/plugin-hono-server`'s rule, so a JSON body sent as `text/plain`, or any other body that declares neither JSON nor a form, is refused `400 INVALID_REQUEST` when the dispatched domain reads it, instead of being parsed or dispatched 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 bodies that declare neither JSON nor a form, which used to be parsed as JSON whatever they declared or dispatched as `{}`, and are now refused when a domain reads them. 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 and the `/auth/*` legacy fallback parsed every body as JSON whatever its `Content-Type`, and handed `dispatch()` `{}` for any body that did not parse. So the two Hono doors answered one request two ways: `@objectstack/plugin-hono-server` (`objectstack dev`, `os serve`) refused a valid query body sent as `text/plain`, and this door honoured it; a garbled body that did not declare JSON reached the data domain here as `{}`, the empty query, and read every row the caller could read.
14+
15+
What changes for an integrator, on a host that serves `createHonoApp`, for a `POST`, `PUT` or `PATCH` body read by a domain that reads the parsed body (the data, metadata and every other domain that takes `body`), and on the `/auth/*` legacy fallback for every method it hands a body (all but `GET` and `HEAD`):
16+
17+
- A JSON body sent as `text/plain`, or as any media type other than JSON or a form: FROM parsed and honoured, TO `400` with `{ "success": false, "error": { "code": "INVALID_REQUEST", "message": "The request body was sent as text/plain, which this server does not read into a request body. It was refused rather than read as an empty body: send it as application/json." } }`, the message naming the media type that was sent.
18+
- Any other body that declares neither JSON nor a form (`text/plain`, `application/octet-stream`, a `+json` suffix type such as `application/merge-patch+json`, or no `Content-Type` at all), when the request declares a body (`Content-Length` other than `0`, or `Transfer-Encoding`): FROM `{}`, TO the same refusal ("sent with no Content-Type" when there is none).
19+
- A `multipart/form-data` or `application/x-www-form-urlencoded` body: FROM `{}`, TO its parsed fields, as `plugin-hono-server` hands them (Hono's `parseBody`: the last value of a repeated name). A form that does not parse: TO the refusal "The request body could not be parsed as form data (it was sent as multipart/form-data). It was refused rather than read as an empty body."
20+
21+
The domain acts on nothing past its read of the body, and the dispatcher's own gates still answer first. Each answer is the status and body `@objectstack/plugin-hono-server` gives the same request. This replaces the earlier `createHonoApp` entry's statement that a body that does not declare JSON is read as before.
22+
23+
**Remedy, in one line.** Send a JSON body with `Content-Type: application/json`, or send no body.
24+
25+
**Unchanged.**
26+
27+
- A body sent as `application/json` (matched case-insensitively) is parsed when well-formed, `{}` when blank or whitespace-only, and refused when it does not parse, as before.
28+
- A request that declares no body is dispatched as `{}`, whatever its media type.
29+
- 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, whatever the body declares: `/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.
30+
- The catch-all dispatches `GET`, `HEAD` and `DELETE` with no body, and the `/auth/*` fallback hands `GET` and `HEAD` `{}`, as before.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): `IHttpRequest.body`'s TSDoc says what a handler reads, and that reading it may refuse
6+
7+
Clause-②: no
8+
9+
- **What it is.** The parsed JSON document for `application/json`, or the parsed fields of a `multipart/form-data` or `application/x-www-form-urlencoded` form; `{}` when the request carries no body.
10+
- **It may refuse.** An implementation that cannot parse the body, or does not read its media type, throws from the read instead of handing over an invented value. The standard transport (`@objectstack/plugin-hono-server`) does, and answers 400 `INVALID_REQUEST` itself, replacing whatever the handler wrote. So a handler must not catch the refusal and carry on.
11+
- **Raw bytes.** A handler that needs them reads `rawBody()` and never reads `body`.
12+
- **Unchanged:** the type (`body?: any`), every other member, and every runtime behaviour. This is prose in the shipped `.d.ts` only.

0 commit comments

Comments
 (0)