@substrat-run/control-plane-api 0.54.0 → 0.56.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 +10 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +112 -46
- package/dist/api.js.map +1 -1
- package/dist/deploy.d.ts +15 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/platform-drain.d.ts +49 -2
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +86 -12
- package/dist/platform-drain.js.map +1 -1
- package/dist/vertical-client.d.ts +41 -1
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +40 -0
- package/dist/vertical-client.js.map +1 -1
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +28 -11
- package/dist/wfp.js.map +1 -1
- package/package.json +5 -4
package/dist/api.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PlatformActorId } from '@substrat-run/contracts';
|
|
|
3
3
|
import type { ScopeHost } from '@substrat-run/kernel';
|
|
4
4
|
import type { PlatformActorAuth, BuilderAuth, Principal } from './auth.js';
|
|
5
5
|
import type { VerticalClient } from './vertical-client.js';
|
|
6
|
-
import type { DeployVerticalFn, FetchVerticalModulesFn } from './deploy.js';
|
|
6
|
+
import type { DeployVerticalFn, FetchVerticalAssetFn, FetchVerticalModulesFn } from './deploy.js';
|
|
7
7
|
import type { PatchScriptBindingsFn } from './wfp.js';
|
|
8
8
|
import type { ObservabilityReader } from './observability.js';
|
|
9
9
|
import type { PlatformRuntime } from './platform-runtime.js';
|
|
@@ -63,6 +63,15 @@ export interface ControlPlaneApiOptions {
|
|
|
63
63
|
* place (the pre-#286 behavior: scopes stay on per-version dispatch).
|
|
64
64
|
*/
|
|
65
65
|
fetchVerticalModules?: FetchVerticalModulesFn;
|
|
66
|
+
/**
|
|
67
|
+
* Reads one static file back from a script's runtime-served assets (#578) — how a
|
|
68
|
+
* serving upload recovers asset bytes the stable script's upload session reports
|
|
69
|
+
* missing (the asset store dedups per script, so the push's upload to the archive
|
|
70
|
+
* script does not cover a re-serve). Host-injected like `fetchVerticalModules`.
|
|
71
|
+
* Absent ⇒ a re-serve of an asset-carrying version refuses when the runtime wants
|
|
72
|
+
* bytes (the pre-#578 behavior).
|
|
73
|
+
*/
|
|
74
|
+
fetchVerticalAsset?: FetchVerticalAssetFn;
|
|
66
75
|
/**
|
|
67
76
|
* Ensures per-tenant store D1 bindings exist on a dispatch script without a redeploy
|
|
68
77
|
* (#301) — the attach step that makes a freshly-minted tenant store reachable in the
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { adminAction, ASSET_PART_PREFIX, assetHash, channelName, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, pageOf, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, registerVerticalInput, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, z, } from '@substrat-run/contracts';
|
|
2
|
+
import { adminAction, ASSET_PART_PREFIX, assetHash, channelName, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, pageOf, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, registerVerticalInput, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, } from '@substrat-run/contracts';
|
|
3
3
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
4
4
|
import { TENANT_HEADER } from './auth.js';
|
|
5
5
|
import { ControlPlaneError } from './client.js';
|
|
@@ -346,6 +346,10 @@ export function createControlPlaneApi(options) {
|
|
|
346
346
|
{ method: 'POST', re: /\/verticals\/[^/]+\/previews$/ },
|
|
347
347
|
{ method: 'GET', re: /\/verticals\/[^/]+\/previews$/ },
|
|
348
348
|
{ method: 'DELETE', re: /\/verticals\/[^/]+\/previews\/[^/]+$/ },
|
|
349
|
+
// The builder's slice of the ops-failure record (#559 step 5): why did MY deploy /
|
|
350
|
+
// preview / provision fail. Tenant-narrowed in the handler (the forced-filter
|
|
351
|
+
// pattern, like GET /scopes); the allowlist alone is not authz.
|
|
352
|
+
{ method: 'GET', re: /\/ops-failures$/ },
|
|
349
353
|
];
|
|
350
354
|
app.use('*', async (c, next) => {
|
|
351
355
|
if (c.get('principal').kind === 'builder') {
|
|
@@ -367,6 +371,31 @@ export function createControlPlaneApi(options) {
|
|
|
367
371
|
})
|
|
368
372
|
.catch(() => undefined);
|
|
369
373
|
};
|
|
374
|
+
// Rides out a transient downstream window: the install path's binding-attach →
|
|
375
|
+
// script-settings propagation race (#424 case 2), and a one-shot DO storage blip
|
|
376
|
+
// during an export→restore or snapshot copy (#559 (2)). Retrying is cheap at THESE
|
|
377
|
+
// call sites specifically — the dump is already in memory and the far end is
|
|
378
|
+
// drop-then-replay idempotent — unlike CI's retry, which burns a pushed version per
|
|
379
|
+
// attempt. Honest refusals (4xx, and 501 = not implemented) surface immediately:
|
|
380
|
+
// retrying a refusal only delays the real message. ~3s worst case on the default
|
|
381
|
+
// delays, well inside a Worker request budget; a persistent fault still exhausts
|
|
382
|
+
// and surfaces (and lands an ops-failure row via the paths that record).
|
|
383
|
+
const PROVISION_RETRY_DELAYS_MS = [750, 2500];
|
|
384
|
+
const retryTransient = async (fn) => {
|
|
385
|
+
const delays = options.provisionRetryDelaysMs ?? PROVISION_RETRY_DELAYS_MS;
|
|
386
|
+
for (let attempt = 0;; attempt++) {
|
|
387
|
+
try {
|
|
388
|
+
return await fn();
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
const transient = e instanceof ControlPlaneError && (e.status === 0 || (e.status >= 500 && e.status !== 501));
|
|
392
|
+
const delay = delays[attempt];
|
|
393
|
+
if (!transient || delay === undefined)
|
|
394
|
+
throw e;
|
|
395
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
370
399
|
// One error boundary for every route: adapters throw plain Errors, and each
|
|
371
400
|
// one is a fail-closed refusal that must reach the caller as a status, not a
|
|
372
401
|
// stack trace.
|
|
@@ -804,7 +833,7 @@ export function createControlPlaneApi(options) {
|
|
|
804
833
|
throw new ControlPlaneError(501, 'adopt-serving needs dispatch resolution for both ends');
|
|
805
834
|
}
|
|
806
835
|
const dump = await source.exportScope(scopeId);
|
|
807
|
-
const restored = await dest.restoreScope(tenantId, scopeId, dump);
|
|
836
|
+
const restored = await retryTransient(() => dest.restoreScope(tenantId, scopeId, dump));
|
|
808
837
|
// Data landed — only now flip routing and move the version pointer.
|
|
809
838
|
await admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref);
|
|
810
839
|
await admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId);
|
|
@@ -910,7 +939,7 @@ export function createControlPlaneApi(options) {
|
|
|
910
939
|
throw new ControlPlaneError(501, 'rebind-vertical needs dispatch resolution for both ends');
|
|
911
940
|
}
|
|
912
941
|
const dump = await source.exportScope(scopeId);
|
|
913
|
-
const restored = await dest.restoreScope(tenantId, scopeId, dump);
|
|
942
|
+
const restored = await retryTransient(() => dest.restoreScope(tenantId, scopeId, dump));
|
|
914
943
|
// Data landed on the target script — only now flip routing and cross the pointer.
|
|
915
944
|
// `bindScopeVersion` rewrites `scopes.vertical` from the version row, audited. No
|
|
916
945
|
// extra snapshot here (adopt-serving's precedent): the source script's copy is the
|
|
@@ -1110,7 +1139,7 @@ export function createControlPlaneApi(options) {
|
|
|
1110
1139
|
forkedAt: new Date().toISOString(),
|
|
1111
1140
|
expiresAt: opts.expiresAt,
|
|
1112
1141
|
});
|
|
1113
|
-
await vertical.snapshotScope({ sourceScopeId: scope.id, newScopeId: snapId });
|
|
1142
|
+
await retryTransient(() => vertical.snapshotScope({ sourceScopeId: scope.id, newScopeId: snapId }));
|
|
1114
1143
|
await admin.activateScope(actor, tenantId, snapId);
|
|
1115
1144
|
// Bound to the SOURCE's current version: source and fork share a deployment, so
|
|
1116
1145
|
// the fork resolves to the DO namespace its bytes actually live in.
|
|
@@ -1633,7 +1662,7 @@ export function createControlPlaneApi(options) {
|
|
|
1633
1662
|
await host.restoreScope(actor, tenantId, scopeId, landing);
|
|
1634
1663
|
const vertical = await verticalForScope(c, scope);
|
|
1635
1664
|
if (vertical)
|
|
1636
|
-
await vertical.restoreScope(tenantId, scopeId, tables);
|
|
1665
|
+
await retryTransient(() => vertical.restoreScope(tenantId, scopeId, tables));
|
|
1637
1666
|
return c.json({ restored: scopeId, tables: tables.length });
|
|
1638
1667
|
}
|
|
1639
1668
|
catch (e) {
|
|
@@ -1826,10 +1855,6 @@ export function createControlPlaneApi(options) {
|
|
|
1826
1855
|
// orphaned scope nobody can see, rather than a directory row promising a scope
|
|
1827
1856
|
// that does not exist. `scopeStatus` has a `provisioning` state for expressing the
|
|
1828
1857
|
// in-between properly, and it is still unused — see the PR.
|
|
1829
|
-
// Rides out the binding-attach → script-settings propagation window (see the
|
|
1830
|
-
// `provisionRetryDelaysMs` option); the same shape as the dashboard's #391
|
|
1831
|
-
// configure retry. ~3s worst case, well inside a Worker request budget.
|
|
1832
|
-
const PROVISION_RETRY_DELAYS_MS = [750, 2500];
|
|
1833
1858
|
app.post('/verticals/:slug/instances', async (c) => {
|
|
1834
1859
|
const slug = c.req.param('slug');
|
|
1835
1860
|
// The install kill-switch: a blocked vertical takes no NEW instances, for anyone
|
|
@@ -1883,29 +1908,13 @@ export function createControlPlaneApi(options) {
|
|
|
1883
1908
|
// #424 case 2: the binding attach above races Cloudflare script-settings
|
|
1884
1909
|
// propagation, so the vertical's FIRST answer can be a transient 5xx that a
|
|
1885
1910
|
// retry moments later heals. `provisionInstance` is idempotent at the far end
|
|
1886
|
-
// (K-31), so ride the window out on a short backoff.
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
instance = await vertical.provisionInstance({
|
|
1894
|
-
...input,
|
|
1895
|
-
entitlements,
|
|
1896
|
-
identityLinks,
|
|
1897
|
-
...(tenantStores.length ? { tenantStores } : {}),
|
|
1898
|
-
});
|
|
1899
|
-
break;
|
|
1900
|
-
}
|
|
1901
|
-
catch (e) {
|
|
1902
|
-
const transient = e instanceof ControlPlaneError && (e.status === 0 || (e.status >= 500 && e.status !== 501));
|
|
1903
|
-
const delay = delays[attempt];
|
|
1904
|
-
if (!transient || delay === undefined)
|
|
1905
|
-
throw e;
|
|
1906
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1911
|
+
// (K-31), so ride the window out on a short backoff.
|
|
1912
|
+
const instance = await retryTransient(() => vertical.provisionInstance({
|
|
1913
|
+
...input,
|
|
1914
|
+
entitlements,
|
|
1915
|
+
identityLinks,
|
|
1916
|
+
...(tenantStores.length ? { tenantStores } : {}),
|
|
1917
|
+
}));
|
|
1909
1918
|
return c.json(instance, 201);
|
|
1910
1919
|
}
|
|
1911
1920
|
catch (e) {
|
|
@@ -2227,12 +2236,26 @@ export function createControlPlaneApi(options) {
|
|
|
2227
2236
|
doClasses: manifest.doClasses,
|
|
2228
2237
|
bindings: [...manifest.bindings, ...storeBindings],
|
|
2229
2238
|
// #340: the version's static files travel with it onto the serving script — from
|
|
2230
|
-
// the RETAINED manifest, with no bytes.
|
|
2231
|
-
//
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2234
|
-
//
|
|
2235
|
-
|
|
2239
|
+
// the RETAINED manifest, with no bytes up front. The asset store dedups PER
|
|
2240
|
+
// SCRIPT (#578), so hashes the push uploaded to the archive script are still
|
|
2241
|
+
// missing for the stable script — each file therefore carries a `fetchContent`
|
|
2242
|
+
// that reads the bytes back from the archive script's runtime-served assets,
|
|
2243
|
+
// invoked (and hash-verified) only for hashes the upload session reports
|
|
2244
|
+
// missing. The archive script gives back the modules (#286) AND the assets:
|
|
2245
|
+
// nothing but the manifest is retained.
|
|
2246
|
+
...(manifest.assets
|
|
2247
|
+
? {
|
|
2248
|
+
assets: {
|
|
2249
|
+
...manifest.assets,
|
|
2250
|
+
files: manifest.assets.files.map((f) => ({
|
|
2251
|
+
...f,
|
|
2252
|
+
...(options.fetchVerticalAsset
|
|
2253
|
+
? { fetchContent: () => options.fetchVerticalAsset(version.deploymentRef, f.path) }
|
|
2254
|
+
: {}),
|
|
2255
|
+
})),
|
|
2256
|
+
},
|
|
2257
|
+
}
|
|
2258
|
+
: {}),
|
|
2236
2259
|
}, serving
|
|
2237
2260
|
? { priorDoClasses: serving.doClasses, priorMigrationTag: serving.migrationTag }
|
|
2238
2261
|
: undefined);
|
|
@@ -2341,6 +2364,21 @@ export function createControlPlaneApi(options) {
|
|
|
2341
2364
|
// silently dropped. An old CLI that sends no pin keeps today's behavior on all paths.
|
|
2342
2365
|
const pinField = form.get('tenant');
|
|
2343
2366
|
const pin = typeof pinField === 'string' && pinField.length > 0 ? pinField : null;
|
|
2367
|
+
// The push's self-reported provenance (git CI vs a terminal) — a label the dashboard
|
|
2368
|
+
// shows, never authority, so a missing or malformed field must not fail the push
|
|
2369
|
+
// (an old CLI sends none). Lenient by construction: safeParse, drop on mismatch.
|
|
2370
|
+
const originField = form.get('origin');
|
|
2371
|
+
const origin = (() => {
|
|
2372
|
+
if (typeof originField !== 'string')
|
|
2373
|
+
return undefined;
|
|
2374
|
+
try {
|
|
2375
|
+
const parsed = versionOrigin.safeParse(JSON.parse(originField));
|
|
2376
|
+
return parsed.success ? parsed.data : undefined;
|
|
2377
|
+
}
|
|
2378
|
+
catch {
|
|
2379
|
+
return undefined;
|
|
2380
|
+
}
|
|
2381
|
+
})();
|
|
2344
2382
|
// Resolve the registry id + owner this push acts on. Checked BEFORE the upload so a
|
|
2345
2383
|
// refused push never leaves an orphaned namespace script.
|
|
2346
2384
|
const bare = c.req.param('slug');
|
|
@@ -2577,6 +2615,7 @@ export function createControlPlaneApi(options) {
|
|
|
2577
2615
|
permissionDigest: manifest.digests.permission,
|
|
2578
2616
|
migrationDigest: manifest.digests.migration,
|
|
2579
2617
|
deploymentRef,
|
|
2618
|
+
...(origin ? { origin } : {}),
|
|
2580
2619
|
// Retained for the serving upload (#286): the archive script keeps the module
|
|
2581
2620
|
// bytes, this keeps their shape (entry, compat, doClasses, bindings).
|
|
2582
2621
|
manifestJson: JSON.stringify(manifest),
|
|
@@ -3014,6 +3053,30 @@ export function createControlPlaneApi(options) {
|
|
|
3014
3053
|
if (existing && stale)
|
|
3015
3054
|
await reapPreview(c, existing);
|
|
3016
3055
|
const previewId = scopeIdSchema.parse(ulid());
|
|
3056
|
+
// The founding #559 case lands its durable row HERE, not in onError: the previews
|
|
3057
|
+
// route answers a ControlPlaneError directly (its own catch, never the app-level
|
|
3058
|
+
// recorder), and only this frame knows the preview's scopeId — the key that lets
|
|
3059
|
+
// the console explain the stranded `provisioning` row this throw leaves behind.
|
|
3060
|
+
const restoreOrRecord = async (fn) => {
|
|
3061
|
+
try {
|
|
3062
|
+
await retryTransient(fn);
|
|
3063
|
+
}
|
|
3064
|
+
catch (e) {
|
|
3065
|
+
if (e instanceof ControlPlaneError && e.status >= 500 && e.status !== 501) {
|
|
3066
|
+
recordFailure({
|
|
3067
|
+
actor,
|
|
3068
|
+
operation: 'preview.create',
|
|
3069
|
+
stage: 'restore',
|
|
3070
|
+
tenantId,
|
|
3071
|
+
scopeId: previewId,
|
|
3072
|
+
vertical: slug,
|
|
3073
|
+
status: e.status,
|
|
3074
|
+
message: e.message,
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
throw e;
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3017
3080
|
if (source) {
|
|
3018
3081
|
// A fresh fork. Export from where the prod data lives TODAY. The canonical
|
|
3019
3082
|
// `admin.exportScope` first — it writes the K-24 audit entry (and the co-located
|
|
@@ -3041,8 +3104,10 @@ export function createControlPlaneApi(options) {
|
|
|
3041
3104
|
expiresAt: expiresAt ?? undefined,
|
|
3042
3105
|
});
|
|
3043
3106
|
// Load the fork into the PR version's deployment (materializes the preview scope DO
|
|
3044
|
-
// there; restore re-projects the vertical's roles from the dump's tuples).
|
|
3045
|
-
|
|
3107
|
+
// there; restore re-projects the vertical's roles from the dump's tuples). A one-shot
|
|
3108
|
+
// DO storage blip heals on the in-request retry WITHOUT burning a CI attempt (which
|
|
3109
|
+
// pushes a fresh version per try) — #559 (2).
|
|
3110
|
+
await restoreOrRecord(() => target.restoreScope(tenantId, previewId, tables));
|
|
3046
3111
|
}
|
|
3047
3112
|
else {
|
|
3048
3113
|
// A clean-room preview (#509 (b)): an EMPTY scope, no source to export. No `forkedFrom`
|
|
@@ -3061,7 +3126,7 @@ export function createControlPlaneApi(options) {
|
|
|
3061
3126
|
expiresAt: expiresAt ?? undefined,
|
|
3062
3127
|
});
|
|
3063
3128
|
if (target)
|
|
3064
|
-
await target.restoreScope(tenantId, previewId, []);
|
|
3129
|
+
await restoreOrRecord(() => target.restoreScope(tenantId, previewId, []));
|
|
3065
3130
|
}
|
|
3066
3131
|
await admin.activateScope(actor, tenantId, previewId);
|
|
3067
3132
|
// Bind the PR version. A private vertical's push self-admitted, so this is accepted; a
|
|
@@ -3215,13 +3280,14 @@ export function createControlPlaneApi(options) {
|
|
|
3215
3280
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
3216
3281
|
});
|
|
3217
3282
|
// The recorded operational failures (#559) — the console's failures view, and the
|
|
3218
|
-
// "what does this `reference = <id>` belong to" lookup.
|
|
3219
|
-
//
|
|
3220
|
-
//
|
|
3221
|
-
//
|
|
3283
|
+
// "what does this `reference = <id>` belong to" lookup. Newest first by default,
|
|
3284
|
+
// unlike /admin-log: an operator asks "what broke lately". A builder reads only its
|
|
3285
|
+
// OWN tenant's rows — the filter is forced, not trusted from the query (step 5: a
|
|
3286
|
+
// red CI run is explainable from the dashboard without staff involvement).
|
|
3222
3287
|
app.get('/ops-failures', async (c) => {
|
|
3288
|
+
const p = c.get('principal');
|
|
3223
3289
|
const filter = opsFailuresQuery.parse({
|
|
3224
|
-
tenantId: c.req.query('tenantId'),
|
|
3290
|
+
tenantId: p.kind === 'builder' ? p.tenantId : c.req.query('tenantId'),
|
|
3225
3291
|
scopeId: c.req.query('scopeId'),
|
|
3226
3292
|
vertical: c.req.query('vertical'),
|
|
3227
3293
|
operation: c.req.query('operation'),
|