@substrat-run/control-plane-api 0.178.0 → 0.180.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.
Files changed (47) hide show
  1. package/dist/aggregate-reads.d.ts.map +1 -1
  2. package/dist/aggregate-reads.js +4 -3
  3. package/dist/aggregate-reads.js.map +1 -1
  4. package/dist/aggregate-source.d.ts +8 -1
  5. package/dist/aggregate-source.d.ts.map +1 -1
  6. package/dist/aggregate-source.js +2 -1
  7. package/dist/aggregate-source.js.map +1 -1
  8. package/dist/api.d.ts +3 -0
  9. package/dist/api.d.ts.map +1 -1
  10. package/dist/api.js +223 -148
  11. package/dist/api.js.map +1 -1
  12. package/dist/backups.d.ts +28 -5
  13. package/dist/backups.d.ts.map +1 -1
  14. package/dist/cf-observability.d.ts.map +1 -1
  15. package/dist/cf-observability.js +26 -16
  16. package/dist/cf-observability.js.map +1 -1
  17. package/dist/observability.d.ts +6 -1
  18. package/dist/observability.d.ts.map +1 -1
  19. package/dist/observability.js +8 -1
  20. package/dist/observability.js.map +1 -1
  21. package/dist/platform-entry.generated.d.ts +1 -1
  22. package/dist/platform-entry.generated.d.ts.map +1 -1
  23. package/dist/platform-entry.generated.js +2 -2
  24. package/dist/platform-entry.generated.js.map +1 -1
  25. package/dist/platform-scope-stores.d.ts +20 -22
  26. package/dist/platform-scope-stores.d.ts.map +1 -1
  27. package/dist/platform-scope-stores.js +16 -19
  28. package/dist/platform-scope-stores.js.map +1 -1
  29. package/dist/r2-backups.d.ts.map +1 -1
  30. package/dist/r2-backups.js +49 -13
  31. package/dist/r2-backups.js.map +1 -1
  32. package/dist/store-entry.generated.d.ts +2 -2
  33. package/dist/store-entry.generated.d.ts.map +1 -1
  34. package/dist/store-entry.generated.js +5 -5
  35. package/dist/store-entry.generated.js.map +1 -1
  36. package/dist/store-move-ends.d.ts +59 -0
  37. package/dist/store-move-ends.d.ts.map +1 -0
  38. package/dist/store-move-ends.js +114 -0
  39. package/dist/store-move-ends.js.map +1 -0
  40. package/dist/store-move.d.ts +65 -0
  41. package/dist/store-move.d.ts.map +1 -0
  42. package/dist/store-move.js +107 -0
  43. package/dist/store-move.js.map +1 -0
  44. package/dist/tenant-request-query.d.ts.map +1 -1
  45. package/dist/tenant-request-query.js +5 -0
  46. package/dist/tenant-request-query.js.map +1 -1
  47. package/package.json +9 -9
package/dist/api.js CHANGED
@@ -14,7 +14,7 @@ import { TENANT_HEADER, confinedTenant } from './auth.js';
14
14
  import { mintTenantToken } from './tenant-token.js';
15
15
  import { abandonCopy, beginTransfer, connectionGrantsForScope, transferScope, isScopeNotHeld, notHeldIsBenign } from './vertical-client.js';
16
16
  import { COPY_JOB_MAX_MS, copyJobAt, copyJobHeld, copyJobId, copyJobLive, copyTagId, versionIdOfOrder, versionOrderOf, } from './copy-job.js';
17
- import { runningVersionOf, runningVersionStoresOf, scopeStoreReachOf, scopeStoresOf, unrouteScope } from './platform-scope-stores.js';
17
+ import { exportStoreDumps, runningVersionOf, runningVersionStoresOf, scopeStoreReachOf, scopeStoresOf, unrouteScope } from './platform-scope-stores.js';
18
18
  import { liveApprovalOf } from './store-script.js';
19
19
  import { STORE_REBUILD_REQUEST_SUBREQUESTS, runStoreFleetRebuild, storeRebuildBudget, storeRebuildStatus } from './store-rebuild.js';
20
20
  import { PUSH_HOLD_TTL_MS, STORE_RELEASE_STORED_MAX_BYTES, approveStoredDriverRelease, readStoreDriverApprovals, runStoreDriverRevoke, runStoreDriverRollout, storeDriverRelease, storedReleaseLoader } from './store-releases.js';
@@ -68,6 +68,8 @@ import { mintPushToken, pushActorFor } from './push-token.js';
68
68
  import { TENANT_SERIES_SCOPE_CAP, REQUEST_FACET_KEYS, LOGS_INCOMPLETE_HEADER } from './observability.js';
69
69
  import { parseTenantRequestQuery, parseTenantWindowQuery, refineTenantWindow, refuseSearchOnCounts, parseTenantLineQuery, REQUEST_WHERE_VALUES_MAX, scriptFamiliesOfScopes, } from './tenant-request-query.js';
70
70
  import { namespacesForScript } from './do-namespaces.js';
71
+ import { discardMoves, moveStores } from './store-move.js';
72
+ import { backupEnds, commitStages, inlineStoreSource, restoreEnds, snapshotEnds, StoreLandFailure, storeHoldsRows, } from './store-move-ends.js';
71
73
  import { backupDirectoryIfDue } from './directory-backup.js';
72
74
  import { deployedDoClasses, platformSweeperDecision } from './platform-entry.js';
73
75
  import { extractedMigrationsDigest, extractedMigrationsProblem, extractedMigrationsRefusal, MigrationExtractionError, MigrationLoaderUnavailable, } from './migration-extract.js';
