@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 CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
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
- // Idempotent on (tenant, vertical, tag): a PR *synchronize* rebinds the new version
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
- if (existing && stale)
5907
- await reapPreview(c, existing);
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
- await options.host.provisionScope(actor, {
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
- await restoreOrRecord(() => target.restoreScope(tenantId, previewId, tables, { sourceScopeId: source.id, exact: true }));
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
- await options.host.provisionScope(actor, {
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
- if (target)
5983
- await restoreOrRecord(() => target.restoreScope(tenantId, previewId, []));
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.