@substrat-run/control-plane-api 0.165.0 → 0.167.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.
Files changed (40) hide show
  1. package/dist/api.d.ts +25 -2
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +496 -49
  4. package/dist/api.js.map +1 -1
  5. package/dist/connection-binding-options.d.ts +13 -0
  6. package/dist/connection-binding-options.d.ts.map +1 -1
  7. package/dist/connection-binding-options.js +17 -0
  8. package/dist/connection-binding-options.js.map +1 -1
  9. package/dist/deploy.d.ts +2 -4
  10. package/dist/deploy.d.ts.map +1 -1
  11. package/dist/deploy.js +0 -2
  12. package/dist/deploy.js.map +1 -1
  13. package/dist/github-app.d.ts +31 -0
  14. package/dist/github-app.d.ts.map +1 -0
  15. package/dist/github-app.js +86 -0
  16. package/dist/github-app.js.map +1 -0
  17. package/dist/index.d.ts +5 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/source-builds.d.ts +6 -0
  22. package/dist/source-builds.d.ts.map +1 -0
  23. package/dist/source-builds.js +55 -0
  24. package/dist/source-builds.js.map +1 -0
  25. package/dist/staff-client.d.ts +2 -2
  26. package/dist/staff-client.d.ts.map +1 -1
  27. package/dist/staff-client.js.map +1 -1
  28. package/dist/store-rebuild.d.ts +7 -2
  29. package/dist/store-rebuild.d.ts.map +1 -1
  30. package/dist/store-rebuild.js +6 -3
  31. package/dist/store-rebuild.js.map +1 -1
  32. package/dist/store-releases.d.ts +56 -7
  33. package/dist/store-releases.d.ts.map +1 -1
  34. package/dist/store-releases.js +221 -20
  35. package/dist/store-releases.js.map +1 -1
  36. package/dist/store-script.d.ts +37 -6
  37. package/dist/store-script.d.ts.map +1 -1
  38. package/dist/store-script.js +50 -27
  39. package/dist/store-script.js.map +1 -1
  40. package/package.json +9 -9
package/dist/api.js CHANGED
@@ -1,21 +1,23 @@
1
+ import { sourceBuildRequest } from '@substrat-run/contracts';
1
2
  import { PREVIEW_CONNECTIONS_REFUSAL } from './connection-relay.js';
2
- import { connectionBindingOptions } from './connection-binding-options.js';
3
+ import { bindingChangeRefusal, connectionBindingOptions, refusesBindingChange } from './connection-binding-options.js';
3
4
  import { resolveObservabilityWindow } from './observability-window.js';
4
5
  import { Hono } from 'hono';
5
- import { permissionKey, parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, lakeEventsInput, ownerTransferInput, memberInviteInput, memberRoleInput, ASSET_PART_PREFIX, assetHash, STORE_DRIVERS, STORE_RELEASE_MAX_BYTES, channelName, connectLinkId, connectionActivity, connectionActivitySource, connectionCredential, connectionCertificate, connectionFilter, connectionHealthState, connectionProvider, connectorDispatchKind, PROVISION_SIBLING_KIND, ARCHIVE_SCOPE_KIND, PROVISION_TENANT_KIND, SET_ENTITLEMENTS_KIND, MODEL_USAGE_KIND, SWEEP_RUNS_KIND, toConnectionHealthEntry, CONNECTION_EXPIRY_WARNING_DAYS, CONNECTION_STALE_DAYS, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, FINDINGS_LIMIT_DEFAULT, FINDINGS_LIMIT_MAX, findingCursorOf, findingKind, findingRuleInput, findingStatus, findingStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, impersonationStamp, promotionAcknowledgement, bindAcknowledgement, rebindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, operationSeriesCount, operationSeriesInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, verticalSlug, scopeText, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, platformScriptCollision, platformScriptRefusal, PLATFORM_SCRIPT_NAMES, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, requestRollupReading, meterDaysReading, verticalOwnersReading, utcDay, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, validationIssuesFrom, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, substratError, errorCodeOf, envVarSpec, capabilityFilterQuery, capabilityId as capabilityIdSchema, PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER, SOCIAL_RELAY_VERTICALS, isVerticalConnectorProvider, } from '@substrat-run/contracts';
6
+ import { permissionKey, parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, lakeEventsInput, ownerTransferInput, memberInviteInput, memberRoleInput, ASSET_PART_PREFIX, assetHash, STORE_DRIVERS, storeCatalogOperation, storeDriverBasisAdmits, channelName, connectLinkId, connectionActivity, connectionActivitySource, connectionCredential, connectionCertificate, connectionFilter, connectionHealthState, connectionProvider, connectorDispatchKind, PROVISION_SIBLING_KIND, ARCHIVE_SCOPE_KIND, PROVISION_TENANT_KIND, SET_ENTITLEMENTS_KIND, MODEL_USAGE_KIND, SWEEP_RUNS_KIND, toConnectionHealthEntry, CONNECTION_EXPIRY_WARNING_DAYS, CONNECTION_STALE_DAYS, connectionProbe, connectionSendTest, connectionSendTestResult, connectionSenders, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, FINDINGS_LIMIT_DEFAULT, FINDINGS_LIMIT_MAX, findingCursorOf, findingKind, findingRuleInput, findingStatus, findingStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, impersonationStamp, promotionAcknowledgement, bindAcknowledgement, rebindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, operationSeriesCount, operationSeriesInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, verticalSlug, scopeText, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, platformScriptCollision, platformScriptRefusal, PLATFORM_SCRIPT_NAMES, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, requestRollupReading, meterDaysReading, verticalOwnersReading, utcDay, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, validationIssuesFrom, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, substratError, errorCodeOf, envVarSpec, capabilityFilterQuery, capabilityId as capabilityIdSchema, PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER, PLATFORM_FEATURE_SOURCE_BUILDS, SOCIAL_RELAY_VERTICALS, isVerticalConnectorProvider, } from '@substrat-run/contracts';
6
7
  import { upstreamHandleIn, upstreamReferenceOf, withUpstreamReference } from '@substrat-run/contracts';
7
8
  import { ENGINE_ENTITLEMENT_KEYS, bareSlug, declarableEntitlementKey, entitlementNamespaceRefusal, grantableEntitlements } from '@substrat-run/contracts';
8
9
  import { SCOPE_COPY_LEASE_MS, copyRestoreFence, publishedMigrationDigest, replayStoreErasures } from '@substrat-run/kernel';
9
10
  import { attributeFailure } from './failure-attribution.js';
10
11
  import { auditedChange, withAuditedOutcomes } from './audited-change.js';
