@substrat-run/control-plane-api 0.146.0 → 0.147.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 CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Hono } from 'hono';
2
- import { type ConnectionCertificate, z } from '@substrat-run/contracts';
2
+ import { type ConnectionCertificate, z, type StoreKindRegistration } from '@substrat-run/contracts';
3
3
  import type { Connection, ConnectionActivity, ConnectionActivitySource, ConnectionCredential, ConnectionProbe, PlatformActorId } from '@substrat-run/contracts';
4
4
  import type { CrossVerticalOptions, HostAdmin, ScopeHost } from '@substrat-run/kernel';
5
5
  import type { PlatformActorAuth, BuilderAuth, Principal, TenantServiceAuth } from './auth.js';
6
6
  import { type VerticalClient } from './vertical-client.js';
7
+ import type { PlatformScopeStores } from './platform-scope-stores.js';
7
8
  import { type ConnectionCandidatePrepare } from './connection-relay.js';
8
9
  import type { DeployVerticalFn, FetchVerticalAssetFn, FetchVerticalModulesFn } from './deploy.js';
9
10
  import type { PatchScriptBindingsFn } from './wfp.js';
@@ -121,6 +122,18 @@ export interface ControlPlaneApiOptions {
121
122
  * resolvers, which is only correct before any scope has adopted the serving script.
122
123
  */
123
124
  resolveVerticalRef?: (deploymentRef: string) => Promise<VerticalClient | undefined>;
125
+ /**
126
+ * Reserved scope-store operations, signed and sent to the store script (#2207).
127
+ * The vertical is not asked: it holds neither the namespace nor the key.
128
+ * Absent on a co-located host, whose own export and restore already carry the
129
+ * stores. Present on the hosted control plane, which is the only caller.
130
+ */
131
+ platformScopeStores?: PlatformScopeStores;
132
+ /**
133
+ * The registered scope-store kinds (#2207) this control plane judges a scope's stores against
134
+ * (`mayHoldScopeStores`, #2242). `STORE_KINDS` unless a test registers one.
135
+ */
136
+ storeKinds?: readonly StoreKindRegistration[];
124
137
  /** #1722: how long a copy move owns its ledger entries, and so how long its restore may land
125
138
  * (`CopyRestoreFence`). `SCOPE_COPY_LEASE_MS` unless a test needs a lease it can outlive. */
126
139
  copyLeaseMs?: number;
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,13 +1,14 @@
1
1
  import { resolveObservabilityWindow } from './observability-window.js';
2
2
  import { Hono } from 'hono';
3
3
  import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, memberInviteInput, memberRoleInput, ASSET_PART_PREFIX, assetHash, 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, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, operationSeriesCount, operationSeriesInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, verticalSlug, 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, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, errorCodeOf, capabilityFilterQuery, capabilityId as capabilityIdSchema, PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER, } from '@substrat-run/contracts';
4
- import { SCOPE_COPY_LEASE_MS, copyRestoreFence } from '@substrat-run/kernel';
4
+ import { SCOPE_COPY_LEASE_MS, copyRestoreFence, replayStoreErasures } from '@substrat-run/kernel';
5
5
  import { attributeFailure } from './failure-attribution.js';
6
6
  import { auditedChange, withAuditedOutcomes } from './audited-change.js';
7
7
  import { BIND_EXPORT_BREAK_REFUSAL, CARRIED_AWAY_KEY, bindExportBreakRefusal, carriedAwayDump, dumpMetaValue, metaValueIn, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, isPrimaryScope, migrationProgress, ulid, auditWarningOf, } from '@substrat-run/kernel';
8
8
  import { TENANT_HEADER, confinedTenant } from './auth.js';
9
9
  import { mintTenantToken } from './tenant-token.js';
10
10
  import { connectionGrantsForScope } from './vertical-client.js';
11
+ import { scopeStoresOf } from './platform-scope-stores.js';
11
12
  import { assertNoUnreachableScopeCopies, listAllScopeScriptCopies, reapScopeScriptCopies, recordStrandedStorage } from './scope-copy-cleanup.js';
