@substrat-run/control-plane-api 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/api.js CHANGED
@@ -1,21 +1,22 @@
1
+ import { sourceBuildRequest } from '@substrat-run/contracts';
1
2
  import { PREVIEW_CONNECTIONS_REFUSAL } from './connection-relay.js';
2
3
  import { connectionBindingOptions } 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, 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, 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, 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';
17
18
  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';
19
+ import { PUSH_HOLD_TTL_MS, STORE_RELEASE_STORED_MAX_BYTES, approveStoredDriverRelease, runStoreDriverRevoke, runStoreDriverRollout, storeDriverRelease, storedReleaseLoader } from './store-releases.js';
19
20
  import { assertNoUnreachableScopeCopies, claimScopeScriptReap, drainScopeScriptCopies, listAllScopeScriptCopies, recordStrandedStorage } from './scope-copy-cleanup.js';
20
21
  import { BACKFILL_PAGE_MAX, backfillScopeScriptCopies } from './scope-copy-backfill.js';
21
22
  import { oidcCallbackUrl, retireClientsOfReapedScope, wirePreviewAuth } from './preview-auth.js';
@@ -715,6 +716,8 @@ const TENANT_ROUTES = [
715
716
  { method: 'POST', re: /\/tenants\/[^/]+\/orgs$/, pin: 'path' },
716
717
  { method: 'POST', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
717
718
  { method: 'DELETE', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
719
+ { method: 'GET', re: /\/tenants\/[^/]+\/entitlements$/, pin: 'path' },
720
+ { method: 'GET', re: /\/tenants\/[^/]+\/metered-usage$/, pin: 'path' },
718
721
  // An install's entitlement. The handler narrows WHICH key a tenant credential may
719
722
  // grant (`tenantGrantableEntitlement`): the pin alone would let it grant itself any.
720
723
  { method: 'PUT', re: /\/tenants\/[^/]+\/entitlements\/[^/]+$/, pin: 'path' },
@@ -820,6 +823,7 @@ const TENANT_ROUTES = [
820
823
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/registry$/, pin: 'owner' },
821
824
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/migrations$/, pin: 'owner' },
822
825
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/model$/, pin: 'owner' },
826
+ { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/concept$/, pin: 'owner' },
823
827
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/flow$/, pin: 'owner' },
824
828
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/assets$/, pin: 'owner' },
825
829
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/schedules$/, pin: 'owner' },
@@ -1146,7 +1150,7 @@ export function createControlPlaneApi(options) {
1146
1150
  const advisory = options.cliAdvisory;
1147
1151
  app.use('*', async (c, next) => {
1148
1152
  await next();
1149
- c.header(PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER);
1153
+ c.header(PLATFORM_FEATURES_HEADER, [PLATFORM_FEATURE_SCOPE_SWEEPER, ...(options.sourceBuilds ? [PLATFORM_FEATURE_SOURCE_BUILDS] : [])].join(','));
1150
1154
  if (advisory?.minVersion)
1151
1155
  c.header(CLI_MIN_VERSION_HEADER, advisory.minVersion);
1152
1156
  if (advisory?.latestVersion)
@@ -1232,10 +1236,13 @@ export function createControlPlaneApi(options) {
1232
1236
  // The emitted entity model of one version (#1214) — owner-narrowed the same way;
1233
1237
  // the dashboard's Model tab reads it.
1234
1238
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/model$/ },
1239
+ { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/concept$/ },
1235
1240
  { method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/schedules$/ },
1236
1241
  { method: 'GET', re: /\/verticals\/[^/]+\/channels$/ },
1237
1242
  { method: 'GET', re: /\/verticals\/[^/]+\/channels\/[^/]+\/history$/ },
1238
1243
  { method: 'POST', re: /\/verticals\/[^/]+\/channels\/[^/]+\/promote$/ },
1244
+ { method: 'GET', re: /\/projects$/ },
1245
+ { method: 'POST', re: /\/projects$/ },
1239
1246
  { method: 'POST', re: /\/verticals\/[^/]+\/deploy$/ },
1240
1247
  // A builder REQUESTS publication of a vertical it owns (marketplace-publish.md §5); the
1241
1248
  // `listing` flip stays staff-only. Ownership is checked in the handler.
@@ -1677,6 +1684,168 @@ export function createControlPlaneApi(options) {
1677
1684
  });
1678
1685
  // -- entitlements (§4.3) ---------------------------------------------------
1679
1686
  app.get('/tenants/:tenantId/entitlements', async (c) => c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantIdSchema.parse(c.req.param('tenantId')))));
