Skip to content

Commit a228d74

Browse files
committed
test(security): pin the standing-audit double to the engine contract, and add the changeset
The new double's `update()` now opens with `assertEngineUpdateDispatch` and its `where` matcher refuses the combinators it does not implement; the boot's unchanged-standing line drops the tracker id from the runtime string. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
1 parent 2734290 commit a228d74

4 files changed

Lines changed: 41 additions & 3 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
"@objectstack/plugin-audit": minor
4+
---
5+
6+
The walled boot records platform-admin standing on the existing audit ledger, so «who held administrator standing, and from when» survives the move off the stored grant row (#18412).
7+
8+
Platform-admin standing moved from a **stored grant row** to **config-derived, request-time resolution** (#11663 re-anchor, ADR-0131). The row carried its own history; config carries none. After the migration the only trace of a grant or a revocation was a change to `OS_PLATFORM_OWNER_EMAIL` plus a restart — the product keeps no environment-variable history and an auditor cannot read one. `sys_audit_log` recorded the ACTIONS all along; what had no writer at all was the **basis** of the authority behind them.
9+
10+
The answer was already being computed and thrown away: `resolvePlatformAdminStanding` builds the per-entry summary at every walled boot and the bootstrap logs it at `info`.
11+
12+
- **`@objectstack/plugin-audit`** — `sys_audit_log.action` declares one new value, `platform_admin_standing_change`, WRITER-FIRST (the only way a value is allowed onto that enum). Its rows appear on the shipped, unfiltered `recent` and `all_events` views; ⛔ no new list view, ⛔ no new object, ⛔ no new configuration key.
13+
- **`@objectstack/plugin-security`** — the walled bootstrap compares the resolved standing against the last snapshot already on the ledger and writes **one entry per CHANGE of standing**, plus the **first-boot baseline**. A restarted rig writes nothing. Each row carries, per declared entry, the declared spelling, whether an account exists, whether it is verified, and which user id holds standing; `old_value` and `new_value` state both sides of the delta, and `old_value` is null on the baseline row and only there.
14+
- **The `single` posture is untouched.** It still promotes the first registrant and still writes a durable grant row, so the durability this restores is walled-posture-specific.
15+
- ⭐ **`organization_id` is NULL on this row, deliberately and by maintainer ruling** (2026-09-18, director batch #153 item 2). The record is deployment-level by construction: ADR-0131 §1.5 rejects inventing a platform organization in its own words («it is the natural repair and the wrong one … exists only to give NULL a new name»), a tenant id would file a whole-deployment fact behind one tenant's wall, and the first-boot baseline is written before any `sys_organization` row exists at all. This follows the tree's four existing deployment-level audit writers, and is the shape ADR-0131 D7 will later make structural by dropping the column. The exception is recorded beside the write, on the card, and in a pin — ⛔ it is not a gap waiting to be repaired.
16+
- **Nothing here widens who holds standing or what standing permits.** The derivation site is untouched; this adds a RECORD of authority, never a grant of it.
17+
- **Best-effort, and never fatal to boot.** A deployment that never mounted the optional `@objectstack/plugin-audit` skips silently — an unmounted ledger is a composition choice, not a fault. A ledger read that is REFUSED writes nothing and says so: «cannot tell» is not «first boot», and reading it that way would file a fresh baseline on every restart. A mounted ledger whose insert fails reports a durability degradation on the `error` channel.

‎packages/plugins/plugin-security/src/bootstrap-platform-admin.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,12 @@ async function recordPlatformAdminStandingChange(
196196
const serialized = serializePlatformAdminStandingSnapshot(snapshot);
197197
const previousSerialized = readRecordedStandingSnapshot(previousRows[0]);
198198
if (!platformAdminStandingChanged(previousSerialized, serialized)) {
199+
// ⛔ The card id stays in this comment and out of the STRING: a runtime
200+
// line reaches operators, who have no tracker to resolve `#NNNN` against
201+
// (#18412; `check:doc-authoring`).
199202
logger?.debug?.(
200203
'[security] platform-admin standing is unchanged since the last recorded entry — no ' +
201-
'audit row written. One entry per CHANGE of standing is the recorded shape (#18412).',
204+
'audit row written. One entry per CHANGE of standing is the recorded shape.',
202205
);
203206
return;
204207
}

‎packages/plugins/plugin-security/src/platform-admin-standing-audit.test.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
21+
import { assertEngineUpdateDispatch } from '@objectstack/metadata-core';
2122
import { bootstrapPlatformAdmin } from './bootstrap-platform-admin.js';
2223
import {
2324
buildPlatformAdminStandingRow,
@@ -83,7 +84,14 @@ function makeQl(
8384
}
8485
const where = q?.where ?? {};
8586
const rows = (tables.get(object) ?? []).filter((r) =>
86-
Object.entries(where).every(([k, v]) => r[k] === v),
87+
Object.entries(where).every(([k, v]) => {
88+
// ⛔ REFUSE what this double does not implement. A matcher that reads
89+
// `$or` as a field name answers `[]` for a query it never ran, and on
90+
// the standing read `[]` means "no record has ever been written" —
91+
// it would pass a suite that cannot see the query it is pinning.
92+
if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`);
93+
return r[k] === v;
94+
}),
8795
);
8896
// The read is ordered IN THE QUERY (`orderBy`), so the double honours it
8997
// rather than handing back insertion order — a double that ignored the
@@ -107,7 +115,12 @@ function makeQl(
107115
tables.get(object)!.push({ ...data });
108116
return { id: data.id };
109117
},
110-
async update() {
118+
// Opens with the PRODUCER's own dispatch predicate rather than a
119+
// hand-mirrored guard (`check:engine-double-contract`): a fixture drifting
120+
// to a call shape ObjectQL.update would refuse fails loudly here. Nothing
121+
// in this file updates, and that is exactly why the refusal must be real.
122+
async update(object: string, data: any, options?: any) {
123+
assertEngineUpdateDispatch(data, options);
111124
return null;
112125
},
113126
auditRows(): any[] {

‎scripts/engine-double-contract.pinned.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,6 +2946,11 @@
29462946
"verb": "findOne",
29472947
"pinned": 1
29482948
},
2949+
{
2950+
"file": "packages/plugins/plugin-security/src/platform-admin-standing-audit.test.ts",
2951+
"verb": "update",
2952+
"pinned": 1
2953+
},
29492954
{
29502955
"file": "packages/plugins/plugin-security/src/record-share-tenant-wall.test.ts",
29512956
"verb": "update",

0 commit comments

Comments
 (0)