@substrat-run/control-plane-api 0.145.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 +14 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +205 -24
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/deploy.d.ts +45 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +53 -1
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +7 -1
- package/dist/platform-drain.js.map +1 -1
- package/dist/platform-entry.d.ts +23 -1
- package/dist/platform-entry.d.ts.map +1 -1
- package/dist/platform-entry.generated.d.ts +9 -1
- package/dist/platform-entry.generated.d.ts.map +1 -1
- package/dist/platform-entry.generated.js +10 -2
- package/dist/platform-entry.generated.js.map +1 -1
- package/dist/platform-entry.js +31 -1
- package/dist/platform-entry.js.map +1 -1
- package/dist/platform-scope-stores.d.ts +98 -0
- package/dist/platform-scope-stores.d.ts.map +1 -0
- package/dist/platform-scope-stores.js +140 -0
- package/dist/platform-scope-stores.js.map +1 -0
- package/dist/staff-client.d.ts +9 -1
- package/dist/staff-client.d.ts.map +1 -1
- package/dist/staff-client.js.map +1 -1
- package/dist/vertical-client.d.ts +17 -7
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js.map +1 -1
- package/dist/wfp.js +1 -1
- package/dist/wfp.js.map +1 -1
- package/package.json +8 -7
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":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAkBL,KAAK,qBAAqB,EA+E1B,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,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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
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({
|