@substrat-run/adapter-cloudflare 0.164.0 → 0.166.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 +82 -19
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +328 -136
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +9 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +215 -94
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +16 -0
- package/dist/scope-do.js.map +1 -1
- package/package.json +10 -10
package/dist/control-plane-do.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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, 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';
|
|
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, readBoundedTenantStorageGauge, REQUEST_ROLLUP_DDL, listRequestDayRows, listRequestRollupDayRows, pruneRequestDayRows, recordRequestDayRows, readTenantRequestMeterRows, readRequestMeterDayRows, 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, approveStoreDriverReleaseIn, legacyStoreDriverConflict, legacyStoreDriverRenames, beginStoreDriverRolloutIn, finishStoreDriverRolloutIn, holdStoreDriverIn, advanceStoreDriverRebuildIn, claimStoreDriverRebuildIn, storeDriverRebuildIn, driverDeclarationsOf, recordStoreDriverReleaseIn, recordStoreDriverRevokeWalkIn, releaseStoreDriverHoldIn, revokeStoreDriverReleaseIn, liveStoreDriverApprovalIn, storeDriverApprovalsIn, storeDriverRevokeWalkIn, 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, servesAsFrom, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, } from '@substrat-run/kernel';
|
|
7
7
|
import { autoAdmittedProdRefusal, listedPromoteRefusal, opsFailureReferenceWhere } from '@substrat-run/kernel';
|
|
8
|
+
import { canonicalInstant } from '@substrat-run/kernel';
|
|
8
9
|
import { doBuiltColumnsOf, doRedactionSql } from './sql.js';
|
|
9
10
|
import { assertReplayableDump, AUTO_ADMISSION_NOTE, instant, opsFailureFingerprint, ROUTE_SCOPE_HOSTNAMES_MAX, SCOPE_GATE_REASONS, substratError } from '@substrat-run/contracts';
|
|
10
11
|
/**
|
|
@@ -652,6 +653,7 @@ const DIRECTORY_DDL = `
|
|
|
652
653
|
elapsed_ms INTEGER NOT NULL
|
|
653
654
|
);
|
|
654
655
|
CREATE INDEX IF NOT EXISTS _substrat_model_usage_tenant ON _substrat_model_usage (tenant_id, at);
|
|
656
|
+
CREATE INDEX IF NOT EXISTS _substrat_model_usage_tenant_page ON _substrat_model_usage (tenant_id, at, id);
|
|
655
657
|
CREATE INDEX IF NOT EXISTS _substrat_model_usage_at ON _substrat_model_usage (at);
|
|
656
658
|
-- #1524: the stored storage gauge — one row per (scope, UTC day), kernel-owned DDL.
|
|
657
659
|
${SCOPE_STORAGE_DDL}
|
|
@@ -797,6 +799,9 @@ export const REPLIED_METHODS = [
|
|
|
797
799
|
// #2300: a store driver's release, rollout and hold writes refuse with conflict, not_found,
|
|
798
800
|
// validation_failed or unavailable.
|
|
799
801
|
'recordStoreDriverRelease',
|
|
802
|
+
'approveStoreDriverReleaseAndAudit',
|
|
803
|
+
'recordStoreDriverRevokeWalk',
|
|
804
|
+
'revokeStoreDriverReleaseAndAudit',
|
|
800
805
|
'storeDriverDeclarations',
|
|
801
806
|
// #2312: a rollout page refuses a limit that is not positive, and an unknown rollout.
|
|
802
807
|
'storeDriverRolloutPending',
|
|
@@ -842,6 +847,25 @@ export const REPLIED_METHODS = [
|
|
|
842
847
|
'deleteHostname',
|
|
843
848
|
'grantEntitlement',
|
|
844
849
|
'revokeEntitlement',
|
|
850
|
+
// #2479: these names are new even where the bare method above already exists. A DO from
|
|
851
|
+
// before the audit unit must refuse the name, not drop a trailing audit argument and commit.
|
|
852
|
+
'putTenantStoreAndAudit',
|
|
853
|
+
'putBlobStoreAndAudit',
|
|
854
|
+
'recordStoreDriverReleaseAndAudit',
|
|
855
|
+
'beginStoreDriverRolloutAndAudit',
|
|
856
|
+
'finishStoreDriverRolloutAndAudit',
|
|
857
|
+
'importDumpAndAudit',
|
|
858
|
+
'registerIdentityPoolAndAudit',
|
|
859
|
+
'insertConnectionAndAudit',
|
|
860
|
+
'updateConnectionSecretAndAudit',
|
|
861
|
+
'revokeConnectionAndAudit',
|
|
862
|
+
'linkIdentityAndAudit',
|
|
863
|
+
'unlinkIdentityAndAudit',
|
|
864
|
+
'markAccessLogDrainedAndAudit',
|
|
865
|
+
'pruneAccessLogAndAudit',
|
|
866
|
+
'writeImpersonationAndAudit',
|
|
867
|
+
'endImpersonationAndAudit',
|
|
868
|
+
'setIssueStatusAndAudit',
|
|
845
869
|
];
|
|
846
870
|
export class ControlPlaneDO extends DurableObject {
|
|
847
871
|
sql;
|
|
@@ -1287,7 +1311,7 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1287
1311
|
* are filled in afterwards, inside the same transaction. A refusal anywhere in it leaves the
|
|
1288
1312
|
* directory as it was.
|
|
1289
1313
|
*/
|
|
1290
|
-
async importDump(tables) {
|
|
1314
|
+
async importDump(tables, audit) {
|
|
1291
1315
|
// Same untrusted dump, same two holes as the scope path (#1143): names reaching SQL as
|
|
1292
1316
|
// identifiers, and a `ddl` that is never executed here but is still held to one CREATE
|
|
1293
1317
|
// TABLE. Pure input validation, so it runs before the first DROP: a refused dump touches
|
|
@@ -1326,6 +1350,10 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1326
1350
|
// A dump never carries one (`exportDump`); a hand-made dump that does is outranked too.
|
|
1327
1351
|
// Every deployment then takes this history's deliveries, whatever its counters say.
|
|
1328
1352
|
this.sql.exec("INSERT OR REPLACE INTO lifecycle_revisions (subject, revision) VALUES ('directory:epoch', ?)", Math.max(epochBefore + 1, this.lifecycleEpoch() + 1, Date.now()));
|
|
1353
|
+
// Append the restore's own row after the dump rows have landed in the rebuilt
|
|
1354
|
+
// admin table, inside this same import transaction.
|
|
1355
|
+
if (audit)
|
|
1356
|
+
this.recordAdmin(audit);
|
|
1329
1357
|
});
|
|
1330
1358
|
// A restore is a new #1764 backfill episode. It replaced the data the old failure count and
|
|
1331
1359
|
// backoff were about, so neither may delay or silence it: the count is cleared and the next
|
|
@@ -1494,21 +1522,26 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1494
1522
|
* callers converge on one canonical row and the loser can tear its orphan mint down.
|
|
1495
1523
|
* Fails closed on an unknown/non-active tenant, exactly as provisionScope does.
|
|
1496
1524
|
*/
|
|
1497
|
-
putTenantStore(row) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
.
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1525
|
+
putTenantStore(row, audit) {
|
|
1526
|
+
let stored;
|
|
1527
|
+
this.writeAndAudit(() => {
|
|
1528
|
+
const tenantRow = this.sql
|
|
1529
|
+
.exec('SELECT status FROM tenants WHERE tenant_id = ?', row.tenantId)
|
|
1530
|
+
.toArray()[0];
|
|
1531
|
+
if (!tenantRow) {
|
|
1532
|
+
throw substratError('not_found', `cannot provision tenant store under unknown tenant: ${row.tenantId}`);
|
|
1533
|
+
}
|
|
1534
|
+
if (tenantRow.status !== 'active') {
|
|
1535
|
+
throw substratError('conflict', `cannot provision tenant store under non-active tenant (status: ${tenantRow.status}): ${row.tenantId}`);
|
|
1536
|
+
}
|
|
1537
|
+
const inserted = this.sql.exec(`INSERT OR IGNORE INTO tenant_stores (tenant_id, vertical, binding, kind, ref, created_at)
|
|
1538
|
+
VALUES (?, ?, ?, ?, ?, ?)`, row.tenantId, row.vertical, row.binding, row.kind, row.ref, row.createdAt).rowsWritten > 0;
|
|
1539
|
+
const found = this.getTenantStore(row.tenantId, row.vertical, row.binding);
|
|
1540
|
+
if (!found)
|
|
1541
|
+
throw new Error(`tenant store write did not land for tenant ${row.tenantId}`);
|
|
1542
|
+
stored = found;
|
|
1543
|
+
return inserted;
|
|
1544
|
+
}, audit);
|
|
1512
1545
|
return stored;
|
|
1513
1546
|
}
|
|
1514
1547
|
listTenantStores(filter) {
|
|
@@ -1538,21 +1571,26 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1538
1571
|
}
|
|
1539
1572
|
/** Record a minted blob store — FIRST WRITER WINS, same race protocol as
|
|
1540
1573
|
* putTenantStore: INSERT OR IGNORE then read back; the loser tears its orphan down. */
|
|
1541
|
-
putBlobStore(row) {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
.
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1574
|
+
putBlobStore(row, audit) {
|
|
1575
|
+
let stored;
|
|
1576
|
+
this.writeAndAudit(() => {
|
|
1577
|
+
const tenantRow = this.sql
|
|
1578
|
+
.exec('SELECT status FROM tenants WHERE tenant_id = ?', row.tenantId)
|
|
1579
|
+
.toArray()[0];
|
|
1580
|
+
if (!tenantRow) {
|
|
1581
|
+
throw substratError('not_found', `cannot provision blob store under unknown tenant: ${row.tenantId}`);
|
|
1582
|
+
}
|
|
1583
|
+
if (tenantRow.status !== 'active') {
|
|
1584
|
+
throw substratError('conflict', `cannot provision blob store under non-active tenant (status: ${tenantRow.status}): ${row.tenantId}`);
|
|
1585
|
+
}
|
|
1586
|
+
const inserted = this.sql.exec(`INSERT OR IGNORE INTO blob_stores (tenant_id, vertical, binding, kind, ref, created_at)
|
|
1587
|
+
VALUES (?, ?, ?, ?, ?, ?)`, row.tenantId, row.vertical, row.binding, row.kind, row.ref, row.createdAt).rowsWritten > 0;
|
|
1588
|
+
const found = this.getBlobStore(row.tenantId, row.vertical, row.binding);
|
|
1589
|
+
if (!found)
|
|
1590
|
+
throw new Error(`blob store write did not land for tenant ${row.tenantId}`);
|
|
1591
|
+
stored = found;
|
|
1592
|
+
return inserted;
|
|
1593
|
+
}, audit);
|
|
1556
1594
|
return stored;
|
|
1557
1595
|
}
|
|
1558
1596
|
listBlobStores(filter) {
|
|
@@ -1575,12 +1613,47 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1575
1613
|
// -- store-driver releases, rollouts and holds (D-63, #2300) ---------------------
|
|
1576
1614
|
// Kernel code over the directory (store-driver-releases.ts), shared with the SQLite host. Each
|
|
1577
1615
|
// call is one transaction, and the object runs one at a time, so a hold is taken atomically.
|
|
1578
|
-
recordStoreDriverRelease(input) {
|
|
1579
|
-
|
|
1616
|
+
recordStoreDriverRelease(input, audit) {
|
|
1617
|
+
let result;
|
|
1618
|
+
this.writeAndAudit(() => {
|
|
1619
|
+
result = recordStoreDriverReleaseIn(this.kernelSql, input);
|
|
1620
|
+
return result.created;
|
|
1621
|
+
}, audit ? () => ({ ...audit, after: result.record }) : undefined);
|
|
1622
|
+
return result;
|
|
1580
1623
|
}
|
|
1581
1624
|
getStoreDriverRelease(driver, sha256) {
|
|
1582
1625
|
return storeDriverReleaseIn(this.kernelSql, driver, sha256);
|
|
1583
1626
|
}
|
|
1627
|
+
storeDriverApprovals(driver) {
|
|
1628
|
+
return storeDriverApprovalsIn(this.kernelSql, driver);
|
|
1629
|
+
}
|
|
1630
|
+
liveStoreDriverApproval(driver, sha256) {
|
|
1631
|
+
return liveStoreDriverApprovalIn(this.kernelSql, driver, sha256);
|
|
1632
|
+
}
|
|
1633
|
+
approveStoreDriverReleaseAndAudit(input, audit) {
|
|
1634
|
+
let record;
|
|
1635
|
+
this.writeAndAudit(() => {
|
|
1636
|
+
record = approveStoreDriverReleaseIn(this.kernelSql, { ...input, approvalId: ulid() });
|
|
1637
|
+
return true;
|
|
1638
|
+
}, () => ({ ...audit, after: record }));
|
|
1639
|
+
return record;
|
|
1640
|
+
}
|
|
1641
|
+
storeDriverRevokeWalk(approvalId) {
|
|
1642
|
+
return storeDriverRevokeWalkIn(this.kernelSql, approvalId);
|
|
1643
|
+
}
|
|
1644
|
+
recordStoreDriverRevokeWalk(input) {
|
|
1645
|
+
return this.ctx.storage.transactionSync(() => recordStoreDriverRevokeWalkIn(this.kernelSql, { ...input, nowMs: Date.now() }));
|
|
1646
|
+
}
|
|
1647
|
+
revokeStoreDriverReleaseAndAudit(input, audit) {
|
|
1648
|
+
let before = null;
|
|
1649
|
+
let record;
|
|
1650
|
+
this.writeAndAudit(() => {
|
|
1651
|
+
before = liveStoreDriverApprovalIn(this.kernelSql, input.driver, input.sha256) ?? null;
|
|
1652
|
+
record = revokeStoreDriverReleaseIn(this.kernelSql, { ...input, nowMs: Date.now() });
|
|
1653
|
+
return true;
|
|
1654
|
+
}, () => ({ ...audit, before, after: record }));
|
|
1655
|
+
return record;
|
|
1656
|
+
}
|
|
1584
1657
|
storeDriverState(driver) {
|
|
1585
1658
|
return this.ctx.storage.transactionSync(() => storeDriverStateIn(this.kernelSql, driver, Date.now()));
|
|
1586
1659
|
}
|
|
@@ -1599,11 +1672,21 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1599
1672
|
releaseStoreDriverHold(input) {
|
|
1600
1673
|
releaseStoreDriverHoldIn(this.kernelSql, input);
|
|
1601
1674
|
}
|
|
1602
|
-
beginStoreDriverRollout(input) {
|
|
1603
|
-
|
|
1675
|
+
beginStoreDriverRollout(input, audit) {
|
|
1676
|
+
let row;
|
|
1677
|
+
this.writeAndAudit(() => {
|
|
1678
|
+
row = beginStoreDriverRolloutIn(this.kernelSql, { ...input, nowMs: Date.now() });
|
|
1679
|
+
return true;
|
|
1680
|
+
}, audit ? () => ({ ...audit, after: row }) : undefined);
|
|
1681
|
+
return row;
|
|
1604
1682
|
}
|
|
1605
|
-
finishStoreDriverRollout(input) {
|
|
1606
|
-
|
|
1683
|
+
finishStoreDriverRollout(input, audit) {
|
|
1684
|
+
let row;
|
|
1685
|
+
this.writeAndAudit(() => {
|
|
1686
|
+
row = finishStoreDriverRolloutIn(this.kernelSql, { ...input, nowMs: Date.now() });
|
|
1687
|
+
return true;
|
|
1688
|
+
}, audit ? () => ({ ...audit, action: row.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', after: row }) : undefined);
|
|
1689
|
+
return row;
|
|
1607
1690
|
}
|
|
1608
1691
|
storeDriverRebuild(driver) {
|
|
1609
1692
|
return storeDriverRebuildIn(this.kernelSql, driver);
|
|
@@ -2730,6 +2813,62 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2730
2813
|
return changed;
|
|
2731
2814
|
});
|
|
2732
2815
|
}
|
|
2816
|
+
/**
|
|
2817
|
+
* The coordinator calls these new RPC names for directory-only audited writes. The old names
|
|
2818
|
+
* remain callable by an older coordinator, which records its own row afterwards. An older DO
|
|
2819
|
+
* knows none of these names and refuses before the state write instead of dropping the row.
|
|
2820
|
+
*/
|
|
2821
|
+
putTenantStoreAndAudit(...args) {
|
|
2822
|
+
return this.putTenantStore(...args);
|
|
2823
|
+
}
|
|
2824
|
+
putBlobStoreAndAudit(...args) {
|
|
2825
|
+
return this.putBlobStore(...args);
|
|
2826
|
+
}
|
|
2827
|
+
recordStoreDriverReleaseAndAudit(...args) {
|
|
2828
|
+
return this.recordStoreDriverRelease(...args);
|
|
2829
|
+
}
|
|
2830
|
+
beginStoreDriverRolloutAndAudit(...args) {
|
|
2831
|
+
return this.beginStoreDriverRollout(...args);
|
|
2832
|
+
}
|
|
2833
|
+
finishStoreDriverRolloutAndAudit(...args) {
|
|
2834
|
+
return this.finishStoreDriverRollout(...args);
|
|
2835
|
+
}
|
|
2836
|
+
importDumpAndAudit(...args) {
|
|
2837
|
+
return this.importDump(...args);
|
|
2838
|
+
}
|
|
2839
|
+
registerIdentityPoolAndAudit(...args) {
|
|
2840
|
+
return this.registerIdentityPool(...args);
|
|
2841
|
+
}
|
|
2842
|
+
insertConnectionAndAudit(...args) {
|
|
2843
|
+
return this.insertConnection(...args);
|
|
2844
|
+
}
|
|
2845
|
+
updateConnectionSecretAndAudit(...args) {
|
|
2846
|
+
return this.updateConnectionSecret(...args);
|
|
2847
|
+
}
|
|
2848
|
+
revokeConnectionAndAudit(...args) {
|
|
2849
|
+
return this.revokeConnection(...args);
|
|
2850
|
+
}
|
|
2851
|
+
linkIdentityAndAudit(...args) {
|
|
2852
|
+
return this.linkIdentity(...args);
|
|
2853
|
+
}
|
|
2854
|
+
unlinkIdentityAndAudit(...args) {
|
|
2855
|
+
return this.unlinkIdentity(...args);
|
|
2856
|
+
}
|
|
2857
|
+
markAccessLogDrainedAndAudit(...args) {
|
|
2858
|
+
return this.markAccessLogDrained(...args);
|
|
2859
|
+
}
|
|
2860
|
+
pruneAccessLogAndAudit(...args) {
|
|
2861
|
+
return this.pruneAccessLog(...args);
|
|
2862
|
+
}
|
|
2863
|
+
writeImpersonationAndAudit(...args) {
|
|
2864
|
+
return this.writeImpersonation(...args);
|
|
2865
|
+
}
|
|
2866
|
+
endImpersonationAndAudit(...args) {
|
|
2867
|
+
return this.endImpersonation(...args);
|
|
2868
|
+
}
|
|
2869
|
+
setIssueStatusAndAudit(...args) {
|
|
2870
|
+
return this.setIssueStatus(...args);
|
|
2871
|
+
}
|
|
2733
2872
|
scopeErasureEpoch(tenantId, scopeId) {
|
|
2734
2873
|
// NULL on every row from before the column (#1722): the same 0 every CAS compares against.
|
|
2735
2874
|
const row = this.sql.exec('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?', tenantId, scopeId)
|
|
@@ -3265,6 +3404,9 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3265
3404
|
attachmentStorage: listScopeAttachmentStorageRows(doRedactionSql(this.sql), tenantId),
|
|
3266
3405
|
};
|
|
3267
3406
|
}
|
|
3407
|
+
readTenantStorageGauge(tenantId, limit) {
|
|
3408
|
+
return readBoundedTenantStorageGauge(doRedactionSql(this.sql), tenantId, limit);
|
|
3409
|
+
}
|
|
3268
3410
|
// -- the stored storage gauge (#1524) — kernel `storage-gauge.ts` --------------
|
|
3269
3411
|
recordScopeStorage(readings) {
|
|
3270
3412
|
return { recorded: this.ctx.storage.transactionSync(() => recordScopeStorageRows(doRedactionSql(this.sql), readings)) };
|
|
@@ -3288,6 +3430,12 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3288
3430
|
listRequestRollupDays(filter) {
|
|
3289
3431
|
return listRequestRollupDayRows(doRedactionSql(this.sql), filter);
|
|
3290
3432
|
}
|
|
3433
|
+
listTenantRequestMeter(tenantId, since, until, limit) {
|
|
3434
|
+
return readTenantRequestMeterRows(doRedactionSql(this.sql), tenantId, since, until, limit);
|
|
3435
|
+
}
|
|
3436
|
+
listRequestMeterDays(since, until) {
|
|
3437
|
+
return readRequestMeterDayRows(doRedactionSql(this.sql), since, until);
|
|
3438
|
+
}
|
|
3291
3439
|
pruneRequestDays(limit, retentionMonths) {
|
|
3292
3440
|
return this.ctx.storage.transactionSync(() => pruneRequestDayRows(doRedactionSql(this.sql), Date.now(), limit, retentionMonths));
|
|
3293
3441
|
}
|
|
@@ -3312,16 +3460,18 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3312
3460
|
.toArray()[0];
|
|
3313
3461
|
}
|
|
3314
3462
|
/** Returns false when an identical registration already exists; throws on a conflicting one. */
|
|
3315
|
-
registerIdentityPool(provider, topology, tenantId, createdAt) {
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
if (existing
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3463
|
+
registerIdentityPool(provider, topology, tenantId, createdAt, audit) {
|
|
3464
|
+
return this.writeAndAudit(() => {
|
|
3465
|
+
const existing = this.readPool(provider);
|
|
3466
|
+
if (existing) {
|
|
3467
|
+
if (existing.topology === topology && existing.tenant_id === tenantId)
|
|
3468
|
+
return false;
|
|
3469
|
+
throw substratError('conflict', `identity pool '${provider}' is already registered as ${existing.topology}` +
|
|
3470
|
+
`${existing.tenant_id ? ` for tenant ${existing.tenant_id}` : ''}`);
|
|
3471
|
+
}
|
|
3472
|
+
this.sql.exec('INSERT INTO _substrat_identity_pools (provider, topology, tenant_id, created_at) VALUES (?, ?, ?, ?)', provider, topology, tenantId, createdAt);
|
|
3473
|
+
return true;
|
|
3474
|
+
}, audit);
|
|
3325
3475
|
}
|
|
3326
3476
|
identityTenants(provider, externalId) {
|
|
3327
3477
|
return this.sql
|
|
@@ -3389,27 +3539,30 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3389
3539
|
// The DO owns the rows; the coordinator owns the SecretBox, so ciphertext
|
|
3390
3540
|
// arrives already sealed and leaves still sealed. This DO has never seen a
|
|
3391
3541
|
// plaintext credential and cannot.
|
|
3392
|
-
insertConnection(row) {
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3542
|
+
insertConnection(row, audit) {
|
|
3543
|
+
this.writeAndAudit(() => {
|
|
3544
|
+
// Live-uniqueness is per ACCOUNT (COALESCEd like the index): a provider
|
|
3545
|
+
// without account refs stays a singleton; a multi-namespace provider may
|
|
3546
|
+
// hold one live connection per external account.
|
|
3547
|
+
const live = this.sql
|
|
3548
|
+
.exec(`SELECT id FROM _substrat_connections
|
|
3398
3549
|
WHERE tenant_id = ? AND vertical = ? AND provider = ?
|
|
3399
3550
|
AND COALESCE(external_account_ref, '') = COALESCE(?, '') AND revoked_at IS NULL`, row.tenantId, row.vertical, row.provider, row.externalAccountRef)
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3551
|
+
.toArray()[0];
|
|
3552
|
+
if (live) {
|
|
3553
|
+
const account = row.externalAccountRef ? ` under account '${row.externalAccountRef}'` : '';
|
|
3554
|
+
throw new Error(`tenant ${row.tenantId} already has a live '${row.provider}' connection ` +
|
|
3555
|
+
`for vertical '${row.vertical}'${account} — revoke it before connecting another`);
|
|
3556
|
+
}
|
|
3557
|
+
this.sql.exec(`INSERT INTO _substrat_connections
|
|
3407
3558
|
(id, tenant_id, vertical, provider, label, status, external_account_ref,
|
|
3408
3559
|
scopes, expires_at, last_ok_at, last_error, last_error_at,
|
|
3409
3560
|
created_by, created_at, revoked_at)
|
|
3410
3561
|
VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, NULL, NULL, NULL, ?, ?, NULL)`, row.id, row.tenantId, row.vertical, row.provider, row.label, row.externalAccountRef, row.scopes, row.expiresAt, row.createdBy, row.createdAt);
|
|
3411
|
-
|
|
3562
|
+
this.sql.exec(`INSERT INTO _substrat_connection_secrets (connection_id, key_id, ciphertext, updated_at)
|
|
3412
3563
|
VALUES (?, ?, ?, ?)`, row.id, row.keyId, row.ciphertext, row.createdAt);
|
|
3564
|
+
return true;
|
|
3565
|
+
}, audit);
|
|
3413
3566
|
}
|
|
3414
3567
|
/** This connection's key rows — every one, current and retired (#687). */
|
|
3415
3568
|
readConnectionKeys(connectionId) {
|
|
@@ -3478,12 +3631,15 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3478
3631
|
}
|
|
3479
3632
|
return rows[0];
|
|
3480
3633
|
}
|
|
3481
|
-
updateConnectionSecret(id, keyId, ciphertext, expiresAt, at) {
|
|
3482
|
-
this.
|
|
3634
|
+
updateConnectionSecret(id, keyId, ciphertext, expiresAt, at, audit) {
|
|
3635
|
+
this.writeAndAudit(() => {
|
|
3636
|
+
this.sql.exec(`UPDATE _substrat_connection_secrets
|
|
3483
3637
|
SET key_id = ?, ciphertext = ?, updated_at = ? WHERE connection_id = ?`, keyId, ciphertext, at, id);
|
|
3484
|
-
|
|
3638
|
+
this.sql.exec(`UPDATE _substrat_connections
|
|
3485
3639
|
SET status = 'active', expires_at = ?, last_error = NULL, last_error_at = NULL
|
|
3486
3640
|
WHERE id = ?`, expiresAt, id);
|
|
3641
|
+
return true;
|
|
3642
|
+
}, audit);
|
|
3487
3643
|
}
|
|
3488
3644
|
/**
|
|
3489
3645
|
* Tombstone the connection and DELETE the sealed blob.
|
|
@@ -3492,21 +3648,23 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3492
3648
|
* only be a liability. Returns false when already revoked, so the caller can
|
|
3493
3649
|
* skip an audit row for a no-op.
|
|
3494
3650
|
*/
|
|
3495
|
-
revokeConnection(id, at) {
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3651
|
+
revokeConnection(id, at, audit) {
|
|
3652
|
+
return this.writeAndAudit(() => {
|
|
3653
|
+
const row = this.readConnection(id);
|
|
3654
|
+
if (!row)
|
|
3655
|
+
throw new Error(`connection not found: ${id}`);
|
|
3656
|
+
if (row.revoked_at)
|
|
3657
|
+
return false;
|
|
3658
|
+
this.sql.exec(`UPDATE _substrat_connections SET status = 'revoked', revoked_at = ? WHERE id = ?`, at, id);
|
|
3659
|
+
this.sql.exec('DELETE FROM _substrat_connection_secrets WHERE connection_id = ?', id);
|
|
3660
|
+
// Connector state dies with the connection — its private bookkeeping.
|
|
3661
|
+
this.sql.exec('DELETE FROM _substrat_connector_state WHERE connection_id = ?', id);
|
|
3662
|
+
// #592: its grants tombstone (K-21 — evidence, not roster). Absent from the
|
|
3663
|
+
// next gather, so no later provision/reconcile delivers them again.
|
|
3664
|
+
this.sql.exec(`UPDATE _substrat_connection_grants SET revoked_at = ?
|
|
3508
3665
|
WHERE connection_id = ? AND revoked_at IS NULL`, at, id);
|
|
3509
|
-
|
|
3666
|
+
return true;
|
|
3667
|
+
}, audit);
|
|
3510
3668
|
}
|
|
3511
3669
|
// -- a vertical's mailed connect links (connections.md §3.5.4) ------------------
|
|
3512
3670
|
// The kernel's statements (`connect-links.ts`), shared with the pure adapter. Each
|
|
@@ -3715,31 +3873,35 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3715
3873
|
.filter((r) => !prefix || r.state_key.startsWith(prefix))
|
|
3716
3874
|
.map((r) => ({ key: r.state_key, value: r.value }));
|
|
3717
3875
|
}
|
|
3718
|
-
linkIdentity(provider, externalId, principal, tenantId, scopeId, createdAt) {
|
|
3719
|
-
|
|
3720
|
-
|
|
3876
|
+
linkIdentity(provider, externalId, principal, tenantId, scopeId, createdAt, audit) {
|
|
3877
|
+
return this.writeAndAudit(() => {
|
|
3878
|
+
const existing = this.sql
|
|
3879
|
+
.exec(`SELECT principal_id FROM _substrat_identities
|
|
3721
3880
|
WHERE tenant_id = ? AND provider = ? AND external_id = ?`, tenantId, provider, externalId)
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3881
|
+
.toArray()[0];
|
|
3882
|
+
if (existing) {
|
|
3883
|
+
if (existing.principal_id === principal)
|
|
3884
|
+
return false;
|
|
3885
|
+
throw substratError('conflict', `identity ${provider}:${externalId} in tenant ${tenantId} is already bound to ${existing.principal_id}`);
|
|
3886
|
+
}
|
|
3887
|
+
this.sql.exec(`INSERT INTO _substrat_identities
|
|
3729
3888
|
(provider, external_id, principal_id, tenant_id, scope_id, created_at)
|
|
3730
3889
|
VALUES (?, ?, ?, ?, ?, ?)`, provider, externalId, principal, tenantId, scopeId, createdAt);
|
|
3731
|
-
|
|
3890
|
+
return true;
|
|
3891
|
+
}, audit);
|
|
3732
3892
|
}
|
|
3733
3893
|
/** Remove a principal's identity link(s) in a tenant. Returns whether a row was
|
|
3734
3894
|
* deleted so a no-op stays silent (idempotent). */
|
|
3735
|
-
unlinkIdentity(tenantId, principal) {
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3895
|
+
unlinkIdentity(tenantId, principal, audit) {
|
|
3896
|
+
return this.writeAndAudit(() => {
|
|
3897
|
+
const before = this.sql
|
|
3898
|
+
.exec(`SELECT 1 FROM _substrat_identities WHERE tenant_id = ? AND principal_id = ?`, tenantId, principal)
|
|
3899
|
+
.toArray();
|
|
3900
|
+
if (before.length === 0)
|
|
3901
|
+
return false;
|
|
3902
|
+
this.sql.exec(`DELETE FROM _substrat_identities WHERE tenant_id = ? AND principal_id = ?`, tenantId, principal);
|
|
3903
|
+
return true;
|
|
3904
|
+
}, audit);
|
|
3743
3905
|
}
|
|
3744
3906
|
resolveIdentity(tenantId, provider, externalId) {
|
|
3745
3907
|
const row = this.sql
|
|
@@ -3802,25 +3964,35 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3802
3964
|
* Stamp `drained_at` on undrained rows up to `upToId`. The `IS NULL` guard makes a
|
|
3803
3965
|
* retried shipment idempotent — it re-stamps nothing and returns 0 (K-24).
|
|
3804
3966
|
*/
|
|
3805
|
-
markAccessLogDrained(upToId, drainedAt) {
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3967
|
+
markAccessLogDrained(upToId, drainedAt, audit) {
|
|
3968
|
+
let drained = 0;
|
|
3969
|
+
this.writeAndAudit(() => {
|
|
3970
|
+
const doomed = this.sql
|
|
3971
|
+
.exec('SELECT id FROM _substrat_access_log WHERE id <= ? AND drained_at IS NULL', upToId)
|
|
3972
|
+
.toArray().map((r) => r.id);
|
|
3973
|
+
for (const id of doomed) {
|
|
3974
|
+
this.sql.exec('UPDATE _substrat_access_log SET drained_at = ? WHERE id = ?', drainedAt, id);
|
|
3975
|
+
}
|
|
3976
|
+
drained = doomed.length;
|
|
3977
|
+
return drained > 0;
|
|
3978
|
+
}, audit ? () => ({ ...audit, after: { drained, upToId, drainedAt } }) : undefined);
|
|
3979
|
+
return drained;
|
|
3813
3980
|
}
|
|
3814
3981
|
/** ONLY drained rows. Age alone is not a licence to delete evidence (K-24). */
|
|
3815
|
-
pruneAccessLog(limit) {
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3982
|
+
pruneAccessLog(limit, audit) {
|
|
3983
|
+
let pruned = 0;
|
|
3984
|
+
this.writeAndAudit(() => {
|
|
3985
|
+
assertRowLimit('limit', limit);
|
|
3986
|
+
const doomed = this.sql
|
|
3987
|
+
.exec('SELECT id FROM _substrat_access_log WHERE drained_at IS NOT NULL ORDER BY id LIMIT ?', limit)
|
|
3988
|
+
.toArray().map((r) => r.id);
|
|
3989
|
+
for (const id of doomed) {
|
|
3990
|
+
this.sql.exec('DELETE FROM _substrat_access_log WHERE id = ?', id);
|
|
3991
|
+
}
|
|
3992
|
+
pruned = doomed.length;
|
|
3993
|
+
return pruned > 0;
|
|
3994
|
+
}, audit ? () => ({ ...audit, after: { pruned } }) : undefined);
|
|
3995
|
+
return pruned;
|
|
3824
3996
|
}
|
|
3825
3997
|
// -- per-subject keys (#37) -------------------------------------------------
|
|
3826
3998
|
// The three row operations `createSubjectKeys` needs. The crypto is the kernel's; what
|
|
@@ -3873,9 +4045,9 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3873
4045
|
// never reads these — the coordinator resolves the session and hands the DO the
|
|
3874
4046
|
// two actors, which keeps the DO's invoke free of a directory round-trip.
|
|
3875
4047
|
/** Insert a session the coordinator minted. Returns nothing; the caller holds it. */
|
|
3876
|
-
writeImpersonation(values) {
|
|
3877
|
-
this.sql.exec(`INSERT INTO _substrat_impersonations (${IMPERSONATION_COLUMNS})
|
|
3878
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ...values);
|
|
4048
|
+
writeImpersonation(values, audit) {
|
|
4049
|
+
this.writeAndAudit(() => this.sql.exec(`INSERT INTO _substrat_impersonations (${IMPERSONATION_COLUMNS})
|
|
4050
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ...values).rowsWritten > 0, audit);
|
|
3879
4051
|
}
|
|
3880
4052
|
/** One session by id, or undefined. The door's lookup and every invoke's re-read. */
|
|
3881
4053
|
readImpersonation(id) {
|
|
@@ -3883,8 +4055,8 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3883
4055
|
return this.sql.exec(q.sql, ...q.params).toArray()[0];
|
|
3884
4056
|
}
|
|
3885
4057
|
/** Close a session. Idempotent — a row already ended keeps its first `ended_at`. */
|
|
3886
|
-
endImpersonation(id, endedAt) {
|
|
3887
|
-
this.sql.exec('UPDATE _substrat_impersonations SET ended_at = ? WHERE id = ? AND ended_at IS NULL', endedAt, id);
|
|
4058
|
+
endImpersonation(id, endedAt, audit) {
|
|
4059
|
+
this.writeAndAudit(() => this.sql.exec('UPDATE _substrat_impersonations SET ended_at = ? WHERE id = ? AND ended_at IS NULL', endedAt, id).rowsWritten > 0, audit);
|
|
3888
4060
|
}
|
|
3889
4061
|
listImpersonations(filter, now) {
|
|
3890
4062
|
const q = impersonationListQuery(filter, now);
|
|
@@ -4415,22 +4587,27 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
4415
4587
|
return rows.map((r) => issueOf(r));
|
|
4416
4588
|
}
|
|
4417
4589
|
/** The lifecycle write (#1233). Returns before/after for the caller's audit, or undefined. */
|
|
4418
|
-
setIssueStatus(fingerprint, status, at) {
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4590
|
+
setIssueStatus(fingerprint, status, at, audit) {
|
|
4591
|
+
let existing;
|
|
4592
|
+
let after;
|
|
4593
|
+
this.writeAndAudit(() => {
|
|
4594
|
+
const rows = this.sql
|
|
4595
|
+
.exec('SELECT * FROM _substrat_issues WHERE fingerprint = ?', fingerprint)
|
|
4596
|
+
.toArray();
|
|
4597
|
+
existing = rows[0];
|
|
4598
|
+
if (!existing)
|
|
4599
|
+
return false;
|
|
4600
|
+
const resolvedAt = status === 'resolved' ? at : null;
|
|
4601
|
+
// "Resolved under X": freeze the newest version at the verdict, so a later
|
|
4602
|
+
// regression can name the pair. A reopen or ignore clears it with the timestamp.
|
|
4603
|
+
const resolvedVersion = status === 'resolved' ? existing.last_version : null;
|
|
4604
|
+
this.sql.exec('UPDATE _substrat_issues SET status = ?, resolved_at = ?, resolved_version = ? WHERE fingerprint = ?', status, resolvedAt, resolvedVersion, fingerprint);
|
|
4605
|
+
after = { ...existing, status, resolved_at: resolvedAt, resolved_version: resolvedVersion };
|
|
4606
|
+
return true;
|
|
4607
|
+
}, audit ? () => ({ ...audit, vertical: after.last_vertical,
|
|
4608
|
+
before: { fingerprint, status: existing.status },
|
|
4609
|
+
after: { fingerprint, status: after.status } }) : undefined);
|
|
4610
|
+
return existing && after ? { before: issueOf(existing), after: issueOf(after) } : undefined;
|
|
4434
4611
|
}
|
|
4435
4612
|
/** #1054: idempotent on request_id — a replayed intent writes nothing. */
|
|
4436
4613
|
recordModelUsage(row) {
|
|
@@ -4484,5 +4661,20 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
4484
4661
|
}
|
|
4485
4662
|
return this.sql.exec(sql, ...params).toArray();
|
|
4486
4663
|
}
|
|
4664
|
+
listTenantModelUsageMeter(query) {
|
|
4665
|
+
const cursor = query.cursor;
|
|
4666
|
+
const since = canonicalInstant(query.since);
|
|
4667
|
+
const until = canonicalInstant(query.until);
|
|
4668
|
+
const rows = this.sql.exec(`SELECT id, at, substr(vertical, 1, 64) AS vertical, substr(model, 1, 128) AS model,
|
|
4669
|
+
CASE WHEN length(vertical) > 64 OR length(model) > 128 THEN 1 ELSE 0 END AS label_truncated,
|
|
4670
|
+
reported, input_tokens, output_tokens
|
|
4671
|
+
FROM _substrat_model_usage
|
|
4672
|
+
WHERE tenant_id = ? AND at >= ? AND at < ?
|
|
4673
|
+
${cursor ? 'AND (at < ? OR (at = ? AND id < ?))' : ''}
|
|
4674
|
+
ORDER BY at DESC, id DESC LIMIT ?`, query.tenantId, since, until, ...(cursor ? [cursor.at, cursor.at, cursor.id] : []), Math.min(assertRowLimit('limit', query.limit), 501)).toArray();
|
|
4675
|
+
return rows.map((row) => ({ id: row.id, at: row.at, vertical: row.vertical, model: row.model,
|
|
4676
|
+
labelTruncated: row.label_truncated !== 0, reported: row.reported !== 0,
|
|
4677
|
+
inputTokens: row.input_tokens, outputTokens: row.output_tokens }));
|
|
4678
|
+
}
|
|
4487
4679
|
}
|
|
4488
4680
|
//# sourceMappingURL=control-plane-do.js.map
|