Skip to content

Commit 703bb28

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21397-null-ordering-message-faces
2 parents 80b111c + ceb4a93 commit 703bb28

175 files changed

Lines changed: 10642 additions & 636 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/metadata': patch
3+
---
4+
5+
Provenance comments in `@objectstack/metadata` cite the commits that decided them, not tracker numbers that no longer resolve
6+
7+
Clause-②: no
8+
9+
Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub.
10+
Each now cites the commit in this repository's history that made the decision it describes, except two
11+
that meant an objectui issue and now spell `objectui#6111`. Some of these
12+
docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` /
13+
`index.d.cts`, `node.d.ts` / `node.d.cts` and `view-container.d.ts` / `view-container.d.cts`, and
14+
comments that esbuild keeps appear in the JavaScript output (`index.js` / `index.cjs`, `node.js` /
15+
`node.cjs`).
16+
17+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/metadata-protocol': minor
5+
'@objectstack/trigger-api': minor
6+
'@objectstack/runtime': minor
7+
---
8+
9+
feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790)
10+
11+
Clause-②: yes (widening)
12+
13+
A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it.
14+
15+
**⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '<name>' (<state>): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification.
16+
17+
What else changes:
18+
19+
- **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`.
20+
- **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential.
21+
- **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials.
22+
- **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing.
23+
- **Refused now, loudly**:
24+
- With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again.
25+
- The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret.
26+
27+
<!-- adr-0087: not-required (no-migration-prescription) the one-time move rewrites stored flow rows through the metadata save door itself, at boot; no authorable key, spelling, export or stored shape is retired, so an author or an upgrading agent has nothing to rewrite. The operator's action is the rotation stated above, which is not a FROM to TO mapping. The gate reads this changeset as non-breaking; the disposition is stated for the migration the ruling named. -->
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/service-analytics': patch
4+
'@objectstack/formula': patch
5+
'@objectstack/objectql': patch
6+
---
7+
8+
Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it
9+
10+
Clause-②: no
11+
12+
`driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships:
13+
14+
- `@objectstack/spec`:
15+
- The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it.
16+
- `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`).
17+
- `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did.
18+
- A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it.
19+
- `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too.
20+
- `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`.
21+
- `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it.
22+
23+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: `having` and the per-aggregation `filter` refuse a `{ $field }` comparison against a column with no comparison class (a file field, a list, a formula) with `INVALID_FILTER` / 400, as `where` refuses it; `applyInMemoryAggregation` takes the same reference rules
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of filter STRUCTURE at the engine's query door and at its published in-memory aggregation function: a { $field } comparison against a column the spec's crossFieldComparisonVerdict answers no-class for, at having and at a per-aggregation filter, and the reference rules engine.aggregate already applied, now applied by applyInMemoryAggregation when its caller passes a field map. No authorable key, spelling, export or stored shape moves: FieldReferenceSchema, every query shape and every object definition parse as before, the package entries export nothing new and nothing less, applyInMemoryAggregation keeps its signature, and no stored row is read or rewritten. What is refused is a comparison the same query's where already refuses on driver-sql, and which comparable column the caller meant is not something a ledger entry can decide. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a filter's comparison class (not registered / already-registered); and the change is runtime behaviour, not a declaration (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING**: this narrows what a `{ $field }` reference may pair at two positions of `engine.aggregate`, and what `applyInMemoryAggregation` accepts when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
12+
13+
**What was accepted before.** The spec's comparison-class verdict (`crossFieldComparisonVerdict`) answers `no-class` for a pair in which either column has no comparison class: a list or an object (a structured-JSON type, a multi-option type, a multi-capable type flagged `multiple: true`), a file field (`FILE_REFERENCE_TYPES`), or a formula. `having` and a per-aggregation `filter` (`aggregations[i].filter`) did not judge that answer. Measured on `SqlDriver` over better-sqlite3 through `engine.aggregate`, beside a `where` twin that `driver-sql` refused `INVALID_FILTER` / 400 each time:
14+
15+
- a per-aggregation `{ customer_id: { $ne: { $field: 'photo' } } }` (text against an image) counted 6 of 6 rows;
16+
- a per-aggregation `{ closed_at: { $lte: { $field: 'due_f' } } }` (datetime against a formula) counted 0 of 6;
17+
- a per-aggregation `{ amount: { $ne: { $field: 'tags' } } }` (number against a multiselect) counted 6 of 6 when the column held no value, 0 on an empty table, and was refused by the per-row array check, in other words, when the column held a list;
18+
- `having: { photo: { $ne: { $field: 'n' } } }` over a groupBy on an image field kept all 6 groups.
19+
20+
A `{ $field, addDays }` pair against a formula was answered at both positions. `applyInMemoryAggregation`, called with a field map, applied none of `engine.aggregate`'s reference rules: a reference to a field the map does not declare, a pair across two classes and a pair against a column with no class were all counted.
21+
22+
**What is refused now.** At `having` and at a per-aggregation `filter`, a scalar comparison (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) whose `{ $field }` comparand, or whose own column, has no comparison class, with or without `addDays`. The refusal is `INVALID_FILTER` / 400, raised before any driver is asked for a row, on an empty set as on a populated one, in the reason `driver-sql`'s `where` logs for the same pair: the column it names (the referenced one first, as `where` asks it first) "has no scalar stored column a comparison can read". The per-aggregation `filter` withholds the fields, the operator and the reason from the message and writes them to the server log, as `where` does; `having` names the two columns of the query's own projection. A `{ $field, addDays }` pair against a file or list column was already refused, in the `addDays` pair rule's words (that rule reads those types as text, so it answered with a cross-class or an offset sentence); it is now refused in this one.
23+
24+
`applyInMemoryAggregation(rows, ast, timezone, fields, reportWithheld)`, when `fields` is passed, judges each per-aggregation `filter` by the reference rules `engine.aggregate` applies at that position, through the same function, before any row is judged: the referenced column (and an `addDays` offset column) is declared, a pair across two classes or against a column with no class is refused, and an `addDays` pair follows its class rule. The refusal is `INVALID_FILTER` / 400; the withheld diagnostic goes to `reportWithheld`, and it names no object (this function is not told one).
25+
26+
**The remedy.** Compare two columns that each have a comparison class, and the same one: a file field, a list and a formula have no stored scalar a comparison can read. Compare the scalar column the value is derived from, or filter the column with a literal.
27+
28+
**Unchanged.** A reference between two columns of one class answers as before, and the cross-class refusal keeps its words. A side with no declaration is not judged at any of the three positions: a host with no registered object, a column the field map does not carry (`id`, and an audit-opt-out object's row-carried `created_at` / `updated_at`), an aggregation over an undeclared field. A declared type outside `FieldType` is not judged either. `applyInMemoryAggregation` called without `fields` judges nothing it did not judge before.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/runtime': minor
3+
'@objectstack/cloud-connection': patch
4+
---
5+
6+
An app installed with `os package install <artifact>` now runs its `type: 'script'` action bodies and its body hooks, and MCP `list_actions` lists a script action only when `run_action` can run it (#21321).
7+
8+
Clause-②: yes (widening)
9+
10+
- **`@objectstack/runtime`.** New export `bindAppArtifactHandlers(ql, bundle, { appId, logger, source? })`. It binds every action `body` of an artifact through `ql.registerAction`, and every hook `body` and bundle function through `ql.bindHooks`, all under the owner `app:<appId>`. `appArtifactHandlerOwner(appId)` returns that owner key. Each call first removes the action handlers and hooks the same owner bound before. A reinstall therefore leaves one handler per action, and an action or hook that the new version dropped stops running. `AppPlugin.start` now binds through this function, with the same log lines and the same results for a boot artifact.
11+
- **`@objectstack/runtime`, MCP `list_actions`.** A `script` action is listed only when the engine has a handler registered for it. The check reads `listRegisteredActions()` and uses the same object and key order as `run_action`. Before, a declared `target` or `body` was enough to be listed, so `list_actions` could list an action that `run_action` refused with "No handler registered". An engine without `listRegisteredActions` gets no script actions listed. Declarative update actions and `flow` actions are listed as before.
12+
- **`@objectstack/cloud-connection`.** The install-local plugin calls `bindAppArtifactHandlers` on `POST /api/v1/marketplace/install-local` and when it rehydrates its ledger at `kernel:ready`. Before, an installed package's script actions answered REST `404 RESOURCE_NOT_FOUND` and MCP "No handler registered", before and after a restart, and its body hooks never ran. The same artifact booted with `os start --artifact` was not affected.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/plugin-sharing": patch
3+
"@objectstack/spec": patch
4+
---
5+
6+
A plain member's share-link list now answers: `GET /api/v1/share-links` is self-scoped for every signed-in caller, as ADR-0111 rules it
7+
8+
Clause-②: no
9+
10+
`ShareLinkService.listLinks` read `sys_share_link` under the caller's context. Both share-link doors force the list's `createdBy` to the caller, but the read still needed an object-level grant on `sys_share_link`, and the platform's member baseline does not grant one. So every plain member's list was refused, with or without an object filter, and the Share dialog, which loads this list when it opens, showed an error for them on every record. An admin's list answered.
11+
12+
- The caller's own list is now read under the system context. This happens only when the caller has a non-empty user identity and the creator filter equals it. The read is constrained server-side to that identity, and each row it returns must pass the creator rule before it leaves.
13+
- One creator rule now serves both `listLinks` and `revokeLink`. It never matches a caller with no user identity. Neither HTTP door reaches that case, because both answer 401 first, so for an internal caller with no user identity, `revokeLink` now refuses a link whose `created_by` is absent or empty instead of treating it as theirs.
14+
- Every other list shape keeps the caller's context, as before: no creator filter, another user as creator, no user identity, or an admin listing someone else's links. A system caller keeps its bypass.
15+
- The rows carry the same columns as before. The token comes back so the console can build the link URL, and the password hash never does.
16+
- `@objectstack/spec`: the `IShareLinkService.listLinks` doc comment now describes the self-scoped own list. It previously said every listing is read under `context`. This is a doc comment only, with no type or export change.
17+
- ⛔ No permission set changes, and no new grant on `sys_share_link`.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
`os environments list | show | create | bind | switch` run on the `os cloud login` session
6+
7+
Clause-②: yes (widening)
8+
9+
The documented hosted flow is `os cloud login`, then `os environments create`. The five
10+
`os environments` subcommands read only `~/.objectstack/credentials.json` (the `os login`
11+
session), so with only `~/.objectstack/cloud.json` they exited 1 with
12+
`Authentication required` before sending any request, while `os login --help` sends hosted
13+
users to `os cloud login`.
14+
15+
All five now choose their session in one shared resolver:
16+
17+
- With no `--url` / `OS_CLOUD_URL`, they use the `os login` session when there is one, which
18+
is the same behaviour as before. Otherwise they use the `os cloud login` session and the URL
19+
it recorded.
20+
- With a `--url`, they use the session whose file names that server, `credentials.json` first.
21+
When neither file names it, they use `credentials.json`'s session as before. The cloud token
22+
is never sent to a URL other than its own.
23+
- The active environment sent with each request comes from the chosen session's file.
24+
`os environments switch` and `create --activate` no longer write a cloud environment id into
25+
`credentials.json` when they ran on the cloud session.
26+
27+
With no session at all, the `Authentication required` message now names `os cloud login` as
28+
well as `os login`. `os package publish` is unchanged: it still reads only `cloud.json`.

0 commit comments

Comments
 (0)