@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.d.ts +14 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +451 -16
- package/dist/api.js.map +1 -1
- package/dist/github-app.d.ts +31 -0
- package/dist/github-app.d.ts.map +1 -0
- package/dist/github-app.js +86 -0
- package/dist/github-app.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/source-builds.d.ts +6 -0
- package/dist/source-builds.d.ts.map +1 -0
- package/dist/source-builds.js +55 -0
- package/dist/source-builds.js.map +1 -0
- package/dist/store-releases.d.ts +40 -1
- package/dist/store-releases.d.ts.map +1 -1
- package/dist/store-releases.js +170 -9
- package/dist/store-releases.js.map +1 -1
- package/dist/store-script.d.ts +6 -0
- package/dist/store-script.d.ts.map +1 -1
- package/dist/store-script.js +14 -11
- package/dist/store-script.js.map +1 -1
- package/dist/vertical-client.d.ts +10 -0
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +5 -0
- package/dist/vertical-client.js.map +1 -1
- package/package.json +9 -9
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,
|
|
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
|
-
|
|
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:
|
|
9419
|
-
"
|
|
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
|
|
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
|
|
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 =
|
|
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);
|