@substrat-run/control-plane-api 0.129.0 → 0.130.1
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.map +1 -1
- package/dist/api.js +90 -29
- package/dist/api.js.map +1 -1
- package/dist/deploy.js +3 -3
- package/dist/deploy.js.map +1 -1
- package/dist/platform-entry.generated.d.ts +1 -1
- package/dist/platform-entry.generated.d.ts.map +1 -1
- package/dist/platform-entry.generated.js +2 -2
- package/dist/platform-entry.generated.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EA6EL,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,6 +1,6 @@
|
|
|
1
1
|
import { resolveObservabilityWindow } from './observability-window.js';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, OWNER_TRANSFER_AUDIT_ERROR_MAX, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, 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, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, bindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, } from '@substrat-run/contracts';
|
|
3
|
+
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, OWNER_TRANSFER_AUDIT_ERROR_MAX, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, 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, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, bindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, errorCodeOf, } from '@substrat-run/contracts';
|
|
4
4
|
import { attributeFailure } from './failure-attribution.js';
|
|
5
5
|
import { BIND_EXPORT_BREAK_REFUSAL, bindExportBreakRefusal, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, migrationProgress, ulid, } from '@substrat-run/kernel';
|
|
6
6
|
import { TENANT_HEADER, confinedTenant } from './auth.js';
|
|
@@ -5648,6 +5648,23 @@ export function createControlPlaneApi(options) {
|
|
|
5648
5648
|
* label is unique within a tenant, so dropping the prefix stays collision-free. Both
|
|
5649
5649
|
* create (provision + reuse-match) and delete (reap-match) run through here, so they agree. */
|
|
5650
5650
|
const previewSlug = (slug, tag) => `${slug.split('/').at(-1)}--${tag}`;
|
|
5651
|
+
/** How long a `provisioning` preview row is presumed to belong to a create still running
|
|
5652
|
+
* (#1920). A create lives only as long as the request that carries it, and the CLI's
|
|
5653
|
+
* `fetch` gives up on a response after five minutes (undici's headers timeout) and never
|
|
5654
|
+
* retries a dropped create; the in-request retries add seconds. Fifteen minutes is three
|
|
5655
|
+
* times that. A create that fails in its own frame does not wait this out: it expires its
|
|
5656
|
+
* row on the way out (see `orchestratedPreview`), so this bound only covers a frame that
|
|
5657
|
+
* died without a chance to say so. */
|
|
5658
|
+
const PREVIEW_CREATE_BOUND_MS = 15 * 60_000;
|
|
5659
|
+
/** When a `provisioning` preview row's create counts as OVER, so the row may be reaped: at
|
|
5660
|
+
* once if its own frame expired it on failure, else `createdAt` plus the bound. `createdAt`
|
|
5661
|
+
* always ages: both hosts parse a listed row through `scopeSchema`, whose `instant` refuses
|
|
5662
|
+
* a value that does not. */
|
|
5663
|
+
const previewReclaimAt = (row, now) => {
|
|
5664
|
+
if (row.expiresAt !== null && Date.parse(row.expiresAt) <= now)
|
|
5665
|
+
return now;
|
|
5666
|
+
return Date.parse(row.createdAt) + PREVIEW_CREATE_BOUND_MS;
|
|
5667
|
+
};
|
|
5651
5668
|
/** Reap one preview: wipe the DO in its own deployment, then drop the directory row and
|
|
5652
5669
|
* its hostnames. Storage-before-row, the same ordering the DELETE route uses — a crash
|
|
5653
5670
|
* between the two converges on retry. Shared by that route and by the create path, which
|
|
@@ -5762,6 +5779,40 @@ export function createControlPlaneApi(options) {
|
|
|
5762
5779
|
throw new ControlPlaneError(422, `scope ${source.id} is pinned to '${source.jurisdiction}' — forking it would move its ` +
|
|
5763
5780
|
`data outside that jurisdiction; refused (K-32, preview-and-snapshots.md §6)`);
|
|
5764
5781
|
}
|
|
5782
|
+
// Idempotent on (tenant, vertical, tag): a PR *synchronize* rebinds the new version
|
|
5783
|
+
// onto the SAME preview, unless `refresh` asks for a fresh one. On a hosted vertical
|
|
5784
|
+
// every push is its own script, so the rebind first CARRIES the preview's data into
|
|
5785
|
+
// the new version's script (#1710). The preview's data then follows it from push to
|
|
5786
|
+
// push, and each push's migrations run forward over it (§4's rehearsal case).
|
|
5787
|
+
const existing = (await admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, opts.tag));
|
|
5788
|
+
// A preview only HAS data once its two-phase create finished: the directory row lands
|
|
5789
|
+
// first as `provisioning` (K-31), the fork's export→restore runs, and `activateScope`
|
|
5790
|
+
// is the last step. So a row still at `provisioning` is a create that DIED mid-fork —
|
|
5791
|
+
// its DO is empty. Reuse must never adopt one: reuse carries whatever the preview holds
|
|
5792
|
+
// and never re-forks from the source, so adopting a half-built row hands back a
|
|
5793
|
+
// permanently EMPTY preview and reports `reused: true` — success for a URL that shows
|
|
5794
|
+
// a reviewer no data at all. That is exactly what a CI retry does (the generated
|
|
5795
|
+
// workflow retries `preview create` on a transient), so the failure mode is the
|
|
5796
|
+
// COMMON one, not a corner: attempt 1 forks and dies, attempt 2 adopts its corpse and
|
|
5797
|
+
// goes green. Instead, reap the leftover and fall through to a fresh fork below —
|
|
5798
|
+
// which is what the retry was asking for. Same for an explicit `refresh`, whose fresh
|
|
5799
|
+
// scope would otherwise collide with the old row's still-bound `--<tag>` hostname.
|
|
5800
|
+
//
|
|
5801
|
+
// Except while that create may still be running (#1920): a `provisioning` row is also
|
|
5802
|
+
// what a create in flight looks like, and reaping it wipes the scope that create is
|
|
5803
|
+
// restoring into. So a young one, not marked dead by its own frame, is refused instead.
|
|
5804
|
+
// `refresh` is the explicit ask to replace whatever is there, and still reaps.
|
|
5805
|
+
if (existing && !opts.refresh && existing.status === 'provisioning') {
|
|
5806
|
+
const now = Date.now();
|
|
5807
|
+
const reclaimAt = previewReclaimAt(existing, now);
|
|
5808
|
+
if (now < reclaimAt) {
|
|
5809
|
+
const minutes = Math.max(1, Math.ceil((reclaimAt - now) / 60_000));
|
|
5810
|
+
throw new ControlPlaneError(409, `a create for preview '${opts.tag}' is in progress (started ${existing.createdAt}). ` +
|
|
5811
|
+
`Retry once it finishes; if it died, the tag can be reclaimed in ${minutes} min, ` +
|
|
5812
|
+
`or now with refresh (substrat preview create --refresh), which replaces it and stops a ` +
|
|
5813
|
+
`create still running: that create then fails`);
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5765
5816
|
// The PR version's deployment (its own dispatch script) — where the fork must land so
|
|
5766
5817
|
// the preview actually runs the PR's code. Falls back to prod/static resolution only
|
|
5767
5818
|
// when a version resolver is not wired (co-located host / tests).
|
|
@@ -5816,24 +5867,7 @@ export function createControlPlaneApi(options) {
|
|
|
5816
5867
|
// The `--<tag>` URL's base — the source's canonical hostname (fork) or the tenant-app
|
|
5817
5868
|
// convention (clean-room). Computed once so reuse and fresh mint the same URL.
|
|
5818
5869
|
const baseHostname = await previewBaseHostname(actor, source, tenantId, slug, surface);
|
|
5819
|
-
//
|
|
5820
|
-
// onto the SAME preview, unless `refresh` asks for a fresh one. On a hosted vertical
|
|
5821
|
-
// every push is its own script, so the rebind first CARRIES the preview's data into
|
|
5822
|
-
// the new version's script (#1710). The preview's data then follows it from push to
|
|
5823
|
-
// push, and each push's migrations run forward over it (§4's rehearsal case).
|
|
5824
|
-
const existing = (await admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, opts.tag));
|
|
5825
|
-
// A preview only HAS data once its two-phase create finished: the directory row lands
|
|
5826
|
-
// first as `provisioning` (K-31), the fork's export→restore runs, and `activateScope`
|
|
5827
|
-
// is the last step. So a row still at `provisioning` is a create that DIED mid-fork —
|
|
5828
|
-
// its DO is empty. Reuse must never adopt one: reuse carries whatever the preview holds
|
|
5829
|
-
// and never re-forks from the source, so adopting a half-built row hands back a
|
|
5830
|
-
// permanently EMPTY preview and reports `reused: true` — success for a URL that shows
|
|
5831
|
-
// a reviewer no data at all. That is exactly what a CI retry does (the generated
|
|
5832
|
-
// workflow retries `preview create` on a transient), so the failure mode is the
|
|
5833
|
-
// COMMON one, not a corner: attempt 1 forks and dies, attempt 2 adopts its corpse and
|
|
5834
|
-
// goes green. Instead, reap the leftover and fall through to a fresh fork below —
|
|
5835
|
-
// which is what the retry was asking for. Same for an explicit `refresh`, whose fresh
|
|
5836
|
-
// scope would otherwise collide with the old row's still-bound `--<tag>` hostname.
|
|
5870
|
+
// Found above, before any remote read, so an in-flight refusal costs one directory read.
|
|
5837
5871
|
const stale = existing !== undefined && (opts.refresh || existing.status !== 'active');
|
|
5838
5872
|
if (existing && !stale) {
|
|
5839
5873
|
// Data first, pointer last (#1710): move the preview's data into this push's script
|
|
@@ -5903,8 +5937,14 @@ export function createControlPlaneApi(options) {
|
|
|
5903
5937
|
}
|
|
5904
5938
|
// Free the tag: the slug is unique per tenant and the `--<tag>` hostname is still bound
|
|
5905
5939
|
// to the old row, so the fresh fork below cannot be provisioned until this one is gone.
|
|
5906
|
-
|
|
5907
|
-
|
|
5940
|
+
// A marked row is also the GC sweep's to reap (its `expiresAt` has passed), so the sweep
|
|
5941
|
+
// can get there between the lookup above and this reap. Gone is what the reap wanted.
|
|
5942
|
+
if (existing && stale) {
|
|
5943
|
+
await reapPreview(c, existing).catch((e) => {
|
|
5944
|
+
if (errorCodeOf(e) !== 'not_found')
|
|
5945
|
+
throw e;
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5908
5948
|
const previewId = scopeIdSchema.parse(ulid());
|
|
5909
5949
|
// The founding #559 case lands its durable row HERE, not in onError: the previews
|
|
5910
5950
|
// route answers a ControlPlaneError directly (its own catch, never the app-level
|
|
@@ -5931,6 +5971,8 @@ export function createControlPlaneApi(options) {
|
|
|
5931
5971
|
throw e;
|
|
5932
5972
|
}
|
|
5933
5973
|
};
|
|
5974
|
+
let row;
|
|
5975
|
+
let restore;
|
|
5934
5976
|
if (source) {
|
|
5935
5977
|
// A fresh fork. Export from where the prod data lives TODAY. The canonical
|
|
5936
5978
|
// `admin.exportScope` first — it writes the K-24 audit entry (and the co-located
|
|
@@ -5944,7 +5986,7 @@ export function createControlPlaneApi(options) {
|
|
|
5944
5986
|
// Directory row FIRST as `provisioning` (K-31 two-phase): a crash before the data
|
|
5945
5987
|
// copy leaves an inert row that — carrying `forkedFrom` + `expiresAt` — the GC sweep
|
|
5946
5988
|
// reaps, never copied data with no record.
|
|
5947
|
-
|
|
5989
|
+
row = {
|
|
5948
5990
|
tenantId,
|
|
5949
5991
|
scopeId: previewId,
|
|
5950
5992
|
kind: 'preview',
|
|
@@ -5956,12 +5998,12 @@ export function createControlPlaneApi(options) {
|
|
|
5956
5998
|
forkedAt: new Date().toISOString(),
|
|
5957
5999
|
// Directory models "absent = pinned", so a pinned preview passes no horizon at all.
|
|
5958
6000
|
expiresAt: expiresAt ?? undefined,
|
|
5959
|
-
}
|
|
6001
|
+
};
|
|
5960
6002
|
// Load the fork into the PR version's deployment (materializes the preview scope DO
|
|
5961
6003
|
// there; restore re-projects the vertical's roles from the dump's tuples). A one-shot
|
|
5962
6004
|
// DO storage blip heals on the in-request retry WITHOUT burning a CI attempt (which
|
|
5963
6005
|
// pushes a fresh version per try) — #559 (2).
|
|
5964
|
-
|
|
6006
|
+
restore = () => target.restoreScope(tenantId, previewId, tables, { sourceScopeId: source.id, exact: true });
|
|
5965
6007
|
}
|
|
5966
6008
|
else {
|
|
5967
6009
|
// A clean-room preview (#509 (b)): an EMPTY scope, no source to export. No `forkedFrom`
|
|
@@ -5969,7 +6011,7 @@ export function createControlPlaneApi(options) {
|
|
|
5969
6011
|
// co-located host migrates the module tables at `provisionScope`; a dispatch deployment
|
|
5970
6012
|
// materializes the empty DO via `restoreScope([])` (its `ensureMigrations` creates the
|
|
5971
6013
|
// schema on first access), so a source-less preview needs no export/restore of data.
|
|
5972
|
-
|
|
6014
|
+
row = {
|
|
5973
6015
|
tenantId,
|
|
5974
6016
|
scopeId: previewId,
|
|
5975
6017
|
kind: 'preview',
|
|
@@ -5978,11 +6020,30 @@ export function createControlPlaneApi(options) {
|
|
|
5978
6020
|
vertical: slug,
|
|
5979
6021
|
jurisdiction: 'global',
|
|
5980
6022
|
expiresAt: expiresAt ?? undefined,
|
|
5981
|
-
}
|
|
5982
|
-
|
|
5983
|
-
|
|
6023
|
+
};
|
|
6024
|
+
restore = target ? () => target.restoreScope(tenantId, previewId, []) : null;
|
|
6025
|
+
}
|
|
6026
|
+
try {
|
|
6027
|
+
// Inside the try: a host writes the directory row FIRST and then migrates, projects and
|
|
6028
|
+
// seats, so a provision that throws can leave a `provisioning` row too. A provision
|
|
6029
|
+
// that never wrote one (the tag's slug taken by a racing create) marks nothing: the
|
|
6030
|
+
// write below targets this create's own id, which does not exist, and is swallowed.
|
|
6031
|
+
await options.host.provisionScope(actor, row);
|
|
6032
|
+
if (restore)
|
|
6033
|
+
await restoreOrRecord(restore);
|
|
6034
|
+
await admin.activateScope(actor, tenantId, previewId);
|
|
6035
|
+
}
|
|
6036
|
+
catch (e) {
|
|
6037
|
+
// Expire the row this create left behind, so the next create reaps it at once rather
|
|
6038
|
+
// than refusing it as in flight for the whole bound (#1920) — that next create is the
|
|
6039
|
+
// CLI's retry after a known-dead attempt (#1918). The row stays until then, or until
|
|
6040
|
+
// the GC sweep's next pass reaps it as expired; the #559 ops-failure row outlives both.
|
|
6041
|
+
// Best-effort: if this write fails too, the age bound still frees the tag.
|
|
6042
|
+
await admin
|
|
6043
|
+
.setScopeExpiresAt(actor, tenantId, previewId, new Date().toISOString())
|
|
6044
|
+
.catch(() => undefined);
|
|
6045
|
+
throw e;
|
|
5984
6046
|
}
|
|
5985
|
-
await admin.activateScope(actor, tenantId, previewId);
|
|
5986
6047
|
// Bind the PR version. A private vertical's push self-admitted, so this is accepted; a
|
|
5987
6048
|
// preview scope also admits a pending version (#513), which is what a clean-room rehearsal
|
|
5988
6049
|
// of not-yet-admitted code needs.
|