11
- import { BIND_EXPORT_BREAK_REFUSAL, CARRIED_AWAY_KEY, bindExportBreakRefusal, carriedAwayDump, dumpMetaValue, metaValueIn, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, listedPromoteRefusal, isExportBreakUnjudged, isPrimaryScope, promoteReviewDigest, rebindReviewDigest, assertRebindReviewCurrent, migrationProgress, ulid, auditWarningOf, REQUEST_ROLLUP_LIMIT_DEFAULT, REQUEST_ROLLUP_LIMIT_MAX, METER_DAYS_LIMIT_DEFAULT, METER_DAYS_LIMIT_MAX, releaseRolesAskScopes, releaseRoleSetRefusal, withheldOpsFailure, } from '@substrat-run/kernel';
12
+ import { BIND_EXPORT_BREAK_REFUSAL, CARRIED_AWAY_KEY, bindExportBreakRefusal, carriedAwayDump, dumpMetaValue, metaValueIn, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, listedPromoteRefusal, isExportBreakUnjudged, isPrimaryScope, promoteReviewDigest, rebindReviewDigest, assertRebindReviewCurrent, migrationProgress, ulid, auditWarningOf, REQUEST_ROLLUP_LIMIT_DEFAULT, REQUEST_ROLLUP_LIMIT_MAX, STORAGE_GAUGE_RETENTION_MONTHS, MODEL_USAGE_RETENTION_DAYS, BILLING_ROLLUP_RETENTION_MONTHS, METER_DAYS_LIMIT_DEFAULT, METER_DAYS_LIMIT_MAX, releaseRolesAskScopes, releaseRoleSetRefusal, withheldOpsFailure, } from '@substrat-run/kernel';
12
13
  import { TENANT_HEADER, confinedTenant } from './auth.js';
13
14
  import { mintTenantToken } from './tenant-token.js';
14
15
  import { beginTransfer, connectionGrantsForScope, transferScope, isScopeNotHeld, notHeldIsBenign } from './vertical-client.js';
15
16
  import { COPY_JOB_MAX_MS, copyJobAt, copyJobHeld, copyJobId, copyJobLive, copyTagId, versionIdOfOrder, versionOrderOf, } from './copy-job.js';
16
17
  import { runningVersionOf, runningVersionStoresOf, scopeStoreReachOf, scopeStoresOf, unrouteScope } from './platform-scope-stores.js';
18
+ import { liveApprovalOf } from './store-script.js';
17
19
  import { STORE_REBUILD_REQUEST_SUBREQUESTS, runStoreFleetRebuild, storeRebuildBudget, storeRebuildStatus } from './store-rebuild.js';
18
- import { PUSH_HOLD_TTL_MS, runStoreDriverRollout, storeDriverRelease, storedReleaseLoader } from './store-releases.js';
20
+ import { PUSH_HOLD_TTL_MS, STORE_RELEASE_STORED_MAX_BYTES, approveStoredDriverRelease, readStoreDriverApprovals, runStoreDriverRevoke, runStoreDriverRollout, storeDriverRelease, storedReleaseLoader } from './store-releases.js';
19
21
  import { assertNoUnreachableScopeCopies, claimScopeScriptReap, drainScopeScriptCopies, listAllScopeScriptCopies, recordStrandedStorage } from './scope-copy-cleanup.js';
20
22
  import { BACKFILL_PAGE_MAX, backfillScopeScriptCopies } from './scope-copy-backfill.js';
21
23
  import { oidcCallbackUrl, retireClientsOfReapedScope, wirePreviewAuth } from './preview-auth.js';
@@ -76,6 +78,8 @@ import { queryLakeEvents } from './lake-gateway.js';
76
78
  import { LakeQueryError } from './r2-sql-engine.js';
77
79
  import { isCustomHostname, validateBindableHostname, } from './custom-hostnames.js';
78
80
  import { isSocialRelayVertical, reconcileScopeSocialRelay, relayHostnames, } from './social-relay.js';
