Skip to content

Commit 5297072

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21934-org-overlay-publish-gate
2 parents 1e271aa + 76fec88 commit 5297072

30 files changed

Lines changed: 1308 additions & 94 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/service-settings": minor
3+
"@objectstack/service-messaging": patch
4+
"@objectstack/service-datasource": minor
5+
"@objectstack/plugin-webhooks": patch
6+
---
7+
8+
Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off.
9+
10+
Clause-②: yes (widening)
11+
12+
- **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes.
13+
- **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too.
14+
- **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`.
15+
- **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in.
16+
- **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt.
17+
- **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged.
18+
- What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects.
19+
- ⛔ No new export on any package entry, and no new elevation API.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **67 of the 233 sites are spelled that way**. A
125+
forwarding shim cannot, and **60 of the 233 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -150,8 +150,8 @@ now **0**: nothing on this surface threads a context that provably lacks the fla
150150

151151
**"No tenant context" counted sites it had not read.** An options argument the
152152
walker could not parse was folded into the same bucket as one it had read and
153-
found empty. That published **84 sites "carrying no tenant context at all"**
154-
when 17 said so and 67 were simply unread — an over-claim in the *alarming*
153+
found empty. That published **69 sites "carrying no tenant context at all"**
154+
when 9 said so and 60 were simply unread — an over-claim in the *alarming*
155155
direction, on the very figure this page tells other cards to cite. `carries` is
156156
now three-valued, and an unreadable argument can never contribute to the
157157
provable count.
@@ -188,9 +188,9 @@ reproduce them. Where it disagrees, it disagrees on the page:
188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190190
| 175 write call sites | quoted in the merged changeset | **233** |
191-
| 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **34** more whose options argument is unreadable |
191+
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **33** more whose options argument is unreadable |
192192
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **155 of 233** decidable, **78** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 114 decidably elevated, 0 decidably not, 102 undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 121 decidably elevated, 0 decidably not, 103 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -207,14 +207,14 @@ would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
114 of 233 (49%) as decidably elevated, with 102 more whose elevation is a
210+
121 of 233 (52%) as decidably elevated, with 103 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `9 / 233`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 233`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217-
without tenant context" — **34 further sites** have an options argument this
217+
without tenant context" — **33 further sites** have an options argument this
218218
cannot read, and they are neither in nor out.
219219

220220
{/* BEGIN GENERATED: tenant-audit-census (scripts/tenant-audit-census.mjs) — DO NOT EDIT */}
@@ -228,14 +228,14 @@ cannot read, and they are neither in nor out.
228228
| …whose object name is chosen at run time | 78 |
229229
| …against an object with tenancy ENABLED | 154 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 149 |
232-
| PROVABLY carrying none (options read, no context key) | **17** |
233-
| …of those, against a decidably tenancy-enabled object | **9** |
234-
| options argument UNREADABLE — may or may not carry one | 67 |
235-
| …of those, against a decidably tenancy-enabled object | 34 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 114 |
231+
| threading a tenant context | 164 |
232+
| PROVABLY carrying none (options read, no context key) | **9** |
233+
| …of those, against a decidably tenancy-enabled object | **2** |
234+
| options argument UNREADABLE — may or may not carry one | 60 |
235+
| …of those, against a decidably tenancy-enabled object | 33 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 121 |
237237
| threading a context that is decidably NOT elevated | 0 |
238-
| threading a context whose elevation is a run-time fact | 102 |
238+
| threading a context whose elevation is a run-time fact | 103 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-05 at `3d34c6efd`.
300+
Measured on 2026-10-06 at `3832674ac`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 607 |
305-
| engine-shaped types recognised | 69 |
304+
| tracked non-test sources scanned | 609 |
305+
| engine-shaped types recognised | 70 |
306306
| declared objects in the registry | 116 |
307-
| same-named calls subtracted as non-engine | 158 |
307+
| same-named calls subtracted as non-engine | 159 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3838
| Object name chosen at run time | 78 |
3939
| Against a tenancy-enabled object | 154 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 149 |
42-
| Provably carrying none | 17 |
43-
| …and decidably tenancy-enabled | 9 |
44-
| Options argument unreadable | 67 |
45-
| …and decidably tenancy-enabled | 34 |
46-
| Threading a decidably elevated context | 114 |
41+
| Threading a tenant context | 164 |
42+
| Provably carrying none | 9 |
43+
| …and decidably tenancy-enabled | 2 |
44+
| Options argument unreadable | 60 |
45+
| …and decidably tenancy-enabled | 33 |
46+
| Threading a decidably elevated context | 121 |
4747
| Threading a decidably non-elevated context | 0 |
48-
| Threading a context of undecidable elevation | 102 |
48+
| Threading a context of undecidable elevation | 103 |
4949