1687
+ // One tenant's persisted usage, with no price-bearing fields (#2689). This read never
1688
+ // wakes a scope or writes a row. A gauge projection is capped at 501 narrow scope rows;
1689
+ // request rows at 10,000 plus 30 status rows; model pages at 25 x 501: at most 28
1690
+ // directory calls and 23,056 projected rows. Labels are capped at the read source,
1691
+ // and no staff ledger row crosses the RPC. The 5-second cooperative budget stops new
1692
+ // reads, though it cannot interrupt one in flight. Every cap marks a lower bound.
1693
+ // The dashboard credential reaches this path only when its signed tenant pin matches.
1694
+ app.get('/tenants/:tenantId/metered-usage', async (c) => {
1695
+ const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
1696
+ if (outsideTenant(c.get('principal'), tenantId))
1697
+ return refuse(c, 'forbidden', 'forbidden');
1698
+ const admin = c.var.admin;
1699
+ const actor = c.get('actor');
1700
+ const usageNow = options.meteredUsageNow ?? (() => performance.now());
1701
+ const deadline = usageNow() + 5_000;
1702
+ const readAt = new Date().toISOString();
1703
+ const until = readAt.slice(0, 10);
1704
+ const since = new Date(Date.parse(`${until}T00:00:00Z`) - 30 * 86_400_000).toISOString().slice(0, 10);
1705
+ const through = new Date(Date.parse(`${until}T00:00:00Z`) - 86_400_000).toISOString().slice(0, 10);
1706
+ const gaugeRead = await admin.readTenantStorageGauge(actor, tenantId, 500);
1707
+ const gauge = gaugeRead.gauge;
1708
+ const stale = gauge?.oldestReadAt ? Date.parse(readAt) - Date.parse(gauge.oldestReadAt) > 2 * 86_400_000 : false;
1709
+ const storageState = gaugeRead.truncated ? 'truncated' : !gauge ? 'unavailable' : gauge.sampled === 0 ? (gauge.total === 0 ? 'empty' : 'not-sampled')
1710
+ : gauge.failing > 0 ? 'failing' : stale ? 'stale' : gauge.sampled < gauge.total ? 'partial' : 'complete';
1711
+ // Literal output allowlist: never spread a meter row (which also carries plan data).
1712
+ const storage = {
1713
+ state: storageState,
1714
+ bytes: gauge && gauge.sampled > 0 ? gauge.bytes : null,
1715
+ estimated: false,
1716
+ lowerBound: gaugeRead.truncated,
1717
+ truncatedReason: gaugeRead.truncated ? 'scope-cap' : null,
1718
+ basis: 'scope-databases',
1719
+ sampled: gauge?.sampled ?? 0,
1720
+ total: gauge?.total ?? 0,
1721
+ failing: gauge?.failing ?? 0,
1722
+ oldestReadAt: gauge?.oldestReadAt ?? null,
1723
+ excluded: gauge?.excluded ?? [],
1724
+ };
1725
+ let requests = {
1726
+ state: 'unavailable', requests: null, estimated: true, lowerBound: false,
1727
+ coveredDays: 0, totalDays: 30, sinceDay: since, untilDay: through,
1728
+ lastReadAt: null, byVertical: [], breakdownTruncated: false,
1729
+ };
1730
+ if (usageNow() < deadline && admin.listTenantRequestMeter && admin.listRequestMeterDays) {
1731
+ const [rows, days] = await Promise.all([
1732
+ admin.listTenantRequestMeter(actor, tenantId, since, through, REQUEST_ROLLUP_LIMIT_MAX),
1733
+ admin.listRequestMeterDays(actor, since, through),
1734
+ ]);
1735
+ const covered = days.filter((d) => d.readAt !== null && !d.saturated);
1736
+ const coveredDaySet = new Set(covered.map((day) => day.day));
1737
+ const clipped = rows.length === REQUEST_ROLLUP_LIMIT_MAX;
1738
+ const groups = new Map();
1739
+ let count = 0;
1740
+ let countedRows = 0;
1741
+ let requestLabelsTruncated = false;
1742
+ for (const row of rows.slice(0, REQUEST_ROLLUP_LIMIT_MAX - 1)) {
1743
+ if (!coveredDaySet.has(row.day))
1744
+ continue;
1745
+ countedRows++;
1746
+ count += row.requests;
1747
+ if (row.labelTruncated)
1748
+ requestLabelsTruncated = true;
1749
+ else
1750
+ groups.set(row.vertical, (groups.get(row.vertical) ?? 0) + row.requests);
1751
+ }
1752
+ const byVertical = [...groups].sort((a, b) => (a[0] ?? '').localeCompare(b[0] ?? ''));
1753
+ const incomplete = covered.length < 30 || clipped;
1754
+ requests = {
1755
+ state: covered.length === 0 ? 'unavailable' : clipped ? 'truncated' : countedRows === 0 ? 'empty' : incomplete ? 'partial' : 'complete',
1756
+ requests: countedRows > 0 ? count : null,
1757
+ estimated: true,
1758
+ lowerBound: countedRows > 0 && incomplete,
1759
+ coveredDays: covered.length,
1760
+ totalDays: 30,
1761
+ sinceDay: since,
1762
+ untilDay: through,
1763
+ lastReadAt: covered.reduce((latest, day) => !latest || day.readAt > latest ? day.readAt : latest, null),
1764
+ byVertical: covered.length > 0 ? byVertical.slice(0, 50).map(([vertical, n]) => ({ vertical, requests: n })) : [],
1765
+ breakdownTruncated: covered.length > 0 && (byVertical.length > 50 || requestLabelsTruncated),
1766
+ };
1767
+ }
1768
+ const groups = new Map();
1769
+ let calls = 0;
1770
+ let reportedCalls = 0;
1771
+ let inputTokens = 0;
1772
+ let outputTokens = 0;
1773
+ let oldestRecordedAt = null;
1774
+ let newestRecordedAt = null;
1775
+ let truncated = false;
1776
+ let truncatedReason = null;
1777
+ let modelSkipped = false;
1778
+ let labelsTruncated = false;
1779
+ const earlier = (a, b) => a === null || b < a ? b : a;
1780
+ const later = (a, b) => a === null || b > a ? b : a;
1781
+ if (typeof admin.listTenantModelUsageMeter === 'function') {
1782
+ let cursor;
1783
+ for (let page = 0; page < 25; page++) {
1784
+ if (usageNow() >= deadline) {
1785
+ if (calls > 0) {
1786
+ truncated = true;
1787
+ truncatedReason = 'time-budget';
1788
+ }
1789
+ else
1790
+ modelSkipped = true;
1791
+ break;
1792
+ }
1793
+ const entries = await admin.listTenantModelUsageMeter(actor, { tenantId, since: `${since}T00:00:00Z`, until: `${until}T00:00:00Z`, limit: 501, ...(cursor ? { cursor } : {}) });
1794
+ for (const entry of entries.slice(0, 500)) {
1795
+ calls++;
1796
+ if (entry.reported) {
1797
+ reportedCalls++;
1798
+ inputTokens += entry.inputTokens;
1799
+ outputTokens += entry.outputTokens;
1800
+ }
1801
+ oldestRecordedAt = earlier(oldestRecordedAt, entry.at);
1802
+ newestRecordedAt = later(newestRecordedAt, entry.at);
1803
+ if (entry.labelTruncated)
1804
+ labelsTruncated = true;
1805
+ else {
1806
+ const key = `${entry.vertical}\u0000${entry.model}`;
1807
+ const row = groups.get(key) ?? { vertical: entry.vertical, model: entry.model, calls: 0, reportedCalls: 0, inputTokens: 0, outputTokens: 0 };
1808
+ row.calls++;
1809
+ if (entry.reported) {
1810
+ row.reportedCalls++;
1811
+ row.inputTokens += entry.inputTokens;
1812
+ row.outputTokens += entry.outputTokens;
1813
+ }
1814
+ groups.set(key, row);
1815
+ }
1816
+ }
1817
+ truncated = entries.length > 500;
1818
+ truncatedReason = truncated ? 'page-cap' : null;
1819
+ if (!truncated)
1820
+ break;
1821
+ cursor = { at: entries[499].at, id: entries[499].id };
1822
+ }
1823
+ }
1824
+ const modelGroups = [...groups.values()].sort((a, b) => a.vertical.localeCompare(b.vertical) || a.model.localeCompare(b.model));
1825
+ const models = {
1826
+ state: typeof admin.listTenantModelUsageMeter !== 'function' || modelSkipped ? 'unavailable' : calls === 0 ? 'empty' : truncated ? 'truncated' : 'complete',
1827
+ calls: calls > 0 ? calls : null,
1828
+ reportedCalls,
1829
+ inputTokens: reportedCalls > 0 ? inputTokens : null,
1830
+ outputTokens: reportedCalls > 0 ? outputTokens : null,
1831
+ estimated: false,
1832
+ lowerBound: truncated,
1833
+ truncatedReason,
1834
+ since: `${since}T00:00:00Z`,
1835
+ until: `${until}T00:00:00Z`,
1836
+ oldestRecordedAt,
1837
+ newestRecordedAt,
1838
+ 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 })),
1839
+ breakdownTruncated: modelGroups.length > 50 || labelsTruncated,
1840
+ };
1841
+ return c.json({
1842
+ readAt,
1843
+ retention: { storageMonths: STORAGE_GAUGE_RETENTION_MONTHS, modelDays: MODEL_USAGE_RETENTION_DAYS, requestMonths: BILLING_ROLLUP_RETENTION_MONTHS },
1844
+ storage,
1845
+ requests,
1846
+ models,
1847
+ });
1848
+ });
1680
1849
  app.put('/tenants/:tenantId/entitlements/:key', async (c) => {
1681
1850
  const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
1682
1851
  // The body is the plan half (#33) and optional — a bodyless PUT is the
@@ -7201,6 +7370,16 @@ export function createControlPlaneApi(options) {
7201
7370
  calls: parsed?.calls ?? null,
7202
7371
  });
7203
7372
  });
