@substrat-run/adapter-cloudflare 0.159.0 → 0.160.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
1
  import { DurableObject } from 'cloudflare:workers';
2
- import { membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, MEMBERSHIP_FENCES_TABLE, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, reservedRoleRefusal, tenantCoverage, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink as insertConnectLinkRow, listConnectLinks as listConnectLinkRows, readConnectLink as readConnectLinkRow, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, clearSwitchOwed, markSwitchOwed, scopeOwesSwitch, switchesOwedOf, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, recordSwitchedOff, recordSwitchedOn, recordWriteSuperseded, restoreSwitchRecord, scopesSwitchedOffFor, switchRecordStatesOf, switchRecordsOf, switchesBackfillSqlOf, switchesDdlOf, switchesTableExists, switchesTableOf, tenantHeldOf, VERSION_MIGRATIONS_DDL, VERTICAL_SCOPE_STORES_DDL, STORE_DRIVER_RELEASES_DDL, legacyStoreDriverConflict, legacyStoreDriverRenames, beginStoreDriverRolloutIn, finishStoreDriverRolloutIn, holdStoreDriverIn, advanceStoreDriverRebuildIn, claimStoreDriverRebuildIn, storeDriverRebuildIn, driverDeclarationsOf, recordStoreDriverReleaseIn, releaseStoreDriverHoldIn, storeDriverReleaseIn, storeDriverStateIn, storeDriverRolloutPendingIn, storeDriverRolloutWalkedIn, forgetVerticalScopeStores, indexVersionScopeStores, verticalScopeStoresOf, splitVersionMigrationsBatch, versionMigrationsOf, versionsAwaitSplit, versionsAwaitDigest, deriveMigrationDigestsBatch, deriveVersionMigrationDigest, writeVersionMigrations, MODEL_USAGE_RETENTION_DAYS, DO_SQL_LIMITS, ulid, isPrimaryScopeRow, samePeerVertical, peerVerticalSpellings, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, LEGACY_SCOPE_ROWS_BACKFILL, loadDirectoryDump, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, unknownOutcomeOf, ADMIN_LOG_INDEX_DDL, ADMIN_LOG_INDEXES_SQL, readAuditedOperations, } from '@substrat-run/kernel';
2
+ import { membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, MEMBERSHIP_FENCES_TABLE, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, reservedRoleDefinitionRefusal, reservedRoleRefusal, TENANT_SEATED_ROLES_SQL, tenantCoverage, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, retireScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeAttachmentStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, REQUEST_ROLLUP_DDL, listRequestDayRows, listRequestRollupDayRows, pruneRequestDayRows, recordRequestDayRows, METER_DAYS_DDL, VERTICAL_OWNERS_BACKFILL, VERTICAL_OWNERS_DDL, closeVerticalOwnerRow, listVerticalOwnerRows, openVerticalOwnerRow, verticalOwnerNow, listMeterDayRows, listMeterDayTakeRows, pruneMeterDayRows, recordMeterDayRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink as insertConnectLinkRow, listConnectLinks as listConnectLinkRows, readConnectLink as readConnectLinkRow, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, clearSwitchOwed, markSwitchOwed, scopeOwesSwitch, switchesOwedOf, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, recordSwitchedOff, recordSwitchedOn, recordWriteSuperseded, restoreSwitchRecord, scopesSwitchedOffFor, switchRecordStatesOf, switchRecordsOf, switchesBackfillSqlOf, switchesDdlOf, switchesTableExists, switchesTableOf, tenantHeldOf, VERSION_MIGRATIONS_DDL, VERTICAL_SCOPE_STORES_DDL, DEPLOYMENT_WIRE_AUTH_DDL, deploymentWireAuthOf, forgetVerticalWireAuth, writeDeploymentWireAuth, STORE_DRIVER_RELEASES_DDL, legacyStoreDriverConflict, legacyStoreDriverRenames, beginStoreDriverRolloutIn, finishStoreDriverRolloutIn, holdStoreDriverIn, advanceStoreDriverRebuildIn, claimStoreDriverRebuildIn, storeDriverRebuildIn, driverDeclarationsOf, recordStoreDriverReleaseIn, releaseStoreDriverHoldIn, storeDriverReleaseIn, storeDriverStateIn, storeDriverRolloutPendingIn, storeDriverRolloutWalkedIn, forgetVerticalScopeStores, indexVersionScopeStores, verticalScopeStoresOf, splitVersionMigrationsBatch, versionMigrationsOf, versionsAwaitSplit, versionsAwaitDigest, deriveMigrationDigestsBatch, deriveVersionMigrationDigest, writeVersionMigrations, MODEL_USAGE_RETENTION_DAYS, DO_SQL_LIMITS, ulid, isPrimaryScopeRow, samePeerVertical, peerVerticalSpellings, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, LEGACY_SCOPE_ROWS_BACKFILL, loadDirectoryDump, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, unknownOutcomeOf, ADMIN_LOG_INDEX_DDL, ADMIN_LOG_INDEXES_SQL, readAuditedOperations, } from '@substrat-run/kernel';
3
3
  import { replyOf } from './do-reply.js';
4
4
  import { switchSqlOver } from './scope-do.js';
5
5
  import { blankSqlComments, boundShapesColumn, executableSqlStatements, installsReconciledOf, PROVISIONED_VERSIONS_SQL, provisionedVersionsParams, } from '@substrat-run/kernel';