5050
## Subtractions the census could NOT defend — enforced
5151

@@ -90,14 +90,14 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-05 at `3d34c6efd`.
93+
Measured on 2026-10-06 at `3832674ac`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 607 |
98-
| engine-shaped types recognised | 69 |
97+
| tracked non-test sources scanned | 609 |
98+
| engine-shaped types recognised | 70 |
9999
| declared objects in the registry | 116 |
100-
| same-named calls subtracted as non-engine | 158 |
100+
| same-named calls subtracted as non-engine | 159 |
101101

102102
## Every site
103103

@@ -208,24 +208,26 @@ Measured on 2026-10-05 at `3d34c6efd`.
208208
| `packages/services/service-automation/src/suspended-run-store.ts` | `delete` | `sys_automation_run` | enabled | elevated | 3 |
209209
| `packages/services/service-automation/src/suspended-run-store.ts` | `insert` | `sys_automation_run` | enabled | elevated | 2 |
210210
| `packages/services/service-automation/src/suspended-run-store.ts` | `update` | `sys_automation_run` | enabled | elevated | 2 |
211-
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `delete` | `sys_metadata` | enabled | PROVABLY NONE | 1 |
212-
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `insert` | `sys_metadata` | enabled | PROVABLY NONE | 1 |
213-
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `update` | `sys_metadata` | enabled | PROVABLY NONE | 2 |
214-
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `delete` | `sys_secret` | enabled | PROVABLY NONE | 1 |
215-
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `insert` | `sys_secret` | enabled | PROVABLY NONE | 1 |
211+
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `delete` | `sys_metadata` | enabled | elevated | 1 |
212+
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `insert` | `sys_metadata` | enabled | elevated | 1 |
213+
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `update` | `sys_metadata` | enabled | elevated | 2 |
214+
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `delete` | `sys_secret` | enabled | elevated | 1 |
215+
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `insert` | `sys_secret` | enabled | elevated | 1 |
216216
| `packages/services/service-job/src/db-job-adapter.ts` | `insert` | `sys_job` | enabled | elevated | 1 |
217217
| `packages/services/service-job/src/db-job-adapter.ts` | `update` | `sys_job` | enabled | elevated | 3 |
218218
| `packages/services/service-job/src/db-job-adapter.ts` | `insert` | `sys_job_run` | enabled | elevated | 1 |
219219
| `packages/services/service-job/src/db-job-adapter.ts` | `update` | `sys_job_run` | enabled | elevated | 1 |
220-
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `objectName` | undecidable | options unreadable | 1 |
221-
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `receiptObject` | undecidable | PROVABLY NONE | 1 |
220+
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `objectName` | undecidable | context, elevation undecidable | 1 |
221+
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `receiptObject` | undecidable | context, elevation undecidable | 1 |
222222
| `packages/services/service-messaging/src/messaging-service.ts` | `insert` | `RECEIPT_OBJECT` | undecidable | PROVABLY NONE | 1 |
223223
| `packages/services/service-messaging/src/messaging-service.ts` | `update` | `RECEIPT_OBJECT` | undecidable | options unreadable | 1 |
224-
| `packages/services/service-messaging/src/messaging-service.ts` | `insert` | `sys_notification` | enabled | options unreadable | 1 |
224+
| `packages/services/service-messaging/src/messaging-service.ts` | `insert` | `sys_notification` | enabled | context, elevation undecidable | 1 |
225225
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
226-
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 5 |
226+
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `update` | `this.objectName` | undecidable | context, elevation undecidable | 2 |
227+
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 3 |
227228
| `packages/services/service-messaging/src/sql-outbox.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
228-
| `packages/services/service-messaging/src/sql-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 4 |
229+
| `packages/services/service-messaging/src/sql-outbox.ts` | `update` | `this.objectName` | undecidable | context, elevation undecidable | 3 |
230+
| `packages/services/service-messaging/src/sql-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 1 |
229231
| `packages/services/service-queue/src/db-queue-adapter.ts` | `delete` | `sys_job_queue` | enabled | context, elevation undecidable | 2 |
230232
| `packages/services/service-queue/src/db-queue-adapter.ts` | `insert` | `sys_job_queue` | enabled | context, elevation undecidable | 1 |
231233
| `packages/services/service-queue/src/db-queue-adapter.ts` | `update` | `sys_job_queue` | enabled | context, elevation undecidable | 6 |
@@ -235,7 +237,7 @@ Measured on 2026-10-05 at `3d34c6efd`.
235237
| `packages/services/service-settings/src/settings-service-plugin.ts` | `delete` | `sys_secret` | enabled | elevated | 1 |
236238
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_secret` | enabled | options unreadable | 1 |
237239
| `packages/services/service-settings/src/settings-service-plugin.ts` | `update` | `sys_secret` | enabled | options unreadable | 1 |
238-
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_setting_audit` | enabled | PROVABLY NONE | 1 |
240+
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_setting_audit` | enabled | elevated | 1 |
239241
| `packages/services/service-settings/src/settings-service.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
240242
| `packages/services/service-settings/src/settings-service.ts` | `update` | `this.objectName` | undecidable | options unreadable | 1 |
241243
| `packages/services/service-storage/src/attachment-lifecycle.ts` | `update` | `sys_file` | enabled | elevated | 3 |

‎packages/plugins/plugin-webhooks/src/auto-enqueuer.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ import {
2222
type LegacyDefinitionCredentialKey,
2323
} from './webhook-legacy-cleartext.js';
2424

25+
/**
26+
* [#21913] The execution context the subscription cache refresh
27+
* ({@link AutoEnqueuer.refresh}) reads `sys_webhook` under: the explicit system
28+
* opt-in. It is the platform reading its own delivery configuration on a boot
29+
* and timer path that has no caller, so it may not rely on a missing principal
30+
* to pass the security middleware's principal-less hand-off, which ADR-0096 D5
31+
* closes.
32+
*/
33+
const SYSTEM_CTX = { isSystem: true } as const;
34+
2535
/**
2636
* The authored trigger vocabulary, taken from the spec rather than restated
2737
* here — this file both validates authored triggers and maps events onto them,
@@ -378,9 +388,11 @@ export class AutoEnqueuer {
378388
private async doRefresh(): Promise<void> {
379389
let rows: any[];
380390
try {
381-
rows = await this.engine.find(this.subscriptionsObject, {
382-
where: { active: true },
383-
});
391+
rows = await this.engine.find(
392+
this.subscriptionsObject,
393+
{ where: { active: true } },
394+
{ context: SYSTEM_CTX },
395+
);
384396
} catch (err) {
385397
this.logger?.warn?.(
386398
`[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,

‎packages/plugins/plugin-webhooks/src/redeliver-guard.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ import {
5959
resolveWebhookSecret,
6060
} from './webhook-secret.js';
6161

62+
/**
63+
* [#21913] The execution context the guard reads `sys_webhook` under: the
64+
* explicit system opt-in. The guard runs inside the messaging service's
65+
* redeliver path, after the delivery row has been read under the requesting
66+
* caller's organization, and reads the subscription that row belongs to only
67+
* for its existence, name and secret posture — the inputs of the refusal
68+
* reason it returns. What it reads and returns is unchanged by the opt-in; it
69+
* may simply no longer rely on a missing principal to pass the security
70+
* middleware's principal-less hand-off, which ADR-0096 D5 closes.
71+
*/
72+
const SYSTEM_CTX = { isSystem: true } as const;
73+
6274
/** The delivery-row fields this guard reads. Structural — no messaging import. */
6375
export interface RedeliverGuardRow {
6476
/** Producer domain; only `'webhook'` rows are this guard's business. */
@@ -79,9 +91,11 @@ export function createWebhookRedeliverGuard(
7991
return async (row) => {
8092
if (row.source !== 'webhook') return undefined;
8193

82-
const subscription = (await engine.findOne(subscriptionsObject, {
83-
where: { id: row.refId },
84-
})) as Record<string, unknown> | null;
94+
const subscription = (await engine.findOne(
95+
subscriptionsObject,
96+
{ where: { id: row.refId } },
97+
{ context: SYSTEM_CTX },
98+
)) as Record<string, unknown> | null;
8599

86100
if (!subscription) {
87101
return (

0 commit comments

Comments
 (0)