7373
+ // Published documentation only, never source files. Same owner/listed visibility as model.
7374
+ app.get('/verticals/:slug/versions/:id/concept', async (c) => {
7375
+ const p = c.get('principal');
7376
+ const slug = await resolveVerticalId(c, c.req.param('slug'));
7377
+ if (await notReadable(p, slug))
7378
+ return refuse(c, 'not_found', 'not found');
7379
+ const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
7380
+ const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
7381
+ return c.json({ concept: parsed?.concept ?? null });
7382
+ });
7204
7383
  app.get('/verticals/:slug/versions/:id/model', async (c) => {
7205
7384
  const p = c.get('principal');
7206
7385
  const slug = await resolveVerticalId(c, c.req.param('slug'));
@@ -7970,12 +8149,98 @@ export function createControlPlaneApi(options) {
7970
8149
  // pointing at a deployment that is not there. The version lands PENDING — except a
7971
8150
  // PRIVATE vertical's, which self-admits below the seam (its blast radius is its own
7972
8151
  // tenant); for everything else admission still gates serving.
8152
+ app.all('/projects', async (c) => {
8153
+ if (!options.sourceBuilds)
8154
+ return refuseStatus(c, 501, 'source builds are not configured');
8155
+ const p = c.get('principal');
8156
+ if (p.kind !== 'builder' && p.kind !== 'staff')
8157
+ return refuse(c, 'forbidden', 'forbidden');
8158
+ const tenant = p.kind === 'builder' ? p.tenantId : c.req.query('tenant');
8159
+ if (!tenant)
8160
+ return refuse(c, 'validation_failed', 'tenant id is required');
8161
+ const tenants = await c.var.admin.listTenants(c.get('actor'));
8162
+ if (!tenants.some((t) => t.id === tenant))
8163
+ return refuse(c, 'not_found', 'unknown workspace');
8164
+ let body;
8165
+ if (c.req.method === 'POST') {
8166
+ const input = z.object({ slug: z.string().min(1), name: z.string().min(1), repository: z.unknown().optional() }).parse(await c.req.json());
8167
+ const team = tenants.find((t) => t.id === tenant);
8168
+ const slug = await ownerOf(c.get('actor'), input.slug) === tenant || input.slug.startsWith(`${team.slug}/`) ? input.slug : `${team.slug}/${input.slug}`;
8169
+ verticalSlug.parse(slug);
8170
+ const owner = await ownerOf(c.get('actor'), slug);
8171
+ if (owner !== undefined && owner !== tenant)
8172
+ return refuse(c, 'forbidden', 'forbidden');
8173
+ body = JSON.stringify({ ...input, slug });
8174
+ }
8175
+ else if (c.req.method !== 'GET')
8176
+ return refuseStatus(c, 405, 'method not allowed');
8177
+ return options.sourceBuilds.projects(tenant, new Request('https://source/projects', {
8178
+ method: c.req.method, ...(body ? { headers: { 'content-type': 'application/json' }, body } : {}),
8179
+ }));
8180
+ });
8181
+ app.post('/source/github-access', async (c) => {
8182
+ if (c.get('principal').kind !== 'staff')
8183
+ return refuse(c, 'forbidden', 'forbidden');
8184
+ if (!options.githubRepositoryAccess)
8185
+ return refuseStatus(c, 501, 'GitHub source access is not configured');
8186
+ 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());
8187
+ const connection = await c.var.admin.openConnection(input.tenantId, 'dashboard', 'github', input.owner);
8188
+ const installationId = connection?.secret.installationId;
8189
+ if (typeof installationId !== 'string')
8190
+ return refuse(c, 'not_found', 'no connected GitHub installation for this tenant and owner');
8191
+ return c.json(await options.githubRepositoryAccess(input.tenantId, input.owner, input.repo, installationId, input.scope));
8192
+ });
7973
8193
  app.post('/verticals/:slug/deploy', async (c) => {
7974
8194
  if (!options.deployVertical) {
7975
8195
  return refuseStatus(c, 501, 'deploy is not configured on this control plane');
7976
8196
  }
7977
8197
  const p = c.get('principal');
7978
- const form = await c.req.formData();
8198
+ let built;
8199
+ let requestedVersion;
8200
+ let form;
8201
+ if (options.sourceBuilds) {
8202
+ if (!c.req.header('content-type')?.startsWith('application/json')) {
8203
+ return refuseStatus(c, 415, 'submit source JSON; client-built module uploads are no longer accepted');
8204
+ }
8205
+ const input = sourceBuildRequest.parse(await c.req.json());
8206
+ const bare = c.req.param('slug');
8207
+ let sourceSlug = bare;
8208
+ let sourceTenant = null;
8209
+ if (p.kind === 'builder') {
8210
+ if (input.tenant && input.tenant !== p.tenantId && input.tenant !== p.tenantSlug) {
8211
+ return refuse(c, 'forbidden', `push is pinned to workspace '${input.tenant}' but this session acts for '${p.tenantSlug}'`);
8212
+ }
8213
+ sourceTenant = p.tenantId;
8214
+ sourceSlug = effectiveSlug(p, bare);
8215
+ }
8216
+ else if (input.tenant) {
8217
+ const tenant = (await c.var.admin.listTenants(c.get('actor'))).find((t) => t.id === input.tenant || t.slug === input.tenant);
8218
+ if (!tenant)
8219
+ return refuse(c, 'not_found', 'unknown workspace');
8220
+ sourceTenant = tenant.id;
8221
+ sourceSlug = bare.startsWith(`${tenant.slug}/`) || await ownerOf(c.get('actor'), bare) === tenant.id ? bare : `${tenant.slug}/${bare}`;
8222
+ }
8223
+ const owner = await ownerOf(c.get('actor'), sourceSlug);
8224
+ if (sourceTenant && owner !== undefined && owner !== sourceTenant)
8225
+ return refuse(c, 'forbidden', 'forbidden');
8226
+ sourceTenant ??= owner ?? null;
8227
+ verticalSlug.parse(sourceSlug);
8228
+ if (platformScriptCollision(sourceSlug))
8229
+ return refuseStatus(c, 422, 'reserved platform script name');
8230
+ requestedVersion = input.version;
8231
+ built = await options.sourceBuilds.build(sourceTenant ?? 'platform', sourceSlug, input);
8232
+ form = built.form;
8233
+ // Addressing is the authenticated caller's, never a field emitted by project code.
8234
+ form.delete('tenant');
8235
+ if (input.tenant)
8236
+ form.set('tenant', input.tenant);
8237
+ form.delete('allowFork');
8238
+ if (input.allowFork)
8239
+ form.set('allowFork', '1');
8240
+ }
8241
+ else {
8242
+ return refuseStatus(c, 501, 'platform source builds are not configured');
8243
+ }
7979
8244
  const raw = form.get('manifest');
7980
8245
  if (typeof raw !== 'string')
7981
8246
  return refuse(c, 'validation_failed', 'missing manifest part');
@@ -7984,6 +8249,8 @@ export function createControlPlaneApi(options) {
7984
8249
  return refuse(c, 'validation_failed', 'manifest registry: required when modules are present');
7985
8250
  }
7986
8251
  const manifest = deployManifest.parse(parsed);
8252
+ if (requestedVersion && manifest.version !== requestedVersion)
8253
+ return refuseStatus(c, 502, 'build service produced a different release version');
7987
8254
  const moduleProblems = pushedModuleRegistryProblems(manifest.modules, manifest.registry);
7988
8255
  if (moduleProblems.length > 0)
7989
8256
  return refuse(c, 'validation_failed', `manifest registry.permissions: ${moduleProblems.join('; ')}`);
@@ -8055,7 +8322,7 @@ export function createControlPlaneApi(options) {
8055
8322
  // Otherwise the claim lands under the tenant prefix, exactly like a builder push —
8056
8323
  // same namespace, no claim race with other workspaces' bare names.
8057
8324
  const bareOwner = await ownerOf(c.get('actor'), bare);
8058
- slug = bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
8325
+ slug = bare.startsWith(`${workspace.slug}/`) || bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
8059
8326
  const existingOwner = slug === bare ? bareOwner : await ownerOf(c.get('actor'), slug);
8060
8327
  if (existingOwner !== undefined && existingOwner !== workspace.id) {
8061
8328
  return refuse(c, 'forbidden', `vertical '${slug}' is not owned by workspace '${pin}'`);
@@ -8333,7 +8600,7 @@ export function createControlPlaneApi(options) {
8333
8600
  }
8334
8601
  }
8335
8602
  const version = await c.var.admin.getVersion(c.get('actor'), id, slug);
8336
- return c.json({ ...version, ...(warnings.length ? { warnings } : {}) }, 201);
8603
+ return c.json({ ...version, ...(built ? { projectId: built.projectId, revision: built.revision, dataLoss: built.dataLoss } : {}), ...(warnings.length ? { warnings } : {}) }, 201);
8337
8604
  });
8338
8605
  // -- where this platform runs (ops ergonomics) -----------------------------
8339
8606
  // The console renders refs it already has — `servingRef`, a store's `ref`, a version's
@@ -9374,6 +9641,47 @@ export function createControlPlaneApi(options) {
9374
9641
  const target = (await options.resolveVerticalVersion?.(slug, opts.versionId, actor)) ??
9375
9642
  options.verticals?.[slug] ??
9376
9643
  (await options.resolveVertical?.(slug, actor));
9644
+ // Both the source and pushed versions must opt in to carrying an ordinary setting. A
9645
+ // missing or invalid source declaration allows none. The parent deployment reads only
9646
+ // these keys; the control plane never copies its whole config store.
9647
+ const carrySettings = async (previewId, parent) => {
9648
+ if (!parent)
9649
+ return;
9650
+ const eligibleKeys = (json) => {
9651
+ if (!json)
9652
+ return new Set();
9653
+ try {
9654
+ const declared = JSON.parse(json).envSpec;
9655
+ const specs = z.array(envVarSpec).safeParse(declared);
9656
+ if (!specs.success)
9657
+ return new Set();
9658
+ return new Set(specs.data.filter((spec) => spec.previewCarry && !spec.secret && spec.kind !== 'model').map((spec) => spec.key));
9659
+ }
9660
+ catch {
9661
+ return new Set();
9662
+ }
9663
+ };
9664
+ const sourceJson = parent.verticalVersionId
9665
+ ? await c.var.admin.versionManifest(actor, slug, parent.verticalVersionId)
9666
+ : null;
9667
+ const sourceKeys = eligibleKeys(sourceJson);
9668
+ if (sourceKeys.size === 0)
9669
+ return;
9670
+ const incomingJson = await c.var.admin.versionManifest(actor, slug, opts.versionId);
9671
+ const keys = [...eligibleKeys(incomingJson)].filter((key) => sourceKeys.has(key));
9672
+ if (keys.length === 0)
9673
+ return;
9674
+ const sourceClient = await verticalForScope(c, parent);
9675
+ if (!sourceClient || !target) {
9676
+ throw new ControlPlaneError(501, 'the parent or preview deployment cannot deliver declared preview settings');
9677
+ }
9678
+ const requested = new Set(keys);
9679
+ const values = await retryTransient(() => sourceClient.readPreviewConfig({ tenantId, scopeId: parent.id, keys }));
9680
+ const entries = values.filter((entry) => requested.has(entry.key));
9681
+ if (entries.length > 0) {
9682
+ await retryTransient(() => target.configureInstance({ tenantId, scopeId: previewId, entries }));
9683
+ }
9684
+ };
9377
9685
  // The preview's own login (#1704), once it serves this version. Delivered into `target` —
9378
9686
  // THIS push's version deployment — and never "whatever the scope is bound to now", so an
9379
9687
  // older push can never overwrite a newer push's config store. A failure lands an
@@ -9415,8 +9723,8 @@ export function createControlPlaneApi(options) {
9415
9723
  };
9416
9724
  const notesFor = (parent) => parent
9417
9725
  ? [
9418
- "Settings: a preview does not carry over the app's per-install settings (the dashboard's Env tab); " +
9419
- "it runs on the deployment's defaults.",
9726
+ "Settings: only non-secret Env tab settings explicitly declared for preview carryover are delivered; " +
9727
+ "other settings use the deployment's defaults.",
9420
9728
  ]
9421
9729
  : [];
9422
9730
  // The `--<tag>` URL's base — the source's canonical hostname (fork) or the tenant-app
@@ -9503,6 +9811,7 @@ export function createControlPlaneApi(options) {
9503
9811
  const servedFork = async (p) => {
9504
9812
  const hostname = await bindPreviewHostname(c.var.admin, actor, baseHostname, tenantId, p.previewId, opts.tag, surface);
9505
9813
  await assertServesBoundVersion(p.previewId);
9814
+ await carrySettings(p.previewId, p.parent);
9506
9815
  const auth = await wireLogin(p.previewId, hostname, p.parent);
9507
9816
  return {
9508
9817
  scopeId: p.previewId,
@@ -9782,6 +10091,7 @@ export function createControlPlaneApi(options) {
9782
10091
  const parent = existing.forkedFrom
9783
10092
  ? ((await c.var.admin.getScopeRecord(actor, tenantId, existing.forkedFrom)) ?? null)
9784
10093
  : null;
10094
+ await carrySettings(existing.id, parent);
9785
10095
  const auth = await wireLogin(existing.id, hostname, parent);
9786
10096
  return {
9787
10097
  scopeId: existing.id,
@@ -10586,10 +10896,7 @@ export function createControlPlaneApi(options) {
10586
10896
  * `text/plain` beacon cannot set it without a CORS preflight, 415) and refuse a request a
10587
10897
  * browser marks as from another origin (403). A request with neither header is the CLI's.
10588
10898
  */
10589
- const byAPerson = async (c, words = { write: 'a store driver release write', person: 'a store driver release is published by a person' }) => {
10590
- if (!/^application\/json\s*(;|$)/i.test(c.req.header('content-type') ?? '')) {
10591
- throw new ControlPlaneError(415, `${words.write} takes application/json`);
10592
- }
10899
+ const staffPerson = async (c, words) => {
10593
10900
  const origin = c.req.header('origin');
10594
10901
  const site = c.req.header('sec-fetch-site');
10595
10902
  if ((origin && origin !== new URL(c.req.url).origin) || (site && site !== 'same-origin' && site !== 'none')) {
@@ -10602,6 +10909,85 @@ export function createControlPlaneApi(options) {
10602
10909
  }
10603
10910
  return actor;
10604
10911
  };
10912
+ const byAPerson = async (c, words = { write: 'a store driver release write', person: 'a store driver release is published by a person' }) => {
10913
+ if (!/^application\/json\s*(;|$)/i.test(c.req.header('content-type') ?? '')) {
10914
+ throw new ControlPlaneError(415, `${words.write} takes application/json`);
10915
+ }
10916
+ return staffPerson(c, words);
10917
+ };
10918
+ const approvalWords = { write: 'a store driver release approval', person: 'a store driver release is approved by a person' };
10919
+ const revokeWords = { write: 'a store driver release revoke', person: 'a store driver release is revoked by a person' };
10920
+ const statusWords = { write: 'store driver release status', person: 'store driver release status is read by a person' };
10921
+ const nonPrinting = /[\p{Cc}\p{Cf}\u2028\u2029]/u;
10922
+ const auditText = z.string().min(1).max(2048).refine((value) => value.trim().length > 0 && !nonPrinting.test(value), { message: 'must be printable text' });
10923
+ const reviewEvidence = z.object({ met: z.boolean(), evidence: auditText }).strict();
10924
+ const checkedEvidence = z.object({ checked: z.literal(true), evidence: auditText }).strict();
10925
+ // D-66 stores the reviewer's exact references: one HTTPS repository URL and one hex revision.
10926
+ const repositoryReference = z.string().min(1).max(1024).refine((value) => {
10927
+ if (!value.startsWith('https://') || /[\s\\?#]/u.test(value) || nonPrinting.test(value))
10928
+ return false;
10929
+ try {
10930
+ const url = new URL(value);
10931
+ return url.protocol === 'https:' && !!url.hostname && !url.username && !url.password &&
10932
+ !url.search && !url.hash && url.pathname.split('/').some(Boolean);
10933
+ }
10934
+ catch {
10935
+ return false;
10936
+ }
10937
+ }, { message: 'a source repository is an HTTPS URL with a host and path' });
10938
+ const sourceCommit = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, { message: 'a source commit is 40 or 64 lowercase hex characters' });
10939
+ // The release entry loads a Python class by identifier; catalog names keep their existing
10940
+ // fixed-label grammar and additionally exclude characters that cannot safely appear in audit.
10941
+ const approvalClassName = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
10942
+ const approvalOperation = storeCatalogOperation.refine((name) => !nonPrinting.test(name));
10943
+ const approvalBody = z.object({
10944
+ reason: auditText,
10945
+ sourceReferences: z.array(z.object({ repository: repositoryReference, commit: sourceCommit }).strict()).min(1).max(64),
10946
+ isolationReview: z.object({
10947
+ separateScriptPerTenant: reviewEvidence,
10948
+ noOutboundAccess: reviewEvidence,
10949
+ envelopeVerifiedOutsideDriver: reviewEvidence,
10950
+ envelopeChecked: checkedEvidence,
10951
+ reservedOperationsChecked: checkedEvidence,
10952
+ noModuleStateChecked: checkedEvidence,
10953
+ }).strict(),
10954
+ schema: z.number().int().min(0),
10955
+ className: approvalClassName,
10956
+ operations: z.array(approvalOperation).max(256),
10957
+ }).strict();
10958
+ const revokeBody = z.object({ reason: auditText, emergency: z.boolean().optional() }).strict();
10959
+ const approvalPathParts = new Set([
10960
+ 'reason', 'sourceReferences', 'repository', 'commit', 'isolationReview', 'separateScriptPerTenant',
10961
+ 'noOutboundAccess', 'envelopeVerifiedOutsideDriver', 'envelopeChecked', 'reservedOperationsChecked',
10962
+ 'noModuleStateChecked', 'met', 'checked', 'evidence', 'schema', 'className', 'operations', 'emergency',
10963
+ ]);
10964
+ const issueCodes = {
10965
+ unrecognized_keys: 'unrecognized_key', invalid_type: 'invalid_type', too_small: 'out_of_range',
10966
+ too_big: 'out_of_range', invalid_format: 'invalid_format', custom: 'invalid_value',
10967
+ };
10968
+ const safeIssue = (issue) => {
10969
+ const knownPath = issue.path.every((part) => (typeof part === 'number' && Number.isSafeInteger(part) && part >= 0) ||
10970
+ (typeof part === 'string' && approvalPathParts.has(part)));
10971
+ const path = knownPath && issue.path.length ? issue.path.join('.') : '(body)';
10972
+ return `${path}: ${issueCodes[issue.code] ?? 'invalid_value'}`;
10973
+ };
10974
+ const releaseSha = (value) => {
10975
+ if (!/^[0-9a-f]{64}$/.test(value))
10976
+ throw new ControlPlaneError(422, 'a release sha256 is 64 lowercase hex characters');
10977
+ return value;
10978
+ };
10979
+ const parseApprovalBody = (raw) => {
10980
+ const parsed = approvalBody.safeParse(raw);
10981
+ if (!parsed.success)
10982
+ throw new ControlPlaneError(422, `not a store driver approval: ${parsed.error.issues.map(safeIssue).join('; ')}`);
10983
+ return parsed.data;
10984
+ };
10985
+ const parseRevokeBody = (raw) => {
10986
+ const parsed = revokeBody.safeParse(raw);
10987
+ if (!parsed.success)
10988
+ throw new ControlPlaneError(422, `not a store driver revoke: ${parsed.error.issues.map(safeIssue).join('; ')}`);
10989
+ return parsed.data;
10990
+ };
10605
10991
  /** The pin a release is run under, or a refusal: the platform runs only what this repo pins. */
10606
10992
  const pinOf = async (c, registration, sha256) => {
10607
10993
  const stored = await c.var.admin.getStoreDriverRelease(c.get('actor'), registration.driver, sha256);
@@ -10656,10 +11042,14 @@ export function createControlPlaneApi(options) {
10656
11042
  return { fingerprint, ...storeRebuildStatus(walk, fingerprint, verticals, options.storeReleases?.settleMs), walk: walk ?? null };
10657
11043
  };
10658
11044
  app.get('/store-drivers/:driver', async (c) => {
11045
+ await staffPerson(c, statusWords);
10659
11046
  const registration = registeredDriver(c.req.param('driver'));
10660
- const state = await c.var.admin.storeDriverState(c.get('actor'), registration.driver);
11047
+ const [state, approvals] = await Promise.all([
11048
+ c.var.admin.storeDriverState(c.get('actor'), registration.driver),
11049
+ c.var.admin.storeDriverApprovals(c.get('actor'), registration.driver),
11050
+ ]);
10661
11051
  const rebuild = await rebuildView(c.var.admin, c.get('actor'), registration.driver);
10662
- return c.json({ driver: registration.driver, pins: registration.releases, ...state, ...(rebuild ? { rebuild } : {}) });
11052
+ return c.json({ driver: registration.driver, pins: registration.releases, approvals, ...state, ...(rebuild ? { rebuild } : {}) });
10663
11053
  });
10664
11054
  // Store a release's bytes, before or after its pin lands (D-63). Idempotent per bundle.
10665
11055
  app.put('/store-drivers/:driver/releases', async (c) => {
@@ -10670,6 +11060,51 @@ export function createControlPlaneApi(options) {
10670
11060
  const release = await storeDriverRelease({ admin: c.var.admin, actor, bytes: flow.bytes, driver: registration.driver, body });
10671
11061
  return c.json({ release, pinned: registration.releases.some((p) => p.sha256 === release.sha256 && p.sourceCommit === release.sourceCommit) }, 201);
10672
11062
  });
11063
+ // The approval is a second, person-only write over bytes the platform already named by digest.
11064
+ app.post('/store-drivers/:driver/releases/:sha256/approvals', async (c) => {
11065
+ const registration = registeredDriver(c.req.param('driver'));
11066
+ const actor = await byAPerson(c, approvalWords);
11067
+ const sha256 = releaseSha(c.req.param('sha256'));
11068
+ const body = parseApprovalBody(await readCappedJson(c.req.raw, 64 * 1024));
11069
+ const flow = options.storeReleases;
11070
+ if (!flow)
11071
+ throw new ControlPlaneError(501, 'this control plane cannot read stored release bytes for approval');
11072
+ const approval = await approveStoredDriverRelease({ admin: c.var.admin, actor, bytes: flow.bytes, driver: registration.driver, sha256, approval: body });
11073
+ const state = await c.var.admin.storeDriverState(actor, registration.driver);
11074
+ const approvals = state.current ? await c.var.admin.storeDriverApprovals(actor, registration.driver) : [];
11075
+ const currentSchema = state.current
11076
+ ? approvals.find((a) => a.sha256 === state.current.sha256)?.schema
11077
+ ?? registration.releases.find((p) => p.sha256 === state.current.sha256)?.schema
11078
+ ?? null
11079
+ : null;
11080
+ return c.json({ approval, currentSchema }, 201);
11081
+ });
11082
+ app.post('/store-drivers/:driver/releases/:sha256/revoke', async (c) => {
11083
+ const registration = registeredDriver(c.req.param('driver'));
11084
+ const actor = await byAPerson(c, revokeWords);
11085
+ const sha256 = releaseSha(c.req.param('sha256'));
11086
+ const body = parseRevokeBody(await readCappedJson(c.req.raw, 8 * 1024));
11087
+ const flow = options.storeReleases;
11088
+ if (!body.emergency && !flow?.readScriptRelease) {
11089
+ throw new ControlPlaneError(501, 'this control plane cannot read store script releases for a guarded revoke');
11090
+ }
11091
+ const result = await runStoreDriverRevoke({
11092
+ admin: c.var.admin,
11093
+ actor,
11094
+ driver: registration.driver,
11095
+ sha256,
11096
+ reason: body.reason,
11097
+ emergency: body.emergency === true,
11098
+ readScriptRelease: flow?.readScriptRelease ?? (async () => ({ exists: false })),
11099
+ ensureCanary: async (to) => {
11100
+ if (!flow)
11101
+ throw new ControlPlaneError(501, 'this control plane cannot move the store canary');
11102
+ const loadRelease = storedReleaseLoader({ admin: c.var.admin, actor, bytes: flow.bytes });
11103
+ await flow.ensureCanary({ driver: registration.driver, to, loadRelease });
11104
+ },
11105
+ });
11106
+ return c.json(result, result.complete ? 200 : 202);
11107
+ });
10673
11108
  // Roll a stored, pinned release out: the canary, then every vertical that ever declared the
10674
11109
  // driver. Posting the same release again converges a rollout that stopped.
10675
11110
  app.post('/store-drivers/:driver/rollouts', async (c) => {
@@ -10882,7 +11317,7 @@ const WIRE_WALK_REQUEST_MAX_BYTES = 64 * 1024;
10882
11317
  * The most a release upload's body may be: the decoded cap, as base64 (4 bytes per 3), plus room
10883
11318
  * for the JSON around it. A bigger body is refused (413) before it is read whole.
10884
11319
  */
10885
- const STORE_RELEASE_REQUEST_MAX_BYTES = Math.ceil((STORE_RELEASE_MAX_BYTES * 4) / 3) + 1024 * 1024;
11320
+ const STORE_RELEASE_REQUEST_MAX_BYTES = STORE_RELEASE_STORED_MAX_BYTES;
10886
11321
  /** A JSON body read to at most `max` bytes: a declared length over it, or a stream past it, is 413. */
10887
11322
  async function readCappedJson(request, max) {
10888
11323
  const declared = Number(request.headers.get('content-length') ?? NaN);