@substrat-run/adapter-cloudflare 0.158.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.
- package/dist/control-plane-do.d.ts +91 -24
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +223 -87
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +818 -6
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +336 -103
- package/dist/host.js.map +1 -1
- package/dist/platform-sweeper-do.d.ts +3 -2
- package/dist/platform-sweeper-do.d.ts.map +1 -1
- package/dist/platform-sweeper-do.js +3 -2
- package/dist/platform-sweeper-do.js.map +1 -1
- package/dist/r2.d.ts +7 -1
- package/dist/r2.d.ts.map +1 -1
- package/dist/r2.js +20 -2
- package/dist/r2.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +130 -40
- package/dist/scope-do.js.map +1 -1
- package/dist/scope-sweeper-do.d.ts +4 -2
- package/dist/scope-sweeper-do.d.ts.map +1 -1
- package/dist/scope-sweeper-do.js +7 -1
- package/dist/scope-sweeper-do.js.map +1 -1
- package/package.json +9 -9
package/dist/control-plane-do.js
CHANGED
|
@@ -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
|
-
/**
|
|
1320
|
-
|
|
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.
|
|
1352
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
-
/**
|
|
1408
|
-
|
|
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
|
-
// #
|
|
1809
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
this.
|
|
2211
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
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,
|
|
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
|
-
|
|
3692
|
-
|
|
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 >= ?');
|