81
+ /** The body of a connection's test message (#2343), plain enough to be the same in HTML and text. */
82
+ const SEND_TEST_TEXT = 'This is a test message from a mail connection. It reached you, so its sender address can send.';
79
83
  /**
80
84
  * Response headers carrying the CLI version advisory (`ControlPlaneApiOptions.cliAdvisory`).
81
85
  * The CLI reads these by the same spelling (`packages/cli/src/version.ts`); exported so a
@@ -700,6 +704,9 @@ const TENANT_ROUTES = [
700
704
  // #2100: the PUBLIC certificate a connection signs in with — same pin as the credential view.
701
705
  { method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/certificate$/, pin: 'path' },
702
706
  { method: 'POST', re: /\/tenants\/[^/]+\/connections\/[^/]+\/verify$/, pin: 'path' },
707
+ // #2343: a mail connection's test send. The dashboard sends it only to the signed-in person.
708
+ { method: 'POST', re: /\/tenants\/[^/]+\/connections\/[^/]+\/send-test$/, pin: 'path' },
709
+ { method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/senders$/, pin: 'path' },
703
710
  // A vertical's mailed connect links (connections.md §3.5.4): the consent landing's liveness
704
711
  // read, the callback's spend and its undo, and the integrations card's list and revoke.
705
712
  { method: 'GET', re: /\/tenants\/[^/]+\/connect-links$/, pin: 'path' },
@@ -715,6 +722,8 @@ const TENANT_ROUTES = [
715
722
  { method: 'POST', re: /\/tenants\/[^/]+\/orgs$/, pin: 'path' },
716
723
  { method: 'POST', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
717
724
  { method: 'DELETE', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
725
+ { method: 'GET', re: /\/tenants\/[^/]+\/entitlements$/, pin: 'path' },
726
+ { method: 'GET', re: /\/tenants\/[^/]+\/metered-usage$/, pin: 'path' },
718
727
  // An install's entitlement. The handler narrows WHICH key a tenant credential may
719
728
  // grant (`tenantGrantableEntitlement`): the pin alone would let it grant itself any.
720
729
  { method: 'PUT', re: /\/tenants\/[^/]+\/entitlements\/[^/]+$/, pin: 'path' },
@@ -1147,7 +1156,7 @@ export function createControlPlaneApi(options) {
1147
1156
  const advisory = options.cliAdvisory;
1148
1157
  app.use('*', async (c, next) => {
1149
1158
  await next();
1150
- c.header(PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER);
1159
+ c.header(PLATFORM_FEATURES_HEADER, [PLATFORM_FEATURE_SCOPE_SWEEPER, ...(options.sourceBuilds ? [PLATFORM_FEATURE_SOURCE_BUILDS] : [])].join(','));
1151
1160
  if (advisory?.minVersion)
1152
1161
  c.header(CLI_MIN_VERSION_HEADER, advisory.minVersion);
1153
1162
  if (advisory?.latestVersion)
@@ -1238,6 +1247,8 @@ export function createControlPlaneApi(options) {
1238
1247
  { method: 'GET', re: /\/verticals\/[^/]+\/channels$/ },
1239
1248
  { method: 'GET', re: /\/verticals\/[^/]+\/channels\/[^/]+\/history$/ },
1240
1249
  { method: 'POST', re: /\/verticals\/[^/]+\/channels\/[^/]+\/promote$/ },
1250
+ { method: 'GET', re: /\/projects$/ },
1251
+ { method: 'POST', re: /\/projects$/ },
1241
1252
  { method: 'POST', re: /\/verticals\/[^/]+\/deploy$/ },
1242
1253
  // A builder REQUESTS publication of a vertical it owns (marketplace-publish.md §5); the
1243
1254
  // `listing` flip stays staff-only. Ownership is checked in the handler.
@@ -1679,6 +1690,168 @@ export function createControlPlaneApi(options) {
1679
1690
  });
1680
1691
  // -- entitlements (§4.3) ---------------------------------------------------
1681
1692
  app.get('/tenants/:tenantId/entitlements', async (c) => c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantIdSchema.parse(c.req.param('tenantId')))));
1693
+ // One tenant's persisted usage, with no price-bearing fields (#2689). This read never
1694
+ // wakes a scope or writes a row. A gauge projection is capped at 501 narrow scope rows;
1695
+ // request rows at 10,000 plus 30 status rows; model pages at 25 x 501: at most 28
1696
+ // directory calls and 23,056 projected rows. Labels are capped at the read source,
1697
+ // and no staff ledger row crosses the RPC. The 5-second cooperative budget stops new
1698
+ // reads, though it cannot interrupt one in flight. Every cap marks a lower bound.
1699
+ // The dashboard credential reaches this path only when its signed tenant pin matches.
1700
+ app.get('/tenants/:tenantId/metered-usage', async (c) => {
1701
+ const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
1702
+ if (outsideTenant(c.get('principal'), tenantId))
1703
+ return refuse(c, 'forbidden', 'forbidden');
1704
+ const admin = c.var.admin;
1705
+ const actor = c.get('actor');
1706
+ const usageNow = options.meteredUsageNow ?? (() => performance.now());
1707
+ const deadline = usageNow() + 5_000;
1708
+ const readAt = new Date().toISOString();
1709
+ const until = readAt.slice(0, 10);
1710
+ const since = new Date(Date.parse(`${until}T00:00:00Z`) - 30 * 86_400_000).toISOString().slice(0, 10);
1711
+ const through = new Date(Date.parse(`${until}T00:00:00Z`) - 86_400_000).toISOString().slice(0, 10);
1712
+ const gaugeRead = await admin.readTenantStorageGauge(actor, tenantId, 500);
1713
+ const gauge = gaugeRead.gauge;
1714
+ const stale = gauge?.oldestReadAt ? Date.parse(readAt) - Date.parse(gauge.oldestReadAt) > 2 * 86_400_000 : false;
1715
+ const storageState = gaugeRead.truncated ? 'truncated' : !gauge ? 'unavailable' : gauge.sampled === 0 ? (gauge.total === 0 ? 'empty' : 'not-sampled')
1716
+ : gauge.failing > 0 ? 'failing' : stale ? 'stale' : gauge.sampled < gauge.total ? 'partial' : 'complete';
1717
+ // Literal output allowlist: never spread a meter row (which also carries plan data).
1718
+ const storage = {
1719
+ state: storageState,
1720
+ bytes: gauge && gauge.sampled > 0 ? gauge.bytes : null,
1721
+ estimated: false,
1722
+ lowerBound: gaugeRead.truncated,
1723
+ truncatedReason: gaugeRead.truncated ? 'scope-cap' : null,
1724
+ basis: 'scope-databases',
1725
+ sampled: gauge?.sampled ?? 0,
1726
+ total: gauge?.total ?? 0,
1727
+ failing: gauge?.failing ?? 0,
1728
+ oldestReadAt: gauge?.oldestReadAt ?? null,
1729
+ excluded: gauge?.excluded ?? [],
1730
+ };
1731
+ let requests = {
1732
+ state: 'unavailable', requests: null, estimated: true, lowerBound: false,
1733
+ coveredDays: 0, totalDays: 30, sinceDay: since, untilDay: through,
1734
+ lastReadAt: null, byVertical: [], breakdownTruncated: false,
1735
+ };
1736
+ if (usageNow() < deadline && admin.listTenantRequestMeter && admin.listRequestMeterDays) {
1737
+ const [rows, days] = await Promise.all([
1738
+ admin.listTenantRequestMeter(actor, tenantId, since, through, REQUEST_ROLLUP_LIMIT_MAX),
1739
+ admin.listRequestMeterDays(actor, since, through),
1740
+ ]);
1741
+ const covered = days.filter((d) => d.readAt !== null && !d.saturated);
1742
+ const coveredDaySet = new Set(covered.map((day) => day.day));
1743
+ const clipped = rows.length === REQUEST_ROLLUP_LIMIT_MAX;
1744
+ const groups = new Map();
1745
+ let count = 0;
1746
+ let countedRows = 0;
1747
+ let requestLabelsTruncated = false;
1748
+ for (const row of rows.slice(0, REQUEST_ROLLUP_LIMIT_MAX - 1)) {
1749
+ if (!coveredDaySet.has(row.day))
1750
+ continue;
1751
+ countedRows++;
1752
+ count += row.requests;
1753
+ if (row.labelTruncated)
1754
+ requestLabelsTruncated = true;
1755
+ else
1756
+ groups.set(row.vertical, (groups.get(row.vertical) ?? 0) + row.requests);
1757
+ }
1758
+ const byVertical = [...groups].sort((a, b) => (a[0] ?? '').localeCompare(b[0] ?? ''));
1759
+ const incomplete = covered.length < 30 || clipped;
1760
+ requests = {
1761
+ state: covered.length === 0 ? 'unavailable' : clipped ? 'truncated' : countedRows === 0 ? 'empty' : incomplete ? 'partial' : 'complete',
1762
+ requests: countedRows > 0 ? count : null,
1763
+ estimated: true,
1764
+ lowerBound: countedRows > 0 && incomplete,
1765
+ coveredDays: covered.length,
1766
+ totalDays: 30,
1767
+ sinceDay: since,
1768
+ untilDay: through,
1769
+ lastReadAt: covered.reduce((latest, day) => !latest || day.readAt > latest ? day.readAt : latest, null),
1770
+ byVertical: covered.length > 0 ? byVertical.slice(0, 50).map(([vertical, n]) => ({ vertical, requests: n })) : [],
1771
+ breakdownTruncated: covered.length > 0 && (byVertical.length > 50 || requestLabelsTruncated),
1772
+ };
1773
+ }
1774
+ const groups = new Map();
1775
+ let calls = 0;
1776
+ let reportedCalls = 0;
1777
+ let inputTokens = 0;
1778
+ let outputTokens = 0;
1779
+ let oldestRecordedAt = null;
1780
+ let newestRecordedAt = null;
1781
+ let truncated = false;
1782
+ let truncatedReason = null;
1783
+ let modelSkipped = false;
1784
+ let labelsTruncated = false;
1785
+ const earlier = (a, b) => a === null || b < a ? b : a;
1786
+ const later = (a, b) => a === null || b > a ? b : a;
1787
+ if (typeof admin.listTenantModelUsageMeter === 'function') {
1788
+ let cursor;
1789
+ for (let page = 0; page < 25; page++) {
1790
+ if (usageNow() >= deadline) {
1791
+ if (calls > 0) {
1792
+ truncated = true;
1793
+ truncatedReason = 'time-budget';
1794
+ }
1795
+ else
1796
+ modelSkipped = true;
1797
+ break;
1798
+ }
1799
+ const entries = await admin.listTenantModelUsageMeter(actor, { tenantId, since: `${since}T00:00:00Z`, until: `${until}T00:00:00Z`, limit: 501, ...(cursor ? { cursor } : {}) });
1800
+ for (const entry of entries.slice(0, 500)) {
1801
+ calls++;
1802
+ if (entry.reported) {
1803
+ reportedCalls++;
1804
+ inputTokens += entry.inputTokens;
1805
+ outputTokens += entry.outputTokens;
1806
+ }
1807
+ oldestRecordedAt = earlier(oldestRecordedAt, entry.at);
1808
+ newestRecordedAt = later(newestRecordedAt, entry.at);
1809
+ if (entry.labelTruncated)
1810
+ labelsTruncated = true;
1811
+ else {
1812
+ const key = `${entry.vertical}\u0000${entry.model}`;
1813
+ const row = groups.get(key) ?? { vertical: entry.vertical, model: entry.model, calls: 0, reportedCalls: 0, inputTokens: 0, outputTokens: 0 };
1814
+ row.calls++;
1815
+ if (entry.reported) {
1816
+ row.reportedCalls++;
1817
+ row.inputTokens += entry.inputTokens;
1818
+ row.outputTokens += entry.outputTokens;
1819
+ }
1820
+ groups.set(key, row);
1821
+ }
1822
+ }
1823
+ truncated = entries.length > 500;
1824
+ truncatedReason = truncated ? 'page-cap' : null;
1825
+ if (!truncated)
1826
+ break;
1827
+ cursor = { at: entries[499].at, id: entries[499].id };
1828
+ }
1829
+ }
1830
+ const modelGroups = [...groups.values()].sort((a, b) => a.vertical.localeCompare(b.vertical) || a.model.localeCompare(b.model));
1831
+ const models = {
1832
+ state: typeof admin.listTenantModelUsageMeter !== 'function' || modelSkipped ? 'unavailable' : calls === 0 ? 'empty' : truncated ? 'truncated' : 'complete',
1833
+ calls: calls > 0 ? calls : null,
1834
+ reportedCalls,
1835
+ inputTokens: reportedCalls > 0 ? inputTokens : null,
1836
+ outputTokens: reportedCalls > 0 ? outputTokens : null,
1837
+ estimated: false,
1838
+ lowerBound: truncated,
1839
+ truncatedReason,
1840
+ since: `${since}T00:00:00Z`,
1841
+ until: `${until}T00:00:00Z`,
1842
+ oldestRecordedAt,
1843
+ newestRecordedAt,
1844
+ byVerticalModel: modelGroups.slice(0, 50).map((row) => ({ vertical: row.vertical, model: row.model, calls: row.calls, reportedCalls: row.reportedCalls, inputTokens: row.reportedCalls > 0 ? row.inputTokens : null, outputTokens: row.reportedCalls > 0 ? row.outputTokens : null })),
1845
+ breakdownTruncated: modelGroups.length > 50 || labelsTruncated,
1846
+ };
1847
+ return c.json({
1848
+ readAt,
1849
+ retention: { storageMonths: STORAGE_GAUGE_RETENTION_MONTHS, modelDays: MODEL_USAGE_RETENTION_DAYS, requestMonths: BILLING_ROLLUP_RETENTION_MONTHS },
1850
+ storage,
1851
+ requests,
1852
+ models,
1853
+ });
1854
+ });
1682
1855
  app.put('/tenants/:tenantId/entitlements/:key', async (c) => {
1683
1856
  const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
1684
1857
  // The body is the plan half (#33) and optional — a bodyless PUT is the
@@ -2361,6 +2534,13 @@ export function createControlPlaneApi(options) {
2361
2534
  if (!grants.some((g) => g.connectionId === row.id && g.permission === input.permission)) {
2362
2535
  return refuseStatus(c, 409, `connection does not hold '${input.permission}' on scope ${scopeId}; grant it through the tenant's authorized connection flow, then bind`);
2363
2536
  }
2537
+ // A change the held binding cannot take in place is the operator's to act on, not a failed write.
2538
+ const held = refusesBindingChange(row.provider)
2539
+ ? (await inspectorOf(row)?.bindings?.(c.var.host, row))?.find((b) => b.scopeId === scopeId)
2540
+ : undefined;
2541
+ const changeRefused = bindingChangeRefusal(row.provider, held, input);
2542
+ if (changeRefused)
2543
+ return refuseStatus(c, 409, changeRefused);
2364
2544
  const operationId = ulid();
2365
2545
  const done = await auditedChange({
2366
2546
  flow: 'connection-binding', operationId,
@@ -2400,6 +2580,70 @@ export function createControlPlaneApi(options) {
2400
2580
  }
2401
2581
  return c.json(connectionProbe.parse(await probe(c.var.host, row)));
2402
2582
  });
2583
+ /** The addresses a mail connection may send as (#2343) — the send test's picker, whole and uncut. */
2584
+ app.get('/tenants/:tenantId/connections/:id/senders', async (c) => {
2585
+ const row = await inspectableConnection(c);
2586
+ if (!row)
2587
+ return refuse(c, 'not_found', 'unknown connection');
2588
+ const sender = options.mailSenders?.[row.provider];
2589
+ if (!sender)
2590
+ return refuseStatus(c, 501, `no mail sender registered for provider '${row.provider}'`);
2591
+ return c.json(connectionSenders.parse({ senders: await sender.senders(c.var.host, row) }));
2592
+ });
2593
+ /**
2594
+ * **Send a test message through a connection** (#2343) — the probe for mail, which cannot be
2595
+ * probed without sending. From one of the connection's own senders, to one recipient; the
2596
+ * dashboard's door sends only to the signed-in person's own address.
2597
+ *
2598
+ * Like `/verify`, a refusal is `200 { ok: false, error }` in the connector's own words (outside
2599
+ * the granted scope, throttled, sign-in failed), and the send rides the connector's recorded
2600
+ * egress, so health is written as for any send. A success is recorded by the connector as that
2601
+ * sender's verification. Not an audited admin-log mutation, the same class as a probe; who
2602
+ * asked is logged on the request record instead, never the recipient.
2603
+ */
2604
+ app.post('/tenants/:tenantId/connections/:id/send-test', async (c) => {
2605
+ const row = await inspectableConnection(c);
2606
+ if (!row)
2607
+ return refuse(c, 'not_found', 'unknown connection');
2608
+ const sender = options.mailSenders?.[row.provider];
2609
+ if (!sender)
2610
+ return refuseStatus(c, 501, `no mail sender registered for provider '${row.provider}'`);
2611
+ const body = connectionSendTest.safeParse(await c.req.json().catch(() => null));
2612
+ if (!body.success)
2613
+ return refuse(c, 'validation_failed', 'from and to must each be one email address');
2614
+ const { from, to } = body.data;
2615
+ const senders = await sender.senders(c.var.host, row);
2616
+ if (!senders.some((s) => s.toLowerCase() === from.toLowerCase())) {
2617
+ return refuse(c, 'validation_failed', `'${from}' is not one of this connection's sender addresses`);
2618
+ }
2619
+ const principal = c.get('principal');
2620
+ console.log('connection.send-test', {
2621
+ tenantId: row.tenantId,
2622
+ connectionId: row.id,
2623
+ provider: row.provider,
2624
+ actor: c.get('actor'),
2625
+ onBehalfOf: principal.kind === 'tenant' ? (principal.onBehalfOf?.principal ?? null) : null,
2626
+ });
2627
+ try {
2628
+ await sender.send(c.var.host, row, {
2629
+ from: { email: from },
2630
+ to: [{ email: to }],
2631
+ subject: 'Substrat test message',
2632
+ html: `<p>${SEND_TEST_TEXT}</p>`,
2633
+ text: SEND_TEST_TEXT,
2634
+ attachments: [],
2635
+ });
2636
+ return c.json(connectionSendTestResult.parse({ ok: true, status: null, error: null }));
2637
+ }
2638
+ catch (e) {
2639
+ const status = e?.status;
2640
+ return c.json(connectionSendTestResult.parse({
2641
+ ok: false,
2642
+ status: typeof status === 'number' ? status : null,
2643
+ error: (e instanceof Error ? e.message : String(e)).slice(0, 600),
2644
+ }));
2645
+ }
2646
+ });
2403
2647
  /**
2404
2648
  * **What the connection has done** (#605) — the connector's dispatch ledger, projected.
2405
2649
  *
@@ -7410,7 +7654,13 @@ export function createControlPlaneApi(options) {
7410
7654
  throw new ControlPlaneError(409, `${operation} refused: the hold on scope store driver '${each}' lapsed during the upload; try again`);
7411
7655
  }
7412
7656
  }
7413
- await options.ensureScopeStores({ vertical: slug, stores: stores.filter((store) => store.driver === driver), loadRelease });
7657
+ const registration = (options.storeDrivers ?? STORE_DRIVERS).find((k) => k.driver === driver);
7658
+ if (!registration)
7659
+ throw new ControlPlaneError(403, `scope store driver '${driver}' is not registered`);
7660
+ // Read under the shared hold (D-65): a revoke takes the driver exclusively, so one written
7661
+ // before this read is seen, and none is written until this upload is done.
7662
+ const approvals = await readStoreDriverApprovals(host.admin, actor, registration);
7663
+ await options.ensureScopeStores({ vertical: slug, stores: stores.filter((store) => store.driver === driver), loadRelease, approvals });
7414
7664
  }
7415
7665
  }
7416
7666
  catch (e) {
@@ -7982,12 +8232,98 @@ export function createControlPlaneApi(options) {
7982
8232
  // pointing at a deployment that is not there. The version lands PENDING — except a
7983
8233
  // PRIVATE vertical's, which self-admits below the seam (its blast radius is its own
7984
8234
  // tenant); for everything else admission still gates serving.
8235
+ app.all('/projects', async (c) => {
8236
+ if (!options.sourceBuilds)
8237
+ return refuseStatus(c, 501, 'source builds are not configured');
8238
+ const p = c.get('principal');
8239
+ if (p.kind !== 'builder' && p.kind !== 'staff')
8240
+ return refuse(c, 'forbidden', 'forbidden');
8241
+ const tenant = p.kind === 'builder' ? p.tenantId : c.req.query('tenant');
8242
+ if (!tenant)
8243
+ return refuse(c, 'validation_failed', 'tenant id is required');
8244
+ const tenants = await c.var.admin.listTenants(c.get('actor'));
8245
+ if (!tenants.some((t) => t.id === tenant))
8246
+ return refuse(c, 'not_found', 'unknown workspace');
8247
+ let body;
8248
+ if (c.req.method === 'POST') {
8249
+ const input = z.object({ slug: z.string().min(1), name: z.string().min(1), repository: z.unknown().optional() }).parse(await c.req.json());
8250
+ const team = tenants.find((t) => t.id === tenant);
8251
+ const slug = await ownerOf(c.get('actor'), input.slug) === tenant || input.slug.startsWith(`${team.slug}/`) ? input.slug : `${team.slug}/${input.slug}`;
8252
+ verticalSlug.parse(slug);
8253
+ const owner = await ownerOf(c.get('actor'), slug);
8254
+ if (owner !== undefined && owner !== tenant)
8255
+ return refuse(c, 'forbidden', 'forbidden');
8256
+ body = JSON.stringify({ ...input, slug });
8257
+ }
8258
+ else if (c.req.method !== 'GET')
8259
+ return refuseStatus(c, 405, 'method not allowed');
8260
+ return options.sourceBuilds.projects(tenant, new Request('https://source/projects', {
8261
+ method: c.req.method, ...(body ? { headers: { 'content-type': 'application/json' }, body } : {}),
8262
+ }));
8263
+ });
8264
+ app.post('/source/github-access', async (c) => {
8265
+ if (c.get('principal').kind !== 'staff')
8266
+ return refuse(c, 'forbidden', 'forbidden');
8267
+ if (!options.githubRepositoryAccess)
8268
+ return refuseStatus(c, 501, 'GitHub source access is not configured');
8269
+ const input = z.object({ tenantId: tenantIdSchema, owner: z.string().regex(/^[\w-]+$/), repo: z.string().regex(/^[\w.-]+$/), scope: z.enum(['read', 'write']).default('read') }).parse(await c.req.json());
8270
+ const connection = await c.var.admin.openConnection(input.tenantId, 'dashboard', 'github', input.owner);
8271
+ const installationId = connection?.secret.installationId;
8272
+ if (typeof installationId !== 'string')
8273
+ return refuse(c, 'not_found', 'no connected GitHub installation for this tenant and owner');
8274
+ return c.json(await options.githubRepositoryAccess(input.tenantId, input.owner, input.repo, installationId, input.scope));
8275
+ });
7985
8276
  app.post('/verticals/:slug/deploy', async (c) => {
7986
8277
  if (!options.deployVertical) {
7987
8278
  return refuseStatus(c, 501, 'deploy is not configured on this control plane');
7988
8279
  }
7989
8280
  const p = c.get('principal');
7990
- const form = await c.req.formData();
8281
+ let built;
8282
+ let requestedVersion;
8283
+ let form;
8284
+ if (options.sourceBuilds) {
8285
+ if (!c.req.header('content-type')?.startsWith('application/json')) {
8286
+ return refuseStatus(c, 415, 'submit source JSON; client-built module uploads are no longer accepted');
8287
+ }
8288
+ const input = sourceBuildRequest.parse(await c.req.json());
8289
+ const bare = c.req.param('slug');
8290
+ let sourceSlug = bare;
8291
+ let sourceTenant = null;
8292
+ if (p.kind === 'builder') {
8293
+ if (input.tenant && input.tenant !== p.tenantId && input.tenant !== p.tenantSlug) {
8294
+ return refuse(c, 'forbidden', `push is pinned to workspace '${input.tenant}' but this session acts for '${p.tenantSlug}'`);
8295
+ }
8296
+ sourceTenant = p.tenantId;
8297
+ sourceSlug = effectiveSlug(p, bare);
8298
+ }
8299
+ else if (input.tenant) {
8300
+ const tenant = (await c.var.admin.listTenants(c.get('actor'))).find((t) => t.id === input.tenant || t.slug === input.tenant);
8301
+ if (!tenant)
8302
+ return refuse(c, 'not_found', 'unknown workspace');
8303
+ sourceTenant = tenant.id;
8304
+ sourceSlug = bare.startsWith(`${tenant.slug}/`) || await ownerOf(c.get('actor'), bare) === tenant.id ? bare : `${tenant.slug}/${bare}`;
8305
+ }
8306
+ const owner = await ownerOf(c.get('actor'), sourceSlug);
8307
+ if (sourceTenant && owner !== undefined && owner !== sourceTenant)
8308
+ return refuse(c, 'forbidden', 'forbidden');
8309
+ sourceTenant ??= owner ?? null;
8310
+ verticalSlug.parse(sourceSlug);
8311
+ if (platformScriptCollision(sourceSlug))
8312
+ return refuseStatus(c, 422, 'reserved platform script name');
8313
+ requestedVersion = input.version;
8314
+ built = await options.sourceBuilds.build(sourceTenant ?? 'platform', sourceSlug, input);
8315
+ form = built.form;
8316
+ // Addressing is the authenticated caller's, never a field emitted by project code.
8317
+ form.delete('tenant');
8318
+ if (input.tenant)
8319
+ form.set('tenant', input.tenant);
8320
+ form.delete('allowFork');
8321
+ if (input.allowFork)
8322
+ form.set('allowFork', '1');
8323
+ }
8324
+ else {
8325
+ return refuseStatus(c, 501, 'platform source builds are not configured');
8326
+ }
7991
8327
  const raw = form.get('manifest');
7992
8328
  if (typeof raw !== 'string')
7993
8329
  return refuse(c, 'validation_failed', 'missing manifest part');
@@ -7996,6 +8332,8 @@ export function createControlPlaneApi(options) {
7996
8332
  return refuse(c, 'validation_failed', 'manifest registry: required when modules are present');
7997
8333
  }
7998
8334
  const manifest = deployManifest.parse(parsed);
8335
+ if (requestedVersion && manifest.version !== requestedVersion)
8336
+ return refuseStatus(c, 502, 'build service produced a different release version');
7999
8337
  const moduleProblems = pushedModuleRegistryProblems(manifest.modules, manifest.registry);
8000
8338
  if (moduleProblems.length > 0)
8001
8339
  return refuse(c, 'validation_failed', `manifest registry.permissions: ${moduleProblems.join('; ')}`);
@@ -8067,7 +8405,7 @@ export function createControlPlaneApi(options) {
8067
8405
  // Otherwise the claim lands under the tenant prefix, exactly like a builder push —
8068
8406
  // same namespace, no claim race with other workspaces' bare names.
8069
8407
  const bareOwner = await ownerOf(c.get('actor'), bare);
8070
- slug = bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
8408
+ slug = bare.startsWith(`${workspace.slug}/`) || bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
8071
8409
  const existingOwner = slug === bare ? bareOwner : await ownerOf(c.get('actor'), slug);
8072
8410
  if (existingOwner !== undefined && existingOwner !== workspace.id) {
8073
8411
  return refuse(c, 'forbidden', `vertical '${slug}' is not owned by workspace '${pin}'`);
@@ -8345,7 +8683,7 @@ export function createControlPlaneApi(options) {
8345
8683
  }
8346
8684
  }
8347
8685
  const version = await c.var.admin.getVersion(c.get('actor'), id, slug);
8348
- return c.json({ ...version, ...(warnings.length ? { warnings } : {}) }, 201);
8686
+ return c.json({ ...version, ...(built ? { projectId: built.projectId, revision: built.revision, dataLoss: built.dataLoss } : {}), ...(warnings.length ? { warnings } : {}) }, 201);
8349
8687
  });
8350
8688
  // -- where this platform runs (ops ergonomics) -----------------------------
8351
8689
  // The console renders refs it already has — `servingRef`, a store's `ref`, a version's
@@ -10641,10 +10979,7 @@ export function createControlPlaneApi(options) {
10641
10979
  * `text/plain` beacon cannot set it without a CORS preflight, 415) and refuse a request a
10642
10980
  * browser marks as from another origin (403). A request with neither header is the CLI's.
10643
10981
  */
