@substrat-run/control-plane-api 0.165.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 +394 -14
- 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/package.json +9 -9
package/dist/api.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SourceBuildService } from '@substrat-run/contracts';
|
|
1
2
|
import type { ConnectionScopeBinding, ConnectionScopeBindingInput } from '@substrat-run/control-plane-client';
|
|
2
3
|
import { Hono } from 'hono';
|
|
3
4
|
import { type ConnectionCertificate, z, type ScopeStoreNeed, type StoreDriverRegistration } from '@substrat-run/contracts';
|
|
@@ -75,6 +76,8 @@ export interface ConnectionInspector {
|
|
|
75
76
|
certificate?: (host: ScopeHost, connection: Connection) => Promise<ConnectionCertificate | null>;
|
|
76
77
|
}
|
|
77
78
|
export interface ControlPlaneApiOptions {
|
|
79
|
+
/** Monotonic clock for the bounded tenant usage composition (injectable in tests). */
|
|
80
|
+
meteredUsageNow?: () => number;
|
|
78
81
|
/**
|
|
79
82
|
* The platform's margin over list price for model usage it provides (#1054), whole
|
|
80
83
|
* percent, applied at read time by `GET /model-usage/summary`. One global number
|
|
@@ -187,6 +190,12 @@ export interface ControlPlaneApiOptions {
|
|
|
187
190
|
* 501s. See `deploy.ts`.
|
|
188
191
|
*/
|
|
189
192
|
deployVertical?: DeployVerticalFn;
|
|
193
|
+
/** Hosted deployments accept source only. The private build service returns release parts. */
|
|
194
|
+
sourceBuilds?: SourceBuildService;
|
|
195
|
+
githubRepositoryAccess?: (tenantId: string, owner: string, repo: string, installationId: string, scope: 'read' | 'write') => Promise<{
|
|
196
|
+
remote: string;
|
|
197
|
+
token: string;
|
|
198
|
+
}>;
|
|
190
199
|
/**
|
|
191
200
|
* Uploads the store script for each declared scope store before the vertical's own
|
|
192
201
|
* upload (#2210). Absent ⇒ a push that declares a store and has a registered driver is
|
|
@@ -217,6 +226,11 @@ export interface ControlPlaneApiOptions {
|
|
|
217
226
|
uploaded: boolean;
|
|
218
227
|
}>;
|
|
219
228
|
callCanary: CanaryCall;
|
|
229
|
+
/** The same script settings release read the ensurer uses; required for a guarded revoke. */
|
|
230
|
+
readScriptRelease?: (scriptName: string) => Promise<{
|
|
231
|
+
exists: boolean;
|
|
232
|
+
release?: string;
|
|
233
|
+
}>;
|
|
220
234
|
/**
|
|
221
235
|
* What the platform builds every store script from now (`storePlatformFingerprint`, #2309).
|
|
222
236
|
* Absent ⇒ no fleet rebuild: the status reports none and the rebuild route answers 501.
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
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' },
|
|
@@ -1147,7 +1150,7 @@ export function createControlPlaneApi(options) {
|
|
|
1147
1150
|
const advisory = options.cliAdvisory;
|
|
1148
1151
|
app.use('*', async (c, next) => {
|
|
1149
1152
|
await next();
|
|
1150
|
-
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(','));
|
|
1151
1154
|
if (advisory?.minVersion)
|
|
1152
1155
|
c.header(CLI_MIN_VERSION_HEADER, advisory.minVersion);
|
|
1153
1156
|
if (advisory?.latestVersion)
|
|
@@ -1238,6 +1241,8 @@ export function createControlPlaneApi(options) {
|
|
|
1238
1241
|
{ method: 'GET', re: /\/verticals\/[^/]+\/channels$/ },
|
|
1239
1242
|
{ method: 'GET', re: /\/verticals\/[^/]+\/channels\/[^/]+\/history$/ },
|
|
1240
1243
|
{ method: 'POST', re: /\/verticals\/[^/]+\/channels\/[^/]+\/promote$/ },
|
|
1244
|
+
{ method: 'GET', re: /\/projects$/ },
|
|
1245
|
+
{ method: 'POST', re: /\/projects$/ },
|
|
1241
1246
|
{ method: 'POST', re: /\/verticals\/[^/]+\/deploy$/ },
|
|
1242
1247
|
// A builder REQUESTS publication of a vertical it owns (marketplace-publish.md §5); the
|
|
1243
1248
|
// `listing` flip stays staff-only. Ownership is checked in the handler.
|
|
@@ -1679,6 +1684,168 @@ export function createControlPlaneApi(options) {
|
|
|
1679
1684
|
});
|
|
1680
1685
|
// -- entitlements (§4.3) ---------------------------------------------------
|
|
1681
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
|
+
});
|
|
1682
1849
|
app.put('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
1683
1850
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1684
1851
|
// The body is the plan half (#33) and optional — a bodyless PUT is the
|
|
@@ -7982,12 +8149,98 @@ export function createControlPlaneApi(options) {
|
|
|
7982
8149
|
// pointing at a deployment that is not there. The version lands PENDING — except a
|
|
7983
8150
|
// PRIVATE vertical's, which self-admits below the seam (its blast radius is its own
|
|
7984
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
|
+
});
|
|
7985
8193
|
app.post('/verticals/:slug/deploy', async (c) => {
|
|
7986
8194
|
if (!options.deployVertical) {
|
|
7987
8195
|
return refuseStatus(c, 501, 'deploy is not configured on this control plane');
|
|
7988
8196
|
}
|
|
7989
8197
|
const p = c.get('principal');
|
|
7990
|
-
|
|
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
|
+
}
|
|
7991
8244
|
const raw = form.get('manifest');
|
|
7992
8245
|
if (typeof raw !== 'string')
|
|
7993
8246
|
return refuse(c, 'validation_failed', 'missing manifest part');
|
|
@@ -7996,6 +8249,8 @@ export function createControlPlaneApi(options) {
|
|
|
7996
8249
|
return refuse(c, 'validation_failed', 'manifest registry: required when modules are present');
|
|
7997
8250
|
}
|
|
7998
8251
|
const manifest = deployManifest.parse(parsed);
|
|
8252
|
+
if (requestedVersion && manifest.version !== requestedVersion)
|
|
8253
|
+
return refuseStatus(c, 502, 'build service produced a different release version');
|
|
7999
8254
|
const moduleProblems = pushedModuleRegistryProblems(manifest.modules, manifest.registry);
|
|
8000
8255
|
if (moduleProblems.length > 0)
|
|
8001
8256
|
return refuse(c, 'validation_failed', `manifest registry.permissions: ${moduleProblems.join('; ')}`);
|
|
@@ -8067,7 +8322,7 @@ export function createControlPlaneApi(options) {
|
|
|
8067
8322
|
// Otherwise the claim lands under the tenant prefix, exactly like a builder push —
|
|
8068
8323
|
// same namespace, no claim race with other workspaces' bare names.
|
|
8069
8324
|
const bareOwner = await ownerOf(c.get('actor'), bare);
|
|
8070
|
-
slug = bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
|
|
8325
|
+
slug = bare.startsWith(`${workspace.slug}/`) || bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
|
|
8071
8326
|
const existingOwner = slug === bare ? bareOwner : await ownerOf(c.get('actor'), slug);
|
|
8072
8327
|
if (existingOwner !== undefined && existingOwner !== workspace.id) {
|
|
8073
8328
|
return refuse(c, 'forbidden', `vertical '${slug}' is not owned by workspace '${pin}'`);
|
|
@@ -8345,7 +8600,7 @@ export function createControlPlaneApi(options) {
|
|
|
8345
8600
|
}
|
|
8346
8601
|
}
|
|
8347
8602
|
const version = await c.var.admin.getVersion(c.get('actor'), id, slug);
|
|
8348
|
-
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);
|
|
8349
8604
|
});
|
|
8350
8605
|
// -- where this platform runs (ops ergonomics) -----------------------------
|
|
8351
8606
|
// The console renders refs it already has — `servingRef`, a store's `ref`, a version's
|
|
@@ -10641,10 +10896,7 @@ export function createControlPlaneApi(options) {
|
|
|
10641
10896
|
* `text/plain` beacon cannot set it without a CORS preflight, 415) and refuse a request a
|
|
10642
10897
|
* browser marks as from another origin (403). A request with neither header is the CLI's.
|
|
10643
10898
|
*/
|
|
10644
|
-
const
|
|
10645
|
-
if (!/^application\/json\s*(;|$)/i.test(c.req.header('content-type') ?? '')) {
|
|
10646
|
-
throw new ControlPlaneError(415, `${words.write} takes application/json`);
|
|
10647
|
-
}
|
|
10899
|
+
const staffPerson = async (c, words) => {
|
|
10648
10900
|
const origin = c.req.header('origin');
|
|
10649
10901
|
const site = c.req.header('sec-fetch-site');
|
|
10650
10902
|
if ((origin && origin !== new URL(c.req.url).origin) || (site && site !== 'same-origin' && site !== 'none')) {
|
|
@@ -10657,6 +10909,85 @@ export function createControlPlaneApi(options) {
|
|
|
10657
10909
|
}
|
|
10658
10910
|
return actor;
|
|
10659
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
|
+
};
|
|
10660
10991
|
/** The pin a release is run under, or a refusal: the platform runs only what this repo pins. */
|
|
10661
10992
|
const pinOf = async (c, registration, sha256) => {
|
|
10662
10993
|
const stored = await c.var.admin.getStoreDriverRelease(c.get('actor'), registration.driver, sha256);
|
|
@@ -10711,10 +11042,14 @@ export function createControlPlaneApi(options) {
|
|
|
10711
11042
|
return { fingerprint, ...storeRebuildStatus(walk, fingerprint, verticals, options.storeReleases?.settleMs), walk: walk ?? null };
|
|
10712
11043
|
};
|
|
10713
11044
|
app.get('/store-drivers/:driver', async (c) => {
|
|
11045
|
+
await staffPerson(c, statusWords);
|
|
10714
11046
|
const registration = registeredDriver(c.req.param('driver'));
|
|
10715
|
-
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
|
+
]);
|
|
10716
11051
|
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 } : {}) });
|
|
11052
|
+
return c.json({ driver: registration.driver, pins: registration.releases, approvals, ...state, ...(rebuild ? { rebuild } : {}) });
|
|
10718
11053
|
});
|
|
10719
11054
|
// Store a release's bytes, before or after its pin lands (D-63). Idempotent per bundle.
|
|
10720
11055
|
app.put('/store-drivers/:driver/releases', async (c) => {
|
|
@@ -10725,6 +11060,51 @@ export function createControlPlaneApi(options) {
|
|
|
10725
11060
|
const release = await storeDriverRelease({ admin: c.var.admin, actor, bytes: flow.bytes, driver: registration.driver, body });
|
|
10726
11061
|
return c.json({ release, pinned: registration.releases.some((p) => p.sha256 === release.sha256 && p.sourceCommit === release.sourceCommit) }, 201);
|
|
10727
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
|
+
});
|
|
10728
11108
|
// Roll a stored, pinned release out: the canary, then every vertical that ever declared the
|
|
10729
11109
|
// driver. Posting the same release again converges a rollout that stopped.
|
|
10730
11110
|
app.post('/store-drivers/:driver/rollouts', async (c) => {
|
|
@@ -10937,7 +11317,7 @@ const WIRE_WALK_REQUEST_MAX_BYTES = 64 * 1024;
|
|
|
10937
11317
|
* The most a release upload's body may be: the decoded cap, as base64 (4 bytes per 3), plus room
|
|
10938
11318
|
* for the JSON around it. A bigger body is refused (413) before it is read whole.
|
|
10939
11319
|
*/
|
|
10940
|
-
const STORE_RELEASE_REQUEST_MAX_BYTES =
|
|
11320
|
+
const STORE_RELEASE_REQUEST_MAX_BYTES = STORE_RELEASE_STORED_MAX_BYTES;
|
|
10941
11321
|
/** A JSON body read to at most `max` bytes: a declared length over it, or a stream past it, is 413. */
|
|
10942
11322
|
async function readCappedJson(request, max) {
|
|
10943
11323
|
const declared = Number(request.headers.get('content-length') ?? NaN);
|