Skip to content

Commit ef624ed

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24583-7b3-global-counter
2 parents 112ea87 + 7c2eab9 commit ef624ed

121 files changed

Lines changed: 6030 additions & 618 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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/driver-sql': minor
3+
'@objectstack/objectql': minor
4+
---
5+
6+
feat(driver-sql)!: a tenant-scoped read no longer answers a row with no organization; the driver's `OR organization_id IS NULL` arm is retired
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) A read-scope narrowing in the SQL driver's tenant chokepoint and the lifecycle sweep's global pass: 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 or error code is added, removed or renamed. The data it stops reaching is retired by the 18.x upgrade ceremony, which certifies zero organization-less rows before a database boots. -->
11+
12+
**BREAKING** read-scope narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D1, D14). Step 7 of the 18.x organization-ownership line: it lands after the upgrade ceremony opens.
13+
14+
- **Before.** `SqlDriver.applyTenantScope` compiled a tenant scope as `(organization_id = :tenant OR organization_id IS NULL)`, and the `group` posture's union as `(organization_id IN (…) OR organization_id IS NULL)`. That was the v17 reading, in which a row with no organization stood for platform state, and every tenant's scoped read, count, update and delete reached it. The Turso remote face compiles the same scope, so it answered the same rows.
15+
- **Now.** The scope is `organization_id = :tenant`, or `organization_id IN (…)` under `group`. NULL is not an owner: a row with no organization is no tenant's, and only an unscoped read (no tenant on the call) reaches it. The lifecycle sweep's global retention and archive pass over an `organization_id` partition is a bare `$nin` of the overriding tenants: it no longer selects organization-less rows.
16+
- **Unchanged.** A call with no tenant context and an object with no tenant column are unscoped, as before. The `single` posture's driver reads carry no tenant scope at all (ADR-0131 D8). The `sys_audit_log` attribution partition (`tenant_id`, ADR-0131 D7) keeps its NULL arm: a deployment-level ledger row is about no organization by design, and the global pass still reaps it. Insert stamping (`injectTenantOnInsert`) is unchanged.
17+
18+
**Who is affected.** Only a database that still holds organization-less rows in a tenant-scoped table, and only under a walled posture (`group` / `isolated`) or a caller that passes `tenantId` to the driver itself. A certified 18.x database holds none: a v17 database is refused at boot until `os migrate organization-ownership` certifies it, and a fresh install never writes one.
19+
20+
**The remedy.** If a scoped read misses a row you expect, the row has no organization: run the upgrade ceremony (`os migrate organization-ownership` to review its plan, then the same command with `--apply`), which stamps each such row with its owner, or stamp it yourself (`organization_id = <the owning organization>`).
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(lint)!: a `lookup` or `master_detail` field whose `reference` is a stored-metadata-body table is refused where it is declared
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No spec key, stored shape or authorable spelling is added, removed or renamed. The expression rule's object pass now refuses one field declaration it accepted before: a `lookup` or `master_detail` whose `reference` is `sys_metadata` or `sys_metadata_history`, which the engine never hydrates. Nothing stored is rewritten, so there is nothing for `objectstack migrate meta` to convert; the `@objectstack/spec` change is the text of the `reference` describe only. -->
11+
12+
**BREAKING** (an authoring narrowing), shipped as `minor` under the launch-window convention for breaking changes.
13+
14+
A `lookup` or `master_detail` whose `reference` names a table of the stored-metadata-body family (`STORED_METADATA_BODY_OBJECTS`: `sys_metadata`, `sys_metadata_history`) is an error. The engine never hydrates such a field: `$expand` keeps the stored id and never nests the row, a formula or a validation rule never reads through it, and a master-detail child's `parent` root stays unbound, so every write a `parent` rule judges is refused. `os validate`, `os build` and the object save door (`PUT /api/v1/meta/object/:name`, and a draft's promotion) run the same pass, under the existing `EXPRESSION_INVALID` rule, located at the field's `reference`.
15+
16+
- FROM: `meta: { type: 'lookup', reference: 'sys_metadata' }` (or a `master_detail`, or `sys_metadata_history`) saved with a 200 and built with exit 0, and then only ever showed an id. TO: refused, with `422 INVALID_METADATA` at the save door and an error at `os validate` / `os build`. The message names the field, its type and the target, and ends on the family's one prescription.
17+
- A reference to any other table, platform tables such as `sys_user` included, is unchanged.
18+
- A `user` field whose authored `reference` names a family table is refused the same way: the parse accepts an authored `reference` on a `user` field, and it is the target every reader resolves.
19+
- The refusal of a formula or a validation rule that reads through such a field is unchanged, and is still reported at that expression.
20+
- A stored object row that already declares such a field keeps loading: no authoring rule runs when a stored row is rehydrated, and the engine guards keep refusing every relationship read into the family. Its next active save, or a draft's promotion, through the metadata API is refused until the field is removed or retargeted. The engine guards stay, for every path that runs no authoring rule.
21+
- `@objectstack/spec` (`patch`): the `reference` describe states the convention, so the generated field reference says it too.
22+
23+
What to do. Remove the field, or retarget it. To point a record at a metadata item, store the item's type and name in `text` fields and read the item through the metadata API (`GET /api/v1/meta/:type/:name`), where its body is projected.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
---
4+
5+
fix(plugin-sharing): a record share's Recipient column names the recipient instead of drawing its raw id
6+
7+
Clause-②: no
8+
9+
- **`sys_record_share.recipient_id` declares `widget: 'recipient-picker'` and `dependsOn: ['recipient_type']`**, the display declaration `sys_sharing_rule.recipient_id` already carries. The console draws a field with this widget by naming the stored id through the row's `recipient_type`, so a user grant reads as the user's name in a list that shows the Recipient Type column beside it (Setup → Record Shares → All). A list that does not fetch `recipient_type` still draws the stored id.
10+
- **`object_name` is not a dependency here**: the face reads it only for the `field` recipient kind, which a record share cannot store.
11+
- **Unchanged:** `recipient_id` stays the same required text column, the object stays engine-owned and read-only through the data API, and `grant()` stays the only writer of a share row.
12+
13+
A patch: one display hint on one field of a shipped platform object. No export, type, accepted value, stored column or authorable key changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
Field and object designer forms: the `expression` help text now states what the engine does instead of claiming the expression "makes it read-only". On a `formula` field the expression is computed on read with no stored column; on any other field the engine computes it from the row each write stores and stores the result, so a client-sent value never stands (only `readonly: true` makes a client value a readonly drop). The object designer's field grid now shows the `expression` row for every field type, as the field designer already did, since `FieldSchema` accepts `expression` on any type. The zh-CN, ja-JP and es-ES translations of both rows are updated to match.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): a delegated administrator's position assignment is judged on every permission set the assignment grants, the set of the position's own name included
6+
7+
Clause-②: no
8+
9+
- **The rule (ADR-0090 D12, as amended 2026-10-17).** The sets a position carries are every set an assignment of it grants: the names its definition's `permissionSets` lists and, while the authorization resolver folds held position names into the requested set names, the permission set of the position's own name when the catalog holds one. The organization-level branch already counted that set. The delegated branch now does too, through the same helper.
10+
- **What a delegate is now refused.** A `sys_user_position` insert, update or delete by an `adminScope` holder whose allowlist does not cover the position's own-name set is refused `403 PERMISSION_DENIED`. The message names the set and says it is the set of the position's own name. An own-name set that carries an `adminScope` is held to strict containment like any other distributed set. A delegatable position whose own-name set carries an `adminScope` can no longer be self-delegated.
11+
- **What the delegable-scope report offers.** `GET /api/v1/security/my-delegable-scope` no longer lists a position for a delegate whose allowlist misses that position's own-name set, so the picker offers only what the write admits.
12+
- **Unchanged.** A position with no permission set of its own name is judged on its listed sets alone, as before. Tenant-level administrators and the organization-level branch are untouched, and the resolver's fold itself is not changed. To keep a delegate able to assign such a position, add the own-name set to the scope's `assignablePermissionSets`.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): the delegable-scope report no longer offers the built-in identity positions to a platform administrator or a delegate
6+
7+
Clause-②: no
8+
9+
`GET /security/my-delegable-scope` (`describeDelegableScope`) listed `platform_admin`, `org_owner`, `org_admin` and `org_member` in `assignablePositions` for a tenant-level administrator and for a delegate holding a placing `adminScope`. `sys_user_position.position` refuses those four names from every writer with `400 VALIDATION_FAILED`, so the invite dialog and Setup's position picker offered choices that could only fail.
10+
11+
Every branch of the report (tenant-level, organization-level, delegated) now reads one position listing that leaves out the ADR-0068 D2 identity names through the same `isReservedIdentityName` predicate the write's validation is built from, beside the audience anchors it already left out. No other position leaves any branch, and placeable business units are unchanged. The write path's refusal and the reserved list are unchanged. No spec key, endpoint or configuration changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/runtime': patch
4+
'@objectstack/cli': patch
5+
'@objectstack/service-settings': patch
6+
---
7+
8+
fix(plugin-auth): the auth IP allow-list judges the client address the server established (#24739)
9+
10+
Clause-②: no
11+
12+
The `allowed_ip_ranges` auth setting (ADR-0069 D5) and the `last_login_ip` login audit (D7) now establish a request's client address by the same rule as the inbound rate limiter: `X-Forwarded-For` (its first entry), then `X-Real-IP`, count only when the stack declares `server.trustProxy`. Otherwise the address is the connection's peer address.
13+
14+
**For operators who set `allowed_ip_ranges`:**
15+
16+
- Behind a reverse proxy or load balancer, declare `server: { trustProxy: true }` in the stack before you upgrade. Without it, the allow-list judges the proxy's own address for every request.
17+
- A request whose client address cannot be established (the transport reports no peer address, and no forwarded header is trusted) is now refused with `403 IP_NOT_ALLOWED`. Before, it was admitted. The first such refusal logs a warning that names `allowed_ip_ranges` and `server.trustProxy`.
18+
- `CF-Connecting-IP` is no longer read. Behind Cloudflare, declare `server.trustProxy`, and the `X-Forwarded-For` it sets is used.
19+
- A dual-stack listener reports an IPv4 client in IPv4-mapped form (`::ffff:203.0.113.7`). That address is now matched as the IPv4 address it denotes.
20+
- With `allowed_ip_ranges` unset or empty, nothing changes.
21+
22+
**`last_login_ip`** records the address the allow-list judged, or no address when none could be established. It is no longer read from a forwarded header the deployment does not trust.
23+
24+
**By package:**
25+
26+
- `@objectstack/plugin-auth`: the rule is published as the dependency-free `@objectstack/plugin-auth/client-address` entry (`resolveClientAddress`). `AuthPlugin` and `AuthManager` take a `trustProxy` option. `AuthManager.handleRequest` takes an optional second argument that carries the transport's `remoteAddress`.
27+
- `@objectstack/runtime`: the inbound rate limiter imports the rule from that entry instead of keeping its own copy. Its keys are unchanged.
28+
- `@objectstack/cli`: `objectstack serve` passes the stack's `server.trustProxy` to `AuthPlugin` as well as to the dispatcher. A host that composes `AuthPlugin` itself passes `trustProxy` the same way.
29+
- `@objectstack/service-settings`: the `allowed_ip_ranges` help text says which address is judged.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/plugin-approvals": minor
3+
---
4+
5+
feat(plugin-approvals): the decision that ends an approval tells its submitter the outcome (`approval.approved` / `approval.rejected`)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What was wrong.** Approving or rejecting a request notified nobody. A send-back (`approval.returned`) and a question (`approval.request_info`) reached the submitter, but the decision itself did not, so a submitter learned that their request was approved or rejected only by opening the Approvals Inbox.
10+
- **What is new.** When a decision ends the approval, the submitter of the request it decides gets one notification on one of two new topics: `approval.approved` (title "Request approved") or `approval.rejected` (title "Request rejected"). The body names the record by its object's label and its id, as every approval notification does (`Your Deal D_123 was rejected.`), followed by the decider's comment as its own paragraph when they wrote one. It links to that request in the Approvals Inbox and, like the other approval notices, carries as its actor the person the decision's context vouches for. It goes through the installed `messaging` service like every other approval topic, so the same channels and delivery preferences apply; a preference row can mute one outcome and keep the other.
11+
- **When "ends the approval" holds.** A co-sign vote that leaves a `unanimous`, `quorum` or `per_group` request pending tells no one. A decision whose flow moves on to another approval step tells no one either; the decision that ends the run is the one that notifies. That is read from the run after the decision resumes it, not from the flow's graph, so when a decision node routes around a later step, the first decision is the outcome.
12+
- **Who is not told.** A submitter who decided their own request. And an SLA escalation's `auto_approve` or `auto_reject`: the escalation's own submitter notice (`approval.sla_breached`, naming the action taken) is the one message about it, so `escalation.notifySubmitter: false` still means the submitter hears nothing. A decision whose flow run cannot be resumed reports its failure to the decider and tells the submitter nothing, as a send-back in the same state already did.
13+
- **What to change in your flows.** If a flow ends with a `notify` node whose only job is to tell the submitter the decision, remove it, or the submitter is told twice. `notify` nodes that address anyone else are unaffected. No flow in this repository's examples has that workaround: the three showcase flows whose `notify` node reports a decision address someone other than the submitter — `showcase_invoice_signoff` and `showcase_project_closure` the record's owner, `showcase_release_signoff` a fixed address — so they are unchanged, and only a record owner who is also the person who submitted now hears both.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix(plugin-approvals): the out-of-office delegation form's Delegator picker offers only the signed-in user, the one delegator the server accepts
6+
7+
Clause-②: no
8+
9+
No accept set moves. The field gains a picker filter, which nothing on the write path reads, and the delegation write guard and `required` are unchanged.
10+
11+
- **Before.** `sys_approval_delegation.delegator_id` was a free `sys_user` lookup, so Approvals → Delegations → New offered every user. The delegation write guard accepts one delegator on a non-system write, the acting user, so picking anyone else was refused with 403 `FORBIDDEN`.
12+
- **Now.** The field declares `lookupFilters: [{ field: 'id', operator: 'eq', value: '{current_user_id}' }]`. The engine resolves the placeholder against the caller on every read, so the picker's candidate query offers the signed-in user and nobody else.
13+
- **Unchanged.** What a write may store. A create or an update that names another delegator is still refused, and there is no admin exemption. An admin covering for an absent approver reassigns, recalls or rejects that approver's pending approvals through the approval override instead.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
fix(service-settings): Setup's AI "Test embedder" and Knowledge "Test connection" buttons are answered, and their answers say they checked the form only
6+
7+
Clause-②: no
8+
9+
- **Before.** Both buttons answered `400 SETTINGS_ACTION_FAILED` with "No handler registered for action 'test_embedder' in 'ai'." and "No handler registered for action 'test' in 'knowledge'." The manifests declared both buttons and the package exported both handlers (`aiTestEmbedderActionHandler`, `knowledgeTestActionHandler`), but `SettingsServicePlugin`'s default `actionHandlers` map did not register them.
10+
- **Now.** The default map registers both, beside `mail.test`, `sms.test`, `storage.test` and `ai.test`. A host that passes its own `actionHandlers` still replaces the whole default map, and a plugin can still replace any one handler with `SettingsService.registerAction`.
11+
- **What each button checks.** Both are form checks and send nothing over the network. "Test embedder" checks that a provider is chosen, that an API key is set (Ollama excepted) and that `custom` / `azure` have a Base URL. "Test connection" checks that a managed `libsql://` Turso URL has an auth token and that RAGFlow has a Base URL and an API key. The in-memory adapter answers with a warning that there is no external service to probe.
12+
- **Answer text.** A passing "Test embedder" now ends "This checks the form only — no embedding request was sent." Every passing "Test connection" on Turso or RAGFlow now says "This checks the form only — no connection was attempted." The old "Mount @objectstack/embedder-openai + a knowledge adapter to exercise live calls" and "Mount @objectstack/knowledge-ragflow to exercise live calls" are gone: neither package registers these actions, so mounting it never made either check live.

0 commit comments

Comments
 (0)