Skip to content

Commit 34de285

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-2ul8sb
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
2 parents 8515184 + a75ddba commit 34de285

388 files changed

Lines changed: 16437 additions & 1101 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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cli': patch
4+
---
5+
6+
feat(spec, cli)!: `bundled` is the one plugin packaging strategy — `manifest-deps` is retired, and `os plugin build` refuses a native addon (#27123)
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: narrowed
10+
11+
<!-- adr-0087: registered manifest-packaging-manifest-deps-removed, manifest-packaging-manifest-deps-retired -->
12+
13+
ADR-0025 v2 §3.3 makes `bundled` the only dependency-packaging strategy. `manifest-deps` kept every dependency external and packed `package.json` and the lockfile beside the bundle, but nothing ever installed them, so a plugin built with it could not load them. A compiled `.node` addon can be neither bundled nor run by every host, so a plugin may not carry one.
14+
15+
- **`@objectstack/spec`**: `PluginPackagingSchema` is `['bundled']`. `manifest-deps` is a retired value: every door that parses a manifest refuses it at `packaging` with the prescription — `ManifestSchema` (an `objectstack.plugin.json` at `os plugin build`, a stack's `manifest` at `defineStack`, the ObjectQL registry), a packaged artifact's `packages[].manifest`, and the `POST /packages` install body — and TypeScript no longer admits it. The ADR-0087 D2 conversion `manifest-packaging-manifest-deps-removed` deletes the value from a stack's `manifest` and from each `packages[].manifest` (`os migrate meta --from 17`), and the D3 entry `manifest-packaging-manifest-deps-retired` carries the change to the upgrade guide. The `packaging` liveness row is `planned`: a one-member enum has no fork left to read.
16+
- **`@objectstack/cli`**: `os plugin build` packs one strategy. It inlines every dependency except the host-provided `@objectstack/*` packages and no longer packs `package.json` or `pnpm-lock.yaml`. It refuses a native addon before it writes anything, naming each one it finds: a `.node` file the bundle imports (with its importer), a bundled package that declares a node-gyp build or ships a `.node` binary, and a `.node` file under `assets/`. An addon loaded at run time from an absolute or computed path outside every bundled package is not visible to the build.
17+
18+
**BREAKING** for a plugin manifest that writes `packaging: 'manifest-deps'`, and for a plugin that carries a native addon.
19+
20+
- **Before.** `packaging: 'manifest-deps'` built an artifact that left the dependencies out and carried `package.json` for an install step that never ran. A plugin importing a `.node` file failed in esbuild with a bare "Bundle failed"; one whose dependency located its binary at run time, or that kept a `.node` file under `assets/`, built an artifact that could not load it on a host without that binary.
21+
- **Now.** `manifest-deps` is refused at the manifest (`objectstack.plugin.json is invalid:` names `packaging`, exit 1, no artifact). A native addon is refused by name (`Native addon refused: …`, exit 1, no artifact).
22+
- **Fix.** Delete the `packaging` key, or write `bundled`. A stack source gets the edit from `os migrate meta --from 17` (`--write` applies it); an `objectstack.plugin.json` is edited by hand. Replace a native addon with a pure-JavaScript or WebAssembly alternative.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/objectql': minor
4+
---
5+
6+
fix(cli)!: `os validate` and `os build` refuse a seed record value the seed load would refuse, with the load's own code and sentence, located at the record's field (#27259)
7+
8+
Clause-②: no (narrowing) — `os validate` and `os build` refuse a stack whose seed record carries a value the record validator refuses at seed load (a currency written `{ value, currency }`, an unreadable number, a value outside a field's options, an unreadable date), which both commands passed before. What the runtime stores and refuses does not move: every row refused here was already refused when the seed loaded. Nothing is widened.
9+
Metadata-protocol: narrowed
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped. The refusal moves earlier, from the seed load to the authoring commands, with the load's own wording: a seed row it names was already dropped at load, so there is nothing for a migration to rewrite and no tombstone. -->
12+
13+
**BREAKING** accept-set narrowing at `os validate` and `os build` (`@objectstack/cli`); one new export on `@objectstack/objectql`.
14+
15+
- **Before.** A seed record such as `budget: { value: 10, currency: 'USD' }` on a currency field passed `os validate` with "Validation passed" (exit 0) and `os build` emitted the artifact. The row was refused only when the seed loaded, by the record validator: `Budget is a currency amount: write it as a bare number …`, one dropped row in the boot log.
16+
- **Now.** Both commands judge every seed record value with the judge the load runs, and refuse with that judge's code and sentence, located at the value: `data[0].records[4].budget: Budget is a currency amount: write it as a bare number … (invalid_number)`, exit 1. `--json` carries each issue as `{ location, object, field, code, message }`.
17+
- **One judge.** The verdict is `validateInsertValues` (`@objectstack/objectql`, new): the insert write's own normalizations (a blank on a typed field is `null`, a numeric string is its number, a scalar on a multi-value field is wrapped) and then the same record-validator call the insert path makes. The field map is the runtime registry's, built from the stack's objects and picklists. So a value the load accepts after a rewrite (`'10'` on a number field) still passes.
18+
- **Left to the load.** `required` on a missing value (a default or a hook may fill it; an explicit `[]` on a required multi-value field is refused `required` here, as at load), a field the object does not declare (`defineSeed` already refuses it), and every value the load rewrites or a deployment decides: an expression envelope (the `cel` template), a `seedFile(path)`, a reference, file or structured-JSON field, a field bound to a picklist another package declares, and a dataset whose object another package declares.
19+
- **Known limits.** A `beforeInsert` hook is not run at authoring time, so a value a hook would rewrite into an accepted shape is refused here though the load stores it. A dataset whose object is declared only under `manifest.objects`, outside `packages[]`, is not judged.
20+
- **Who sees it.** A stack shipping a seed row the load already drops. The four example apps in this repository (`app-todo`, `app-crm`, `app-showcase`, `app-multi-package`) carry none and pass unchanged.
21+
22+
**The remedy.** Write the value in the shape the message names, for a currency a bare number such as `10`, with the currency in the field's `currencyConfig`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
feat(plugin-auth): a sign-in that skips the OIDC seams can apply the IdP locale rule through `AuthManager.applyIdpLocaleClaim`
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
10+
An external OIDC sign-in already sets the user's language preference (`sys_user.locale`) from the IdP's `locale` claim. A host whose own door signs a user in without passing through better-auth's OIDC seams, such as the cloud's sso-exchange hand-off, had no way to apply the same rule, because the package entry exported none of it. Now it can, through the running auth manager (`ctx.getService('auth')`):
11+
12+
- **`AuthManager.applyIdpLocaleClaim(userId, claim)`** applies a claim (`{ providerId?, value }`) under the same rule and the same `idp-locale:` record as the OIDC path: the column is written only while it is unset or still holds the value an IdP sign-in last wrote, so a language the user chose in the environment is kept. The claim is closed against the i18n service's locales, and an unsupported one clears only an IdP-written value. It never throws: a failure is reported at `warn` and the user keeps the preference they had.
13+
- **`IdpLocaleClaim`** is exported as a type from the package entry.
14+
- **One owner for the rule.** The OIDC JIT path now settles its staged claim through the same method, so its behaviour is unchanged.
15+
16+
Nothing to do for existing hosts.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/mcp': patch
3+
---
4+
5+
fix(mcp): `validate_expression` refuses a blank or whitespace-only expression with the save gate's verdict
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `validate_expression` MCP tool used to answer `ok: true` for an empty or whitespace-only expression, while saving that same expression is refused with `422 INVALID_METADATA` ("An expression in an evaluated slot needs a non-blank `source` …"). An agent that validated first and then saved was told the expression was fine.
11+
12+
The tool now parses the expression with the same spec input schema the save/build gate uses for the slot (`EvaluatedExpressionInputSchema` for the `formula`, `validation` and `flow_condition` sites, `TemplateExpressionInputSchema` for the `template` site) before it runs the formula checks. When that schema refuses, the tool answers `ok: false` with that schema's own issue: the `code` is the schema's issue code and the `message` is the published sentence (`EVALUATED_EXPRESSION_SOURCE_REQUIRED`, or `TYPED_EXPRESSION_SOURCE_REQUIRED.template` for a template). The fields and functions in scope still come back. A non-blank expression gets the same answer as before.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(service-storage): the chunk upload door refuses a `chunkIndex` the upload did not declare, and the presigned upload door refuses a `size` that is not a byte count
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) A REST request value is refused at the presigned upload door (a negative size): no key of any metadata schema is removed, renamed or re-shaped, no export, type or error code is removed or renamed, and the refusal reuses the existing 400 INVALID_REQUEST envelope. There is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
11+
12+
Narrowed: a negative `size`, which the spec number (`GetPresignedUrlRequestSchema.size`, a bare `z.number()`) admits, is now refused `400` at the presigned door.
13+
14+
- **Chunk door** (`PUT /api/v1/storage/upload/chunked/:uploadId/chunk/:chunkIndex`). **Before:** the index was read with `parseInt`, so `0.5` was read as `0`, `-1` reached the storage backend as part number 0 and answered `500 INTERNAL`, and an index at or past the upload's `totalChunks` was stored, counted in the progress, and then made `POST .../complete` answer `409` for good, since no door removes a stored chunk. **Now:** the index must be a whole number (decimal digits only) in `[0, totalChunks)`; anything else answers `400 INVALID_REQUEST` naming the valid range, with `details: { chunkIndex, totalChunks }`. Nothing is stored and the progress is unchanged, so the upload can still be completed. The check runs after the resume-token check, so a caller without the token still gets `403 INVALID_RESUME_TOKEN` first.
15+
- **Presigned upload door** (`POST /api/v1/storage/upload/presigned`). **Before:** only a missing `size` was refused. A non-numeric `size` such as `"abc"` reached the `sys_file` insert and failed there as `500 INTERNAL`, and a negative `size` such as `-5` answered `200` and stored a pending `sys_file` row with that size. **Now:** `size` must be a finite, non-negative JSON number (the `GetPresignedUrlRequestSchema.size` number); a string (a numeric string such as `"100"` included), `NaN`, an infinity, a negative number or a boolean answers `400 INVALID_REQUEST` naming `size`, and no `sys_file` row is written. `0` and any positive number behave as before.
16+
- **Who sees it.** Only a client that sent an index outside the range it was given, or a size that is not a JSON number of bytes. The SDK (`storage.upload`, `resumeUpload`) and the Console upload client send `0 .. totalChunks - 1` and `File.size`, so they are not affected.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): creating, updating or cloning a flow over `/automation` now records which user saved it
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`POST /api/v1/automation`, `PUT /api/v1/automation/:name` and `POST /api/v1/automation/:name/clone` store the flow as a `sys_metadata` row. Until now that row named nobody: `created_by` and `updated_by`, and `recorded_by` on its `sys_metadata_history` entry, were null even when a signed-in administrator made the request. The `/meta` door already recorded the user. All three `/automation` doors now pass the signed-in user's id as the save's actor, so the row names that user, the same way `PUT /api/v1/meta/flow/:name` does. A request with no user, such as the engine's own system call, still saves with no actor, as before.
11+
12+
The `sys_audit_log` row for the same write is not changed by this fix. It still has a null `user_id` on every metadata door, `/meta` included.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/trigger-schedule': patch
3+
---
4+
5+
fix(trigger-schedule): a time-relative flow whose `timeRelative` descriptor is refused now reads `bound: false` on the automation status, not `bound: true`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`TimeRelativeTrigger.start` refused an invalid `config.timeRelative` descriptor (both `withinDays` and `offsetDays`, an unknown key, `schedule` nested inside the descriptor) by logging "not bound" and returning. The engine read that return as a successful bind, so `GET /api/v1/automation/_status` reported the flow as `bound: true` with no `unboundCause`, and no sweep ever ran.
11+
12+
The refusal now throws, the same way the trigger's organization and deployment-policy refusals already do. The engine records the flow as unbound: its status row reads `bound: false` with `unboundCause: 'binding_failure'`, and the boot-time binding audit lists it. The `warn` line the server logs is unchanged, and the thrown text is the same sentence. A re-bind with a broken descriptor also cancels the sweep an earlier valid bind armed.
13+
14+
Nothing changes for a valid descriptor. A host that calls `TimeRelativeTrigger.start` directly, outside the engine, should expect it to throw on a refused descriptor.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/console': patch
4+
---
5+
6+
fix(app-shell): opening a notification from Home's "My work → Notifications" card marks it read, as the bell does
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Clicking a notification in the top-bar bell marked it read and then opened its link. Clicking the same notification on the Home page's "My work → Notifications" card only opened the link: no `POST /api/v1/notifications/read` was sent, the server's `unreadCount` did not change, and the bell kept its badge after a reload.
12+
13+
Both places now use one open action, `openInboxNotification`. It marks the notification read first, then opens its link, or opens the inbox when the notification has no link. The Home card marks read through the bell's own write (`useInboxBell().markRead`), so it sends the same request, keyed by the notification's event id. Once the inbox feed re-reads, the Home card's unread count and the bell's badge drop.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/console': patch
4+
---
5+
6+
fix(console): an app-root or console-root deep link keeps its query and hash when it redirects to the landing page
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Opening `/apps/APP?palette=1` on an app whose root redirects to its first navigation entry landed on that page with no query string, so the command palette never opened; the same held for every URL overlay param (`?recordId=`, `?form=`) and for the hash, and for `/?palette=1` at the console root. Both redirects now carry search and hash across. When the landing entry has a query of its own (a `page` entry's `params`, an object entry's `filters` or `runAction`), the two merge: a key the link names replaces the landing's value for that key instead of doubling it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): a CEL field shows the server's verdict only on the text it judged
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
In Studio's permission-set editor, an RLS `using` or `check` clause kept showing the previous draft's server error under text the author had since changed. An edit the CEL syntax check holds is never saved, so the draft is not read again and the server's verdict on the old text stayed on screen beside the new error: two different errors at once, one of them about text no longer in the field.
11+
12+
The clause now shows the server's errors on the same terms on which they hold the save: only while it still shows the text the server judged. Editing the text away hides them, and the next save's re-read brings the new verdict. Typing the judged text back shows the verdict again, because it describes that text.

0 commit comments

Comments
 (0)