10644
- const byAPerson = async (c, words = { write: 'a store driver release write', person: 'a store driver release is published by a person' }) => {
10645
- if (!/^application\/json\s*(;|$)/i.test(c.req.header('content-type') ?? '')) {
10646
- throw new ControlPlaneError(415, `${words.write} takes application/json`);
10647
- }
10982
+ const staffPerson = async (c, words) => {
10648
10983
  const origin = c.req.header('origin');
10649
10984
  const site = c.req.header('sec-fetch-site');
10650
10985
  if ((origin && origin !== new URL(c.req.url).origin) || (site && site !== 'same-origin' && site !== 'none')) {
@@ -10657,20 +10992,87 @@ export function createControlPlaneApi(options) {
10657
10992
  }
10658
10993
  return actor;
10659
10994
  };
10660
- /** The pin a release is run under, or a refusal: the platform runs only what this repo pins. */
10661
- const pinOf = async (c, registration, sha256) => {
10662
- const stored = await c.var.admin.getStoreDriverRelease(c.get('actor'), registration.driver, sha256);
10663
- if (!stored)
10664
- throw new ControlPlaneError(404, `release ${sha256} of '${registration.driver}' is not stored`);
10665
- const pin = registration.releases.find((p) => p.sha256 === sha256);
10666
- if (!pin) {
10667
- throw new ControlPlaneError(422, `release ${sha256} of '${registration.driver}' is not pinned in its registration; it runs once a reviewed change pins it and the control plane is deployed`);
10668
- }
10669
- if (pin.sourceCommit !== stored.sourceCommit) {
10670
- throw new ControlPlaneError(422, `release ${sha256} is pinned at ${pin.sourceCommit} and was stored as built from ${stored.sourceCommit}`);
10671
- }
10672
- return pin;
10995
+ const byAPerson = async (c, words = { write: 'a store driver release write', person: 'a store driver release is published by a person' }) => {
10996
+ if (!/^application\/json\s*(;|$)/i.test(c.req.header('content-type') ?? '')) {
10997
+ throw new ControlPlaneError(415, `${words.write} takes application/json`);
10998
+ }
10999
+ return staffPerson(c, words);
10673
11000
  };
11001
+ const approvalWords = { write: 'a store driver release approval', person: 'a store driver release is approved by a person' };
11002
+ const revokeWords = { write: 'a store driver release revoke', person: 'a store driver release is revoked by a person' };
11003
+ const statusWords = { write: 'store driver release status', person: 'store driver release status is read by a person' };
11004
+ const nonPrinting = /[\p{Cc}\p{Cf}\u2028\u2029]/u;
11005
+ const auditText = z.string().min(1).max(2048).refine((value) => value.trim().length > 0 && !nonPrinting.test(value), { message: 'must be printable text' });
11006
+ const reviewEvidence = z.object({ met: z.boolean(), evidence: auditText }).strict();
11007
+ const checkedEvidence = z.object({ checked: z.literal(true), evidence: auditText }).strict();
11008
+ // D-66 stores the reviewer's exact references: one HTTPS repository URL and one hex revision.
11009
+ const repositoryReference = z.string().min(1).max(1024).refine((value) => {
11010
+ if (!value.startsWith('https://') || /[\s\\?#]/u.test(value) || nonPrinting.test(value))
11011
+ return false;
11012
+ try {
11013
+ const url = new URL(value);
11014
+ return url.protocol === 'https:' && !!url.hostname && !url.username && !url.password &&
11015
+ !url.search && !url.hash && url.pathname.split('/').some(Boolean);
11016
+ }
11017
+ catch {
11018
+ return false;
11019
+ }
11020
+ }, { message: 'a source repository is an HTTPS URL with a host and path' });
11021
+ const sourceCommit = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, { message: 'a source commit is 40 or 64 lowercase hex characters' });
11022
+ // The release entry loads a Python class by identifier; catalog names keep their existing
11023
+ // fixed-label grammar and additionally exclude characters that cannot safely appear in audit.
11024
+ const approvalClassName = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
11025
+ const approvalOperation = storeCatalogOperation.refine((name) => !nonPrinting.test(name));
11026
+ const approvalBody = z.object({
11027
+ reason: auditText,
11028
+ sourceReferences: z.array(z.object({ repository: repositoryReference, commit: sourceCommit }).strict()).min(1).max(64),
11029
+ isolationReview: z.object({
11030
+ separateScriptPerTenant: reviewEvidence,
11031
+ noOutboundAccess: reviewEvidence,
11032
+ envelopeVerifiedOutsideDriver: reviewEvidence,
11033
+ envelopeChecked: checkedEvidence,
11034
+ reservedOperationsChecked: checkedEvidence,
11035
+ noModuleStateChecked: checkedEvidence,
11036
+ }).strict(),
11037
+ schema: z.number().int().min(0),
11038
+ className: approvalClassName,
11039
+ operations: z.array(approvalOperation).max(256),
11040
+ }).strict();
11041
+ const revokeBody = z.object({ reason: auditText, emergency: z.boolean().optional() }).strict();
11042
+ const approvalPathParts = new Set([
11043
+ 'reason', 'sourceReferences', 'repository', 'commit', 'isolationReview', 'separateScriptPerTenant',
11044
+ 'noOutboundAccess', 'envelopeVerifiedOutsideDriver', 'envelopeChecked', 'reservedOperationsChecked',
11045
+ 'noModuleStateChecked', 'met', 'checked', 'evidence', 'schema', 'className', 'operations', 'emergency',
11046
+ ]);
11047
+ const issueCodes = {
11048
+ unrecognized_keys: 'unrecognized_key', invalid_type: 'invalid_type', too_small: 'out_of_range',
11049
+ too_big: 'out_of_range', invalid_format: 'invalid_format', custom: 'invalid_value',
11050
+ };
11051
+ const safeIssue = (issue) => {
11052
+ const knownPath = issue.path.every((part) => (typeof part === 'number' && Number.isSafeInteger(part) && part >= 0) ||
11053
+ (typeof part === 'string' && approvalPathParts.has(part)));
11054
+ const path = knownPath && issue.path.length ? issue.path.join('.') : '(body)';
11055
+ return `${path}: ${issueCodes[issue.code] ?? 'invalid_value'}`;
11056
+ };
11057
+ const releaseSha = (value) => {
11058
+ if (!/^[0-9a-f]{64}$/.test(value))
11059
+ throw new ControlPlaneError(422, 'a release sha256 is 64 lowercase hex characters');
11060
+ return value;
11061
+ };
11062
+ const parseApprovalBody = (raw) => {
11063
+ const parsed = approvalBody.safeParse(raw);
11064
+ if (!parsed.success)
11065
+ throw new ControlPlaneError(422, `not a store driver approval: ${parsed.error.issues.map(safeIssue).join('; ')}`);
11066
+ return parsed.data;
11067
+ };
11068
+ const parseRevokeBody = (raw) => {
11069
+ const parsed = revokeBody.safeParse(raw);
11070
+ if (!parsed.success)
11071
+ throw new ControlPlaneError(422, `not a store driver revoke: ${parsed.error.issues.map(safeIssue).join('; ')}`);
11072
+ return parsed.data;
11073
+ };
11074
+ // Whether the release is stored, approved and keeps the schema is judged inside
11075
+ // `runStoreDriverRollout`, under its exclusive hold (D-65), never here before it.
10674
11076
  const rollOut = async (c, registration, sha256, action) => {
10675
11077
  const { flow, ensureScopeStores } = releaseFlow();
10676
11078
  const actor = c.get('actor');
@@ -10685,22 +11087,11 @@ export function createControlPlaneApi(options) {
10685
11087
  const result = await ensureScopeStores({ ...input, loadRelease });
10686
11088
  return result ? result.uploaded > 0 : true;
10687
11089
  },
10688
- ensureCanary: (to) => flow.ensureCanary({ driver: registration.driver, to, loadRelease }),
11090
+ ensureCanary: (to, approvals) => flow.ensureCanary({ driver: registration.driver, to, loadRelease, approvals }),
10689
11091
  callCanary: flow.callCanary,
10690
11092
  });
10691
11093
  return c.json(result, result.complete ? 200 : 202);
10692
11094
  };
10693
- /** Neither a rollout nor a rollback lowers the stored schema: a driver migrates forward only. */
10694
- const assertSchemaKept = (registration, from, to) => {
10695
- if (from === null || from === to.sha256)
10696
- return;
10697
- const leaving = registration.releases.find((p) => p.sha256 === from);
10698
- if (!leaving)
10699
- throw new ControlPlaneError(409, `the current release ${from} of '${registration.driver}' is not pinned, so its schema cannot be judged`);
10700
- if (to.schema < leaving.schema) {
10701
- throw new ControlPlaneError(409, `release ${to.sha256} has schema ${to.schema}, below the current release's ${leaving.schema}: objects may already have migrated`);
10702
- }
10703
- };
10704
11095
  /** Where the platform's rebuild of every script of the driver stands (#2309), if it rebuilds them. */
10705
11096
  const rebuildView = async (admin, actor, driver, known) => {
10706
11097
  const fingerprint = known ?? (await options.storeReleases?.fingerprint?.());
@@ -10711,29 +11102,87 @@ export function createControlPlaneApi(options) {
10711
11102
  return { fingerprint, ...storeRebuildStatus(walk, fingerprint, verticals, options.storeReleases?.settleMs), walk: walk ?? null };
10712
11103
  };
10713
11104
  app.get('/store-drivers/:driver', async (c) => {
11105
+ await staffPerson(c, statusWords);
10714
11106
  const registration = registeredDriver(c.req.param('driver'));
10715
- const state = await c.var.admin.storeDriverState(c.get('actor'), registration.driver);
11107
+ const [state, approvals] = await Promise.all([
11108
+ c.var.admin.storeDriverState(c.get('actor'), registration.driver),
11109
+ readStoreDriverApprovals(c.var.admin, c.get('actor'), registration),
11110
+ ]);
10716
11111
  const rebuild = await rebuildView(c.var.admin, c.get('actor'), registration.driver);
10717
- return c.json({ driver: registration.driver, pins: registration.releases, ...state, ...(rebuild ? { rebuild } : {}) });
11112
+ // `pins` stays for released CLIs (RFC §5 step 3): the releases a move may go to now, one per live approval.
11113
+ const pins = approvals.records
11114
+ .filter((a) => liveApprovalOf(approvals, a.sha256) === a)
11115
+ .map((a) => ({ sha256: a.sha256, sourceCommit: a.sourceCommit, schema: a.schema }));
11116
+ return c.json({ driver: registration.driver, pins, approvals: approvals.records, ...state, ...(rebuild ? { rebuild } : {}) });
10718
11117
  });
10719
- // Store a release's bytes, before or after its pin lands (D-63). Idempotent per bundle.
11118
+ // Store a release's bytes, before or after a person approves them (D-65). Idempotent per bundle.
10720
11119
  app.put('/store-drivers/:driver/releases', async (c) => {
10721
11120
  const registration = registeredDriver(c.req.param('driver'));
10722
11121
  const actor = await byAPerson(c);
10723
11122
  const { flow } = releaseFlow();
10724
11123
  const body = await readCappedJson(c.req.raw, STORE_RELEASE_REQUEST_MAX_BYTES);
10725
11124
  const release = await storeDriverRelease({ admin: c.var.admin, actor, bytes: flow.bytes, driver: registration.driver, body });
10726
- return c.json({ release, pinned: registration.releases.some((p) => p.sha256 === release.sha256 && p.sourceCommit === release.sourceCommit) }, 201);
11125
+ const live = liveApprovalOf(await readStoreDriverApprovals(c.var.admin, actor, registration), release.sha256);
11126
+ const approved = live !== undefined && live.sourceCommit === release.sourceCommit;
11127
+ // `pinned` stays beside `approved` for released CLIs, meaning the same (RFC §5 step 3).
11128
+ return c.json({ release, pinned: approved, approved }, 201);
11129
+ });
11130
+ // The approval is a second, person-only write over bytes the platform already named by digest.
11131
+ app.post('/store-drivers/:driver/releases/:sha256/approvals', async (c) => {
11132
+ const registration = registeredDriver(c.req.param('driver'));
11133
+ const actor = await byAPerson(c, approvalWords);
11134
+ const sha256 = releaseSha(c.req.param('sha256'));
11135
+ const body = parseApprovalBody(await readCappedJson(c.req.raw, 64 * 1024));
11136
+ // Refused here, not only by the readers (D-65 §3): a row its basis does not admit could never
11137
+ // run, yet as a live approval it would hold the one slot a corrected approval needs. Fixed text,
11138
+ // no input echoed.
11139
+ if (!storeDriverBasisAdmits(registration.isolationBasis, body.sourceReferences)) {
11140
+ throw new ControlPlaneError(422, 'not a store driver approval: sourceReferences: outside_isolation_basis');
11141
+ }
11142
+ const flow = options.storeReleases;
11143
+ if (!flow)
11144
+ throw new ControlPlaneError(501, 'this control plane cannot read stored release bytes for approval');
11145
+ const approval = await approveStoredDriverRelease({ admin: c.var.admin, actor, bytes: flow.bytes, driver: registration.driver, sha256, approval: body });
11146
+ const state = await c.var.admin.storeDriverState(actor, registration.driver);
11147
+ const approvals = state.current ? await c.var.admin.storeDriverApprovals(actor, registration.driver) : [];
11148
+ // The current release's approval facts, revoked or not: they are fixed at its first approval.
11149
+ const currentSchema = state.current ? approvals.find((a) => a.sha256 === state.current.sha256)?.schema ?? null : null;
11150
+ return c.json({ approval, currentSchema }, 201);
11151
+ });
11152
+ app.post('/store-drivers/:driver/releases/:sha256/revoke', async (c) => {
11153
+ const registration = registeredDriver(c.req.param('driver'));
11154
+ const actor = await byAPerson(c, revokeWords);
11155
+ const sha256 = releaseSha(c.req.param('sha256'));
11156
+ const body = parseRevokeBody(await readCappedJson(c.req.raw, 8 * 1024));
11157
+ const flow = options.storeReleases;
11158
+ if (!body.emergency && !flow?.readScriptRelease) {
11159
+ throw new ControlPlaneError(501, 'this control plane cannot read store script releases for a guarded revoke');
11160
+ }
11161
+ const result = await runStoreDriverRevoke({
11162
+ admin: c.var.admin,
11163
+ actor,
11164
+ driver: registration.driver,
11165
+ sha256,
11166
+ reason: body.reason,
11167
+ emergency: body.emergency === true,
11168
+ readScriptRelease: flow?.readScriptRelease ?? (async () => ({ exists: false })),
11169
+ ensureCanary: async (to) => {
11170
+ if (!flow)
11171
+ throw new ControlPlaneError(501, 'this control plane cannot move the store canary');
11172
+ const loadRelease = storedReleaseLoader({ admin: c.var.admin, actor, bytes: flow.bytes });
11173
+ // Called under the revoke's exclusive hold, so the approvals read here are the ones it judges.
11174
+ const approvals = await readStoreDriverApprovals(c.var.admin, actor, registration);
11175
+ await flow.ensureCanary({ driver: registration.driver, to, loadRelease, approvals });
11176
+ },
11177
+ });
11178
+ return c.json(result, result.complete ? 200 : 202);
10727
11179
  });
10728
- // Roll a stored, pinned release out: the canary, then every vertical that ever declared the
11180
+ // Roll a stored, approved release out: the canary, then every vertical that ever declared the
10729
11181
  // driver. Posting the same release again converges a rollout that stopped.
10730
11182
  app.post('/store-drivers/:driver/rollouts', async (c) => {
10731
11183
  const registration = registeredDriver(c.req.param('driver'));
10732
11184
  await byAPerson(c);
10733
11185
  const { sha256 } = z.object({ sha256: z.string().regex(/^[0-9a-f]{64}$/) }).strict().parse(await c.req.json());
10734
- const pin = await pinOf(c, registration, sha256);
10735
- const { current } = await c.var.admin.storeDriverState(c.get('actor'), registration.driver);
10736
- assertSchemaKept(registration, current?.sha256 ?? null, pin);
10737
11186
  return rollOut(c, registration, sha256, 'rollout');
10738
11187
  });
10739
11188
  // Return to the immediately previous good release (D-63), never further and never a guess. After
@@ -10746,8 +11195,6 @@ export function createControlPlaneApi(options) {
10746
11195
  const { current, rollbackTarget } = await c.var.admin.storeDriverState(c.get('actor'), registration.driver);
10747
11196
  if (!current || !rollbackTarget)
10748
11197
  throw new ControlPlaneError(409, `driver '${registration.driver}' has no previous good release to roll back to`);
10749
- const pin = await pinOf(c, registration, rollbackTarget);
10750
- assertSchemaKept(registration, current.sha256, pin);
10751
11198
  return rollOut(c, registration, rollbackTarget, 'rollback');
10752
11199
  });
10753
11200
  // Run a pass of the platform's rebuild of every script of the driver now (#2309): the scheduled
@@ -10937,7 +11384,7 @@ const WIRE_WALK_REQUEST_MAX_BYTES = 64 * 1024;
10937
11384
  * The most a release upload's body may be: the decoded cap, as base64 (4 bytes per 3), plus room
10938
11385
  * for the JSON around it. A bigger body is refused (413) before it is read whole.
10939
11386
  */
10940
- const STORE_RELEASE_REQUEST_MAX_BYTES = Math.ceil((STORE_RELEASE_MAX_BYTES * 4) / 3) + 1024 * 1024;
11387
+ const STORE_RELEASE_REQUEST_MAX_BYTES = STORE_RELEASE_STORED_MAX_BYTES;
10941
11388
  /** A JSON body read to at most `max` bytes: a declared length over it, or a stream past it, is 413. */
10942
11389
  async function readCappedJson(request, max) {
10943
11390
  const declared = Number(request.headers.get('content-length') ?? NaN);