@@ -1016,7 +1018,7 @@ export function createControlPlaneApi(options) {
1016
1018
  /** `substrat.wipe` on each declared store, before the scope's own bytes go. */
1017
1019
  const wipePlatformStores = async (c, scope) => (await platformStoresWipe(c, scope))();
1018
1020
  /**
1019
- * A dump in pages (`substrat.export-page`) of each store the lifecycle reaches, for a backup, a full pull or a fork.
1021
+ * The stores a backup, a full pull or a fork copies: those the scope's running version declares.
1020
1022
  *
1021
1023
  * A dump is only worth taking if it restores, and a restore imports only what the destination's
1022
1024
  * version declares (L8). So a binding only an earlier version declared (#2286) is left out of the
@@ -1025,24 +1027,29 @@ export function createControlPlaneApi(options) {
1025
1027
  * way out, rather than produce a dump its own scope could not restore. Wipe, reap and erasure
1026
1028
  * never export, so they reach such a store regardless.
1027
1029
  */
1028
- const exportPlatformStores = async (c, scope) => {
1030
+ const platformStoreReach = async (c, scope) => {
1029
1031
  const caller = options.platformScopeStores;
1030
1032
  if (!caller || !scope.vertical)
1031
1033
  return undefined;
1032
1034
  const { running, all } = await scopeStoreReachOf(c.var.admin, c.get('actor'), scope, options.storeDrivers);
1033
1035
  if (all.length === 0)
1034
1036
  return undefined;
1035
- const exported = await caller.exportStores({ tenantId: scope.tenantId, scopeId: scope.id, vertical: scope.vertical, stores: all });
1037
+ const place = { tenantId: scope.tenantId, scopeId: scope.id, vertical: scope.vertical };
1036
1038
  const current = new Set(running.map((store) => store.binding));
1037
- const dropped = exported.filter((store) => !current.has(store.binding));
1038
- const held = dropped.find((store) => store.tables.some((table) => table.rows.length > 0));
1039
- if (held) {
1040
- throw new ControlPlaneError(409, `scope ${scope.id}'s store '${held.binding}' was declared by an earlier version of '${scope.vertical}' and ` +
1039
+ for (const dropped of all.filter((store) => !current.has(store.binding))) {
1040
+ if (!(await storeHoldsRows(caller, place, dropped)))
1041
+ continue;
1042
+ throw new ControlPlaneError(409, `scope ${scope.id}'s store '${dropped.binding}' was declared by an earlier version of '${scope.vertical}' and ` +
1041
1043
  `still holds data, and the version the scope runs does not declare it, so a dump of it could not be ` +
1042
- `restored. Declare '${held.binding}' as a '${held.driver}' again in a version the scope runs, then export; ` +
1044
+ `restored. Declare '${dropped.binding}' as a '${dropped.driver}' again in a version the scope runs, then export; ` +
1043
1045
  `or reap the scope without a backup, which wipes it`);
1044
1046
  }
1045
- return exported.filter((store) => current.has(store.binding));
1047
+ return { caller, place, stores: running };
1048
+ };
1049
+ /** The stores a pull or a tenant export answers inline, each read whole (`exportStoreDumps`). */
1050
+ const exportPlatformStores = async (c, scope) => {
1051
+ const reach = await platformStoreReach(c, scope);
1052
+ return reach && exportStoreDumps(reach.caller, reach.place, reach.stores);
1046
1053
  };
1047
1054
  /**
1048
1055
  * `substrat.erase` on the destination's stores for each subject tombstoned in the destination
@@ -1264,13 +1271,23 @@ export function createControlPlaneApi(options) {
1264
1271
  // #977: a tenant credential minted for a person writes through a view that names
1265
1272
  // them on every admin row. Only the credential's own claim can supply the person —
1266
1273
  // never a header or a body — and only the staff mint writes that claim.
1267
- const view = principal.kind === 'tenant' && principal.onBehalfOf && host.attributed
1268
- ? host.attributed(principal.onBehalfOf)
1269
- : host;
1274
+ const view = viewOf(principal);
1270
1275
  c.set('host', view);
1271
1276
  c.set('admin', view.admin);
1272
1277
  await next();
1273
1278
  });
1279
+ /**
1280
+ * #977: the host a principal's request writes through — the attributed view for a tenant
1281
+ * credential minted for a person, the configured host otherwise. The middleware sets it on
1282
+ * the request; a helper handed only the principal derives the same view here (#2860), so its
1283
+ * access rows name the person too.
1284
+ */
1285
+ function viewOf(principal) {
1286
+ return principal.kind === 'tenant' && principal.onBehalfOf && host.attributed
1287
+ ? host.attributed(principal.onBehalfOf)
1288
+ : host;
1289
+ }
1290
+ const adminFor = (principal) => viewOf(principal).admin;
1274
1291
  // Confine a BUILDER to the vertical-management surface, fail-CLOSED (builder-plane.md
1275
1292
  // §4). Default-deny by design: a builder reaches only the routes on this allowlist,
1276
1293
  // and only for verticals its tenant owns (the per-route ownership checks below). A
@@ -1547,7 +1564,7 @@ export function createControlPlaneApi(options) {
1547
1564
  const vertical = await resolveVerticalId(c, raw);
1548
1565
  const owner = (pathTenant ?? confined)
1549
1566
  ? null
1550
- : ((await verticalOf(c.get('actor'), vertical))?.ownerTenant ?? null);
1567
+ : ((await verticalOf(c.var.admin, c.get('actor'), vertical))?.ownerTenant ?? null);
1551
1568
  return { vertical, tenantId: pathTenant ?? confined ?? owner };
1552
1569
  }
1553
1570
  catch {
@@ -2949,7 +2966,7 @@ export function createControlPlaneApi(options) {
2949
2966
  resolveVertical: options.resolveVertical,
2950
2967
  resolveVerticalVersion: options.resolveVerticalVersion,
2951
2968
  resolveVerticalRef: options.resolveVerticalRef,
2952
- ownerOf,
2969
+ ownerOf: (actor, slug) => ownerOf(c.var.admin, actor, slug),
2953
2970
  tenantSlugOf: async (actor, tenantId) => (await c.var.admin.getTenant(actor, tenantId).catch(() => null))?.slug ?? null,
2954
2971
  }, c.get('actor'), scope);
2955
2972
  /**
@@ -2964,11 +2981,11 @@ export function createControlPlaneApi(options) {
2964
2981
  * exact split so it is a seconds-long diagnosis, not a multi-hour hunt. A couple of
2965
2982
  * directory reads only when a call has already failed to resolve.
2966
2983
  */
2967
- const diagnoseUnboundScope = async (actor, scope) => {
2984
+ const diagnoseUnboundScope = async (admin, actor, scope) => {
2968
2985
  const slug = scope.vertical;
2969
2986
  if (!slug)
2970
2987
  return 'scope has no vertical bound — nothing to deliver to';
2971
- const versions = await host.admin.listVersions(actor, slug).catch(() => []);
2988
+ const versions = await admin.listVersions(actor, slug).catch(() => []);
2972
2989
  const boundId = scope.verticalVersionId;
2973
2990
  if (boundId && !versions.some((v) => v.id === boundId)) {
2974
2991
  return (`scope is bound to version '${boundId}', which is not among vertical '${slug}'’s ${versions.length} ` +
@@ -2981,7 +2998,7 @@ export function createControlPlaneApi(options) {
2981
2998
  `pushes are on different slugs (a lineage fork). Push under '${slug}' by setting \`substrat.slug\`, ` +
2982
2999
  `or rebind the install to the slug the versions live under.`);
2983
3000
  }
2984
- const serving = await host.admin.verticalServing(actor, slug).catch(() => null);
3001
+ const serving = await admin.verticalServing(actor, slug).catch(() => null);
2985
3002
  if (!serving) {
2986
3003
  return `vertical '${slug}' has ${versions.length} version(s) but none is promoted to a serving channel — promote one to prod.`;
2987
3004
  }
@@ -2995,10 +3012,10 @@ export function createControlPlaneApi(options) {
2995
3012
  * `servingRef` is the route the scope takes in the same act (an adopt). The host's read also
2996
3013
  * refuses, whatever is acknowledged, a move that strands people a link bound (#2178).
2997
3014
  */
2998
- const refuseOnBreaks = async (actor, tenantId, scopeId, versionId, opts) => {
3015
+ const refuseOnBreaks = async (admin, actor, tenantId, scopeId, versionId, opts) => {
2999
3016
  // Asked even when acknowledged: the read also refuses a move that strands people a link
3000
3017
  // bound (#2178), which no acknowledgement passes.
3001
- const breaks = await host.admin.bindingImpact(actor, tenantId, scopeId, versionId, opts.servingRef !== undefined ? { servingRef: opts.servingRef } : undefined);
3018
+ const breaks = await admin.bindingImpact(actor, tenantId, scopeId, versionId, opts.servingRef !== undefined ? { servingRef: opts.servingRef } : undefined);
3002
3019
  if (opts.acknowledge?.exportBreak)
3003
3020
  return;
3004
3021
  if (breaks.length > 0)
@@ -3030,7 +3047,7 @@ export function createControlPlaneApi(options) {
3030
3047
  * as it stood before the failed attempt. Every caller still re-asserts after it (the
3031
3048
  * fallback), passing what this reports.
3032
3049
  */
3033
- const restoreCarryingSwitches = async (actor, dest, tenantId, scopeId, tables,
3050
+ const restoreCarryingSwitches = async (admin, actor, dest, tenantId, scopeId, tables,
3034
3051
  /** #1869: the scope `tables` were captured from — `scopeId` itself unless the copy moves —
3035
3052
  * and `exact` when the platform exported them itself rather than a caller supplying them.
3036
3053
  * #1722: `loadStamp`, the stamp a carry leaves on the copy it lands, and `expect`, the marker
@@ -3040,10 +3057,10 @@ export function createControlPlaneApi(options) {
3040
3057
  // the destination is not primary, the vertical marks it a copy in its own storage — a carry
3041
3058
  // of a copy made before every copy carried the marker brings none, and a CP-less host reads
3042
3059
  // the marker for primacy. Idempotent there; an install is never marked.
3043
- const rec = await host.admin.getScopeRecord(actor, tenantId, scopeId);
3060
+ const rec = await admin.getScopeRecord(actor, tenantId, scopeId);
3044
3061
  const markCopy = rec ? copyLineageOf(rec) : undefined;
3045
3062
  return retryTransient(async () => dest.restoreScope(tenantId, scopeId, tables, {
3046
- ...(await switchCarryFor(host.admin, actor, { tenantId, scopeId })),
3063
+ ...(await switchCarryFor(admin, actor, { tenantId, scopeId })),
3047
3064
  sourceScopeId: source.scopeId,
3048
3065
  exact: source.exact,
3049
3066
  loadStamp: source.loadStamp,
@@ -3077,7 +3094,7 @@ export function createControlPlaneApi(options) {
3077
3094
  }
3078
3095
  // #1756: onto the serving script, the scope runs the served version from the moment routing
3079
3096
  // flips — whatever its own version exported. Asked before the export below moves anything.
3080
- await refuseOnBreaks(actor, tenantId, scopeId, serving.versionId, {
3097
+ await refuseOnBreaks(c.var.admin, actor, tenantId, scopeId, serving.versionId, {
3081
3098
  servingRef: serving.ref,
3082
3099
  acknowledge: opts.acknowledge,
3083
3100
  });
@@ -3120,7 +3137,7 @@ export function createControlPlaneApi(options) {
3120
3137
  const serving = await c.var.admin.verticalServing(actor, slug);
3121
3138
  if (!serving)
3122
3139
  return none; // embedded / not dispatch-backed — the host cascade handled rebinds
3123
- const v = await verticalOf(actor, slug);
3140
+ const v = await verticalOf(c.var.admin, actor, slug);
3124
3141
  if (!v || v.ownerTenant === null || v.listed)
3125
3142
  return none; // private only, like the host cascade
3126
3143
  const owned = (await c.var.admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
@@ -3218,7 +3235,7 @@ export function createControlPlaneApi(options) {
3218
3235
  const { review, ...bindAck } = opts.acknowledge ?? {};
3219
3236
  // #1756: within one lineage this is an adopt, and judged as one before anything moves. A
3220
3237
  // crossing is not judged (`bindExportBreaksOf`): the producer's slug itself changes.
3221
- await refuseOnBreaks(actor, tenantId, scopeId, serving.versionId, {
3238
+ await refuseOnBreaks(c.var.admin, actor, tenantId, scopeId, serving.versionId, {
3222
3239
  servingRef: serving.ref,
3223
3240
  acknowledge: bindAck,
3224
3241
  });
@@ -3324,7 +3341,7 @@ export function createControlPlaneApi(options) {
3324
3341
  const erasureEpoch = await c.var.admin.scopeErasureEpoch(actor, tenantId, scopeId);
3325
3342
  // #2450: a page at a time into the destination's stage, so a scope of any size moves.
3326
3343
  const { dump, loadStamp: sourceStamp, revision: sourceRevision } = await transferScope(source, dest, { from: scopeId }, { retry: retryTransient });
3327
- const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump, {
3344
+ const restored = await restoreCarryingSwitches(c.var.admin, actor, dest, tenantId, scopeId, dump, {
3328
3345
  scopeId, exact: true, loadStamp: restoredStamp,
3329
3346
  ...(moveId ? { fence: copyRestoreFence(moveId, recordedAt, copyLeaseMs, erasureEpoch) } : {}),
3330
3347
  });
@@ -3749,7 +3766,7 @@ export function createControlPlaneApi(options) {
3749
3766
  throw new ControlPlaneError(412, `the copy of scope ${scope.id} in '${from}' was already carried to another script and wiped, so nothing ` +
3750
3767
  `was moved or bound (#1722). Reload the scope and retry.`);
3751
3768
  }
3752
- const restored = await restoreCarryingSwitches(actor, dest, scope.tenantId, scope.id, dump, {
3769
+ const restored = await restoreCarryingSwitches(c.var.admin, actor, dest, scope.tenantId, scope.id, dump, {
3753
3770
  scopeId: scope.id,
3754
3771
  exact: true,
3755
3772
  loadStamp: restoredStamp,
@@ -4116,7 +4133,7 @@ export function createControlPlaneApi(options) {
4116
4133
  await c.var.admin.recordScopeScriptCopy(actor, scope.tenantId, scope.id, carried.to, recarryId, { role: 'destination', loadStamp: restoredStamp, leaseMs: copyLeaseMs });
4117
4134
  try {
4118
4135
  const erasureEpoch = await c.var.admin.scopeErasureEpoch(actor, scope.tenantId, scope.id);
4119
- await restoreCarryingSwitches(actor, carried.dest, scope.tenantId, scope.id, dump, {
4136
+ await restoreCarryingSwitches(c.var.admin, actor, carried.dest, scope.tenantId, scope.id, dump, {
4120
4137
  scopeId: scope.id,
4121
4138
  exact: true,
4122
4139
  loadStamp: restoredStamp,
@@ -4282,13 +4299,13 @@ export function createControlPlaneApi(options) {
4282
4299
  const p = c.get('principal');
4283
4300
  const scopePin = confinedTenant(p);
4284
4301
  if (scopePin) {
4285
- const v = scope.vertical ? await verticalOf(actor, scope.vertical) : undefined;
4302
+ const v = scope.vertical ? await verticalOf(c.var.admin, actor, scope.vertical) : undefined;
4286
4303
  if (!v || v.ownerTenant !== scopePin)
4287
4304
  return refuse(c, 'forbidden', 'forbidden');
4288
4305
  }
4289
4306
  const reached = await deploymentForScope(c, scope);
4290
4307
  if (!reached)
4291
- return refuseStatus(c, 501, await diagnoseUnboundScope(c.get('actor'), scope));
4308
+ return refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, c.get('actor'), scope));
4292
4309
  const vertical = reached.client;
4293
4310
  const entitlements = await c.var.admin.listEntitlements(actor, tenantId);
4294
4311
  // #406: re-gathered and re-delivered like entitlements, so a reconcile also repairs a
@@ -4831,7 +4848,7 @@ export function createControlPlaneApi(options) {
4831
4848
  return refuse(c, 'not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
4832
4849
  const vertical = await verticalForScope(c, scope);
4833
4850
  if (!vertical)
4834
- return refuseStatus(c, 501, await diagnoseUnboundScope(c.get('actor'), scope));
4851
+ return refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, c.get('actor'), scope));
4835
4852
  try {
4836
4853
  return c.json(await vertical.ownerSeat(tenantId, scopeId));
4837
4854
  }
@@ -4867,7 +4884,7 @@ export function createControlPlaneApi(options) {
4867
4884
  return refuse(c, 'not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
4868
4885
  const vertical = await verticalForScope(c, scope);
4869
4886
  if (!vertical)
4870
- return refuseStatus(c, 501, await diagnoseUnboundScope(actor, scope));
4887
+ return refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, actor, scope));
4871
4888
  const origin = await instanceOrigin(c.var.admin, actor, scopeId);
4872
4889
  if (!origin) {
4873
4890
  return refuse(c, 'conflict', `scope ${scopeId} has no hostname bound — a claim link needs an address to open on`);
@@ -4908,7 +4925,7 @@ export function createControlPlaneApi(options) {
4908
4925
  return { refusal: refuse(c, 'not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`) };
4909
4926
  const vertical = await verticalForScope(c, scope);
4910
4927
  if (!vertical)
4911
- return { refusal: refuseStatus(c, 501, await diagnoseUnboundScope(actor, scope)) };
4928
+ return { refusal: refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, actor, scope)) };
4912
4929
  return { tenantId, scopeId, actor, vertical, person: memberPerson(principal) };
4913
4930
  };
4914
4931
  /**
@@ -5028,7 +5045,7 @@ export function createControlPlaneApi(options) {
5028
5045
  return refuse(c, 'not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
5029
5046
  const vertical = await verticalForScope(c, scope);
5030
5047
  if (!vertical)
5031
- return refuseStatus(c, 501, await diagnoseUnboundScope(actor, scope));
5048
+ return refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, actor, scope));
5032
5049
  const operationId = ulid();
5033
5050
  // An abandon (`abandon: true`) is audited the same way, every row marked, so the log tells
5034
5051
  // a hand-over closed without finishing from one that finished.
@@ -5064,7 +5081,7 @@ export function createControlPlaneApi(options) {
5064
5081
  return refuse(c, 'not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
5065
5082
  const vertical = await verticalForScope(c, scope);
5066
5083
  if (!vertical) {
5067
- return refuseStatus(c, 501, await diagnoseUnboundScope(c.get('actor'), scope));
5084
+ return refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, c.get('actor'), scope));
5068
5085
  }
5069
5086
  try {
5070
5087
  await vertical.configureInstance({ tenantId, scopeId, entries: input.entries });
@@ -5217,12 +5234,17 @@ export function createControlPlaneApi(options) {
5217
5234
  }
5218
5235
  await retryTransient(() => copier.snapshotScope({ sourceScopeId: scope.id, newScopeId: snapId, tenantId }));
5219
5236
  // The scope file was copied inside the script. The store objects are per scope, so the
5220
- // fork gets its own copy through the reserved export and import, signed by the
5221
- // control plane. A version that declares no stores has nothing to copy.
5237
+ // fork gets its own copy, page by page from the source's store into a stage at the fork's,
5238
+ // signed by the control plane (D-69); every store is staged before any is committed. A
5239
+ // version that declares no stores has nothing to copy.
5222
5240
  if (reach) {
5223
- const stores = await reach.caller.exportStores({ tenantId, scopeId: scope.id, vertical: reach.vertical, stores: reach.stores });
5224
- if (stores.length > 0) {
5225
- await reach.caller.importStores({ tenantId, scopeId: snapId, vertical: reach.vertical, stores });
5241
+ const from = { tenantId, scopeId: scope.id, vertical: reach.vertical };
5242
+ const to = { ...from, scopeId: snapId };
5243
+ const moved = await moveStores(snapshotEnds(reach.caller, from, to), reach.stores, `scope ${scope.id}'s stores`, options.storeMoveBudget);
5244
+ try {
5245
+ await commitStages(reach.caller, to, moved, retryTransient);
5246
+ }
5247
+ finally {
5226
5248
  replay = await replayPlatformStoreErasures(c, { ...scope, id: snapId }, scope.id);
5227
5249
  }
5228
5250
  }
@@ -5404,9 +5426,11 @@ export function createControlPlaneApi(options) {
5404
5426
  const { tables: hostTables, ...header } = await c.var.admin.exportScope(c.get('actor'), tenantId, scope.id);
5405
5427
  const vertical = await verticalForScope(c, scope);
5406
5428
  // Store tables are opaque and stay unsealed. The platform reads them from the store script
5407
- // (#2207); a store it cannot name or reach refuses the backup rather than omitting it.
5408
- const stores = (await exportPlatformStores(c, scope)) ?? header.stores;
5409
- const backup = backupStage(store.begin({ vertical: scope.vertical, header: { ...header, stores } }), sealerFor(c, tenantId, scope.id));
5429
+ // (#2207), a page at a time into entries of their own (D-69); a store it cannot name or reach
5430
+ // refuses the backup rather than omitting it. Resolved before anything is written.
5431
+ const reach = await platformStoreReach(c, scope);
5432
+ const writer = store.begin({ vertical: scope.vertical, header: { ...header, stores: reach ? [] : header.stores } });
5433
+ const backup = backupStage(writer, sealerFor(c, tenantId, scope.id));
5410
5434
  // #2839: from a vertical, the data moves into the store a page at a time, as a paged copy
5411
5435
  // moves it into another scope (#2450): captured, so a scope taking writes meanwhile is still
5412
5436
  // copied as it was at one moment, and never held whole here. A deployment that predates
@@ -5416,11 +5440,18 @@ export function createControlPlaneApi(options) {
5416
5440
  : hostTables;
5417
5441
  if (Array.isArray(moved))
5418
5442
  await backup.dest.stageScopePage(scope.id, ulid(), 0, moved);
5443
+ // The scope, then its stores: two objects, so not one moment (D-69, question 3).
5444
+ let stores = [];
5419
5445
  try {
5420
- return await backup.commit();
5446
+ if (reach) {
5447
+ stores = await moveStores(backupEnds(reach.caller, reach.place, writer), reach.stores, `scope ${scope.id}'s stores`, options.storeMoveBudget);
5448
+ }
5449
+ return await backup.commit(stores.map((s) => ({ binding: s.binding, driver: s.driver, stageId: s.moveId, entries: s.pages, revision: s.revision })));
5421
5450
  }
5422
5451
  catch (e) {
5423
5452
  await backup.discard();
5453
+ for (const s of stores)
5454
+ await writer.discard(s.moveId).catch(() => undefined);
5424
5455
  throw e;
5425
5456
  }
5426
5457
  };
@@ -5459,7 +5490,7 @@ export function createControlPlaneApi(options) {
5459
5490
  },
5460
5491
  discardStagedScope: (_, stageId) => writer.discard(stageId),
5461
5492
  },
5462
- commit: () => writer.commit({ stageId: last.stageId, entries: last.entries, tables: last.tables.size }),
5493
+ commit: (storeEntries) => writer.commit({ stageId: last.stageId, entries: last.entries, tables: last.tables.size, storeEntries }),
5463
5494
  discard: () => writer.discard(last.stageId),
5464
5495
  };
5465
5496
  };
@@ -5499,6 +5530,16 @@ export function createControlPlaneApi(options) {
5499
5530
  if (held.entries > 1) {
5500
5531
  return refuseStatus(c, 413, `backup ${capturedAt} of scope ${scopeId} is stored in ${held.entries} entries (its pages, and any writes caught while it was taken), which only a restore applies; restore it with POST /tenants/${tenantId}/scopes/${scopeId}/backups/${capturedAt}/restore`);
5501
5532
  }
5533
+ // A store held in one page is served inline, as `stores` always was; one held in more is a
5534
+ // restore's to apply (D-69), so the backup is refused whole rather than handed back in part.
5535
+ const paged = held.storeEntries.find((store) => store.entries > 1);
5536
+ if (paged) {
5537
+ return refuseStatus(c, 413, `backup ${capturedAt} of scope ${scopeId} holds scope store '${paged.binding}' in ${paged.entries} pages, which only a restore applies; restore it with POST /tenants/${tenantId}/scopes/${scopeId}/backups/${capturedAt}/restore`);
5538
+ }
5539
+ const pagedStores = [];
5540
+ for (const store of held.storeEntries) {
5541
+ pagedStores.push({ binding: store.binding, driver: store.driver, tables: mergeDumpPages([await held.storePage(store, 0)]) });
5542
+ }
5502
5543
  const { tables } = await held.entry(0);
5503
5544
  // Unseal on the way out (#37). The bytes in the store stay sealed — this is the
5504
5545
  // authorized read opening them with the keys the platform holds. A subject shredded
@@ -5506,7 +5547,7 @@ export function createControlPlaneApi(options) {
5506
5547
  // working: the ciphertext never left the object, and nothing turns it back into a
5507
5548
  // person. A dump taken before sealing existed passes through untouched.
5508
5549
  const opened = await openDump(mergeDumpPages([tables]), sealerFor(c, tenantId, scopeId));
5509
- return c.json({ ...held.header, tables: opened });
5550
+ return c.json({ ...held.header, stores: [...held.header.stores, ...pagedStores], tables: opened });
5510
5551
  });
5511
5552
  // Take a backup WITHOUT reaping — the standalone copy (a pre-migration checkpoint, an
5512
5553
  // export-to-keep). The reap route below takes its own; this is the same act made
@@ -5917,7 +5958,7 @@ export function createControlPlaneApi(options) {
5917
5958
  }
5918
5959
  const vertical = await verticalForScope(c, scope);
5919
5960
  if (!vertical)
5920
- return refuseStatus(c, 501, await diagnoseUnboundScope(actor, scope));
5961
+ return refuseStatus(c, 501, await diagnoseUnboundScope(c.var.admin, actor, scope));
5921
5962
  const { cleared } = await vertical.clearCopyMark(scopeId, { kind: scope.kind, forkedFrom: scope.forkedFrom });
5922
5963
  await c.var.admin.recordCopyMark(actor, { action: 'clear', tenantId, scopeId, outcome: cleared ? 'cleared' : 'absent' });
5923
5964
  return c.json({ cleared });
@@ -6381,9 +6422,15 @@ export function createControlPlaneApi(options) {
6381
6422
  if (!held)
6382
6423
  return refuse(c, 'not_found', `no backup for scope ${scopeId} at ${capturedAt}`);
6383
6424
  const { header } = held;
6425
+ // A backup taken since D-69 holds its stores as store pages; one taken before, inline in its header.
6426
+ const pagedStores = held.storeEntries.map((entry) => ({
6427
+ binding: entry.binding,
6428
+ driver: entry.driver,
6429
+ source: { pages: entry.entries, revision: entry.revision, page: (seq) => held.storePage(entry, seq) },
6430
+ }));
6384
6431
  if (options.platformScopeStores) {
6385
6432
  try {
6386
- await refuseUndeclaredDumpStores(c, scope, header.stores);
6433
+ await refuseUndeclaredDumpStores(c, scope, [...header.stores, ...pagedStores]);
6387
6434
  }
6388
6435
  catch (e) {
6389
6436
  if (e instanceof ControlPlaneError)
@@ -6398,7 +6445,7 @@ export function createControlPlaneApi(options) {
6398
6445
  if (tables.length === 0) {
6399
6446
  return refuseStatus(c, 422, 'restore refused: the backup has no tables — not a scope dump');
6400
6447
  }
6401
- return c.json(await loadIntoScope(c, scope, { header, tables: await openDump(mergeDumpPages([tables]), sealer) }));
6448
+ return c.json(await loadIntoScope(c, scope, { header, tables: await openDump(mergeDumpPages([tables]), sealer), pagedStores }));
6402
6449
  }
6403
6450
  // The host judges the roles the backup seats or defines before anything is replaced (#2555), and
6404
6451
  // records the tables it restores. So it is handed every table's schema and the role tables' rows,
@@ -6432,7 +6479,7 @@ export function createControlPlaneApi(options) {
6432
6479
  }
6433
6480
  return { dump: { stageId }, tables: judged.tables() };
6434
6481
  };
6435
- return c.json(await loadIntoScope(c, scope, { header, tables: [], stage }));
6482
+ return c.json(await loadIntoScope(c, scope, { header, tables: [], stage, pagedStores }));
6436
6483
  }
6437
6484
  catch (e) {
6438
6485
  return restoreFailed(c, scope, e);
@@ -6461,32 +6508,55 @@ export function createControlPlaneApi(options) {
6461
6508
  // #2185: cleared before the load as well as after it (below): a step after the load can still
6462
6509
  // throw (the deployment's half, the re-assert), and the data that landed may predate a reconcile.
6463
6510
  await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, null);
6464
- const staged = input.stage && vertical ? await input.stage(vertical) : undefined;
6465
- const tables = staged?.tables ?? input.tables;
6511
+ // D-69: every store is staged first, then the scope is loaded, then the stores are committed,
6512
+ // then erasures are replayed. Until the first commit nothing of a store has changed, so a page
6513
+ // that cannot be read, a refused stage or a refused load leaves every store as it was. Once a
6514
+ // commit was attempted the replay runs whatever follows, and a restore run again converges.
6515
+ const storesCaller = options.platformScopeStores;
6516
+ const platform = storesCaller && vertical && scope.vertical
6517
+ ? { caller: storesCaller, place: { tenantId, scopeId, vertical: scope.vertical } }
6518
+ : undefined;
6519
+ const pagedStores = input.pagedStores ?? [];
6520
+ if (!platform && pagedStores.length > 0) {
6521
+ throw new ControlPlaneError(409, `the backup holds scope stores in pages, and scope ${scopeId} has no deployment and no route to its stores to load them into`);
6522
+ }
6523
+ // With a route to the stores, an inline store moves as a paged one does; without one the host carries it.
6524
+ const stores = platform ? [...inlineStoreSources(header.stores), ...pagedStores] : [];
6525
+ const sources = new Map(stores.map((store) => [store.binding, store.source]));
6526
+ const storeEnds = platform && restoreEnds(platform.caller, platform.place, (binding) => sources.get(binding));
6527
+ const movedStores = storeEnds && stores.length > 0
6528
+ ? await moveStores(storeEnds, stores.map(({ binding, driver }) => ({ binding, driver })), `the backup's stores for scope ${scopeId}`, options.storeMoveBudget)
6529
+ : [];
6530
+ let staged;
6531
+ let committing = false;
6466
6532
  try {
6467
- const storesCaller = options.platformScopeStores;
6468
- const landing = { ...header, tables, stores: storesCaller && vertical ? [] : header.stores };
6533
+ staged = input.stage && vertical ? await input.stage(vertical) : undefined;
6534
+ const tables = staged?.tables ?? input.tables;
6535
+ const landing = { ...header, tables, stores: platform ? [] : header.stores };
6469
6536
  const hostReplay = await c.var.host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
6470
- if (storesCaller && vertical && scope.vertical && header.stores.length > 0) {
6471
- await storesCaller.importStores({
6472
- tenantId,
6473
- scopeId,
6474
- vertical: scope.vertical,
6475
- stores: header.stores,
6476
- });
6477
- }
6478
- const platformReplay = storesCaller && scope.vertical
6479
- ? await replayPlatformStoreErasures(c, scope, sourceHint ?? origin)
6480
- : undefined;
6481
- const replays = [hostReplay, platformReplay].filter((r) => !!r);
6482
6537
  // #1742: the recorded OFF positions ride the restore, so the deployment switches them
6483
6538
  // off in the replay's own event — its own sweeper cannot land in between.
6484
6539
  const restored = vertical
6485
- ? await restoreCarryingSwitches(actor, vertical, tenantId, scopeId, staged?.dump ?? tables, {
6540
+ ? await restoreCarryingSwitches(c.var.admin, actor, vertical, tenantId, scopeId, staged?.dump ?? tables, {
6486
6541
  scopeId: sourceHint ?? origin,
6487
6542
  exact: false,
6488
6543
  })
6489
6544
  : undefined;
6545
+ let landFailure;
6546
+ if (platform && movedStores.length > 0) {
6547
+ committing = true;
6548
+ await commitStages(platform.caller, platform.place, movedStores, retryTransient).catch((e) => {
6549
+ if (!(e instanceof StoreLandFailure))
6550
+ throw e;
6551
+ landFailure = e;
6552
+ });
6553
+ }
6554
+ const platformReplay = storesCaller && scope.vertical
6555
+ ? await replayPlatformStoreErasures(c, scope, sourceHint ?? origin)
6556
+ : undefined;
6557
+ if (landFailure)
6558
+ throw landFailure;
6559
+ const replays = [hostReplay, platformReplay].filter((r) => !!r);
6490
6560
  // #1674: a backup from before a module was switched off brings it back on; the
6491
6561
  // directory's record puts it back off now, not at the next reconcile. Where the
6492
6562
  // deployment already did (#1742), this finds it done and audits what it reported.
@@ -6506,12 +6576,17 @@ export function createControlPlaneApi(options) {
6506
6576
  };
6507
6577
  }
6508
6578
  catch (e) {
6509
- // A stage the refused load left at the deployment goes now rather than waiting for a wipe.
6579
+ // A stage the refused load left at the deployment goes now rather than waiting for a wipe,
6580
+ // and so do the stores' stages, none of which was committed.
6510
6581
  if (staged && vertical)
6511
6582
  await abandonCopy({ dest: vertical }, { from: scopeId, into: scopeId, stageId: staged.dump.stageId });
6583
+ if (storeEnds && !committing)
6584
+ await discardMoves(storeEnds, movedStores);
6512
6585
  throw e;
6513
6586
  }
6514
6587
  };
6588
+ /** A dump's inline stores as restore sources, each cut at its driver's page size (D-69). */
6589
+ const inlineStoreSources = (stores) => stores.map((store) => ({ binding: store.binding, driver: store.driver, source: inlineStoreSource(store) }));
6515
6590
  /** How a refused or failed restore answers, on either route. */
6516
6591
  const restoreFailed = async (c, scope, e) => {
6517
6592
  // #2185: and once more on the way out of a failure, for a sweep reconcile that wrote the
@@ -6633,11 +6708,11 @@ export function createControlPlaneApi(options) {
6633
6708
  const actor = c.get('actor');
6634
6709
  const adoptPin = confinedTenant(p);
6635
6710
  if (adoptPin) {
6636
- const v = await verticalOf(actor, slug);
6711
+ const v = await verticalOf(c.var.admin, actor, slug);
6637
6712
  if (!v || v.ownerTenant !== adoptPin)
6638
6713
  return refuse(c, 'forbidden', 'forbidden');
6639
6714
  }
6640
- const v = await verticalOf(actor, slug);
6715
+ const v = await verticalOf(c.var.admin, actor, slug);
6641
6716
  if (!v)
6642
6717
  return refuse(c, 'not_found', `unknown vertical '${slug}'`);
6643
6718
  if (v.ownerTenant === null) {
@@ -6943,7 +7018,7 @@ export function createControlPlaneApi(options) {
6943
7018
  // #2185: before the load as well as after it, so a throw once the kept copy has landed (the
6944
7019
  // re-assert) cannot leave the receipt set over older data.
6945
7020
  await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, null);
6946
- const restored = await restoreCarryingSwitches(actor, live, tenantId, scopeId, dump, {
7021
+ const restored = await restoreCarryingSwitches(c.var.admin, actor, live, tenantId, scopeId, dump, {
6947
7022
  scopeId,
6948
7023
  exact: true,
6949
7024
  loadStamp: ulid(),
@@ -7010,7 +7085,7 @@ export function createControlPlaneApi(options) {
7010
7085
  // first (a version not admitted) the same way. An unknown scope has no impact to read, and
7011
7086
  // the bind below refuses it as it always has.
7012
7087
  if (scope)
7013
- await refuseOnBreaks(actor, tenantId, scopeId, versionId, { acknowledge });
7088
+ await refuseOnBreaks(c.var.admin, actor, tenantId, scopeId, versionId, { acknowledge });
7014
7089
  // A refusal throws a ControlPlaneError, which the app's error boundary relays with its
7015
7090
  // status and records as an ops failure. An unknown scope carries nothing, and the bind
7016
7091
  // below refuses it as it always has.
@@ -7056,7 +7131,7 @@ export function createControlPlaneApi(options) {
7056
7131
  try {
7057
7132
  // Asked again after the carry: a promote landing since the first question would
7058
7133
  // otherwise leave an archive behind for a bind the host then refuses.
7059
- await refuseOnBreaks(actor, tenantId, scopeId, versionId, { acknowledge });
7134
+ await refuseOnBreaks(c.var.admin, actor, tenantId, scopeId, versionId, { acknowledge });
7060
7135
  await orchestratedSnapshot(c, tenantId, scope, {});
7061
7136
  }
7062
7137
  catch (e) {
@@ -7252,14 +7327,14 @@ export function createControlPlaneApi(options) {
7252
7327
  // registry row's owner_tenant (undefined ⇒ not registered — a claimable slug).
7253
7328
  // `listVerticals` returns every row (the adapter does not narrow); the transport is
7254
7329
  // where per-principal filtering happens — here and on GET /verticals.
7255
- const ownerOf = async (actor, slug) => {
7256
- const v = (await host.admin.listVerticals(actor)).find((x) => x.slug === slug);
7330
+ const ownerOf = async (admin, actor, slug) => {
7331
+ const v = (await admin.listVerticals(actor)).find((x) => x.slug === slug);
7257
7332
  return v ? v.ownerTenant : undefined;
7258
7333
  };
7259
7334
  // The full registry row, for the checks that need more than the owner — whether the
7260
7335
  // vertical is PRIVATE (owned + not listed), which is what scopes a builder's prod
7261
7336
  // self-serve below.
7262
- const verticalOf = async (actor, slug) => (await host.admin.listVerticals(actor)).find((x) => x.slug === slug);
7337
+ const verticalOf = async (admin, actor, slug) => (await admin.listVerticals(actor)).find((x) => x.slug === slug);
7263
7338
  /**
7264
7339
  * "This principal is confined, and this vertical is not its tenant's." The one
7265
7340
  * predicate every slug-addressed route asks.
@@ -7271,7 +7346,7 @@ export function createControlPlaneApi(options) {
7271
7346
  */
7272
7347
  const notOwned = async (p, slug) => {
7273
7348
  const pin = confinedTenant(p);
7274
- return pin !== null && (await ownerOf(p.actor, slug)) !== pin;
7349
+ return pin !== null && (await ownerOf(adminFor(p), p.actor, slug)) !== pin;
7275
7350
  };
7276
7351
  /**
7277
7352
  * The READ twin of `notOwned`, for the routes an installed app's own pages ask
@@ -7284,7 +7359,7 @@ export function createControlPlaneApi(options) {
7284
7359
  * the prod history).
7285
7360
  */
7286
7361
  /** The bare slug of every registered vertical: what a manager's SKU may not be (#2388). */
7287
- const verticalSlugsOf = async (actor) => (await host.admin.listVerticals(actor)).map((v) => bareSlug(v.slug));
7362
+ const verticalSlugsOf = async (admin, actor) => (await admin.listVerticals(actor)).map((v) => bareSlug(v.slug));
7288
7363
  /**
7289
7364
  * Whether a CONFINED principal may grant its own tenant `key` (#977): some vertical it
7290
7365
  * can read — its own, or a listed one — declares the key, or is the vertical whose
@@ -7299,8 +7374,8 @@ export function createControlPlaneApi(options) {
7299
7374
  * the first push). Its own slug and the engine keys are always declarable, so they reserve nothing.
7300
7375
  * Only a new slug is judged; the sentence names no other vertical.
7301
7376
  */
7302
- const slugClaimsSku = async (actor, slug) => {
7303
- const all = await host.admin.listVerticals(actor);
7377
+ const slugClaimsSku = async (admin, actor, slug) => {
7378
+ const all = await admin.listVerticals(actor);
7304
7379
  if (all.some((v) => v.slug === slug))
7305
7380
  return undefined;
7306
7381
  const name = bareSlug(slug);
@@ -7317,7 +7392,7 @@ export function createControlPlaneApi(options) {
7317
7392
  const pin = confinedTenant(p);
7318
7393
  if (pin === null)
7319
7394
  return true;
7320
- const all = await host.admin.listVerticals(p.actor);
7395
+ const all = await adminFor(p).listVerticals(p.actor);
7321
7396
  const slugs = all.map((v) => bareSlug(v.slug));
7322
7397
  const visible = all.filter((v) => v.ownerTenant === pin || v.listed);
7323
7398
  return visible.some((v) => {
@@ -7331,7 +7406,7 @@ export function createControlPlaneApi(options) {
7331
7406
  const pin = confinedTenant(p);
7332
7407
  if (pin === null)
7333
7408
  return false;
7334
- const v = await verticalOf(p.actor, slug);
7409
+ const v = await verticalOf(adminFor(p), p.actor, slug);
7335
7410
  return !v || (v.ownerTenant !== pin && !v.listed);
7336
7411
  };
7337
7412
  /**
@@ -7377,10 +7452,10 @@ export function createControlPlaneApi(options) {
7377
7452
  const workspace = (await c.var.admin.listTenants(actor)).find((t) => t.slug === pin || t.id === pin);
7378
7453
  if (!workspace || raw.startsWith(`${workspace.slug}/`))
7379
7454
  return raw;
7380
- if ((await ownerOf(actor, raw)) === workspace.id)
7455
+ if ((await ownerOf(c.var.admin, actor, raw)) === workspace.id)
7381
7456
  return raw;
7382
7457
  const prefixed = `${workspace.slug}/${raw}`;
7383
- return (await ownerOf(actor, prefixed)) !== undefined ? prefixed : raw;
7458
+ return (await ownerOf(c.var.admin, actor, prefixed)) !== undefined ? prefixed : raw;
7384
7459
  };
7385
7460
  app.get('/verticals', async (c) => {
7386
7461
  const page = pageParams(c);
@@ -7428,13 +7503,13 @@ export function createControlPlaneApi(options) {
7428
7503
  * same enumeration, and #977's observability narrowing must not be a second copy of
7429
7504
  * it that can drift when a third ref scheme appears.
7430
7505
  */
7431
- const tenantServiceRefs = async (actor, tenantId, slug) => {
7432
- const all = (await host.admin.listVerticals(actor)).filter((v) => v.ownerTenant === tenantId);
7506
+ const tenantServiceRefs = async (admin, actor, tenantId, slug) => {
7507
+ const all = (await admin.listVerticals(actor)).filter((v) => v.ownerTenant === tenantId);
7433
7508
  const considered = slug === undefined ? all : all.filter((v) => v.slug === slug);
7434
7509
  const verticalsTruncated = considered.length > MAX_SERVICE_REF_VERTICALS;
7435
7510
  const entries = [];
7436
7511
  for (const v of considered.slice(0, MAX_SERVICE_REF_VERTICALS)) {
7437
- const versions = await host.admin.listVersions(actor, v.slug);
7512
+ const versions = await admin.listVersions(actor, v.slug);
7438
7513
  const labelOf = new Map(versions.map((ver) => [ver.id, ver.version]));
7439
7514
  // The stable serving script — where real traffic lands. Its version is the
7440
7515
  // registry row's own servingVersionId: authoritative, not the scope-derived
@@ -7474,7 +7549,7 @@ export function createControlPlaneApi(options) {
7474
7549
  const pin = confinedTenant(p);
7475
7550
  if (pin === null)
7476
7551
  return null;
7477
- const refs = await tenantServiceRefs(actor, pin);
7552
+ const refs = await tenantServiceRefs(adminFor(p), actor, pin);
7478
7553
  if (refs.verticalsTruncated) {
7479
7554
  throw new ControlPlaneError(503, 'service map truncated: too many verticals to resolve in one answer');
7480
7555
  }
@@ -7493,7 +7568,7 @@ export function createControlPlaneApi(options) {
7493
7568
  .parse({ tenantId, vertical: c.req.query('vertical') });
7494
7569
  const actor = c.get('actor');
7495
7570
  const slug = query.vertical === undefined ? undefined : await resolveVerticalId(c, query.vertical);
7496
- const { entries, verticalsTruncated, matched } = await tenantServiceRefs(actor, query.tenantId, slug);
7571
+ const { entries, verticalsTruncated, matched } = await tenantServiceRefs(c.var.admin, actor, query.tenantId, slug);
7497
7572
  // Non-ownership reads as absence (K-3), exactly like the per-slug registry reads.
7498
7573
  if (slug !== undefined && matched === 0)
7499
7574
  throw new ControlPlaneError(404, 'not found');
@@ -7508,17 +7583,17 @@ export function createControlPlaneApi(options) {
7508
7583
  // the prefix, backstopped by the ownership check). Owner + prefix come from the
7509
7584
  // principal, never trusted from the body.
7510
7585
  input.slug = effectiveSlug(p, input.slug);
7511
- const owner = await ownerOf(p.actor, input.slug);
7586
+ const owner = await ownerOf(adminFor(p), p.actor, input.slug);
7512
7587
  if (owner !== undefined && owner !== p.tenantId)
7513
7588
  return refuse(c, 'forbidden', 'forbidden');
7514
7589
  input.ownerTenant = p.tenantId;
7515
7590
  }
7516
- const claimed = await slugClaimsSku(c.get('actor'), input.slug);
7591
+ const claimed = await slugClaimsSku(c.var.admin, c.get('actor'), input.slug);
7517
7592
  if (claimed)
7518
7593
  return refuseStatus(c, 422, claimed);
7519
7594
  // #2388: the same declaration rule as a push, for any principal that is not staff.
7520
7595
  if (p.kind !== 'staff') {
7521
- const foreignKey = entitlementNamespaceRefusal(input.slug, input.entitlements ?? [], input.provisions?.length ? await verticalSlugsOf(c.get('actor')) : undefined);
7596
+ const foreignKey = entitlementNamespaceRefusal(input.slug, input.entitlements ?? [], input.provisions?.length ? await verticalSlugsOf(c.var.admin, c.get('actor')) : undefined);
7522
7597
  if (foreignKey)
7523
7598
  return refuseStatus(c, 422, foreignKey);
7524
7599
  }
@@ -7606,7 +7681,7 @@ export function createControlPlaneApi(options) {
7606
7681
  // (backup, wipe, erasure, the meter), so its store scripts are put in place first, as a push
7607
7682
  // does: a driver with no approved release refuses here, and nothing is published. The publish is
7608
7683
  // made under the push's shared hold (#2816), so a rollout walks this vertical or waits for it.
7609
- await ensureScopeStoresOf(c.get('actor'), 'deploy', slug, scopeStores, async () => (await ownerOf(c.get('actor'), slug)) ?? null, async (renew) => {
7684
+ await ensureScopeStoresOf(c.var.admin, c.get('actor'), 'deploy', slug, scopeStores, async () => (await ownerOf(c.var.admin, c.get('actor'), slug)) ?? null, async (renew) => {
7610
7685
  await renew();
7611
7686
  await c.var.admin.publishVersion(c.get('actor'), input);
7612
7687
  });
@@ -7827,7 +7902,7 @@ export function createControlPlaneApi(options) {
7827
7902
  const slug = c.req.param('slug');
7828
7903
  if (await notOwned(c.get('principal'), slug))
7829
7904
  return refuse(c, 'forbidden', 'forbidden');
7830
- const vertical = await verticalOf(c.get('actor'), slug);
7905
+ const vertical = await verticalOf(c.var.admin, c.get('actor'), slug);
7831
7906
  if (!vertical)
7832
7907
  return refuse(c, 'not_found', 'vertical not found');
7833
7908
  return c.json({ slug, emailSender: !!vertical.emailSender });
@@ -7913,7 +7988,7 @@ export function createControlPlaneApi(options) {
7913
7988
  * (409) once one has lapsed; `then` calls it right before it records. A failure in `then` is not
7914
7989
  * recorded here: it is the caller's to answer.
7915
7990
  */
7916
- const ensureScopeStoresOf = async (actor, operation, slug, stores, owner, then = async () => undefined) => {
7991
+ const ensureScopeStoresOf = async (admin, actor, operation, slug, stores, owner, then = async () => undefined) => {
7917
7992
  if (stores.length === 0)
7918
7993
  return then(async () => undefined);
7919
7994
  // D-67: a driver whose registration names its verticals serves those only, refused before any
@@ -7929,7 +8004,7 @@ export function createControlPlaneApi(options) {
7929
8004
  const held = [];
7930
8005
  const renewHolds = async (when) => {
7931
8006
  for (const each of held) {
7932
- if (!(await host.admin.holdStoreDriver(actor, { driver: each, holder, mode: 'shared', ttlMs: PUSH_HOLD_TTL_MS, renew: true }))) {
8007
+ if (!(await admin.holdStoreDriver(actor, { driver: each, holder, mode: 'shared', ttlMs: PUSH_HOLD_TTL_MS, renew: true }))) {
7933
8008
  throw new ControlPlaneError(409, `${operation} refused: the hold on scope store driver '${each}' lapsed ${when}; try again`);
7934
8009
  }
7935
8010
  }
@@ -7944,13 +8019,13 @@ export function createControlPlaneApi(options) {
7944
8019
  // a script's release before a rollout moved it would upload the old release back over it.
7945
8020
  // Neither the refusal nor anything else here names another vertical: a push answers its own.
7946
8021
  for (const driver of drivers) {
7947
- if (!(await host.admin.holdStoreDriver(actor, { driver, holder, mode: 'shared', ttlMs: PUSH_HOLD_TTL_MS }))) {
8022
+ if (!(await admin.holdStoreDriver(actor, { driver, holder, mode: 'shared', ttlMs: PUSH_HOLD_TTL_MS }))) {
7948
8023
  throw new ControlPlaneError(409, `${operation} refused: scope store driver '${driver}' is being rolled out; try again in a few minutes`);
7949
8024
  }
7950
8025
  held.push(driver);
7951
8026
  }
7952
8027
  const loadRelease = options.storeReleases
7953
- ? storedReleaseLoader({ admin: host.admin, actor, bytes: options.storeReleases.bytes })
8028
+ ? storedReleaseLoader({ admin, actor, bytes: options.storeReleases.bytes })
7954
8029
  : async () => undefined;
7955
8030
  // One driver at a time, each renewing every hold first: a push whose uploads outlast a hold
7956
8031
  // stops (409) rather than upload after a rollout could have taken the driver.
@@ -7961,7 +8036,7 @@ export function createControlPlaneApi(options) {
7961
8036
  throw new ControlPlaneError(403, `scope store driver '${driver}' is not registered`);
7962
8037
  // Read under the shared hold (D-65): a revoke takes the driver exclusively, so one written
7963
8038
  // before this read is seen, and none is written until this upload is done.
7964
- const approvals = await readStoreDriverApprovals(host.admin, actor, registration);
8039
+ const approvals = await readStoreDriverApprovals(admin, actor, registration);
7965
8040
  await options.ensureScopeStores({ vertical: slug, stores: stores.filter((store) => store.driver === driver), loadRelease, approvals });
7966
8041
  }
7967
8042
  }
@@ -7981,7 +8056,7 @@ export function createControlPlaneApi(options) {
7981
8056
  }
7982
8057
  finally {
7983
8058
  for (const driver of held)
7984
- await host.admin.releaseStoreDriverHold(actor, { driver, holder }).catch(() => undefined);
8059
+ await admin.releaseStoreDriverHold(actor, { driver, holder }).catch(() => undefined);
7985
8060
  }
7986
8061
  };
7987
8062
  /**
@@ -8086,12 +8161,12 @@ export function createControlPlaneApi(options) {
8086
8161
  const serveVersionInPlace = async (admin, actor, slug, versionId) => {
8087
8162
  if (!options.deployVertical || !options.fetchVerticalModules)
8088
8163
  return; // not configured: pre-#286 behavior
8089
- const version = await host.admin.getVersion(actor, versionId, slug);
8164
+ const version = await admin.getVersion(actor, versionId, slug);
8090
8165
  if (!version?.deploymentRef) {
8091
8166
  throw new ControlPlaneError(502, `version ${versionId} has no archive script to serve from`);
8092
8167
  }
8093
8168
  const archiveRef = version.deploymentRef;
8094
- const manifestJson = await host.admin.versionManifest(actor, slug, versionId);
8169
+ const manifestJson = await admin.versionManifest(actor, slug, versionId);
8095
8170
  if (!manifestJson) {
8096
8171
  throw new ControlPlaneError(502, `version ${versionId} retained no manifest — pushed pre-#286; push it again to serve in place`);
8097
8172
  }
@@ -8101,7 +8176,7 @@ export function createControlPlaneApi(options) {
8101
8176
  // serving record below all read this one set; the manifest's own `doClasses` would leave
8102
8177
  // the record a class short of the script, and the next serve re-declaring a live class.
8103
8178
  const doClasses = deployedDoClasses(manifest.doClasses, manifest.platformSweeper);
8104
- const serving = await host.admin.verticalServing(actor, slug);
8179
+ const serving = await admin.verticalServing(actor, slug);
8105
8180
  const ref = serving?.ref ?? stableDeploymentRefFor(slug);
8106
8181
  // #1923: the serving upload is the other door a script name is minted through.
8107
8182
  const collision = platformScriptCollision(slug) ?? (PLATFORM_SCRIPT_NAMES.has(ref) ? ref : null);
@@ -8115,16 +8190,16 @@ export function createControlPlaneApi(options) {
8115
8190
  // tenant's store. Platform-granted after the §4 sandbox check, like injectSecrets:
8116
8191
  // the builder never declared these and never named the ids.
8117
8192
  const storeBindings = [
8118
- ...tenantStoreBindings(await host.admin.listTenantStores(actor, { vertical: slug })),
8193
+ ...tenantStoreBindings(await admin.listTenantStores(actor, { vertical: slug })),
8119
8194
  // #473: the per-tenant blob-store r2_bucket bindings, re-derived from the ledger on
8120
8195
  // every serving upload for the same reason — an upload replaces the script's binding
8121
8196
  // set, so a re-deploy must never drop a tenant's attachment bucket.
8122
- ...blobStoreBindings(await host.admin.listBlobStores(actor, { vertical: slug })),
8197
+ ...blobStoreBindings(await admin.listBlobStores(actor, { vertical: slug })),
8123
8198
  ].map((b) => b.type === 'd1'
8124
8199
  ? { type: 'd1', name: b.name, id: b.id }
8125
8200
  : { type: 'r2_bucket', name: b.name, bucket_name: b.bucketName });
8126
8201
  // #2210: the store scripts first, so the vertical is never served before what it reaches.
8127
- await ensureScopeStoresOf(actor, 'serve', slug, manifest.scopeStores, async () => (await ownerOf(actor, slug)) ?? null);
8202
+ await ensureScopeStoresOf(admin, actor, 'serve', slug, manifest.scopeStores, async () => (await ownerOf(admin, actor, slug)) ?? null);
8128
8203
  const wire = await deployVerticalScript(options.deployVertical, actor, slug, ref, {
8129
8204
  entry: manifest.entry,
8130
8205
  compatibilityDate: manifest.compatibilityDate,
@@ -8286,14 +8361,14 @@ export function createControlPlaneApi(options) {
8286
8361
  if (!options.deployVertical || !options.fetchVerticalModules)
8287
8362
  return none; // no in-place serve moves anything
8288
8363
  const actor = c.get('actor');
8289
- const incoming = await host.admin.getVersion(actor, versionId, slug);
8364
+ const incoming = await c.var.admin.getVersion(actor, versionId, slug);
8290
8365
  if (!incoming?.deploymentRef)
8291
8366
  return none;
8292
- const manifestJson = await host.admin.versionManifest(actor, slug, versionId);
8367
+ const manifestJson = await c.var.admin.versionManifest(actor, slug, versionId);
8293
8368
  const registry = manifestJson ? storedDeployManifest.parse(JSON.parse(manifestJson)).registry : undefined;
8294
8369
  if (!registry || !releaseRolesAskScopes(registry))
8295
8370
  return none;
8296
- const v = await verticalOf(actor, slug);
8371
+ const v = await verticalOf(c.var.admin, actor, slug);
8297
8372
  const owner = v && v.ownerTenant !== null && !v.listed ? v.ownerTenant : null;
8298
8373
  const moved = (await c.var.admin.listScopes(actor, { vertical: slug, status: ['active', 'suspended'] }))
8299
8374
  .filter((s) => !s.forkedFrom && s.kind !== 'preview' && (!!s.servingRef || (s.tenantId === owner && s.status === 'active')))
@@ -8347,23 +8422,23 @@ export function createControlPlaneApi(options) {
8347
8422
  * Over-approximates for a host that cannot serve in place, where no adopt runs: an extra line
8348
8423
  * costs a read, a missing one a silent stall.
8349
8424
  */
8350
- const promoteImpactOf = async (actor, slug, channel, versionId) => {
8351
- const gate = await host.admin.promotionImpact(actor, slug, channel, versionId);
8425
+ const promoteImpactOf = async (admin, actor, slug, channel, versionId) => {
8426
+ const gate = await admin.promotionImpact(actor, slug, channel, versionId);
8352
8427
  if (channel !== 'prod')
8353
8428
  return gate;
8354
- const v = await verticalOf(actor, slug);
8429
+ const v = await verticalOf(admin, actor, slug);
8355
8430
  if (!v || v.ownerTenant === null || v.listed)
8356
8431
  return gate;
8357
- const incoming = await host.admin.getVersion(actor, versionId, slug);
8432
+ const incoming = await admin.getVersion(actor, versionId, slug);
8358
8433
  if (!incoming?.deploymentRef || incoming.admission !== 'admitted')
8359
8434
  return gate;
8360
- const lagging = (await host.admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview' && !s.servingRef && s.verticalVersionId !== versionId);
8435
+ const lagging = (await admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview' && !s.servingRef && s.verticalVersionId !== versionId);
8361
8436
  const seen = new Set(gate.map((b) => `${b.scopeId}|${b.type}`));
8362
8437
  const out = [...gate];
8363
8438
  for (const s of lagging) {
8364
8439
  // After the adopt it runs the promoted version, served in place: that pointer move, from
8365
8440
  // what it runs now, is the move to judge.
8366
- for (const b of await host.admin.bindingImpact(actor, s.tenantId, s.id, versionId)) {
8441
+ for (const b of await admin.bindingImpact(actor, s.tenantId, s.id, versionId)) {
8367
8442
  const key = `${b.scopeId}|${b.type}`;
8368
8443
  if (!seen.has(key))
8369
8444
  (seen.add(key), out.push(b));
@@ -8382,11 +8457,11 @@ export function createControlPlaneApi(options) {
8382
8457
  const versionId = z.string().min(1).parse(c.req.query('versionId'));
8383
8458
  const pin = confinedTenant(p);
8384
8459
  if (pin) {
8385
- const v = await verticalOf(p.actor, slug);
8460
+ const v = await verticalOf(adminFor(p), p.actor, slug);
8386
8461
  if (!v || v.ownerTenant !== pin)
8387
8462
  return refuse(c, 'forbidden', 'forbidden');
8388
8463
  }
8389
- const breaks = await promoteImpactOf(c.get('actor'), slug, channel, versionId);
8464
+ const breaks = await promoteImpactOf(c.var.admin, c.get('actor'), slug, channel, versionId);
8390
8465
  const { visible, otherTenants } = narrowToCaller(breaks, pin);
8391
8466
  return c.json({ affected: visible, ...(otherTenants ? { otherTenants } : {}) });
8392
8467
  });
@@ -8404,7 +8479,7 @@ export function createControlPlaneApi(options) {
8404
8479
  const promotePin = confinedTenant(p);
8405
8480
  if (promotePin) {
8406
8481
  // A confined principal promotes only verticals its tenant owns.
8407
- const v = await verticalOf(p.actor, slug);
8482
+ const v = await verticalOf(adminFor(p), p.actor, slug);
8408
8483
  if (!v || v.ownerTenant !== promotePin)
8409
8484
  return refuse(c, 'forbidden', 'forbidden');
8410
8485
  // …and a BUILDER promotes prod only while the vertical is PRIVATE (not listed).
@@ -8438,7 +8513,7 @@ export function createControlPlaneApi(options) {
8438
8513
  // counts only the gate's half of them.
8439
8514
  let counted = [];
8440
8515
  const breaksOf = async () => {
8441
- const breaks = await promoteImpactOf(c.get('actor'), slug, channel, versionId);
8516
+ const breaks = await promoteImpactOf(c.var.admin, c.get('actor'), slug, channel, versionId);
8442
8517
  counted = breaks;
8443
8518
  if (breaks.length === 0)
8444
8519
  return null;
@@ -8609,9 +8684,9 @@ export function createControlPlaneApi(options) {
8609
8684
  if (c.req.method === 'POST') {
8610
8685
  const input = z.object({ slug: z.string().min(1), name: z.string().min(1), repository: z.unknown().optional() }).parse(await c.req.json());
8611
8686
  const team = tenants.find((t) => t.id === tenant);
8612
- const slug = await ownerOf(c.get('actor'), input.slug) === tenant || input.slug.startsWith(`${team.slug}/`) ? input.slug : `${team.slug}/${input.slug}`;
8687
+ const slug = await ownerOf(c.var.admin, c.get('actor'), input.slug) === tenant || input.slug.startsWith(`${team.slug}/`) ? input.slug : `${team.slug}/${input.slug}`;
8613
8688
  verticalSlug.parse(slug);
8614
- const owner = await ownerOf(c.get('actor'), slug);
8689
+ const owner = await ownerOf(c.var.admin, c.get('actor'), slug);
8615
8690
  if (owner !== undefined && owner !== tenant)
8616
8691
  return refuse(c, 'forbidden', 'forbidden');
8617
8692
  body = JSON.stringify({ ...input, slug });
@@ -8666,9 +8741,9 @@ export function createControlPlaneApi(options) {
8666
8741
  if (!tenant)
8667
8742
  return refuse(c, 'not_found', 'unknown workspace');
8668
8743
  sourceTenant = tenant.id;
8669
- sourceSlug = bare.startsWith(`${tenant.slug}/`) || await ownerOf(c.get('actor'), bare) === tenant.id ? bare : `${tenant.slug}/${bare}`;
8744
+ sourceSlug = bare.startsWith(`${tenant.slug}/`) || await ownerOf(c.var.admin, c.get('actor'), bare) === tenant.id ? bare : `${tenant.slug}/${bare}`;
8670
8745
  }
8671
- const owner = await ownerOf(c.get('actor'), sourceSlug);
8746
+ const owner = await ownerOf(c.var.admin, c.get('actor'), sourceSlug);
8672
8747
  if (sourceTenant && owner !== undefined && owner !== sourceTenant)
8673
8748
  return refuse(c, 'forbidden', 'forbidden');
8674
8749
  sourceTenant ??= owner ?? null;
@@ -8760,7 +8835,7 @@ export function createControlPlaneApi(options) {
8760
8835
  }
8761
8836
  slug = effectiveSlug(p, bare);
8762
8837
  // A builder pushes to a slug it owns, or claims an unregistered one (§3).
8763
- const existingOwner = await ownerOf(c.get('actor'), slug);
8838
+ const existingOwner = await ownerOf(c.var.admin, c.get('actor'), slug);
8764
8839
  if (existingOwner !== undefined && existingOwner !== p.tenantId) {
8765
8840
  return refuse(c, 'forbidden', 'forbidden');
8766
8841
  }
@@ -8774,9 +8849,9 @@ export function createControlPlaneApi(options) {
8774
8849
  // hand-registration predating prefixed claims) stays addressable as itself.
8775
8850
  // Otherwise the claim lands under the tenant prefix, exactly like a builder push —
8776
8851
  // same namespace, no claim race with other workspaces' bare names.
8777
- const bareOwner = await ownerOf(c.get('actor'), bare);
8852
+ const bareOwner = await ownerOf(c.var.admin, c.get('actor'), bare);
8778
8853
  slug = bare.startsWith(`${workspace.slug}/`) || bareOwner === workspace.id ? bare : `${workspace.slug}/${bare}`;
8779
- const existingOwner = slug === bare ? bareOwner : await ownerOf(c.get('actor'), slug);
8854
+ const existingOwner = slug === bare ? bareOwner : await ownerOf(c.var.admin, c.get('actor'), slug);
8780
8855
  if (existingOwner !== undefined && existingOwner !== workspace.id) {
8781
8856
  return refuse(c, 'forbidden', `vertical '${slug}' is not owned by workspace '${pin}'`);
8782
8857
  }
@@ -8786,7 +8861,7 @@ export function createControlPlaneApi(options) {
8786
8861
  // No pin: the raw slug is the identity and an existing owner is preserved
8787
8862
  // (null ⇒ platform-owned for a first-party vertical) — unchanged staff behavior.
8788
8863
  slug = bare;
8789
- ownerTenant = (await ownerOf(c.get('actor'), slug)) ?? null;
8864
+ ownerTenant = (await ownerOf(c.var.admin, c.get('actor'), slug)) ?? null;
8790
8865
  }
8791
8866
  // #1923: a slug that would deploy under one of the platform's own script names is refused
8792
8867
  // before anything is uploaded. Registration refuses it too; this is the deploy half, for a
@@ -8864,10 +8939,10 @@ export function createControlPlaneApi(options) {
8864
8939
  // is judged here and honoured by the grant paths only for a tenant-provisioner. Earlier versions
8865
8940
  // are not re-judged.
8866
8941
  // Declaring `provisions` marks a manager: the capability itself is granted after a first push.
8867
- const foreignKey = entitlementNamespaceRefusal(slug, manifest.entitlements ?? [], manifest.provisions?.length ? await verticalSlugsOf(c.get('actor')) : undefined);
8942
+ const foreignKey = entitlementNamespaceRefusal(slug, manifest.entitlements ?? [], manifest.provisions?.length ? await verticalSlugsOf(c.var.admin, c.get('actor')) : undefined);
8868
8943
  if (foreignKey)
8869
8944
  return refuseStatus(c, 422, foreignKey);
8870
- const claimed = await slugClaimsSku(c.get('actor'), slug);
8945
+ const claimed = await slugClaimsSku(c.var.admin, c.get('actor'), slug);
8871
8946
  if (claimed)
8872
8947
  return refuseStatus(c, 422, claimed);
8873
8948
  // The static-asset half of the §4 sandbox contract (self-serve-deploy.md §4.1). The
@@ -8938,7 +9013,7 @@ export function createControlPlaneApi(options) {
8938
9013
  // the publish run under the store push's shared hold (#2816): a rollout that starts before the
8939
9014
  // version records its stores is refused, and one after walks this vertical.
8940
9015
  const deployVertical = options.deployVertical;
8941
- const pushed = await ensureScopeStoresOf(c.get('actor'), 'deploy', slug, manifest.scopeStores, async () => ownerTenant ?? null, async (renew) => {
9016
+ const pushed = await ensureScopeStoresOf(c.var.admin, c.get('actor'), 'deploy', slug, manifest.scopeStores, async () => ownerTenant ?? null, async (renew) => {
8942
9017
  let wire;
8943
9018
  try {
8944
9019
  wire = await deployVerticalScript(deployVertical, c.get('actor'), slug, deploymentRef, {
@@ -9343,7 +9418,7 @@ export function createControlPlaneApi(options) {
9343
9418
  limit: c.req.query('limit'),
9344
9419
  });
9345
9420
  // #1877: scoped to the app's own scripts — the scan and trust boundary.
9346
- const services = await appServices(c.get('actor'), input.tenantId, input.scopeId, input.vertical);
9421
+ const services = await appServices(c.var.admin, c.get('actor'), input.tenantId, input.scopeId, input.vertical);
9347
9422
  const { operation, principalKind, ...rest } = input;
9348
9423
  const { events, incomplete } = await options.observability.tenantLogs({
9349
9424
  ...rest,
@@ -9380,7 +9455,7 @@ export function createControlPlaneApi(options) {
9380
9455
  catch (e) {
9381
9456
  throw new ControlPlaneError(400, e.message);
9382
9457
  }
9383
- const scopes = await host.admin.listScopes(c.get('actor'), { tenantId: input.tenantId });
9458
+ const scopes = await c.var.admin.listScopes(c.get('actor'), { tenantId: input.tenantId });
9384
9459
  const installed = scopes.find((s) => s.id === input.scopeId);
9385
9460
  if (!installed?.vertical)
9386
9461
  return refuse(c, 'not_found', 'app not found');
@@ -9420,7 +9495,7 @@ export function createControlPlaneApi(options) {
9420
9495
  * nothing. `listScopes`, not `getScopeRecord`: a dashboard read must not write an access
9421
9496
  * row per page view.
9422
9497
  */
9423
- const appServices = async (actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await host.admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
9498
+ const appServices = async (admin, actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
9424
9499
  app.get('/observability/tenant-request-volume', async (c) => {
9425
9500
  if (!options.observability?.tenantRequestVolume) {
9426
9501
  return refuseStatus(c, 501, 'request aggregation is not configured on this control plane');
@@ -9430,7 +9505,7 @@ export function createControlPlaneApi(options) {
9430
9505
  // A bucket COUNT: zooming in keeps it and narrows the window, so the bucket shrinks.
9431
9506
  // 90 is the design's histogram; the bounds keep a chart drawable and a query cheap.
9432
9507
  const buckets = z.coerce.number().int().min(10).max(240).default(90).parse(c.req.query('buckets'));
9433
- const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9508
+ const services = await appServices(c.var.admin, c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9434
9509
  return c.json(await options.observability.tenantRequestVolume({ ...scope, services, buckets }));
9435
9510
  });
9436
9511
  app.get('/observability/tenant-request-facets', async (c) => {
@@ -9443,7 +9518,7 @@ export function createControlPlaneApi(options) {
9443
9518
  // that shows three asks for three.
9444
9519
  const asked = (c.req.queries('facet') ?? []).filter((k) => k.length > 0);
9445
9520
  const keys = asked.length === 0 ? undefined : z.array(z.enum(REQUEST_FACET_KEYS)).parse([...new Set(asked)]);
9446
- const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9521
+ const services = await appServices(c.var.admin, c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9447
9522
  return c.json(await options.observability.tenantRequestFacets({ ...scope, services, ...(keys ? { keys } : {}) }));
9448
9523
  });
9449
9524
  // #1747: the tenant's `ctx.log` lines grouped by the template they were written from.
@@ -9472,7 +9547,7 @@ export function createControlPlaneApi(options) {
9472
9547
  buckets: narrow.buckets,
9473
9548
  ...(narrow.level.length ? { level: narrow.level } : {}),
9474
9549
  ...(narrow.operation.length ? { operation: narrow.operation } : {}),
9475
- services: await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical),
9550
+ services: await appServices(c.var.admin, c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical),
9476
9551
  }));
9477
9552
  });
9478
9553
  // #2762: the Lines histogram and facets — a tenant's request lines and `ctx.log` lines, counted
@@ -9483,7 +9558,7 @@ export function createControlPlaneApi(options) {
9483
9558
  }
9484
9559
  const scope = parseTenantLineQuery(requestTenantOf(c), requestQueryOf(c));
9485
9560
  const buckets = z.coerce.number().int().min(10).max(240).default(90).parse(c.req.query('buckets'));
9486
- const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9561
+ const services = await appServices(c.var.admin, c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9487
9562
  return c.json(await options.observability.tenantLogVolume({ ...scope, services, buckets }));
9488
9563
  });
9489
9564
  app.get('/observability/tenant-log-facets', async (c) => {
@@ -9491,7 +9566,7 @@ export function createControlPlaneApi(options) {
9491
9566
  return refuseStatus(c, 501, 'line aggregation is not configured on this control plane');
9492
9567
  }
9493
9568
  const scope = parseTenantLineQuery(requestTenantOf(c), requestQueryOf(c));
9494
- const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9569
+ const services = await appServices(c.var.admin, c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9495
9570
  return c.json(await options.observability.tenantLogFacets({ ...scope, services }));
9496
9571
  });
9497
9572
  app.get('/observability/tenant-requests', async (c) => {
@@ -9500,7 +9575,7 @@ export function createControlPlaneApi(options) {
9500
9575
  }
9501
9576
  const scope = parseTenantRequestQuery(requestTenantOf(c), requestQueryOf(c));
9502
9577
  const limit = z.coerce.number().int().min(1).max(200).default(50).parse(c.req.query('limit'));
9503
- const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9578
+ const services = await appServices(c.var.admin, c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
9504
9579
  return c.json(await options.observability.tenantRequests({ ...scope, services, limit }));
9505
9580
  });
9506
9581
  /**
@@ -9959,7 +10034,7 @@ export function createControlPlaneApi(options) {
9959
10034
  if (!parsed)
9960
10035
  throw new ControlPlaneError(400, `'${baseHostname}' is not a hostname a preview can be minted beside`);
9961
10036
  const hostname = withLabel(parsed, `${parsed.label}${RESERVED_LABEL_SEPARATOR}${tag}`);
9962
- const existing = (await host.admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
10037
+ const existing = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
9963
10038
  if (!existing) {
9964
10039
  await admin.bindHostname(actor, {
9965
10040
  hostname,
@@ -9970,7 +10045,7 @@ export function createControlPlaneApi(options) {
9970
10045
  canonical: false,
9971
10046
  });
9972
10047
  }
9973
- const bound = (await host.admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
10048
+ const bound = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
9974
10049
  if (bound)
9975
10050
  await runIssuance(admin, actor, bound); // platform mint rides the wildcard cert → active
9976
10051
  return hostname;
@@ -9979,9 +10054,9 @@ export function createControlPlaneApi(options) {
9979
10054
  * source scope's canonical URL; a clean-room preview (no source, #509 ask (b)) has none,
9980
10055
  * so it is built from the platform tenant-app convention `<vertical>-<tenant>.<base>` —
9981
10056
  * the same scheme provisioning mints (`callout-sesamy.global.substrat.run`). */
9982
- const previewBaseHostname = async (actor, source, tenantId, slug, surface) => {
10057
+ const previewBaseHostname = async (admin, actor, source, tenantId, slug, surface) => {
9983
10058
  if (source) {
9984
- const own = await host.admin.listHostnames(actor, { scopeId: source.id });
10059
+ const own = await admin.listHostnames(actor, { scopeId: source.id });
9985
10060
  const src = own.find((h) => h.canonical && h.surface === surface) ??
9986
10061
  own.find((h) => h.canonical) ??
9987
10062
  own[0];
@@ -10003,7 +10078,7 @@ export function createControlPlaneApi(options) {
10003
10078
  // bare apex and stranded clean-room previews on a hostname that never resolves.
10004
10079
  const baseDomain = [...platformBaseDomains].sort((a, b) => a.length - b.length)[0];
10005
10080
  const jurisdiction = 'global';
10006
- const tenant = await host.admin.getTenant(actor, tenantId);
10081
+ const tenant = await admin.getTenant(actor, tenantId);
10007
10082
  const handle = tenant?.slug ?? tenantId;
10008
10083
  return `${slug.split('/').at(-1)}-${handle}.${jurisdiction}.${baseDomain}`;
10009
10084
  };
@@ -10241,7 +10316,7 @@ export function createControlPlaneApi(options) {
10241
10316
  : [];
10242
10317
  // The `--<tag>` URL's base — the source's canonical hostname (fork) or the tenant-app
10243
10318
  // convention (clean-room). Computed once so reuse and fresh mint the same URL.
10244
- const baseHostname = await previewBaseHostname(actor, source, tenantId, slug, surface);
10319
+ const baseHostname = await previewBaseHostname(c.var.admin, actor, source, tenantId, slug, surface);
10245
10320
  // The founding #559 case lands its durable row HERE, not in onError: the previews
10246
10321
  // route answers a ControlPlaneError directly (its own catch, never the app-level
10247
10322
  // recorder), and only this frame knows the preview's scopeId (`intoId`) — the key that lets
@@ -10807,7 +10882,7 @@ export function createControlPlaneApi(options) {
10807
10882
  const previewVertical = async (c) => {
10808
10883
  const p = c.get('principal');
10809
10884
  const slug = await resolveVerticalId(c, c.req.param('slug'));
10810
- const v = await verticalOf(c.get('actor'), slug);
10885
+ const v = await verticalOf(c.var.admin, c.get('actor'), slug);
10811
10886
  if (!v)
10812
10887
  return { refused: 'not_found', error: `unknown vertical '${slug}'` };
10813
10888
  if (confinedTenant(p) !== null && v.ownerTenant !== confinedTenant(p)) {
@@ -11588,7 +11663,7 @@ export function createControlPlaneApi(options) {
11588
11663
  const workspace = (await c.var.admin.listTenants(c.get('actor'))).find((t) => t.slug === pin || t.id === pin);
11589
11664
  if (!workspace)
11590
11665
  return refuse(c, 'not_found', `unknown workspace '${pin}'`);
11591
- const bareOwner = await ownerOf(c.get('actor'), vertical);
11666
+ const bareOwner = await ownerOf(c.var.admin, c.get('actor'), vertical);
11592
11667
  id = vertical.startsWith(`${workspace.slug}/`) || bareOwner === workspace.id ? vertical : `${workspace.slug}/${vertical}`;
11593
11668
  }
11594
11669
  const drivers = (options.storeDrivers ?? STORE_DRIVERS).map((d) => d.verticals ? { ...d, verticals: storeDriverServesVertical(d, id) ? [vertical] : [] } : d);