6
6
  import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, COPY_MOVE_RENEW_SQL, copyMoveRenewParams, PENDING_COPY_KEEP_ALIVE_SQL, pendingCopyKeepAliveParams, copyBackfillParams, copyBackfillRefusal, SUBJECT_ERASURE_CLAIM_SQL, subjectErasureClaimParams, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, } from '@substrat-run/kernel';
7
+ import { opsFailureReferenceWhere } from '@substrat-run/kernel';
7
8
  import { doBuiltColumnsOf, doRedactionSql } from './sql.js';
8
- import { assertReplayableDump, opsFailureFingerprint, ROUTE_SCOPE_HOSTNAMES_MAX, SCOPE_GATE_REASONS, substratError } from '@substrat-run/contracts';
9
+ import { assertReplayableDump, instant, opsFailureFingerprint, ROUTE_SCOPE_HOSTNAMES_MAX, SCOPE_GATE_REASONS, substratError } from '@substrat-run/contracts';
9
10
  /**
10
11
  * The router's per-request directory read (`readRoute`), lifted out so its plan can be asked for
11
12
  * (#2005): the scope's hostname set is a correlated subquery on the hot path every request takes,
@@ -315,6 +316,7 @@ const DIRECTORY_DDL = `
315
316
  );
316
317
  ${VERSION_MIGRATIONS_DDL}
317
318
  ${VERTICAL_SCOPE_STORES_DDL}
319
+ ${DEPLOYMENT_WIRE_AUTH_DDL}
318
320
  CREATE TABLE IF NOT EXISTS vertical_channels (
319
321
  vertical_slug TEXT NOT NULL,
320
322
  channel TEXT NOT NULL,
@@ -653,6 +655,12 @@ const DIRECTORY_DDL = `
653
655
  CREATE INDEX IF NOT EXISTS _substrat_model_usage_at ON _substrat_model_usage (at);
654
656
  -- #1524: the stored storage gauge — one row per (scope, UTC day), kernel-owned DDL.
655
657
  ${SCOPE_STORAGE_DDL}
658
+ -- #2501: the daily request rollup — one row per (UTC day, tenant, vertical, scope), kernel-owned DDL.
659
+ ${REQUEST_ROLLUP_DDL}
660
+ -- #2501: the daily meter snapshot — one row per (UTC day, tenant), kernel-owned DDL.
661
+ ${METER_DAYS_DDL}
662
+ -- #2501: who owned each vertical slug, period by period — kernel-owned DDL.
663
+ ${VERTICAL_OWNERS_DDL}
656
664
  CREATE INDEX IF NOT EXISTS scopes_tenant ON scopes (tenant_id, scope_id);
657
665
  -- #1713: the lifecycle each hosted scope's deployment last acknowledged, as
658
666
  -- "<scope status>/<tenant status>" (lifecycleReceipt). A CP-less deployment has no
@@ -1177,6 +1185,8 @@ export class ControlPlaneDO extends DurableObject {
1177
1185
  this.addColumn('_substrat_entitlements', 'granted_by TEXT');
1178
1186
  for (const stmt of LEGACY_SCOPE_ROWS_BACKFILL)
1179
1187
  this.sql.exec(stmt);
1188
+ // #2501: an ownership period for every vertical registered before the history existed.
1189
+ this.sql.exec(VERTICAL_OWNERS_BACKFILL);
1180
1190
  // After the backfill: a UNIQUE index over NULL slugs would permit the
1181
1191
  // duplicates it exists to forbid (SQLite treats NULLs as distinct).
1182
1192
  //
@@ -1316,8 +1326,11 @@ export class ControlPlaneDO extends DurableObject {
1316
1326
  .toArray()[0];
1317
1327
  return row ? this.mapTenant(row) : undefined;
1318
1328
  }
1319
- /** Idempotent on the id; return the new tenant, or null if it already existed. */
1320
- createTenant(id, slug, name, createdAt, provisionedByTenant = null) {
1329
+ /**
1330
+ * Idempotent on the id; return the new tenant, or null if it already existed. `audit` is the
1331
+ * coordinator's row, written in the insert's unit with the tenant as its `after`.
1332
+ */
1333
+ createTenant(id, slug, name, createdAt, provisionedByTenant = null, audit) {
1321
1334
  if (this.readTenant(id))
1322
1335
  return null; // idempotent — nothing created
1323
1336
  // Checked explicitly rather than left to `INSERT OR IGNORE` + the
@@ -1330,12 +1343,12 @@ export class ControlPlaneDO extends DurableObject {
1330
1343
  if (slugOwner) {
1331
1344
  throw substratError('conflict', `tenant slug '${slug}' already taken by ${slugOwner.tenant_id} (slugs are unique)`);
1332
1345
  }
1333
- this.sql.exec(`INSERT INTO tenants (tenant_id, slug, name, status, created_at, provisioned_by_tenant)
1334
- VALUES (?, ?, ?, 'active', ?, ?)`, id, slug, name, createdAt, provisionedByTenant);
1346
+ this.writeAndAudit(() => this.sql.exec(`INSERT INTO tenants (tenant_id, slug, name, status, created_at, provisioned_by_tenant)
1347
+ VALUES (?, ?, ?, 'active', ?, ?)`, id, slug, name, createdAt, provisionedByTenant).rowsWritten > 0, audit && (() => ({ ...audit, after: this.readTenant(id) })));
1335
1348
  return this.readTenant(id) ?? null;
1336
1349
  }
1337
- /** Throw if absent; else UPDATE and return the previous status. */
1338
- setTenantStatus(tenantId, status) {
1350
+ /** Throw if absent; else UPDATE and return the previous status, which `audit`'s `before` records. */
1351
+ setTenantStatus(tenantId, status, audit) {
1339
1352
  const before = this.readTenant(tenantId);
1340
1353
  if (!before)
1341
1354
  throw substratError('not_found', `unknown tenant: ${tenantId}`);
@@ -1348,15 +1361,13 @@ export class ControlPlaneDO extends DurableObject {
1348
1361
  // Stamp/clear deleting_at so the grace-window sweep can age tenants (§4.8):
1349
1362
  // entering `deleting` stamps, leaving it (an un-delete) clears — the same shape
1350
1363
  // transitionScope uses for archived_at.
1351
- this.ctx.storage.transactionSync(() => {
1352
- if (status === 'deleting') {
1353
- this.sql.exec('UPDATE tenants SET status = ?, deleting_at = ? WHERE tenant_id = ?', status, new Date().toISOString(), tenantId);
1354
- }
1355
- else {
1356
- this.sql.exec('UPDATE tenants SET status = ?, deleting_at = NULL WHERE tenant_id = ?', status, tenantId);
1357
- }
1364
+ this.writeAndAudit(() => {
1365
+ const moved = (status === 'deleting'
1366
+ ? this.sql.exec('UPDATE tenants SET status = ?, deleting_at = ? WHERE tenant_id = ?', status, new Date().toISOString(), tenantId)
1367
+ : this.sql.exec('UPDATE tenants SET status = ?, deleting_at = NULL WHERE tenant_id = ?', status, tenantId)).rowsWritten > 0;
1358
1368
  this.bumpLifecycleRevision(`tenant:${tenantId}`); // #1713, with the change it counts
1359
- });
1369
+ return moved;
1370
+ }, audit && { ...audit, before: { status: before.status } });
1360
1371
  return before.status;
1361
1372
  }
1362
1373
  /**
@@ -1367,7 +1378,7 @@ export class ControlPlaneDO extends DurableObject {
1367
1378
  * previous status (for the audit before/after). Idempotent: set-to-empty DELETEs
1368
1379
  * plus the status flip converge on a re-run after a partial failure.
1369
1380
  */
1370
- reapTenant(tenantId) {
1381
+ reapTenant(tenantId, audit) {
1371
1382
  const before = this.readTenant(tenantId);
1372
1383
  if (!before)
1373
1384
  throw substratError('not_found', `unknown tenant: ${tenantId}`);
@@ -1376,40 +1387,47 @@ export class ControlPlaneDO extends DurableObject {
1376
1387
  }
1377
1388
  // KEPT as the tombstone: the `tenants` row itself and `_substrat_admin_log` (the
1378
1389
  // compliance witness — never swept). Scope rows were already reaped individually.
1379
- for (const table of [
1380
- '_substrat_identities', // PII: external subjects/emails bound to principals
1381
- '_substrat_identity_pools', // the tenant's IdP topology declarations
1382
- '_substrat_tenant_tuples', // membership + tenant-level grants
1383
- '_substrat_roles', // operator-defined roles
1384
- '_substrat_entitlements', // per-tenant SKU flags
1385
- 'orgs', // K-22 org records
1386
- '_substrat_system_switches', // #1674: the schedule switch's record, per scope
1387
- '_substrat_membership_fences', // #1184: the latest removal, per principal
1388
- '_substrat_peer_switches', // #2029: the peer switch's record, per scope
1389
- 'peer_bindings', // #1720: a tenant's explicit caller-to-target choices
1390
- '_substrat_switch_owed', // #2045: subjects owed a re-assert, per scope
1391
- '_substrat_findings', // #1748: the tenant's findings
1392
- '_substrat_finding_rules', // #1748: the tenant's suppress rules
1393
- '_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
1394
- '_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
1395
- '_substrat_scope_store_storage', // #2212: scope-store samples, gone with the tenant
1396
- ]) {
1397
- this.sql.exec(`DELETE FROM ${table} WHERE tenant_id = ?`, tenantId);
1398
- }
1399
- this.ctx.storage.transactionSync(() => {
1400
- this.sql.exec("UPDATE tenants SET status = 'reaped', deleting_at = NULL WHERE tenant_id = ?", tenantId);
1390
+ // One unit with the flip and `audit`, whose `before` is the status read here.
1391
+ this.writeAndAudit(() => {
1392
+ for (const table of [
1393
+ '_substrat_identities', // PII: external subjects/emails bound to principals
1394
+ '_substrat_identity_pools', // the tenant's IdP topology declarations
1395
+ '_substrat_tenant_tuples', // membership + tenant-level grants
1396
+ '_substrat_roles', // operator-defined roles
1397
+ '_substrat_entitlements', // per-tenant SKU flags
1398
+ 'orgs', // K-22 org records
1399
+ '_substrat_system_switches', // #1674: the schedule switch's record, per scope
1400
+ '_substrat_membership_fences', // #1184: the latest removal, per principal
1401
+ '_substrat_peer_switches', // #2029: the peer switch's record, per scope
1402
+ 'peer_bindings', // #1720: a tenant's explicit caller-to-target choices
1403
+ '_substrat_switch_owed', // #2045: subjects owed a re-assert, per scope
1404
+ '_substrat_findings', // #1748: the tenant's findings
1405
+ '_substrat_finding_rules', // #1748: the tenant's suppress rules
1406
+ '_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
1407
+ '_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
1408
+ '_substrat_scope_store_storage', // #2212: scope-store samples, gone with the tenant
1409
+ '_substrat_scope_attachment_storage', // #2501: attachment samples, gone with the tenant
1410
+ ]) {
1411
+ this.sql.exec(`DELETE FROM ${table} WHERE tenant_id = ?`, tenantId);
1412
+ }
1413
+ const reaped = this.sql.exec("UPDATE tenants SET status = 'reaped', deleting_at = NULL WHERE tenant_id = ?", tenantId).rowsWritten > 0;
1401
1414
  // Its scopes were reaped one by one, each forgetting its own; any left over go here.
1402
1415
  this.forgetLifecycleDeliveries('scope_id IN (SELECT scope_id FROM scopes WHERE tenant_id = ?)', tenantId);
1403
1416
  this.bumpLifecycleRevision(`tenant:${tenantId}`); // #1713
1404
- });
1417
+ return reaped;
1418
+ }, audit && { ...audit, before: { status: before.status } });
1405
1419
  return before.status;
1406
1420
  }
1407
- /** Rename the DISPLAY name (never the slug); throw if absent; return the previous name. */
1408
- setTenantName(tenantId, name) {
1421
+ /**
1422
+ * Rename the DISPLAY name (never the slug); throw if absent; return the previous name. A rename
1423
+ * to the name it already has writes nothing, `audit` included; otherwise `audit`'s `before` is
1424
+ * the name read here.
1425
+ */
1426
+ setTenantName(tenantId, name, audit) {
1409
1427
  const before = this.readTenant(tenantId);
1410
1428
  if (!before)
1411
1429
  throw substratError('not_found', `unknown tenant: ${tenantId}`);
1412
- this.sql.exec('UPDATE tenants SET name = ? WHERE tenant_id = ?', name, tenantId);
1430
+ this.writeAndAudit(() => before.name !== name && this.sql.exec('UPDATE tenants SET name = ? WHERE tenant_id = ?', name, tenantId).rowsWritten > 0, audit && { ...audit, before: { name: before.name } });
1413
1431
  return before.name;
1414
1432
  }
1415
1433
  getTenant(tenantId) {
@@ -1805,20 +1823,53 @@ export class ControlPlaneDO extends DurableObject {
1805
1823
  if (to === 'reaped') {
1806
1824
  forgetSwitchesOf(this.kernelSql, scopeId);
1807
1825
  this.forgetLifecycleDeliveries('scope_id = ?', scopeId);
1808
- // #1524: its storage is gone, so its samples describe nothing.
1809
- forgetScopeStorage(doRedactionSql(this.sql), scopeId);
1826
+ // #2515: its storage is gone, and its measured days are kept for the invoice (retention ends them).
1827
+ retireScopeStorage(doRedactionSql(this.sql), scopeId);
1810
1828
  }
1811
1829
  this.bumpLifecycleRevision(`scope:${scopeId}`); // #1713, with the status it counts
1812
1830
  });
1813
1831
  return { ok: true, status: row.status, vertical: row.vertical };
1814
1832
  }
1815
1833
  // -- roles (checker rule 1) -------------------------------------------------
1816
- /** INSERT OR REPLACE; return the previous role (for the audit `before`) or null. */
1834
+ /**
1835
+ * Kept for a coordinator from before #2447, which calls it and reads the previous role (for the
1836
+ * audit `before`) or null. The definition is judged as `defineRoleOrRefusal` judges it, and a
1837
+ * refusal is THROWN, so such a coordinator cannot read it as the previous role and audit a change
1838
+ * that never happened.
1839
+ */
1817
1840
  defineRole(tenantId, role) {
1841
+ const outcome = this.defineRoleOrRefusal(tenantId, role);
1842
+ if ('refused' in outcome)
1843
+ throw new Error(outcome.refused);
1844
+ return outcome.before;
1845
+ }
1846
+ /**
1847
+ * INSERT OR REPLACE; answer the previous role (for the audit `before`) or null. Judged first, in
1848
+ * the same unit (#2447, `reservedRoleDefinitionRefusal`): against `reservation`, what the
1849
+ * coordinator's modules reserve and declare, and the tenant's roles as stored now, so no
1850
+ * concurrent definition lands between the read and the write. Whether the key is seated decides a
1851
+ * system-held definition of a key with no system-held definition stored (none at all included): its tenant-level seats are read here, in the unit, and
1852
+ * `seatedInScopes` carries what the coordinator read off the tenant's scopes. A refusal is answered
1853
+ * as data, so its text survives the hop, and writes nothing. The current coordinator calls this one
1854
+ * (the same split as `transitionScope` and `transitionScopeOrRefusal`).
1855
+ */
1856
+ defineRoleOrRefusal(tenantId, role, reservation = { reserved: [], declared: [] }, seatedInScopes = false) {
1857
+ const tenantRoles = this.listRoles({ tenantId }).map((r) => ({
1858
+ key: r.role_key,
1859
+ permissions: JSON.parse(r.permissions),
1860
+ holder: r.holder ?? undefined,
1861
+ }));
1862
+ const stored = tenantRoles.find((r) => r.key === role.key);
1863
+ const seated = stored?.holder !== 'system' && role.holder === 'system' &&
1864
+ (seatedInScopes ||
1865
+ this.sql.exec(TENANT_SEATED_ROLES_SQL, tenantId, new Date().toISOString()).toArray().some((r) => r.role_key === role.key));
1866
+ const refused = reservedRoleDefinitionRefusal(role, { reserved: new Set(reservation.reserved), declared: new Set(reservation.declared) }, tenantRoles, seated);
1867
+ if (refused)
1868
+ return { refused };
1818
1869
  const before = this.getRole(tenantId, role.key) ?? null;
1819
1870
  this.sql.exec(`INSERT OR REPLACE INTO _substrat_roles (tenant_id, role_key, permissions, source, holder)
1820
1871
  VALUES (?, ?, ?, ?, ?)`, tenantId, role.key, JSON.stringify(role.permissions), String(role.source), role.holder ?? 'person');
1821
- return before;
1872
+ return { before };
1822
1873
  }
1823
1874
  /** Raw rows; the coordinator parses them through the `tenantRole` contract. */
1824
1875
  listRoles(filter) {
@@ -2149,8 +2200,14 @@ export class ControlPlaneDO extends DurableObject {
2149
2200
  .exec('SELECT * FROM verticals WHERE slug = ?', slug)
2150
2201
  .toArray()[0];
2151
2202
  }
2152
- insertVertical(slug, name, source, ownerTenant, envSpec, installSpec, listed, createdAt) {
2153
- this.sql.exec('INSERT INTO verticals (slug, name, source, owner_tenant, env_spec, install_spec, listed, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', slug, name, source, ownerTenant, envSpec, installSpec, listed, createdAt);
2203
+ insertVertical(slug, name, source, ownerTenant, envSpec, installSpec, listed, createdAt, audit) {
2204
+ // #2501: the slug's ownership period opens with its row, in the same transaction.
2205
+ this.writeAndAudit(() => {
2206
+ const changed = this.sql.exec('INSERT INTO verticals (slug, name, source, owner_tenant, env_spec, install_spec, listed, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', slug, name, source, ownerTenant, envSpec, installSpec, listed, createdAt).rowsWritten > 0;
2207
+ if (changed)
2208
+ openVerticalOwnerRow(doRedactionSql(this.sql), slug, ownerTenant, createdAt);
2209
+ return changed;
2210
+ }, audit);
2154
2211
  }
2155
2212
  /** Refresh a vertical's declared env-spec + install-spec (both evolve with the manifest).
2156
2213
  * `listed` rides along ONLY for builtin re-seeds (null ⇒ leave it alone): a pushed
@@ -2162,24 +2219,29 @@ export class ControlPlaneDO extends DurableObject {
2162
2219
  }
2163
2220
  }
2164
2221
  /** Publish/unpublish a vertical to the public marketplace + resolve any pending request. */
2165
- updateVerticalListed(slug, listed) {
2166
- this.sql.exec('UPDATE verticals SET listed = ?, publish_requested_at = NULL WHERE slug = ?', listed, slug);
2222
+ updateVerticalListed(slug, listed, audit) {
2223
+ this.writeAndAudit(() => this.sql.exec('UPDATE verticals SET listed = ?, publish_requested_at = NULL WHERE slug = ?', listed, slug)
2224
+ .rowsWritten > 0, audit);
2167
2225
  }
2168
2226
  /** Record a builder's pending publish request (marketplace-publish.md §5). */
2169
- updateVerticalPublishRequest(slug, requestedAt) {
2170
- this.sql.exec('UPDATE verticals SET publish_requested_at = ? WHERE slug = ?', requestedAt, slug);
2227
+ updateVerticalPublishRequest(slug, requestedAt, audit) {
2228
+ this.writeAndAudit(() => this.sql.exec('UPDATE verticals SET publish_requested_at = ? WHERE slug = ?', requestedAt, slug)
2229
+ .rowsWritten > 0, audit);
2171
2230
  }
2172
2231
  /** Block/unblock new installs of a vertical (staff kill-switch). */
2173
- updateVerticalInstallsBlocked(slug, blocked) {
2174
- this.sql.exec('UPDATE verticals SET installs_blocked = ? WHERE slug = ?', blocked, slug);
2232
+ updateVerticalInstallsBlocked(slug, blocked, audit) {
2233
+ this.writeAndAudit(() => this.sql.exec('UPDATE verticals SET installs_blocked = ? WHERE slug = ?', blocked, slug)
2234
+ .rowsWritten > 0, audit);
2175
2235
  }
2176
2236
  /** Grant/revoke the tenant-provisioner capability (#412, staff grant). */
2177
- updateVerticalTenantProvisioner(slug, granted) {
2178
- this.sql.exec('UPDATE verticals SET tenant_provisioner = ? WHERE slug = ?', granted, slug);
2237
+ updateVerticalTenantProvisioner(slug, granted, audit) {
2238
+ this.writeAndAudit(() => this.sql.exec('UPDATE verticals SET tenant_provisioner = ? WHERE slug = ?', granted, slug)
2239
+ .rowsWritten > 0, audit);
2179
2240
  }
2180
2241
  /** Grant/revoke the email-sender capability (#303, staff grant). */
2181
- updateVerticalEmailSender(slug, granted) {
2182
- this.sql.exec('UPDATE verticals SET email_sender = ? WHERE slug = ?', granted, slug);
2242
+ updateVerticalEmailSender(slug, granted, audit) {
2243
+ this.writeAndAudit(() => this.sql.exec('UPDATE verticals SET email_sender = ? WHERE slug = ?', granted, slug)
2244
+ .rowsWritten > 0, audit);
2183
2245
  }
2184
2246
  /**
2185
2247
  * How many scopes a vertical still backs, split by liveness — deleteVertical's refusal
@@ -2201,14 +2263,29 @@ export class ControlPlaneDO extends DurableObject {
2201
2263
  * Remove a vertical's registry presence: channels, versions, the row. The bound-scope
2202
2264
  * refusal lives host-side (the audited seam), not here — this is the storage verb.
2203
2265
  */
2204
- deleteVertical(slug) {
2205
- this.sql.exec('DELETE FROM vertical_channels WHERE vertical_slug = ?', slug);
2206
- this.sql.exec('DELETE FROM vertical_channel_history WHERE vertical_slug = ?', slug);
2207
- this.sql.exec(`DELETE FROM vertical_version_migrations
2208
- WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`, slug);
2209
- forgetVerticalScopeStores(this.kernelSql, slug);
2210
- this.sql.exec('DELETE FROM vertical_versions WHERE vertical_slug = ?', slug);
2211
- this.sql.exec('DELETE FROM verticals WHERE slug = ?', slug);
2266
+ deleteVertical(slug, audit, until) {
2267
+ // #2501: one transaction, so the row and its ownership period close together. `until` is the
2268
+ // host's stamp, on the clock the period's `since` (the row's created_at) was read on. It is
2269
+ // optional and trailing so a caller from before it, sending (slug, audit), still closes the
2270
+ // period, on this DO's own reading of that clock; anything but an instant is refused.
2271
+ const closedAt = until === undefined ? verticalOwnerNow() : instant.parse(until);
2272
+ this.writeAndAudit(() => {
2273
+ this.sql.exec('DELETE FROM vertical_channels WHERE vertical_slug = ?', slug);
2274
+ this.sql.exec('DELETE FROM vertical_channel_history WHERE vertical_slug = ?', slug);
2275
+ this.sql.exec(`DELETE FROM vertical_version_migrations
2276
+ WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`, slug);
2277
+ forgetVerticalScopeStores(this.kernelSql, slug);
2278
+ forgetVerticalWireAuth(this.kernelSql, slug);
2279
+ this.sql.exec('DELETE FROM vertical_versions WHERE vertical_slug = ?', slug);
2280
+ const changed = this.sql.exec('DELETE FROM verticals WHERE slug = ?', slug).rowsWritten > 0;
2281
+ if (changed)
2282
+ closeVerticalOwnerRow(doRedactionSql(this.sql), slug, closedAt);
2283
+ return changed;
2284
+ }, audit);
2285
+ }
2286
+ /** #2501: every period of a slug's ownership, oldest first — kernel `vertical-owners.ts`. */
2287
+ listVerticalOwners(slug) {
2288
+ return listVerticalOwnerRows(doRedactionSql(this.sql), slug);
2212
2289
  }
2213
2290
  listVerticals(page) {
2214
2291
  const where = [];
@@ -2234,8 +2311,8 @@ export class ControlPlaneDO extends DurableObject {
2234
2311
  * the SQL already split off (`splitManifestMigrations`); `migrations` is null for a version
2235
2312
  * that carries none to show.
2236
2313
  */
2237
- insertVersion(v) {
2238
- this.ctx.storage.transactionSync(() => {
2314
+ insertVersion(v, audit) {
2315
+ this.writeAndAudit(() => {
2239
2316
  this.sql.exec(`INSERT INTO vertical_versions
2240
2317
  (id, vertical_slug, version, manifest_digest, permission_digest,
2241
2318
  migration_digest, deployment_ref, admission, admission_note, manifest_json,
@@ -2244,7 +2321,8 @@ export class ControlPlaneDO extends DurableObject {
2244
2321
  writeVersionMigrations(this.kernelSql, v.id, v.migrations);
2245
2322
  // #2286: the version's scope-store bindings, refusing one moved to another driver, in the insert's transaction.
2246
2323
  indexVersionScopeStores(this.kernelSql, v.verticalSlug, v.id, v.manifestJson);
2247
- });
2324
+ return true;
2325
+ }, audit);
2248
2326
  }
2249
2327
  /** Every scope-store binding any version of the vertical declared (#2286); indexes versions not indexed yet. */
2250
2328
  verticalScopeStores(slug) {
@@ -2256,9 +2334,25 @@ export class ControlPlaneDO extends DurableObject {
2256
2334
  }
2257
2335
  /** Record what the serving script now runs — written only after a successful
2258
2336
  * in-place upload, so a failed serve leaves the trailing state visible. */
2259
- setVerticalServing(slug, s) {
2260
- this.sql.exec(`UPDATE verticals SET serving_ref = ?, serving_version_id = ?,
2261
- serving_do_classes = ?, serving_migration_tag = ? WHERE slug = ?`, s.ref, s.versionId, s.doClassesJson, s.migrationTag, slug);
2337
+ setVerticalServing(slug, s, audit) {
2338
+ this.writeAndAudit(() => this.sql.exec(`UPDATE verticals SET serving_ref = ?, serving_version_id = ?,
2339
+ serving_do_classes = ?, serving_migration_tag = ? WHERE slug = ?`, s.ref, s.versionId, s.doClassesJson, s.migrationTag, slug).rowsWritten > 0, audit);
2340
+ }
2341
+ /** One upload's capability record (RFC wire-signing §7, Q8), replacing the script's earlier one. */
2342
+ recordWireAuth(row, audit) {
2343
+ this.writeAndAudit(() => {
2344
+ writeDeploymentWireAuth(this.kernelSql, row);
2345
+ return true;
2346
+ }, audit);
2347
+ }
2348
+ /**
2349
+ * One page of the stored capability records the filter's reach confines it to (one tenant's
2350
+ * verticals, or the fleet), by script name. The host judged the filter before the call; it is judged
2351
+ * again here (`deploymentWireAuthOf`), so no caller of the object reaches the table unconfined or
2352
+ * unbounded.
2353
+ */
2354
+ listWireAuth(filter) {
2355
+ return deploymentWireAuthOf(this.kernelSql, filter);
2262
2356
  }
2263
2357
  /** Point a scope's routing at the serving script its data now lives in (#286). False when a
2264
2358
  * conditional move lost its compare-and-set: the host throws, since a refusal thrown here
@@ -2297,8 +2391,8 @@ export class ControlPlaneDO extends DurableObject {
2297
2391
  await deriveVersionMigrationDigest(this.kernelSql, r.id);
2298
2392
  return read();
2299
2393
  }
2300
- setAdmission(id, admission, note) {
2301
- this.sql.exec('UPDATE vertical_versions SET admission = ?, admission_note = ? WHERE id = ?', admission, note, id);
2394
+ setAdmission(id, admission, note, audit) {
2395
+ this.writeAndAudit(() => this.sql.exec('UPDATE vertical_versions SET admission = ?, admission_note = ? WHERE id = ?', admission, note, id).rowsWritten > 0, audit);
2302
2396
  }
2303
2397
  /**
2304
2398
  * False when a conditional bind lost its compare-and-set (the host throws, as above). #2347:
@@ -2338,12 +2432,15 @@ export class ControlPlaneDO extends DurableObject {
2338
2432
  return true;
2339
2433
  }, audit);
2340
2434
  }
2341
- /** The directory effect and its admin row either both commit or both roll back. */
2435
+ /**
2436
+ * The directory effect and its admin row either both commit or both roll back. A thunk is
2437
+ * read after the write, in the same unit, for a row that carries what only the write knows.
2438
+ */
2342
2439
  writeAndAudit(write, audit) {
2343
2440
  return this.ctx.storage.transactionSync(() => {
2344
2441
  const changed = write();
2345
2442
  if (changed && audit)
2346
- this.recordAdmin(audit);
2443
+ this.recordAdmin(typeof audit === 'function' ? audit() : audit);
2347
2444
  return changed;
2348
2445
  });
2349
2446
  }
@@ -2521,10 +2618,10 @@ export class ControlPlaneDO extends DurableObject {
2521
2618
  * #2333: move a channel's pointer, and record the move, only if it still names `from` (null: the
2522
2619
  * channel has none yet). A compare-and-set in one synchronous transaction, so no other promote
2523
2620
  * runs between the read and the write: a promote judged its gates against `from` across awaits,
2524
- * and the one that lost the race is told so (`false`) and writes nothing.
2621
+ * and the one that lost the race is told so (`false`) and writes nothing, `audit` included.
2525
2622
  */
2526
- moveChannel(from, history) {
2527
- return this.ctx.storage.transactionSync(() => {
2623
+ moveChannel(from, history, audit) {
2624
+ return this.writeAndAudit(() => {
2528
2625
  const now = this.readChannel(history.vertical_slug, history.channel);
2529
2626
  if ((now?.version_id ?? null) !== from)
2530
2627
  return false;
@@ -2533,7 +2630,7 @@ export class ControlPlaneDO extends DurableObject {
2533
2630
  ON CONFLICT (vertical_slug, channel) DO UPDATE SET version_id = ?, updated_at = ?`, history.vertical_slug, history.channel, history.version_id, history.at, history.version_id, history.at);
2534
2631
  this.insertChannelHistory(history);
2535
2632
  return true;
2536
- });
2633
+ }, audit);
2537
2634
  }
2538
2635
  listChannels(verticalSlug, page) {
2539
2636
  const where = ['vertical_slug = ?'];
@@ -2819,6 +2916,7 @@ export class ControlPlaneDO extends DurableObject {
2819
2916
  storage: listScopeStorageRows(doRedactionSql(this.sql), { tenantId: tenantId, latest: true }),
2820
2917
  storageAttempts: listScopeStorageAttemptRows(doRedactionSql(this.sql), tenantId),
2821
2918
  storeStorage: listScopeStoreStorageRows(doRedactionSql(this.sql), tenantId),
2919
+ attachmentStorage: listScopeAttachmentStorageRows(doRedactionSql(this.sql), tenantId),
2822
2920
  };
2823
2921
  }
2824
2922
  // -- the stored storage gauge (#1524) — kernel `storage-gauge.ts` --------------
@@ -2834,6 +2932,33 @@ export class ControlPlaneDO extends DurableObject {
2834
2932
  pruneScopeStorage(limit) {
2835
2933
  return pruneScopeStorageRows(doRedactionSql(this.sql), Date.now(), limit);
2836
2934
  }
2935
+ // -- the daily request rollup (#2501) — kernel `request-rollup.ts` -------------
2936
+ recordRequestDay(input) {
2937
+ return { recorded: this.ctx.storage.transactionSync(() => recordRequestDayRows(doRedactionSql(this.sql), input)) };
2938
+ }
2939
+ listRequestDays(filter) {
2940
+ return listRequestDayRows(doRedactionSql(this.sql), filter);
2941
+ }
2942
+ listRequestRollupDays(filter) {
2943
+ return listRequestRollupDayRows(doRedactionSql(this.sql), filter);
2944
+ }
2945
+ pruneRequestDays(limit, retentionMonths) {
2946
+ return this.ctx.storage.transactionSync(() => pruneRequestDayRows(doRedactionSql(this.sql), Date.now(), limit, retentionMonths));
2947
+ }
2948
+ // -- the daily meter snapshot (#2501) — kernel `meter-days.ts` -------------------
2949
+ // One transaction: the snapshot's reads and its rows are one instant of the directory.
2950
+ recordMeterDay(input) {
2951
+ return this.ctx.storage.transactionSync(() => recordMeterDayRows(doRedactionSql(this.sql), input));
2952
+ }
2953
+ listMeterDays(filter) {
2954
+ return listMeterDayRows(doRedactionSql(this.sql), filter);
2955
+ }
2956
+ listMeterDayTakes(filter) {
2957
+ return listMeterDayTakeRows(doRedactionSql(this.sql), filter);
2958
+ }
2959
+ pruneMeterDays(limit, retentionMonths) {
2960
+ return this.ctx.storage.transactionSync(() => pruneMeterDayRows(doRedactionSql(this.sql), Date.now(), limit, retentionMonths));
2961
+ }
2837
2962
  // -- identity pools (K-23) --------------------------------------------------
2838
2963
  readPool(provider) {
2839
2964
  return this.sql
@@ -3607,9 +3732,15 @@ export class ControlPlaneDO extends DurableObject {
3607
3732
  }
3608
3733
  /** The body of `recordOpsFailure`, for a caller already inside a transaction. */
3609
3734
  insertOpsFailure(row) {
3610
- this.sql.exec(`INSERT INTO _substrat_ops_failures
3611
- (id, actor, operation, stage, tenant_id, scope_id, vertical, version, status, message, reference, origin, code, fingerprint, at)
3612
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, row.id, row.actor, row.operation, row.stage, row.tenant_id, row.scope_id, row.vertical, row.version, row.status, row.message, row.reference, row.origin, row.code, row.fingerprint, row.at);
3735
+ // #2540: a tenant's row with a `detail` is a pair — the tenant's row says what was answered, and
3736
+ // a row with no tenant, which only staff read, carries the failure's own text. Counted once.
3737
+ const detail = row.tenant_id && row.detail_id && typeof row.detail === 'string' ? row.detail.slice(0, 2000) : null;
3738
+ const insert = (id, tenantId, message) => this.sql.exec(`INSERT INTO _substrat_ops_failures
3739
+ (id, actor, operation, stage, tenant_id, scope_id, vertical, version, status, message, reference, origin, code, fingerprint, at)
3740
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, row.actor, row.operation, row.stage, tenantId, row.scope_id, row.vertical, row.version, row.status, message, row.reference, row.origin, row.code, row.fingerprint, row.at);
3741
+ if (detail !== null)
3742
+ insert(row.detail_id, null, detail);
3743
+ insert(row.id, row.tenant_id, row.message);
3613
3744
  // Prune-on-write (#559): the retention lives HERE, not in a cron — every insert
3614
3745
  // pays for its own housekeeping, so the table stays bounded even on a deployment
3615
3746
  // whose scheduled pass is broken (the exact circumstance this table records).
@@ -3632,7 +3763,11 @@ export class ControlPlaneDO extends DurableObject {
3632
3763
  last_vertical = COALESCE(excluded.last_vertical, last_vertical),
3633
3764
  last_version = COALESCE(excluded.last_version, last_version),
3634
3765
  origin = COALESCE(excluded.origin, origin),
3635
- status = CASE WHEN status = 'resolved' THEN 'regressed' ELSE status END`, row.fingerprint, row.operation, row.stage, row.origin, row.code, row.at, row.at, row.message, row.tenant_id, issueExemplarOwner(row.tenant_id), row.vertical, row.version);
3766
+ status = CASE WHEN status = 'resolved' THEN 'regressed' ELSE status END`, row.fingerprint, row.operation, row.stage, row.origin, row.code, row.at, row.at,
3767
+ // The issues are staff's (#2540): a pair's exemplar is its own text, not the tenant's
3768
+ // sentence, and it is owned as the row it is copied from is, the platform's — so erasure
3769
+ // reaches it on an id match, as it reaches that row.
3770
+ detail ?? row.message, detail !== null ? null : row.tenant_id, issueExemplarOwner(detail !== null ? null : row.tenant_id), row.vertical, row.version);
3636
3771
  const issueHorizon = new Date(Date.now() - ISSUE_RETENTION_DAYS * 86_400_000).toISOString();
3637
3772
  this.sql.exec('DELETE FROM _substrat_issues WHERE last_seen < ?', issueHorizon);
3638
3773
  // #1748: the tenant's Recurring finding, over the tenant's own occurrences only.
@@ -3688,8 +3823,9 @@ export class ControlPlaneDO extends DurableObject {
3688
3823
  params.push(query.fingerprint);
3689
3824
  }
3690
3825
  if (query.reference) {
3691
- where.push('reference = ?');
3692
- params.push(query.reference);
3826
+ const byReference = opsFailureReferenceWhere(query.reference, query.referenceInText === true);
3827
+ where.push(byReference.sql);
3828
+ params.push(...byReference.params);
3693
3829
  }
3694
3830
  if (query.since) {
3695
3831
  where.push('at >= ?');