12
13
  import { BACKFILL_PAGE_MAX, backfillScopeScriptCopies } from './scope-copy-backfill.js';
13
14
  import { oidcCallbackUrl, retireClientsOfReapedScope, wirePreviewAuth } from './preview-auth.js';
@@ -21,7 +22,7 @@ import { mapError } from './errors.js';
21
22
  import { maskDump, maskRecords } from './mask.js';
22
23
  import { createPseudonymizer } from './pseudonymize.js';
23
24
  import { openDump, sealDump } from './seal.js';
24
- import { assertSandboxContract, deployManifest, storedDeployManifest, pushedPermissionRegistry, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
25
+ import { assertSandboxContract, verticalUploadBindings, deployManifest, storedDeployManifest, pushedPermissionRegistry, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
25
26
  /**
26
27
  * Answer with a problem document (#113 phase 4).
27
28
  *
@@ -817,6 +818,100 @@ async function answeredMessage(res) {
817
818
  export function createControlPlaneApi(options) {
818
819
  const { host, authenticate, authenticateBuilder, authenticateTenantService } = options;
819
820
  const copyLeaseMs = options.copyLeaseMs ?? SCOPE_COPY_LEASE_MS;
821
+ /** The stores of the version the scope's deployment runs (`scopeStoresOf`, #2207). */
822
+ const manifestStores = (c, scope) => scopeStoresOf(c.var.admin, c.get('actor'), scope, options.storeKinds);
823
+ /**
824
+ * The platform's path to a scope's stores, or nothing: no caller is a co-located host whose
825
+ * own export, restore, wipe and erasure carry its stores, and a scope that declares none has
826
+ * none to reach. A hosted control plane always passes a caller (`unreachablePlatformScopeStores`
827
+ * when it has no route), so a declared store is never skipped there.
828
+ */
829
+ const platformStoresOf = async (c, scope) => {
830
+ const caller = options.platformScopeStores;
831
+ if (!caller || !scope.vertical)
832
+ return undefined;
833
+ const stores = await manifestStores(c, scope);
834
+ return stores.length > 0 ? { caller, vertical: scope.vertical, stores } : undefined;
835
+ };
836
+ /** `substrat.wipe` on each declared store, before the scope's own bytes go. */
837
+ const wipePlatformStores = async (c, scope) => {
838
+ const reach = await platformStoresOf(c, scope);
839
+ if (!reach)
840
+ return;
841
+ await reach.caller.wipeStores({ tenantId: scope.tenantId, scopeId: scope.id, vertical: reach.vertical, stores: reach.stores });
842
+ };
843
+ /** `substrat.export` on each declared store, for a backup, a full pull or a fork. */
844
+ const exportPlatformStores = async (c, scope) => {
845
+ const reach = await platformStoresOf(c, scope);
846
+ if (!reach)
847
+ return undefined;
848
+ return reach.caller.exportStores({ tenantId: scope.tenantId, scopeId: scope.id, vertical: reach.vertical, stores: reach.stores });
849
+ };
850
+ /**
851
+ * `substrat.erase` on the destination's stores for each subject tombstoned in the destination
852
+ * or the source, each subject once (`replayStoreErasures`, #2207): one store call per subject
853
+ * per declared binding, all inside this request. A failed call marks the replay incomplete,
854
+ * and the next restore repeats every call from the first subject, so it converges only while
855
+ * the whole replay fits one request's subrequest and duration budget. Nothing when the platform
856
+ * calls no stores for the destination.
857
+ */
858
+ const replayPlatformStoreErasures = async (c, destination, source) => {
859
+ // Called once the data has landed: a store list that cannot be resolved is reported as an
860
+ // incomplete replay, never thrown past a restore that already wrote.
861
+ let reach;
862
+ try {
863
+ reach = await platformStoresOf(c, destination);
864
+ }
865
+ catch (e) {
866
+ if (e instanceof ControlPlaneError && e.status === 503)
867
+ return { storeErasuresReplayed: 0, storeErasureIncomplete: true };
868
+ throw e;
869
+ }
870
+ if (!reach)
871
+ return undefined;
872
+ const { caller, vertical, stores } = reach;
873
+ return replayStoreErasures({
874
+ admin: c.var.admin,
875
+ actor: c.get('actor'),
876
+ tenantId: destination.tenantId,
877
+ target: destination.id,
878
+ source,
879
+ erase: async (subjectId) => (await caller.eraseSubject({ tenantId: destination.tenantId, scopeId: destination.id, vertical, subjectId, stores })).length,
880
+ });
881
+ };
882
+ /**
883
+ * The store replay on a route's answer (#2207). Present only with something to report, as
884
+ * `StoreErasureReplay` is. `sourceUnknown`: the source is not a scope of this tenant here,
885
+ * so its marks were not read, and a retry does not change that.
886
+ */
887
+ const storeReplayFields = (replays) => replays.length === 0
888
+ ? {}
889
+ : {
890
+ storeErasuresReplayed: replays.reduce((n, r) => n + r.storeErasuresReplayed, 0),
891
+ ...(replays.some((r) => r.storeErasureIncomplete) ? { incomplete: true } : {}),
892
+ ...(replays.some((r) => r.storeErasureSourceUnknown) ? { sourceUnknown: true } : {}),
893
+ };
894
+ /**
895
+ * A dump's stores, checked against what the destination's version declares (#2207), before
896
+ * any write: a binding it does not declare, or one of another kind, would be imported into an
897
+ * object nothing reads, or into one of the wrong shape.
898
+ */
899
+ const refuseUndeclaredDumpStores = async (c, scope, stores) => {
900
+ if (stores.length === 0)
901
+ return;
902
+ if (!scope.vertical)
903
+ throw new ControlPlaneError(422, 'restore refused: the dump names stores and the scope names no vertical');
904
+ const declared = new Map((await manifestStores(c, scope)).map((d) => [d.binding, d.kind]));
905
+ for (const store of stores) {
906
+ const kind = declared.get(store.binding);
907
+ if (kind === undefined) {
908
+ throw new ControlPlaneError(422, `restore refused: the dump names store '${store.binding}', which this scope's version does not declare`);
909
+ }
910
+ if (kind !== store.kind) {
911
+ throw new ControlPlaneError(422, `restore refused: the dump's store '${store.binding}' is a '${store.kind}', and this scope's version declares a '${kind}'`);
912
+ }
913
+ }
914
+ };
820
915
  const app = new Hono();
821
916
  // The 5xx backstop: every 5xx this API answers lands an ops-failure row, however it was
822
917
  // answered. A 5xx is the PLATFORM failing (an unmapped throw, or a downstream vertical's own
@@ -1280,6 +1375,7 @@ export function createControlPlaneApi(options) {
1280
1375
  // retiring (not erasing) a tenant pass `backup: true` deliberately.
1281
1376
  const backup = wantsBackup === true ? await backupScope(c, tenantId, scope) : null;
1282
1377
  const vertical = await verticalForScope(c, scope);
1378
+ await wipePlatformStores(c, scope);
1283
1379
  if (vertical)
1284
1380
  await vertical.deleteScope({ tenantId, scopeId: scope.id });
1285
1381
  // Tenant teardown reaps every scope and releases every name by design — force past
@@ -3173,6 +3269,9 @@ export function createControlPlaneApi(options) {
3173
3269
  connectionGrants,
3174
3270
  connectionKeys,
3175
3271
  ...(entityGrants ? { entityGrants } : {}),
3272
+ // #2263: a vertical with no owner of record still seats what its version declares, and
3273
+ // answers `owner: null`, which the CLI reports as a lockout NOT repaired.
3274
+ ownerless: 'seat',
3176
3275
  ...carry,
3177
3276
  // Handed over exactly as at provision: a store minted HERE has never been migrated
3178
3277
  // by the vertical, so the reconcile must carry it into the same ready-gate — a bound
@@ -3864,7 +3963,7 @@ export function createControlPlaneApi(options) {
3864
3963
  const deployment = await deploymentForScope(c, scope);
3865
3964
  const vertical = deployment?.client;
3866
3965
  if (!vertical)
3867
- return c.var.host.snapshotScope(actor, tenantId, scope.id, opts);
3966
+ return { snapId: await c.var.host.snapshotScope(actor, tenantId, scope.id, opts) };
3868
3967
  const snapId = scopeIdSchema.parse(ulid());
3869
3968
  // #1722: the script the copy lands in (the source's own), named in the ledger before the
3870
3969
  // copy, so a bind that fails, or a request that dies, leaves a fork reap and erasure reach.
@@ -3891,6 +3990,9 @@ export function createControlPlaneApi(options) {
3891
3990
  ref = null;
3892
3991
  }
3893
3992
  }
3993
+ // #2207: the stores are resolved before anything is written, so one that cannot be named
3994
+ // refuses the snapshot rather than leave a fork row behind.
3995
+ const reach = await platformStoresOf(c, scope);
3894
3996
  // Directory row FIRST, as `provisioning` (K-31's two-phase shape, used as
3895
3997
  // intended): a crash between the row and the data copy leaves an inert
3896
3998
  // provisioning row — which, carrying provenance and an expiry, the GC sweep
@@ -3906,6 +4008,7 @@ export function createControlPlaneApi(options) {
3906
4008
  expiresAt: opts.expiresAt,
3907
4009
  });
3908
4010
  const moveId = ref ? ulid() : null;
4011
+ let replay;
3909
4012
  try {
3910
4013
  if (moveId && ref) {
3911
4014
  // No load stamp: the vertical's in-script copy leaves none, so a crash-recovery sweep keeps
@@ -3913,6 +4016,16 @@ export function createControlPlaneApi(options) {
3913
4016
  await c.var.admin.recordScopeScriptCopy(actor, tenantId, snapId, ref, moveId, { role: 'destination', leaseMs: copyLeaseMs });
3914
4017
  }
3915
4018
  await retryTransient(() => copier.snapshotScope({ sourceScopeId: scope.id, newScopeId: snapId, tenantId }));
4019
+ // The scope file was copied inside the script. The store objects are per scope, so the
4020
+ // fork gets its own copy through the reserved export and import, signed by the
4021
+ // control plane. A version that declares no stores has nothing to copy.
4022
+ if (reach) {
4023
+ const stores = await reach.caller.exportStores({ tenantId, scopeId: scope.id, vertical: reach.vertical, stores: reach.stores });
4024
+ if (stores.length > 0) {
4025
+ await reach.caller.importStores({ tenantId, scopeId: snapId, vertical: reach.vertical, stores });
4026
+ replay = await replayPlatformStoreErasures(c, { ...scope, id: snapId }, scope.id);
4027
+ }
4028
+ }
3916
4029
  await c.var.admin.activateScope(actor, tenantId, snapId);
3917
4030
  // Bound to the SOURCE's current version: source and fork share a deployment, so
3918
4031
  // the fork resolves to the DO namespace its bytes actually live in.
@@ -3934,7 +4047,7 @@ export function createControlPlaneApi(options) {
3934
4047
  }
3935
4048
  throw e;
3936
4049
  }
3937
- return snapId;
4050
+ return { snapId, replay };
3938
4051
  };
3939
4052
  // The forks OF one scope — what a Snapshots UI lists. A directory read (kind,
3940
4053
  // provenance, expiry all live on the scope row); newest first.
@@ -3954,8 +4067,10 @@ export function createControlPlaneApi(options) {
3954
4067
  if (!scope)
3955
4068
  return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
3956
4069
  try {
3957
- const snapId = await orchestratedSnapshot(c, tenantId, scope, body);
3958
- return c.json(await c.var.admin.getScopeRecord(c.get('actor'), tenantId, snapId), 201);
4070
+ const { snapId, replay } = await orchestratedSnapshot(c, tenantId, scope, body);
4071
+ const record = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, snapId);
4072
+ // #2207: the fork's erasure replay, under the restore's rule: present only with something to report.
4073
+ return c.json({ ...record, ...storeReplayFields(replay ? [replay] : []) }, 201);
3959
4074
  }
3960
4075
  catch (e) {
3961
4076
  if (e instanceof ControlPlaneError) {
@@ -3979,11 +4094,11 @@ export function createControlPlaneApi(options) {
3979
4094
  * 5xx, a timeout) still aborts — an implemented wipe that failed is a retry, not a
3980
4095
  * shrug. Returns true when the storage was stranded.
3981
4096
  */
3982
- const deleteScopeStorageOrStrand = async (vertical, tenantId, scopeId) => {
4097
+ const deleteScopeStorageOrStrand = async (c, vertical, scope) => {
3983
4098
  if (!vertical)
3984
4099
  return false;
3985
4100
  try {
3986
- await vertical.deleteScope({ tenantId, scopeId });
4101
+ await vertical.deleteScope({ tenantId: scope.tenantId, scopeId: scope.id });
3987
4102
  return false;
3988
4103
  }
3989
4104
  catch (e) {
@@ -4005,10 +4120,13 @@ export function createControlPlaneApi(options) {
4005
4120
  */
4006
4121
  const reapScopeStorage = async (c, scope) => {
4007
4122
  const actor = c.get('actor');
4123
+ // #2207: the stores first, on every path below: a routed scope's copies are reaped through
4124
+ // their scripts, and a store is per scope, not per script.
4125
+ await wipePlatformStores(c, scope);
4008
4126
  // The deployment the scope's vertical resolves to, deleted through; a 501 there is recorded
4009
4127
  // as stranded storage, as `reapScopeScriptCopies` records its own.
4010
4128
  const viaDeployment = async () => {
4011
- const stranded = await deleteScopeStorageOrStrand(await verticalForScope(c, scope), scope.tenantId, scope.id);
4129
+ const stranded = await deleteScopeStorageOrStrand(c, await verticalForScope(c, scope), scope);
4012
4130
  if (stranded) {
4013
4131
  await recordStrandedStorage(c.var.admin, actor, scope.tenantId, scope.id, `the deployment its vertical '${scope.vertical}' resolves to`);
4014
4132
  }
@@ -4073,6 +4191,9 @@ export function createControlPlaneApi(options) {
4073
4191
  const dump = await c.var.admin.exportScope(c.get('actor'), tenantId, scope.id);
4074
4192
  const vertical = await verticalForScope(c, scope);
4075
4193
  const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
4194
+ // Store tables are opaque and stay unsealed. The platform reads them from the store script
4195
+ // (#2207); a store it cannot name or reach refuses the backup rather than omitting it.
4196
+ const stores = (await exportPlatformStores(c, scope)) ?? dump.stores;
4076
4197
  // Seal classified payloads per subject on the way in (#37). This copy is full-fidelity
4077
4198
  // and the platform keeps it, which is precisely why an erasure could never reach it: a
4078
4199
  // redaction fixes the live scope and does nothing to the object already in R2. Sealed
@@ -4080,7 +4201,7 @@ export function createControlPlaneApi(options) {
4080
4201
  // Restores open it again (`restoreFromBackup`) — except for subjects shredded in the
4081
4202
  // meantime, which is the mechanism working rather than the copy being damaged.
4082
4203
  const sealed = await sealDump(tables, sealerFor(c, tenantId, scope.id));
4083
- return store.put({ vertical: scope.vertical, dump: { ...dump, tables: sealed } });
4204
+ return store.put({ vertical: scope.vertical, dump: { ...dump, tables: sealed, stores } });
4084
4205
  };
4085
4206
  // The copies held for one scope — metadata only, so listing a reaped scope's backups
4086
4207
  // is cheap and hands out no bytes. Readable AFTER the reap (that is the point): the
@@ -4172,17 +4293,31 @@ export function createControlPlaneApi(options) {
4172
4293
  }
4173
4294
  const erasureEpoch = await c.var.admin.scopeErasureEpoch(c.get('actor'), tenantId, scopeId);
4174
4295
  const currentRef = await routeOf(c, scope);
4296
+ // #2207: the stores of the version the deployment runs, resolved before anything is erased. A
4297
+ // version that declares none is not asked. One that cannot be resolved while a kind is
4298
+ // registered refuses (503): the key must not be destroyed while a store may hold the subject.
4299
+ const reach = await platformStoresOf(c, scope);
4300
+ // Not asked is `undefined`, not `[]` (#2242): with no platform path the host's own stores
4301
+ // (the pure adapter's co-located runners) are erased in its shred or its finalize.
4302
+ const eraseStores = async () => reach ? reach.caller.eraseSubject({ tenantId, scopeId, vertical: reach.vertical, subjectId, stores: reach.stores }) : undefined;
4303
+ /** The host's own shred, with the platform's store erasure first: a store that errors keeps the key. */
4304
+ const shredHere = async () => {
4305
+ const storeErasures = await eraseStores();
4306
+ const receipt = await c.var.admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId);
4307
+ return { ...receipt, storeErasures: [...receipt.storeErasures, ...(storeErasures ?? [])] };
4308
+ };
4175
4309
  if (!options.resolveVerticalRef) {
4176
4310
  if (currentRef || copies.some((copy) => copy.state !== 'done')) {
4177
4311
  throw new ControlPlaneError(502, `scope ${scopeId} has script copies, but this control plane cannot reach their deployments`);
4178
4312
  }
4179
- return c.json(await c.var.admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId));
4313
+ return c.json(await shredHere());
4180
4314
  }
4181
4315
  // A scope nothing routes may still keep its store where its vertical resolves by slug (where
4182
4316
  // it was provisioned), so erasure reaches that deployment too, as reap does (`reapScopeStorage`).
4183
- const bySlug = currentRef ? undefined : await verticalForScope(c, scope);
4317
+ const reached = currentRef ? undefined : await deploymentForScope(c, scope);
4318
+ const bySlug = reached?.client;
4184
4319
  if (!currentRef && !bySlug && copies.every((copy) => copy.state === 'done')) {
4185
- return c.json(await c.var.admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId));
4320
+ return c.json(await shredHere());
4186
4321
  }
4187
4322
  const refs = new Set(copies.filter((copy) => copy.state !== 'done').map((copy) => copy.scriptRef));
4188
4323
  if (currentRef)
@@ -4196,10 +4331,11 @@ export function createControlPlaneApi(options) {
4196
4331
  }
4197
4332
  if (bySlug)
4198
4333
  redactions.push(await bySlug.redactSubject(scopeId, subjectId));
4334
+ const storeErasures = await eraseStores();
4199
4335
  return c.json(await c.var.admin.finalizeSubjectShred(c.get('actor'), tenantId, scopeId, subjectId, redactions, {
4200
4336
  versionId: scope.verticalVersionId, servingRef: scope.servingRef ?? null, epoch: erasureEpoch,
4201
4337
  copyCount: copies.length,
4202
- }).catch(relayHostRefusal));
4338
+ }, storeErasures).catch(relayHostRefusal));
4203
4339
  });
4204
4340
  // -- directory backups (#40) -----------------------------------------------
4205
4341
  // The platform's own disaster recovery, on the same store posture as the scope
@@ -4693,14 +4829,20 @@ export function createControlPlaneApi(options) {
4693
4829
  const dump = await c.var.admin.exportScope(actor, tenantId, scopeId);
4694
4830
  const vertical = await verticalForScope(c, scope);
4695
4831
  const tables = vertical ? await vertical.exportScope(scopeId) : dump.tables;
4696
- if (full)
4697
- return c.json({ ...dump, tables, masked: false });
4832
+ if (full) {
4833
+ // Store tables are opaque. The platform reads them from the store script (#2207); a
4834
+ // store it cannot name or reach refuses the pull rather than omitting it.
4835
+ const stores = (await exportPlatformStores(c, scope)) ?? dump.stores;
4836
+ return c.json({ ...dump, tables, stores, masked: false });
4837
+ }
4698
4838
  // One pseudonymizer for the whole response (#1034): the same customer has to read
4699
4839
  // the same in their own row, in every event payload that quoted them, and in the
4700
4840
  // timeline — a per-table generator would break exactly the joins that make a
4701
4841
  // masked copy worth pulling.
4702
4842
  const mask = await createPseudonymizer(options.maskSalt ?? crypto.randomUUID());
4703
- return c.json({ ...dump, tables: await maskDump(tables, mask), masked: true });
4843
+ // A masked pull cannot see inside a scope store, so it ships none rather than
4844
+ // the raw tables beside pseudonymized scope rows.
4845
+ return c.json({ ...dump, tables: await maskDump(tables, mask), stores: [], masked: true });
4704
4846
  }
4705
4847
  catch (e) {
4706
4848
  if (e instanceof ControlPlaneError) {
@@ -4780,7 +4922,15 @@ export function createControlPlaneApi(options) {
4780
4922
  const dump = await c.var.admin.exportScope(actor, tenantId, scope.id);
4781
4923
  const vertical = await verticalForScope(c, scope);
4782
4924
  const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
4783
- data.push({ ...dump, tables: mask ? await maskDump(tables, mask) : tables });
4925
+ data.push({
4926
+ ...dump,
4927
+ tables: mask ? await maskDump(tables, mask) : tables,
4928
+ // A scope store's tables are opaque, so the pseudonymizer cannot see inside them.
4929
+ // A masked export omits them rather than shipping them unmasked. A full one reads them
4930
+ // where they live, as a backup does (#2207): the host's own dump has none on the hosted
4931
+ // plane, and a store that cannot be named refuses the export rather than leave it out.
4932
+ stores: mask ? [] : ((await exportPlatformStores(c, scope)) ?? dump.stores),
4933
+ });
4784
4934
  }
4785
4935
  // The admin log is FULL-only (#36): it records what STAFF did, so it is not the
4786
4936
  // customer's Art. 20 data, and it carries staff actor ids and internal action
@@ -4853,6 +5003,17 @@ export function createControlPlaneApi(options) {
4853
5003
  if (dump.tables.length === 0) {
4854
5004
  return c.json({ error: 'restore refused: the backup has no tables — not a scope dump' }, 422);
4855
5005
  }
5006
+ // #2207: the dump's stores against the destination's declaration, before anything is written.
5007
+ if (options.platformScopeStores) {
5008
+ try {
5009
+ await refuseUndeclaredDumpStores(c, scope, dump.stores);
5010
+ }
5011
+ catch (e) {
5012
+ if (e instanceof ControlPlaneError)
5013
+ return c.json({ error: e.message }, failedWith(c, e));
5014
+ throw e;
5015
+ }
5016
+ }
4856
5017
  try {
4857
5018
  // Belt and braces for #37: a caller who fetched the object bytes by some other route
4858
5019
  // may POST a still-sealed dump, and a scope restored full of ciphertext is a silent
@@ -4866,12 +5027,25 @@ export function createControlPlaneApi(options) {
4866
5027
  // the destination's would find no key, null every payload, and call it a restore.
4867
5028
  const origin = { tenantId: tenantIdSchema.parse(dump.tenantId), scopeId: scopeIdSchema.parse(dump.scopeId) };
4868
5029
  const tables = await openDump(dump.tables, sealerFor(c, origin.tenantId, origin.scopeId));
4869
- const landing = { ...dump, tables };
5030
+ const vertical = await verticalForScope(c, scope);
5031
+ const storesCaller = options.platformScopeStores;
5032
+ const landing = { ...dump, tables, stores: storesCaller && vertical ? [] : dump.stores };
4870
5033
  // #2185: cleared before the load as well as after it (below): a step after the load can still
4871
5034
  // throw (the deployment's half, the re-assert), and the data that landed may predate a reconcile.
4872
5035
  await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, null);
4873
- await c.var.host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
4874
- const vertical = await verticalForScope(c, scope);
5036
+ const hostReplay = await c.var.host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
5037
+ if (storesCaller && vertical && scope.vertical && dump.stores.length > 0) {
5038
+ await storesCaller.importStores({
5039
+ tenantId,
5040
+ scopeId,
5041
+ vertical: scope.vertical,
5042
+ stores: dump.stores,
5043
+ });
5044
+ }
5045
+ const platformReplay = storesCaller && scope.vertical
5046
+ ? await replayPlatformStoreErasures(c, scope, sourceHint ?? origin.scopeId)
5047
+ : undefined;
5048
+ const replays = [hostReplay, platformReplay].filter((r) => !!r);
4875
5049
  // #1742: the recorded OFF positions ride the restore, so the deployment switches them
4876
5050
  // off in the replay's own event — its own sweeper cannot land in between.
4877
5051
  const restored = vertical
@@ -4890,7 +5064,11 @@ export function createControlPlaneApi(options) {
4890
5064
  // finished: the install reads as not reconciled, and the sweep's next pass reconciles it.
4891
5065
  // Again here, after the load, for a sweep reconcile that wrote the receipt while it ran.
4892
5066
  await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, null);
4893
- return c.json({ restored: scopeId, tables: tables.length });
5067
+ return c.json({
5068
+ restored: scopeId,
5069
+ tables: tables.length,
5070
+ ...storeReplayFields(replays),
5071
+ });
4894
5072
  }
4895
5073
  catch (e) {
4896
5074
  // #2185: and once more on the way out of a failure, for a sweep reconcile that wrote the
@@ -6160,7 +6338,9 @@ export function createControlPlaneApi(options) {
6160
6338
  compatibilityFlags: manifest.compatibilityFlags,
6161
6339
  modules,
6162
6340
  doClasses,
6163
- bindings: [...manifest.bindings, ...storeBindings],
6341
+ // #2210: a declared scope store adds no binding to either upload (L1), held at both call
6342
+ // sites with a kind registered by upload-scope-store-bindings.test.ts.
6343
+ bindings: verticalUploadBindings({ vertical: slug, manifest, platformBindings: storeBindings }),
6164
6344
  // #1242: the version now being served, injected as `SUBSTRAT_VERSION_ID` so the
6165
6345
  // scope host stamps the signals `version` dimension. The serving script is one
6166
6346
  // stable script updated in place, so this refresh is what keeps the binding
@@ -6684,7 +6864,7 @@ export function createControlPlaneApi(options) {
6684
6864
  compatibilityFlags: manifest.compatibilityFlags,
6685
6865
  modules,
6686
6866
  doClasses: manifest.doClasses,
6687
- bindings: manifest.bindings,
6867
+ bindings: verticalUploadBindings({ vertical: slug, manifest, platformBindings: [] }),
6688
6868
  // #1242: the freshly minted version id — this archive script IS that version,
6689
6869
  // and a preview routed at it stamps the signals `version` dimension honestly.
6690
6870
  versionId: id,
@@ -7541,6 +7721,7 @@ export function createControlPlaneApi(options) {
7541
7721
  // place, so the retry a PR-close job makes still finds it — after the row is gone, nothing
7542
7722
  // would name those clients again. A clean room never had one.
7543
7723
  await retireClientsOfReapedScope(previewAuthDeps(c), preview);
7724
+ await wipePlatformStores(c, preview);
7544
7725
  let storageStranded = false;
7545
7726
  if (options.resolveVerticalRef) {
7546
7727
  ({ storageStranded } = await reapScopeScriptCopies({