@substrat-run/control-plane-api 0.130.1 → 0.131.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 +11 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +460 -337
- package/dist/api.js.map +1 -1
- package/dist/auth.d.ts +7 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/tenant-token.d.ts +7 -1
- package/dist/tenant-token.d.ts.map +1 -1
- package/dist/tenant-token.js +35 -8
- package/dist/tenant-token.js.map +1 -1
- package/package.json +6 -6
package/dist/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveObservabilityWindow } from './observability-window.js';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, OWNER_TRANSFER_AUDIT_ERROR_MAX, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionHealthState, connectionProvider, connectorDispatchKind, PROVISION_SIBLING_KIND, ARCHIVE_SCOPE_KIND, PROVISION_TENANT_KIND, SET_ENTITLEMENTS_KIND, MODEL_USAGE_KIND, SWEEP_RUNS_KIND, toConnectionHealthEntry, CONNECTION_EXPIRY_WARNING_DAYS, CONNECTION_STALE_DAYS, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, bindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, errorCodeOf, } from '@substrat-run/contracts';
|
|
3
|
+
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, OWNER_TRANSFER_AUDIT_ERROR_MAX, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionHealthState, connectionProvider, connectorDispatchKind, PROVISION_SIBLING_KIND, ARCHIVE_SCOPE_KIND, PROVISION_TENANT_KIND, SET_ENTITLEMENTS_KIND, MODEL_USAGE_KIND, SWEEP_RUNS_KIND, toConnectionHealthEntry, CONNECTION_EXPIRY_WARNING_DAYS, CONNECTION_STALE_DAYS, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, impersonationStamp, promotionAcknowledgement, bindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, errorCodeOf, } from '@substrat-run/contracts';
|
|
4
4
|
import { attributeFailure } from './failure-attribution.js';
|
|
5
5
|
import { BIND_EXPORT_BREAK_REFUSAL, bindExportBreakRefusal, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, migrationProgress, ulid, } from '@substrat-run/kernel';
|
|
6
6
|
import { TENANT_HEADER, confinedTenant } from './auth.js';
|
|
@@ -492,15 +492,78 @@ export const DEFAULT_MODEL_MARGIN_PERCENT = 20;
|
|
|
492
492
|
* Cloudflare's `internal error; reference = <id>` shape (#559). */
|
|
493
493
|
const UPSTREAM_REFERENCE = /\breference\s*=\s*([a-z0-9]+)/i;
|
|
494
494
|
const TENANT_ROUTES = [
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
// the
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
495
|
+
// One tenant's own path, route by route (#977). It used to be ONE catch-all entry per
|
|
496
|
+
// method — `/tenants/<pin>/…` cannot address another tenant's row, so it looked safe —
|
|
497
|
+
// but a path that is confined is not a route that is harmless: it reached entitlement
|
|
498
|
+
// self-grant, the tenant's own status and admission, and the staff-only redrain doors,
|
|
499
|
+
// none of which the dashboard calls. The pin says WHOSE rows; this list says WHICH
|
|
500
|
+
// acts, and a route the dashboard does not issue is reach nobody asked for. The
|
|
501
|
+
// dashboard's `credential-reach` test holds the other direction — every route its
|
|
502
|
+
// authority seam issues is on this list — so a new call site fails in its own PR.
|
|
503
|
+
//
|
|
504
|
+
// The tenant row: read it, rename it. Not `/status` — suspending or admitting a
|
|
505
|
+
// tenant is the platform's act, not the tenant's.
|
|
506
|
+
{ method: 'GET', re: /\/tenants\/[^/]+$/, pin: 'path' },
|
|
507
|
+
{ method: 'PATCH', re: /\/tenants\/[^/]+$/, pin: 'path' },
|
|
508
|
+
// Integrations (#605/#726): connections, their grants, the consent round's verify.
|
|
509
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connection-grants$/, pin: 'path' },
|
|
510
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connections$/, pin: 'path' },
|
|
511
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/connections$/, pin: 'path' },
|
|
512
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/connections\/[^/]+$/, pin: 'path' },
|
|
513
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/activity$/, pin: 'path' },
|
|
514
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/credential$/, pin: 'path' },
|
|
515
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/connections\/[^/]+\/verify$/, pin: 'path' },
|
|
516
|
+
// Members: identity links mirrored from the dashboard, orgs, role assignments.
|
|
517
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/identities$/, pin: 'path' },
|
|
518
|
+
{ method: 'PUT', re: /\/tenants\/[^/]+\/identities$/, pin: 'path' },
|
|
519
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/identities\/[^/]+$/, pin: 'path' },
|
|
520
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/orgs$/, pin: 'path' },
|
|
521
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/orgs$/, pin: 'path' },
|
|
522
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
|
|
523
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
|
|
524
|
+
// An install's entitlement. The handler narrows WHICH key a tenant credential may
|
|
525
|
+
// grant (`tenantGrantableEntitlement`): the pin alone would let it grant itself any.
|
|
526
|
+
{ method: 'PUT', re: /\/tenants\/[^/]+\/entitlements\/[^/]+$/, pin: 'path' },
|
|
527
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/cross-vertical\/edges$/, pin: 'path' },
|
|
528
|
+
// Scopes: provision, read, and the lifecycle acts the Data and Deployments tabs offer.
|
|
529
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes$/, pin: 'path' },
|
|
530
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+$/, pin: 'path' },
|
|
531
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/scopes\/[^/]+$/, pin: 'path' },
|
|
532
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/binding-impact$/, pin: 'path' },
|
|
533
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/bookmarks$/, pin: 'path' },
|
|
534
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/cause$/, pin: 'path' },
|
|
535
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/dead-letters$/, pin: 'path' },
|
|
536
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/denials$/, pin: 'path' },
|
|
537
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/denials\/summary$/, pin: 'path' },
|
|
538
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/effects$/, pin: 'path' },
|
|
539
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/export$/, pin: 'path' },
|
|
540
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/facets$/, pin: 'path' },
|
|
541
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/history$/, pin: 'path' },
|
|
542
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/intents$/, pin: 'path' },
|
|
543
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/invocation$/, pin: 'path' },
|
|
544
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/migrations$/, pin: 'path' },
|
|
545
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/owner-seat$/, pin: 'path' },
|
|
546
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
547
|
+
// The peer switch (#1706): on is POST, off is DELETE, on the tenant's own scope.
|
|
548
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
549
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
550
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/snapshots$/, pin: 'path' },
|
|
551
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/tables$/, pin: 'path' },
|
|
552
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/tables\/[^/]+$/, pin: 'path' },
|
|
553
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/activate$/, pin: 'path' },
|
|
554
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/archive$/, pin: 'path' },
|
|
555
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/configure$/, pin: 'path' },
|
|
556
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/import-cursor$/, pin: 'path' },
|
|
557
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/lifecycle-flow$/, pin: 'path' },
|
|
558
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/owner-claim$/, pin: 'path' },
|
|
559
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/query$/, pin: 'path' },
|
|
560
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/reap$/, pin: 'path' },
|
|
561
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/rebind-vertical$/, pin: 'path' },
|
|
562
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/restore$/, pin: 'path' },
|
|
563
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/rewind$/, pin: 'path' },
|
|
564
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/snapshots$/, pin: 'path' },
|
|
565
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/suspend$/, pin: 'path' },
|
|
566
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/version$/, pin: 'path' },
|
|
504
567
|
// Create the tenant's own directory row (idempotent) — `id` is the pin.
|
|
505
568
|
{ method: 'POST', re: /\/tenants$/, pin: 'body' },
|
|
506
569
|
// Provision an app + materialize its instance.
|
|
@@ -627,7 +690,6 @@ const namedTenants = (c) => {
|
|
|
627
690
|
*/
|
|
628
691
|
export function createControlPlaneApi(options) {
|
|
629
692
|
const { host, authenticate, authenticateBuilder, authenticateTenantService } = options;
|
|
630
|
-
const admin = host.admin;
|
|
631
693
|
const app = new Hono();
|
|
632
694
|
// The CLI version advisory (#971), stamped on EVERY response — including the 401 the
|
|
633
695
|
// auth middleware below answers with, since a CLI too old to authenticate is exactly
|
|
@@ -659,7 +721,12 @@ export function createControlPlaneApi(options) {
|
|
|
659
721
|
if (authenticateTenantService) {
|
|
660
722
|
const tenant = await authenticateTenantService(c.req.raw);
|
|
661
723
|
if (tenant) {
|
|
662
|
-
principal = {
|
|
724
|
+
principal = {
|
|
725
|
+
kind: 'tenant',
|
|
726
|
+
actor: tenant.actor,
|
|
727
|
+
tenantId: tenant.tenantId,
|
|
728
|
+
...(tenant.onBehalfOf ? { onBehalfOf: tenant.onBehalfOf } : {}),
|
|
729
|
+
};
|
|
663
730
|
}
|
|
664
731
|
}
|
|
665
732
|
const staff = principal ? null : await authenticate(c.req.raw);
|
|
@@ -680,6 +747,14 @@ export function createControlPlaneApi(options) {
|
|
|
680
747
|
return c.json({ error: 'unauthenticated' }, 401);
|
|
681
748
|
c.set('principal', principal);
|
|
682
749
|
c.set('actor', principal.actor);
|
|
750
|
+
// #977: a tenant credential minted for a person writes through a view that names
|
|
751
|
+
// them on every admin row. Only the credential's own claim can supply the person —
|
|
752
|
+
// never a header or a body — and only the staff mint writes that claim.
|
|
753
|
+
const view = principal.kind === 'tenant' && principal.onBehalfOf && host.attributed
|
|
754
|
+
? host.attributed(principal.onBehalfOf)
|
|
755
|
+
: host;
|
|
756
|
+
c.set('host', view);
|
|
757
|
+
c.set('admin', view.admin);
|
|
683
758
|
await next();
|
|
684
759
|
});
|
|
685
760
|
// Confine a BUILDER to the vertical-management surface, fail-CLOSED (builder-plane.md
|
|
@@ -839,7 +914,7 @@ export function createControlPlaneApi(options) {
|
|
|
839
914
|
// regex a message. A caller with no throw in hand leaves the columns null,
|
|
840
915
|
// which reads as "nobody classified this" rather than a guess nobody made.
|
|
841
916
|
const attributed = cause === NO_CAUSE ? undefined : attributeFailure(cause);
|
|
842
|
-
void admin
|
|
917
|
+
void host.admin
|
|
843
918
|
.recordOpsFailure({
|
|
844
919
|
...entry,
|
|
845
920
|
origin: entry.origin ?? attributed?.origin ?? null,
|
|
@@ -927,7 +1002,7 @@ export function createControlPlaneApi(options) {
|
|
|
927
1002
|
// -- tenant registry (§4.1) ------------------------------------------------
|
|
928
1003
|
app.get('/tenants', async (c) => {
|
|
929
1004
|
const page = pageParams(c);
|
|
930
|
-
const entries = await admin.listTenants(c.get('actor'), page);
|
|
1005
|
+
const entries = await c.var.admin.listTenants(c.get('actor'), page);
|
|
931
1006
|
return c.json(pageOf(entries, page.limit, (t) => t.id));
|
|
932
1007
|
});
|
|
933
1008
|
app.post('/tenants', async (c) => {
|
|
@@ -936,14 +1011,14 @@ export function createControlPlaneApi(options) {
|
|
|
936
1011
|
// (from the manager scope's directory row) — never caller-supplied. A direct staff
|
|
937
1012
|
// create is first-class by definition, so force it null even if a body carries it,
|
|
938
1013
|
// so the field can't be forged into a false ownership relationship.
|
|
939
|
-
await admin.createTenant(c.get('actor'), { ...input, provisionedByTenant: null });
|
|
1014
|
+
await c.var.admin.createTenant(c.get('actor'), { ...input, provisionedByTenant: null });
|
|
940
1015
|
// Idempotent (§4.1): re-creating an existing tenant is a no-op, not an error,
|
|
941
1016
|
// so this reads back rather than reporting a create that may not have happened.
|
|
942
|
-
return c.json(await admin.getTenant(c.get('actor'), input.id), 201);
|
|
1017
|
+
return c.json(await c.var.admin.getTenant(c.get('actor'), input.id), 201);
|
|
943
1018
|
});
|
|
944
1019
|
app.get('/tenants/:tenantId', async (c) => {
|
|
945
1020
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
946
|
-
const tenant = await admin.getTenant(c.get('actor'), tenantId);
|
|
1021
|
+
const tenant = await c.var.admin.getTenant(c.get('actor'), tenantId);
|
|
947
1022
|
if (!tenant)
|
|
948
1023
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
949
1024
|
return c.json(tenant);
|
|
@@ -955,8 +1030,8 @@ export function createControlPlaneApi(options) {
|
|
|
955
1030
|
app.patch('/tenants/:tenantId', async (c) => {
|
|
956
1031
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
957
1032
|
const { name } = z.object({ name: z.string().trim().min(1).max(100) }).parse(await c.req.json());
|
|
958
|
-
await admin.setTenantName(c.get('actor'), tenantId, name);
|
|
959
|
-
return c.json(await admin.getTenant(c.get('actor'), tenantId));
|
|
1033
|
+
await c.var.admin.setTenantName(c.get('actor'), tenantId, name);
|
|
1034
|
+
return c.json(await c.var.admin.getTenant(c.get('actor'), tenantId));
|
|
960
1035
|
});
|
|
961
1036
|
app.patch('/tenants/:tenantId/status', async (c) => {
|
|
962
1037
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
@@ -964,8 +1039,8 @@ export function createControlPlaneApi(options) {
|
|
|
964
1039
|
// The live weapon (§7): `suspended` fails getScope closed for EVERY scope
|
|
965
1040
|
// under the tenant. The blast radius is the console's to show; the audit row
|
|
966
1041
|
// is this layer's to guarantee.
|
|
967
|
-
await admin.setTenantStatus(c.get('actor'), tenantId, status);
|
|
968
|
-
return c.json(await admin.getTenant(c.get('actor'), tenantId));
|
|
1042
|
+
await c.var.admin.setTenantStatus(c.get('actor'), tenantId, status);
|
|
1043
|
+
return c.json(await c.var.admin.getTenant(c.get('actor'), tenantId));
|
|
969
1044
|
});
|
|
970
1045
|
// Reap a DELETING tenant now (control-plane.md §4.8) — the staff "reap now" that skips
|
|
971
1046
|
// the grace window, the tenant analogue of the scope reap route below. Refuses a tenant
|
|
@@ -980,7 +1055,7 @@ export function createControlPlaneApi(options) {
|
|
|
980
1055
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
981
1056
|
const actor = c.get('actor');
|
|
982
1057
|
const { backup: wantsBackup } = reapScopeBody.parse(await c.req.json().catch(() => ({})));
|
|
983
|
-
const tenant = await admin.getTenant(actor, tenantId);
|
|
1058
|
+
const tenant = await c.var.admin.getTenant(actor, tenantId);
|
|
984
1059
|
if (!tenant)
|
|
985
1060
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
986
1061
|
if (tenant.status !== 'deleting') {
|
|
@@ -989,11 +1064,11 @@ export function createControlPlaneApi(options) {
|
|
|
989
1064
|
}, 409);
|
|
990
1065
|
}
|
|
991
1066
|
try {
|
|
992
|
-
for (const scope of await admin.listScopes(actor, { tenantId })) {
|
|
1067
|
+
for (const scope of await c.var.admin.listScopes(actor, { tenantId })) {
|
|
993
1068
|
if (scope.status === 'reaped')
|
|
994
1069
|
continue;
|
|
995
1070
|
if (scope.status !== 'archived')
|
|
996
|
-
await admin.archiveScope(actor, tenantId, scope.id);
|
|
1071
|
+
await c.var.admin.archiveScope(actor, tenantId, scope.id);
|
|
997
1072
|
// A backup here is OPT-IN, the inverse of the per-scope reap's default (#493).
|
|
998
1073
|
// A scope reap is operational cleanup, so leaving a copy is the safe default; a
|
|
999
1074
|
// TENANT reap is the deletion of a customer, and §4.8 exists partly to serve an
|
|
@@ -1006,13 +1081,13 @@ export function createControlPlaneApi(options) {
|
|
|
1006
1081
|
await vertical.deleteScope({ scopeId: scope.id });
|
|
1007
1082
|
// Tenant teardown reaps every scope and releases every name by design — force past
|
|
1008
1083
|
// the bound-hostname guard (which fences the interactive per-scope reap route below).
|
|
1009
|
-
await admin.reapScope(actor, tenantId, scope.id, {
|
|
1084
|
+
await c.var.admin.reapScope(actor, tenantId, scope.id, {
|
|
1010
1085
|
force: true,
|
|
1011
1086
|
...(backup ? { backupRef: backupRefOf(backup) } : {}),
|
|
1012
1087
|
});
|
|
1013
1088
|
}
|
|
1014
|
-
await admin.reapTenant(actor, tenantId);
|
|
1015
|
-
return c.json(await admin.getTenant(actor, tenantId));
|
|
1089
|
+
await c.var.admin.reapTenant(actor, tenantId);
|
|
1090
|
+
return c.json(await c.var.admin.getTenant(actor, tenantId));
|
|
1016
1091
|
}
|
|
1017
1092
|
catch (e) {
|
|
1018
1093
|
if (e instanceof ControlPlaneError) {
|
|
@@ -1022,20 +1097,35 @@ export function createControlPlaneApi(options) {
|
|
|
1022
1097
|
}
|
|
1023
1098
|
});
|
|
1024
1099
|
// -- entitlements (§4.3) ---------------------------------------------------
|
|
1025
|
-
app.get('/tenants/:tenantId/entitlements', async (c) => c.json(await admin.listEntitlements(c.get('actor'), tenantIdSchema.parse(c.req.param('tenantId')))));
|
|
1100
|
+
app.get('/tenants/:tenantId/entitlements', async (c) => c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantIdSchema.parse(c.req.param('tenantId')))));
|
|
1026
1101
|
app.put('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
1027
1102
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1028
1103
|
// The body is the plan half (#33) and optional — a bodyless PUT is the
|
|
1029
1104
|
// pre-widening bare flag grant, and PATCH semantics in the store mean it
|
|
1030
1105
|
// preserves whatever plan fields the grant already carries.
|
|
1031
1106
|
const plan = entitlementGrantInput.parse(await c.req.json().catch(() => ({})));
|
|
1032
|
-
|
|
1033
|
-
|
|
1107
|
+
const key = c.req.param('key');
|
|
1108
|
+
const p = c.get('principal');
|
|
1109
|
+
if (p.kind === 'tenant') {
|
|
1110
|
+
// #977: a tenant credential may grant its own tenant only what an INSTALL grants —
|
|
1111
|
+
// a key some vertical it can see declares (or that vertical's bare slug, the key a
|
|
1112
|
+
// vertical declaring none gates on). Anything else is the platform's to grant:
|
|
1113
|
+
// `builder`, a plan-gated feature, a key no vertical asked for. And never the plan
|
|
1114
|
+
// half: an expiry, a quota or a plan name is pricing, which is not the tenant's.
|
|
1115
|
+
if (plan.expiresAt != null || plan.quota != null || plan.plan != null) {
|
|
1116
|
+
return c.json({ error: 'a tenant credential grants entitlements without plan fields' }, 403);
|
|
1117
|
+
}
|
|
1118
|
+
if (!(await tenantGrantable(p, key))) {
|
|
1119
|
+
return c.json({ error: `entitlement '${key}' is not declared by a vertical this tenant can install` }, 403);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
await c.var.admin.grantEntitlement(c.get('actor'), tenantId, key, plan);
|
|
1123
|
+
return c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantId));
|
|
1034
1124
|
});
|
|
1035
1125
|
app.delete('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
1036
1126
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1037
|
-
await admin.revokeEntitlement(c.get('actor'), tenantId, c.req.param('key'));
|
|
1038
|
-
return c.json(await admin.listEntitlements(c.get('actor'), tenantId));
|
|
1127
|
+
await c.var.admin.revokeEntitlement(c.get('actor'), tenantId, c.req.param('key'));
|
|
1128
|
+
return c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantId));
|
|
1039
1129
|
});
|
|
1040
1130
|
// -- the meters (§5, #38) --------------------------------------------------
|
|
1041
1131
|
// Meters 1 and 2, read fleet-wide or narrowed with `?tenantId=`. Meters 3 and 4 have
|
|
@@ -1049,7 +1139,7 @@ export function createControlPlaneApi(options) {
|
|
|
1049
1139
|
app.get('/meters', async (c) => {
|
|
1050
1140
|
const raw = c.req.query('tenantId');
|
|
1051
1141
|
const tenantId = raw ? tenantIdSchema.parse(raw) : undefined;
|
|
1052
|
-
return c.json(await admin.readMeters(c.get('actor'), tenantId ? { tenantId } : undefined));
|
|
1142
|
+
return c.json(await c.var.admin.readMeters(c.get('actor'), tenantId ? { tenantId } : undefined));
|
|
1053
1143
|
});
|
|
1054
1144
|
// Storage, read on demand (#1524): one tenant's scope-database sizes and their sum.
|
|
1055
1145
|
// Staff-only like `/meters` (on neither allowlist, so a builder or a tenant token is
|
|
@@ -1076,7 +1166,7 @@ export function createControlPlaneApi(options) {
|
|
|
1076
1166
|
limit: c.req.query('limit') || undefined,
|
|
1077
1167
|
});
|
|
1078
1168
|
const actor = c.get('actor');
|
|
1079
|
-
const scopes = await admin.listScopes(actor, { tenantId: q.tenantId });
|
|
1169
|
+
const scopes = await c.var.admin.listScopes(actor, { tenantId: q.tenantId });
|
|
1080
1170
|
return c.json(await readStoragePage({
|
|
1081
1171
|
tenantId: q.tenantId,
|
|
1082
1172
|
readAt: new Date().toISOString(),
|
|
@@ -1085,13 +1175,13 @@ export function createControlPlaneApi(options) {
|
|
|
1085
1175
|
limit: q.limit,
|
|
1086
1176
|
read: async (scope) => {
|
|
1087
1177
|
if (!scope.vertical)
|
|
1088
|
-
return admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1178
|
+
return c.var.admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1089
1179
|
const vertical = await verticalForScope(c, scope);
|
|
1090
1180
|
if (vertical)
|
|
1091
1181
|
return vertical.databaseSize(scope.id);
|
|
1092
1182
|
if (delegatesToVerticals)
|
|
1093
1183
|
throw new Error(`no deployment resolves for vertical ${scope.vertical}`);
|
|
1094
|
-
return admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1184
|
+
return c.var.admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1095
1185
|
},
|
|
1096
1186
|
}));
|
|
1097
1187
|
});
|
|
@@ -1106,8 +1196,8 @@ export function createControlPlaneApi(options) {
|
|
|
1106
1196
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1107
1197
|
const actor = c.get('actor');
|
|
1108
1198
|
const [tenantStores, blobStores] = await Promise.all([
|
|
1109
|
-
admin.listTenantStores(actor, { tenantId }),
|
|
1110
|
-
admin.listBlobStores(actor, { tenantId }),
|
|
1199
|
+
c.var.admin.listTenantStores(actor, { tenantId }),
|
|
1200
|
+
c.var.admin.listBlobStores(actor, { tenantId }),
|
|
1111
1201
|
]);
|
|
1112
1202
|
return c.json({ tenantStores, blobStores });
|
|
1113
1203
|
});
|
|
@@ -1124,26 +1214,26 @@ export function createControlPlaneApi(options) {
|
|
|
1124
1214
|
const link = mirrorIdentityBody.parse(await c.req.json());
|
|
1125
1215
|
// The pool must exist before a link can land in it (central topology, K-23);
|
|
1126
1216
|
// registering an existing pool is a no-op.
|
|
1127
|
-
await admin.registerIdentityPool(c.get('actor'), { provider: link.provider, topology: 'central', tenantId: null });
|
|
1128
|
-
await admin.linkIdentity(c.get('actor'), { ...link, tenantId });
|
|
1217
|
+
await c.var.admin.registerIdentityPool(c.get('actor'), { provider: link.provider, topology: 'central', tenantId: null });
|
|
1218
|
+
await c.var.admin.linkIdentity(c.get('actor'), { ...link, tenantId });
|
|
1129
1219
|
return c.body(null, 204);
|
|
1130
1220
|
});
|
|
1131
1221
|
app.delete('/tenants/:tenantId/identities/:principal', async (c) => {
|
|
1132
1222
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1133
|
-
await admin.unlinkIdentity(c.get('actor'), tenantId, principalIdSchema.parse(c.req.param('principal')));
|
|
1223
|
+
await c.var.admin.unlinkIdentity(c.get('actor'), tenantId, principalIdSchema.parse(c.req.param('principal')));
|
|
1134
1224
|
return c.body(null, 204);
|
|
1135
1225
|
});
|
|
1136
1226
|
// The read half of the mirror seam (#406): what the directory currently links for a
|
|
1137
1227
|
// tenant — the console's offboarding view, and the way to verify an unlink landed.
|
|
1138
1228
|
app.get('/tenants/:tenantId/identities', async (c) => {
|
|
1139
1229
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1140
|
-
return c.json(await admin.listIdentityLinks(c.get('actor'), tenantId));
|
|
1230
|
+
return c.json(await c.var.admin.listIdentityLinks(c.get('actor'), tenantId));
|
|
1141
1231
|
});
|
|
1142
1232
|
// The tenant's live connection grants (#592) — the readable "what may this connection
|
|
1143
1233
|
// invoke" (connections.md §6.2.4 Q2), and the rows provision/reconcile deliver from.
|
|
1144
1234
|
app.get('/tenants/:tenantId/connection-grants', async (c) => {
|
|
1145
1235
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1146
|
-
return c.json(await admin.listConnectionGrants(c.get('actor'), tenantId));
|
|
1236
|
+
return c.json(await c.var.admin.listConnectionGrants(c.get('actor'), tenantId));
|
|
1147
1237
|
});
|
|
1148
1238
|
// -- the connection store, tenant-scoped (connections.md §3.5) -------------
|
|
1149
1239
|
//
|
|
@@ -1162,7 +1252,7 @@ export function createControlPlaneApi(options) {
|
|
|
1162
1252
|
provider: c.req.query('provider'),
|
|
1163
1253
|
includeRevoked: c.req.query('includeRevoked') === '1' ? true : undefined,
|
|
1164
1254
|
});
|
|
1165
|
-
return c.json(await admin.listConnections(c.get('actor'), filter));
|
|
1255
|
+
return c.json(await c.var.admin.listConnections(c.get('actor'), filter));
|
|
1166
1256
|
});
|
|
1167
1257
|
// -- connection health, fleet-wide (#1690) -----------------------------------
|
|
1168
1258
|
//
|
|
@@ -1198,7 +1288,7 @@ export function createControlPlaneApi(options) {
|
|
|
1198
1288
|
order: c.req.query('order'),
|
|
1199
1289
|
});
|
|
1200
1290
|
const now = new Date();
|
|
1201
|
-
const rows = await admin.listConnections(c.get('actor'), {
|
|
1291
|
+
const rows = await c.var.admin.listConnections(c.get('actor'), {
|
|
1202
1292
|
...(q.tenantId ? { tenantId: q.tenantId } : {}),
|
|
1203
1293
|
...(q.provider ? { provider: q.provider } : {}),
|
|
1204
1294
|
});
|
|
@@ -1243,7 +1333,7 @@ export function createControlPlaneApi(options) {
|
|
|
1243
1333
|
const since = new Date(now.getTime() - CONNECTOR_DEAD_LETTER_WINDOW_DAYS * 86_400_000).toISOString();
|
|
1244
1334
|
const providers = q.provider ? [q.provider] : [...new Set(rows.map((r) => r.provider))].sort();
|
|
1245
1335
|
const deadLetters = await Promise.all(providers.map(async (provider) => {
|
|
1246
|
-
const found = await admin.listOpsFailures(c.get('actor'), {
|
|
1336
|
+
const found = await c.var.admin.listOpsFailures(c.get('actor'), {
|
|
1247
1337
|
operation: `intent.${connectorDispatchKind(provider)}`,
|
|
1248
1338
|
...(q.tenantId ? { tenantId: q.tenantId } : {}),
|
|
1249
1339
|
since,
|
|
@@ -1312,7 +1402,7 @@ export function createControlPlaneApi(options) {
|
|
|
1312
1402
|
// per provider that has (or had) a live connection anywhere in the fleet — the same
|
|
1313
1403
|
// derivation the dead-letter count above uses, since that kind only exists for a
|
|
1314
1404
|
// provider some connection actually names.
|
|
1315
|
-
const providers = [...new Set((await admin.listConnections(c.get('actor'), { includeRevoked: true })).map((r) => r.provider))];
|
|
1405
|
+
const providers = [...new Set((await c.var.admin.listConnections(c.get('actor'), { includeRevoked: true })).map((r) => r.provider))];
|
|
1316
1406
|
const kinds = [
|
|
1317
1407
|
PROVISION_SIBLING_KIND,
|
|
1318
1408
|
ARCHIVE_SCOPE_KIND,
|
|
@@ -1325,7 +1415,7 @@ export function createControlPlaneApi(options) {
|
|
|
1325
1415
|
// The drain's newest pass is independent of the failure counts, so it rides alongside them.
|
|
1326
1416
|
const [counts, [lastPass]] = await Promise.all([
|
|
1327
1417
|
Promise.all(kinds.map(async (kind) => {
|
|
1328
|
-
const found = await admin.listOpsFailures(c.get('actor'), {
|
|
1418
|
+
const found = await c.var.admin.listOpsFailures(c.get('actor'), {
|
|
1329
1419
|
operation: `intent.${kind}`,
|
|
1330
1420
|
since,
|
|
1331
1421
|
limit: PLATFORM_REQUEST_BACKLOG_CAP + 1,
|
|
@@ -1335,7 +1425,7 @@ export function createControlPlaneApi(options) {
|
|
|
1335
1425
|
capped: found.length > PLATFORM_REQUEST_BACKLOG_CAP,
|
|
1336
1426
|
};
|
|
1337
1427
|
})),
|
|
1338
|
-
admin.listSweepRuns(c.get('actor'), { kind: 'platform-request', unit: 'fleet', limit: 1 }),
|
|
1428
|
+
c.var.admin.listSweepRuns(c.get('actor'), { kind: 'platform-request', unit: 'fleet', limit: 1 }),
|
|
1339
1429
|
]);
|
|
1340
1430
|
const body = {
|
|
1341
1431
|
total: counts.reduce((sum, k) => sum + k.count, 0),
|
|
@@ -1363,7 +1453,7 @@ export function createControlPlaneApi(options) {
|
|
|
1363
1453
|
return c.json({ error: 'body tenantId disagrees with the route tenant' }, 400);
|
|
1364
1454
|
}
|
|
1365
1455
|
try {
|
|
1366
|
-
return c.json(await relayConnectionUpsert(host, c.get('actor'), { ...body, tenantId }, { probeCandidate }));
|
|
1456
|
+
return c.json(await relayConnectionUpsert(c.var.host, c.get('actor'), { ...body, tenantId }, { probeCandidate }));
|
|
1367
1457
|
}
|
|
1368
1458
|
catch (err) {
|
|
1369
1459
|
if (err instanceof ConnectionRelayError && err.status < 500) {
|
|
@@ -1385,12 +1475,12 @@ export function createControlPlaneApi(options) {
|
|
|
1385
1475
|
app.delete('/tenants/:tenantId/connections/:id', async (c) => {
|
|
1386
1476
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1387
1477
|
const id = c.req.param('id');
|
|
1388
|
-
const rows = await admin.listConnections(c.get('actor'), { tenantId, includeRevoked: true });
|
|
1478
|
+
const rows = await c.var.admin.listConnections(c.get('actor'), { tenantId, includeRevoked: true });
|
|
1389
1479
|
const row = rows.find((r) => r.id === id);
|
|
1390
1480
|
if (!row)
|
|
1391
1481
|
return c.json({ error: 'unknown connection' }, 404);
|
|
1392
1482
|
if (row.status !== 'revoked')
|
|
1393
|
-
await admin.revokeConnection(c.get('actor'), row.id);
|
|
1483
|
+
await c.var.admin.revokeConnection(c.get('actor'), row.id);
|
|
1394
1484
|
return c.body(null, 204);
|
|
1395
1485
|
});
|
|
1396
1486
|
/**
|
|
@@ -1408,7 +1498,7 @@ export function createControlPlaneApi(options) {
|
|
|
1408
1498
|
const inspectableConnection = async (c) => {
|
|
1409
1499
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1410
1500
|
const id = c.req.param('id');
|
|
1411
|
-
const rows = await admin.listConnections(c.get('actor'), { tenantId });
|
|
1501
|
+
const rows = await c.var.admin.listConnections(c.get('actor'), { tenantId });
|
|
1412
1502
|
return rows.find((r) => r.id === id);
|
|
1413
1503
|
};
|
|
1414
1504
|
/**
|
|
@@ -1433,7 +1523,7 @@ export function createControlPlaneApi(options) {
|
|
|
1433
1523
|
if (!probe) {
|
|
1434
1524
|
return c.json({ error: `no probe registered for provider '${row.provider}'` }, 501);
|
|
1435
1525
|
}
|
|
1436
|
-
return c.json(connectionProbe.parse(await probe(host, row)));
|
|
1526
|
+
return c.json(connectionProbe.parse(await probe(c.var.host, row)));
|
|
1437
1527
|
});
|
|
1438
1528
|
/**
|
|
1439
1529
|
* **What the connection has done** (#605) — the connector's dispatch ledger, projected.
|
|
@@ -1457,7 +1547,7 @@ export function createControlPlaneApi(options) {
|
|
|
1457
1547
|
}
|
|
1458
1548
|
const source = connectionActivitySource.catch('ledger').parse(c.req.query('source'));
|
|
1459
1549
|
const live = c.req.query('live') === '1';
|
|
1460
|
-
return c.json(connectionActivity.parse(await activity(host, row, { live, source })));
|
|
1550
|
+
return c.json(connectionActivity.parse(await activity(c.var.host, row, { live, source })));
|
|
1461
1551
|
});
|
|
1462
1552
|
/**
|
|
1463
1553
|
* **The stored credential, reduced** (#605) — identifiers whole, secrets masked.
|
|
@@ -1476,7 +1566,7 @@ export function createControlPlaneApi(options) {
|
|
|
1476
1566
|
if (!credential) {
|
|
1477
1567
|
return c.json({ error: `no credential view registered for provider '${row.provider}'` }, 501);
|
|
1478
1568
|
}
|
|
1479
|
-
return c.json(connectionCredential.parse(await credential(host, row)));
|
|
1569
|
+
return c.json(connectionCredential.parse(await credential(c.var.host, row)));
|
|
1480
1570
|
});
|
|
1481
1571
|
// -- the scope directory (§3.2/§4.2) ---------------------------------------
|
|
1482
1572
|
app.get('/scopes', async (c) => {
|
|
@@ -1489,7 +1579,7 @@ export function createControlPlaneApi(options) {
|
|
|
1489
1579
|
vertical: c.req.query('vertical'),
|
|
1490
1580
|
});
|
|
1491
1581
|
const page = pageParams(c);
|
|
1492
|
-
const entries = await admin.listScopes(c.get('actor'), { ...filter, ...page });
|
|
1582
|
+
const entries = await c.var.admin.listScopes(c.get('actor'), { ...filter, ...page });
|
|
1493
1583
|
return c.json(pageOf(entries, page.limit, (s) => s.id));
|
|
1494
1584
|
});
|
|
1495
1585
|
// -- fleet migration progress (kernel-design §5.3, #49) ---------------------
|
|
@@ -1501,7 +1591,7 @@ export function createControlPlaneApi(options) {
|
|
|
1501
1591
|
// an unfiltered read is meaningful only where one deployment runs everything.
|
|
1502
1592
|
app.get('/fleet/migrations', async (c) => {
|
|
1503
1593
|
const filter = fleetMigrationsQuery.parse({ vertical: c.req.query('vertical') });
|
|
1504
|
-
const scopes = await admin.listScopes(c.get('actor'), {
|
|
1594
|
+
const scopes = await c.var.admin.listScopes(c.get('actor'), {
|
|
1505
1595
|
status: ['active', 'provisioning'],
|
|
1506
1596
|
...(filter.vertical ? { vertical: filter.vertical } : {}),
|
|
1507
1597
|
});
|
|
@@ -1509,8 +1599,8 @@ export function createControlPlaneApi(options) {
|
|
|
1509
1599
|
});
|
|
1510
1600
|
app.post('/scopes', async (c) => {
|
|
1511
1601
|
const input = provisionScopeBody.parse(await c.req.json());
|
|
1512
|
-
await host.provisionScope(c.get('actor'), input);
|
|
1513
|
-
const record = await admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
1602
|
+
await c.var.host.provisionScope(c.get('actor'), input);
|
|
1603
|
+
const record = await c.var.admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
1514
1604
|
return c.json(record, 201);
|
|
1515
1605
|
});
|
|
1516
1606
|
// Add a SIBLING scope — a new "site" — to an app the tenant already runs (multi-scope
|
|
@@ -1538,7 +1628,7 @@ export function createControlPlaneApi(options) {
|
|
|
1538
1628
|
// The provisioning sequence lives in one place — `provisionSiblingScope` — shared with the
|
|
1539
1629
|
// platform-intent drain's `provision-sibling` handler (platform-intents.md B2).
|
|
1540
1630
|
try {
|
|
1541
|
-
const result = await provisionSiblingScope({ host, actor, resolveVerticalForScope: (scope) => verticalForScope(
|
|
1631
|
+
const result = await provisionSiblingScope({ host: c.var.host, actor, resolveVerticalForScope: (scope) => verticalForScope(c, scope) }, {
|
|
1542
1632
|
tenantId,
|
|
1543
1633
|
parentScopeId: input.parentScopeId,
|
|
1544
1634
|
scopeId: input.scopeId,
|
|
@@ -1548,7 +1638,7 @@ export function createControlPlaneApi(options) {
|
|
|
1548
1638
|
});
|
|
1549
1639
|
if (!result.ok)
|
|
1550
1640
|
return c.json({ error: result.error }, result.status);
|
|
1551
|
-
return c.json(await admin.getScopeRecord(actor, tenantId, input.scopeId), 201);
|
|
1641
|
+
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, input.scopeId), 201);
|
|
1552
1642
|
}
|
|
1553
1643
|
catch (e) {
|
|
1554
1644
|
if (e instanceof ControlPlaneError)
|
|
@@ -1564,7 +1654,7 @@ export function createControlPlaneApi(options) {
|
|
|
1564
1654
|
if (outsideTenant(p, tenantId)) {
|
|
1565
1655
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
1566
1656
|
}
|
|
1567
|
-
const record = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
1657
|
+
const record = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
1568
1658
|
// Absent, or present under another tenant — indistinguishable on purpose (K-3).
|
|
1569
1659
|
if (!record)
|
|
1570
1660
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
@@ -1594,8 +1684,8 @@ export function createControlPlaneApi(options) {
|
|
|
1594
1684
|
* protect a subject's data is itself a thing an incident asks about.
|
|
1595
1685
|
*/
|
|
1596
1686
|
const sealerFor = (c, tenantId, scopeId) => ({
|
|
1597
|
-
seal: (items) => admin.sealSubjectPayloads(c.get('actor'), tenantId, scopeId, items),
|
|
1598
|
-
open: (items) => admin.openSubjectPayloads(c.get('actor'), tenantId, scopeId, items),
|
|
1687
|
+
seal: (items) => c.var.admin.sealSubjectPayloads(c.get('actor'), tenantId, scopeId, items),
|
|
1688
|
+
open: (items) => c.var.admin.openSubjectPayloads(c.get('actor'), tenantId, scopeId, items),
|
|
1599
1689
|
});
|
|
1600
1690
|
const verticalForScope = async (c, scope) => (await deploymentForScope(c, scope))?.client;
|
|
1601
1691
|
/**
|
|
@@ -1637,7 +1727,7 @@ export function createControlPlaneApi(options) {
|
|
|
1637
1727
|
// under its bare spelling. Retry once under the registry id; anything else still
|
|
1638
1728
|
// misses and surfaces through `diagnoseUnboundScope` as before.
|
|
1639
1729
|
if (scope.tenantId && !scope.vertical.includes('/') && (await ownerOf(actor, scope.vertical)) === undefined) {
|
|
1640
|
-
const tenant = await admin.getTenant(actor, scope.tenantId).catch(() => null);
|
|
1730
|
+
const tenant = await c.var.admin.getTenant(actor, scope.tenantId).catch(() => null);
|
|
1641
1731
|
const prefixed = tenant ? `${tenant.slug}/${scope.vertical}` : null;
|
|
1642
1732
|
if (prefixed && (await ownerOf(actor, prefixed)) !== undefined)
|
|
1643
1733
|
return bySlug(prefixed);
|
|
@@ -1660,7 +1750,7 @@ export function createControlPlaneApi(options) {
|
|
|
1660
1750
|
const slug = scope.vertical;
|
|
1661
1751
|
if (!slug)
|
|
1662
1752
|
return 'scope has no vertical bound — nothing to deliver to';
|
|
1663
|
-
const versions = await admin.listVersions(actor, slug).catch(() => []);
|
|
1753
|
+
const versions = await host.admin.listVersions(actor, slug).catch(() => []);
|
|
1664
1754
|
const boundId = scope.verticalVersionId;
|
|
1665
1755
|
if (boundId && !versions.some((v) => v.id === boundId)) {
|
|
1666
1756
|
return (`scope is bound to version '${boundId}', which is not among vertical '${slug}'’s ${versions.length} ` +
|
|
@@ -1673,7 +1763,7 @@ export function createControlPlaneApi(options) {
|
|
|
1673
1763
|
`pushes are on different slugs (a lineage fork). Push under '${slug}' by setting \`substrat.slug\`, ` +
|
|
1674
1764
|
`or rebind the install to the slug the versions live under.`);
|
|
1675
1765
|
}
|
|
1676
|
-
const serving = await admin.verticalServing(actor, slug).catch(() => null);
|
|
1766
|
+
const serving = await host.admin.verticalServing(actor, slug).catch(() => null);
|
|
1677
1767
|
if (!serving) {
|
|
1678
1768
|
return `vertical '${slug}' has ${versions.length} version(s) but none is promoted to a serving channel — promote one to prod.`;
|
|
1679
1769
|
}
|
|
@@ -1689,7 +1779,7 @@ export function createControlPlaneApi(options) {
|
|
|
1689
1779
|
const refuseOnBreaks = async (actor, tenantId, scopeId, versionId, opts) => {
|
|
1690
1780
|
if (opts.acknowledge?.exportBreak)
|
|
1691
1781
|
return;
|
|
1692
|
-
const breaks = await admin.bindingImpact(actor, tenantId, scopeId, versionId, opts.servingRef !== undefined ? { servingRef: opts.servingRef } : undefined);
|
|
1782
|
+
const breaks = await host.admin.bindingImpact(actor, tenantId, scopeId, versionId, opts.servingRef !== undefined ? { servingRef: opts.servingRef } : undefined);
|
|
1693
1783
|
if (breaks.length > 0)
|
|
1694
1784
|
throw new ExportBreakRefused(breaks);
|
|
1695
1785
|
};
|
|
@@ -1724,14 +1814,14 @@ export function createControlPlaneApi(options) {
|
|
|
1724
1814
|
* and `exact` when the platform exported them itself rather than a caller supplying them. */
|
|
1725
1815
|
source) => {
|
|
1726
1816
|
return retryTransient(async () => dest.restoreScope(tenantId, scopeId, tables, {
|
|
1727
|
-
...(await switchCarryFor(admin, actor, { tenantId, scopeId })),
|
|
1817
|
+
...(await switchCarryFor(host.admin, actor, { tenantId, scopeId })),
|
|
1728
1818
|
sourceScopeId: source.scopeId,
|
|
1729
1819
|
exact: source.exact,
|
|
1730
1820
|
}));
|
|
1731
1821
|
};
|
|
1732
1822
|
const adoptScopeOntoServing = async (c, tenantId, scopeId, opts = {}) => {
|
|
1733
1823
|
const actor = c.get('actor');
|
|
1734
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1824
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1735
1825
|
if (!scope) {
|
|
1736
1826
|
throw new ControlPlaneError(404, `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
1737
1827
|
}
|
|
@@ -1742,13 +1832,13 @@ export function createControlPlaneApi(options) {
|
|
|
1742
1832
|
// #1674 (Copilot review): an adopt that flipped routing and then failed to re-assert is
|
|
1743
1833
|
// retried HERE, so the retry re-asserts too — else a store that lost its OFF marker in
|
|
1744
1834
|
// the copy would stay on for good. Cheap and idempotent when nothing is owed.
|
|
1745
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1835
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1746
1836
|
return { servingRef: scope.servingRef, alreadyAdopted: true };
|
|
1747
1837
|
}
|
|
1748
1838
|
if (!scope.vertical) {
|
|
1749
1839
|
throw new ControlPlaneError(409, 'scope has no vertical — nothing to adopt onto');
|
|
1750
1840
|
}
|
|
1751
|
-
const serving = await admin.verticalServing(actor, scope.vertical);
|
|
1841
|
+
const serving = await c.var.admin.verticalServing(actor, scope.vertical);
|
|
1752
1842
|
if (!serving) {
|
|
1753
1843
|
throw new ControlPlaneError(409, `vertical '${scope.vertical}' has no serving script yet — promote a version to prod first`);
|
|
1754
1844
|
}
|
|
@@ -1767,15 +1857,15 @@ export function createControlPlaneApi(options) {
|
|
|
1767
1857
|
// #1742: the recorded OFF positions ride the restore, applied in the replay's own event.
|
|
1768
1858
|
const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump, { scopeId, exact: true });
|
|
1769
1859
|
// Data landed — only now flip routing and move the version pointer.
|
|
1770
|
-
await admin
|
|
1860
|
+
await c.var.admin
|
|
1771
1861
|
.setScopeServingRef(actor, tenantId, scopeId, serving.ref, { acknowledge: opts.acknowledge })
|
|
1772
1862
|
.catch(relayHostRefusal);
|
|
1773
|
-
await admin
|
|
1863
|
+
await c.var.admin
|
|
1774
1864
|
.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, { acknowledge: opts.acknowledge })
|
|
1775
1865
|
.catch(relayHostRefusal);
|
|
1776
1866
|
// #1674: the scope now routes to a different store, so put the directory's recorded
|
|
1777
1867
|
// OFF positions back there — cheap and idempotent when the dump or the restore did.
|
|
1778
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
1868
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
1779
1869
|
return { servingRef: serving.ref, tables: restored.tables };
|
|
1780
1870
|
};
|
|
1781
1871
|
/**
|
|
@@ -1798,13 +1888,13 @@ export function createControlPlaneApi(options) {
|
|
|
1798
1888
|
acknowledge) => {
|
|
1799
1889
|
const actor = c.get('actor');
|
|
1800
1890
|
const none = { refused: [] };
|
|
1801
|
-
const serving = await admin.verticalServing(actor, slug);
|
|
1891
|
+
const serving = await c.var.admin.verticalServing(actor, slug);
|
|
1802
1892
|
if (!serving)
|
|
1803
1893
|
return none; // embedded / not dispatch-backed — the host cascade handled rebinds
|
|
1804
1894
|
const v = await verticalOf(actor, slug);
|
|
1805
1895
|
if (!v || v.ownerTenant === null || v.listed)
|
|
1806
1896
|
return none; // private only, like the host cascade
|
|
1807
|
-
const owned = (await admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
1897
|
+
const owned = (await c.var.admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
1808
1898
|
// An install refused for what it would break is left where it is, and the rest still move:
|
|
1809
1899
|
// one lagging app must not keep every other install's pointer, or the store backfill after
|
|
1810
1900
|
// this, from happening. The caller answers with what was refused.
|
|
@@ -1822,7 +1912,7 @@ export function createControlPlaneApi(options) {
|
|
|
1822
1912
|
// Already on the serving script (born there, or adopted earlier): routing is
|
|
1823
1913
|
// pinned to servingRef, so advancing the version pointer only affects Update
|
|
1824
1914
|
// offers. Snapshot on a migration-digest crossing (fork-before-promote, §4).
|
|
1825
|
-
await admin.bindScopeVersion(actor, s.tenantId, s.id, versionId, { snapshot: true, acknowledge });
|
|
1915
|
+
await c.var.admin.bindScopeVersion(actor, s.tenantId, s.id, versionId, { snapshot: true, acknowledge });
|
|
1826
1916
|
}
|
|
1827
1917
|
}
|
|
1828
1918
|
catch (e) {
|
|
@@ -1854,7 +1944,7 @@ export function createControlPlaneApi(options) {
|
|
|
1854
1944
|
*/
|
|
1855
1945
|
const rebindScopeOntoVertical = async (c, tenantId, scopeId, target, opts) => {
|
|
1856
1946
|
const actor = c.get('actor');
|
|
1857
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1947
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1858
1948
|
if (!scope) {
|
|
1859
1949
|
throw new ControlPlaneError(404, `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
1860
1950
|
}
|
|
@@ -1864,13 +1954,13 @@ export function createControlPlaneApi(options) {
|
|
|
1864
1954
|
if (!scope.vertical) {
|
|
1865
1955
|
throw new ControlPlaneError(409, 'scope has no vertical — nothing to rebind');
|
|
1866
1956
|
}
|
|
1867
|
-
const serving = await admin.verticalServing(actor, target);
|
|
1957
|
+
const serving = await c.var.admin.verticalServing(actor, target);
|
|
1868
1958
|
if (!serving) {
|
|
1869
1959
|
throw new ControlPlaneError(409, `vertical '${target}' has no serving script yet — promote a version to prod first`);
|
|
1870
1960
|
}
|
|
1871
1961
|
if (scope.vertical === target && scope.servingRef === serving.ref) {
|
|
1872
1962
|
// #1674: a rebind retried after its re-assert failed lands here; re-assert, as adopt does.
|
|
1873
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1963
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1874
1964
|
return { servingRef: serving.ref, versionId: serving.versionId, alreadyBound: true };
|
|
1875
1965
|
}
|
|
1876
1966
|
// #1756: within one lineage this is an adopt, and judged as one before anything moves. A
|
|
@@ -1885,8 +1975,8 @@ export function createControlPlaneApi(options) {
|
|
|
1885
1975
|
// protect — the target provisions its own schema from scratch. The source
|
|
1886
1976
|
// script's copy is untouched and remains the backout, same as a carried rebind.
|
|
1887
1977
|
// The scope serves nothing until `/verticals/:slug/instances` re-provisions it.
|
|
1888
|
-
await admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref, move).catch(relayHostRefusal);
|
|
1889
|
-
await admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, move).catch(relayHostRefusal);
|
|
1978
|
+
await c.var.admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref, move).catch(relayHostRefusal);
|
|
1979
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, move).catch(relayHostRefusal);
|
|
1890
1980
|
return { servingRef: serving.ref, versionId: serving.versionId, dataAbandoned: true };
|
|
1891
1981
|
}
|
|
1892
1982
|
// The frontier gate. Digest equality proves the target's migration set is exactly
|
|
@@ -1894,9 +1984,9 @@ export function createControlPlaneApi(options) {
|
|
|
1894
1984
|
// brought — the crossing is then no riskier than an ordinary version bind. Anything
|
|
1895
1985
|
// else (differing digests, or a scope with no bound version to compare) needs the
|
|
1896
1986
|
// operator's explicit acknowledgement.
|
|
1897
|
-
const targetV = await admin.getVersion(actor, serving.versionId, target);
|
|
1987
|
+
const targetV = await c.var.admin.getVersion(actor, serving.versionId, target);
|
|
1898
1988
|
const currentV = scope.verticalVersionId
|
|
1899
|
-
? await admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
1989
|
+
? await c.var.admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
1900
1990
|
: undefined;
|
|
1901
1991
|
const digestsMatch = Boolean(currentV && targetV && currentV.migrationDigest === targetV.migrationDigest);
|
|
1902
1992
|
if (!digestsMatch && !opts.ackMigrations) {
|
|
@@ -1914,10 +2004,10 @@ export function createControlPlaneApi(options) {
|
|
|
1914
2004
|
// `bindScopeVersion` rewrites `scopes.vertical` from the version row, audited. No
|
|
1915
2005
|
// extra snapshot here (adopt-serving's precedent): the source script's copy is the
|
|
1916
2006
|
// pre-migration state, and it is never deleted — that copy is the backout.
|
|
1917
|
-
await admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref, move).catch(relayHostRefusal);
|
|
1918
|
-
await admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, move).catch(relayHostRefusal);
|
|
2007
|
+
await c.var.admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref, move).catch(relayHostRefusal);
|
|
2008
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, move).catch(relayHostRefusal);
|
|
1919
2009
|
// #1674: re-assert the recorded OFF positions in the store the scope now routes to.
|
|
1920
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
2010
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
1921
2011
|
return { servingRef: serving.ref, versionId: serving.versionId, tables: restored.tables };
|
|
1922
2012
|
};
|
|
1923
2013
|
/**
|
|
@@ -1960,7 +2050,7 @@ export function createControlPlaneApi(options) {
|
|
|
1960
2050
|
if (!scope.vertical)
|
|
1961
2051
|
return null;
|
|
1962
2052
|
const actor = c.get('actor');
|
|
1963
|
-
const incoming = await admin.getVersion(actor, versionId, scope.vertical);
|
|
2053
|
+
const incoming = await c.var.admin.getVersion(actor, versionId, scope.vertical);
|
|
1964
2054
|
// The bind refuses a version that is not admitted, except onto a preview. Check that
|
|
1965
2055
|
// BEFORE the copy. Otherwise unreviewed code would receive the scope's data through its
|
|
1966
2056
|
// own restore handler, only for the bind to refuse afterwards.
|
|
@@ -1974,7 +2064,7 @@ export function createControlPlaneApi(options) {
|
|
|
1974
2064
|
return null;
|
|
1975
2065
|
const to = incoming.deploymentRef ?? null;
|
|
1976
2066
|
const bound = scope.verticalVersionId
|
|
1977
|
-
? await admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
2067
|
+
? await c.var.admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
1978
2068
|
: undefined;
|
|
1979
2069
|
const from = scope.servingRef ?? bound?.deploymentRef ?? null;
|
|
1980
2070
|
if (!to || !from || from === to)
|
|
@@ -2005,12 +2095,12 @@ export function createControlPlaneApi(options) {
|
|
|
2005
2095
|
app.get('/tenants/:tenantId/scopes/:scopeId/tables', async (c) => {
|
|
2006
2096
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2007
2097
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2008
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2098
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2009
2099
|
if (!scope)
|
|
2010
2100
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2011
2101
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'listScopeTables', null, {
|
|
2012
2102
|
viaVertical: (v) => v.listScopeTables(scopeId),
|
|
2013
|
-
colocated: () => admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2103
|
+
colocated: () => c.var.admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2014
2104
|
}, (r) => r.length));
|
|
2015
2105
|
});
|
|
2016
2106
|
// Scope health (#321, criterion #3). The silent failure the field report chased was an
|
|
@@ -2039,21 +2129,21 @@ export function createControlPlaneApi(options) {
|
|
|
2039
2129
|
if (outsideTenant(principal, tenantId)) {
|
|
2040
2130
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2041
2131
|
}
|
|
2042
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2132
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2043
2133
|
if (!scope)
|
|
2044
2134
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2045
2135
|
// Health reads the scope the same way the Data view does, so it leaves the same
|
|
2046
2136
|
// row: an auditor should not be able to tell which route reached the tables.
|
|
2047
2137
|
const tables = await delegatedRead(c, tenantId, scopeId, scope, 'listScopeTables', null, {
|
|
2048
2138
|
viaVertical: (v) => v.listScopeTables(scopeId),
|
|
2049
|
-
colocated: () => admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2139
|
+
colocated: () => c.var.admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2050
2140
|
}, (r) => r.length);
|
|
2051
2141
|
const roles = tables.find((t) => t.name === '_substrat_roles');
|
|
2052
2142
|
const roleCount = roles ? roles.rowCount : null;
|
|
2053
2143
|
const roleProjectionEmpty = scope.status === 'active' && roleCount === 0;
|
|
2054
2144
|
const missingStores = scope.vertical
|
|
2055
2145
|
? await missingStoresForTenant({
|
|
2056
|
-
host:
|
|
2146
|
+
host: c.var.host,
|
|
2057
2147
|
actor: c.get('actor'),
|
|
2058
2148
|
slug: scope.vertical,
|
|
2059
2149
|
tenantId,
|
|
@@ -2096,7 +2186,7 @@ export function createControlPlaneApi(options) {
|
|
|
2096
2186
|
status: c.req.query('status'),
|
|
2097
2187
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2098
2188
|
});
|
|
2099
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2189
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2100
2190
|
if (!scope)
|
|
2101
2191
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2102
2192
|
// Same delegation ladder as the table reads: the intents live in the DO of the scope's BOUND
|
|
@@ -2104,7 +2194,7 @@ export function createControlPlaneApi(options) {
|
|
|
2104
2194
|
const vertical = await verticalForScope(c, scope);
|
|
2105
2195
|
return c.json(vertical
|
|
2106
2196
|
? await vertical.listPlatformRequestHistory(tenantId, scopeId, filter)
|
|
2107
|
-
: await host.listPlatformRequestHistory(tenantId, scopeId, filter));
|
|
2197
|
+
: await c.var.host.listPlatformRequestHistory(tenantId, scopeId, filter));
|
|
2108
2198
|
});
|
|
2109
2199
|
// #332: re-provision a scope stuck at "roles projected, zero tuples" — the enforcement flip
|
|
2110
2200
|
// switched on against an empty tuple table, so every login denies and the owner is locked out
|
|
@@ -2117,7 +2207,7 @@ export function createControlPlaneApi(options) {
|
|
|
2117
2207
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2118
2208
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2119
2209
|
const actor = c.get('actor');
|
|
2120
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2210
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2121
2211
|
if (!scope)
|
|
2122
2212
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2123
2213
|
const p = c.get('principal');
|
|
@@ -2131,10 +2221,10 @@ export function createControlPlaneApi(options) {
|
|
|
2131
2221
|
if (!reached)
|
|
2132
2222
|
return c.json({ error: await diagnoseUnboundScope(c.get('actor'), scope) }, 501);
|
|
2133
2223
|
const vertical = reached.client;
|
|
2134
|
-
const entitlements = await admin.listEntitlements(actor, tenantId);
|
|
2224
|
+
const entitlements = await c.var.admin.listEntitlements(actor, tenantId);
|
|
2135
2225
|
// #406: re-gathered and re-delivered like entitlements, so a reconcile also repairs a
|
|
2136
2226
|
// dropped identity-link delivery — and is the channel a link/unlink after provision rides.
|
|
2137
|
-
const identityLinks = (await admin.listIdentityLinks(actor, tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
2227
|
+
const identityLinks = (await c.var.admin.listIdentityLinks(actor, tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
2138
2228
|
// #592: connection grants ride the same authoritative gather — the back-fill for a
|
|
2139
2229
|
// scope provisioned before `grantToConnection` ran, and how a revoked connection's
|
|
2140
2230
|
// grants stop being delivered (they are absent from the directory's live rows).
|
|
@@ -2148,18 +2238,18 @@ export function createControlPlaneApi(options) {
|
|
|
2148
2238
|
// Best-effort: healing reaches the directory and must never take the reconcile down
|
|
2149
2239
|
// with it. A failure here leaves exactly the behaviour that shipped before it existed.
|
|
2150
2240
|
try {
|
|
2151
|
-
await reconcileConnectionGrants({ admin, actor, declared: options.connectorGrants ?? {} }, tenantId, scope.vertical);
|
|
2241
|
+
await reconcileConnectionGrants({ admin: c.var.admin, actor, declared: options.connectorGrants ?? {} }, tenantId, scope.vertical);
|
|
2152
2242
|
}
|
|
2153
2243
|
catch {
|
|
2154
2244
|
// The next reconcile tries again; the read-back still shows what is missing.
|
|
2155
2245
|
}
|
|
2156
|
-
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(actor, tenantId), scope.vertical, scopeId);
|
|
2246
|
+
const connectionGrants = connectionGrantsForScope(await c.var.admin.listConnectionGrants(actor, tenantId), scope.vertical, scopeId);
|
|
2157
2247
|
// #687: connection sealing keys ride the same gather — the back-fill for a scope
|
|
2158
2248
|
// provisioned before its connection existed, and the channel a connection made AFTER
|
|
2159
2249
|
// provision rides. Minted on first ask, so this is also how a connection older than
|
|
2160
2250
|
// the feature acquires a keypair without being reconnected.
|
|
2161
2251
|
const connectionKeys = scope.vertical
|
|
2162
|
-
? await admin.connectionSealingKeys(tenantId, scope.vertical)
|
|
2252
|
+
? await c.var.admin.connectionSealingKeys(tenantId, scope.vertical)
|
|
2163
2253
|
: [];
|
|
2164
2254
|
// #825: the BACKFILL seam for per-tenant stores. Minting lived only in the
|
|
2165
2255
|
// tenant-creation lifecycle, so a tenant created before its vertical declared a
|
|
@@ -2213,7 +2303,7 @@ export function createControlPlaneApi(options) {
|
|
|
2213
2303
|
// need a second run to discover the other.
|
|
2214
2304
|
const tenantStores = scope.vertical
|
|
2215
2305
|
? ((await mintBestEffort('tenant-stores', () => collectTenantStoreHandles({
|
|
2216
|
-
host:
|
|
2306
|
+
host: c.var.host,
|
|
2217
2307
|
actor,
|
|
2218
2308
|
slug: scope.vertical,
|
|
2219
2309
|
tenantId,
|
|
@@ -2222,7 +2312,7 @@ export function createControlPlaneApi(options) {
|
|
|
2222
2312
|
: [];
|
|
2223
2313
|
if (scope.vertical) {
|
|
2224
2314
|
await mintBestEffort('blob-stores', () => collectBlobStoreHandles({
|
|
2225
|
-
host:
|
|
2315
|
+
host: c.var.host,
|
|
2226
2316
|
actor,
|
|
2227
2317
|
slug: scope.vertical,
|
|
2228
2318
|
tenantId,
|
|
@@ -2237,12 +2327,12 @@ export function createControlPlaneApi(options) {
|
|
|
2237
2327
|
// one before. Best-effort: without it the receipt falls back to the bound version, the
|
|
2238
2328
|
// pre-#1653 answer, which costs at most one more reconcile.
|
|
2239
2329
|
const serving = reached.via === 'serving-script' && scope.vertical
|
|
2240
|
-
? await admin.verticalServing(actor, scope.vertical).catch(() => null)
|
|
2330
|
+
? await c.var.admin.verticalServing(actor, scope.vertical).catch(() => null)
|
|
2241
2331
|
: null;
|
|
2242
2332
|
const ranAs = versionReachedAt(reached.via, scope, serving);
|
|
2243
2333
|
try {
|
|
2244
2334
|
// #1674: the recorded OFF positions go back after the reconcile, before the receipt.
|
|
2245
|
-
const result = await reconcileThenReassert(admin, actor, { tenantId, scopeId }, (carry) => vertical.reconcileInstance({
|
|
2335
|
+
const result = await reconcileThenReassert(c.var.admin, actor, { tenantId, scopeId }, (carry) => vertical.reconcileInstance({
|
|
2246
2336
|
tenantId,
|
|
2247
2337
|
scopeId,
|
|
2248
2338
|
entitlements,
|
|
@@ -2261,7 +2351,7 @@ export function createControlPlaneApi(options) {
|
|
|
2261
2351
|
// button means nothing to the thing that watches. Both name it with
|
|
2262
2352
|
// `versionReachedAt`, from the deployment each actually reached.
|
|
2263
2353
|
if (ranAs) {
|
|
2264
|
-
await admin.markScopeProvisioned(actor, tenantId, scopeId, ranAs);
|
|
2354
|
+
await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, ranAs);
|
|
2265
2355
|
}
|
|
2266
2356
|
// The reconcile succeeded; a store that did not mint rides back as a diagnosis
|
|
2267
2357
|
// (`substrat scope provision` prints it, and `/health` keeps reporting the gap
|
|
@@ -2295,14 +2385,14 @@ export function createControlPlaneApi(options) {
|
|
|
2295
2385
|
until: c.req.query('until') ?? undefined,
|
|
2296
2386
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2297
2387
|
});
|
|
2298
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2388
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2299
2389
|
if (!scope)
|
|
2300
2390
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2301
2391
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'facetEvents', input, {
|
|
2302
2392
|
// #1762: only the vertical's answer needs the guard. The co-located read runs
|
|
2303
2393
|
// this platform's own kernel, which withholds by construction.
|
|
2304
2394
|
viaVertical: async (v) => withholdIfPredatesRule(input, await v.facetEvents(scopeId, input)),
|
|
2305
|
-
colocated: () => admin.facetEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2395
|
+
colocated: () => c.var.admin.facetEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2306
2396
|
},
|
|
2307
2397
|
// The buckets RETURNED, not `total`: the row says how much this read handed
|
|
2308
2398
|
// back, and a capped facet hands back fewer than it counted.
|
|
@@ -2343,7 +2433,7 @@ export function createControlPlaneApi(options) {
|
|
|
2343
2433
|
if (!vertical)
|
|
2344
2434
|
return run.colocated();
|
|
2345
2435
|
const answer = await run.viaVertical(vertical);
|
|
2346
|
-
await admin.recordDelegatedRead(c.get('actor'), {
|
|
2436
|
+
await c.var.admin.recordDelegatedRead(c.get('actor'), {
|
|
2347
2437
|
method,
|
|
2348
2438
|
tenantId,
|
|
2349
2439
|
scopeId,
|
|
@@ -2364,12 +2454,12 @@ export function createControlPlaneApi(options) {
|
|
|
2364
2454
|
eventId: c.req.query('eventId'),
|
|
2365
2455
|
maxDepth: c.req.query('maxDepth') ? Number(c.req.query('maxDepth')) : undefined,
|
|
2366
2456
|
});
|
|
2367
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2457
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2368
2458
|
if (!scope)
|
|
2369
2459
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2370
2460
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'eventCause', input, {
|
|
2371
2461
|
viaVertical: (v) => v.eventCause(scopeId, input),
|
|
2372
|
-
colocated: () => admin.eventCause(c.get('actor'), tenantId, scopeId, input),
|
|
2462
|
+
colocated: () => c.var.admin.eventCause(c.get('actor'), tenantId, scopeId, input),
|
|
2373
2463
|
}, (r) => r.chain.length));
|
|
2374
2464
|
});
|
|
2375
2465
|
// #1237 forward: what one event set off. Delegated like its backwards twin.
|
|
@@ -2380,12 +2470,12 @@ export function createControlPlaneApi(options) {
|
|
|
2380
2470
|
eventId: c.req.query('eventId'),
|
|
2381
2471
|
maxNodes: c.req.query('maxNodes') ? Number(c.req.query('maxNodes')) : undefined,
|
|
2382
2472
|
});
|
|
2383
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2473
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2384
2474
|
if (!scope)
|
|
2385
2475
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2386
2476
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'eventEffects', input, {
|
|
2387
2477
|
viaVertical: (v) => v.eventEffects(scopeId, input),
|
|
2388
|
-
colocated: () => admin.eventEffects(c.get('actor'), tenantId, scopeId, input),
|
|
2478
|
+
colocated: () => c.var.admin.eventEffects(c.get('actor'), tenantId, scopeId, input),
|
|
2389
2479
|
}, (r) => r.count));
|
|
2390
2480
|
});
|
|
2391
2481
|
// #1237: everything one call emitted — the siblings neither walk reaches. Delegated
|
|
@@ -2397,12 +2487,12 @@ export function createControlPlaneApi(options) {
|
|
|
2397
2487
|
invocationId: c.req.query('invocationId'),
|
|
2398
2488
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2399
2489
|
});
|
|
2400
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2490
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2401
2491
|
if (!scope)
|
|
2402
2492
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2403
2493
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'invocationEvents', input, {
|
|
2404
2494
|
viaVertical: (v) => v.invocationEvents(scopeId, input),
|
|
2405
|
-
colocated: () => admin.invocationEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2495
|
+
colocated: () => c.var.admin.invocationEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2406
2496
|
}, (r) => r.events.length));
|
|
2407
2497
|
});
|
|
2408
2498
|
// #1525: every delivery in the scope that gave up — the list the walks can only reach
|
|
@@ -2414,12 +2504,12 @@ export function createControlPlaneApi(options) {
|
|
|
2414
2504
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2415
2505
|
cursor: c.req.query('cursor') ?? undefined,
|
|
2416
2506
|
});
|
|
2417
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2507
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2418
2508
|
if (!scope)
|
|
2419
2509
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2420
2510
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'deadLetters', input, {
|
|
2421
2511
|
viaVertical: (v) => v.deadLetters(scopeId, input),
|
|
2422
|
-
colocated: () => admin.deadLetters(c.get('actor'), tenantId, scopeId, input),
|
|
2512
|
+
colocated: () => c.var.admin.deadLetters(c.get('actor'), tenantId, scopeId, input),
|
|
2423
2513
|
}, (r) => r.entries.length));
|
|
2424
2514
|
});
|
|
2425
2515
|
// #1744: one entity's declared lifecycle replayed over the scope's outbox — the process
|
|
@@ -2429,12 +2519,12 @@ export function createControlPlaneApi(options) {
|
|
|
2429
2519
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2430
2520
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2431
2521
|
const input = lifecycleFlowInput.parse(await c.req.json());
|
|
2432
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2522
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2433
2523
|
if (!scope)
|
|
2434
2524
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2435
2525
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'lifecycleFlow', input, {
|
|
2436
2526
|
viaVertical: (v) => v.lifecycleFlow(scopeId, input),
|
|
2437
|
-
colocated: () => admin.lifecycleFlow(c.get('actor'), tenantId, scopeId, input),
|
|
2527
|
+
colocated: () => c.var.admin.lifecycleFlow(c.get('actor'), tenantId, scopeId, input),
|
|
2438
2528
|
}, (r) => r.observation.events));
|
|
2439
2529
|
});
|
|
2440
2530
|
// #1235: one record's event history — the per-entity pivot of the audit spine.
|
|
@@ -2449,12 +2539,12 @@ export function createControlPlaneApi(options) {
|
|
|
2449
2539
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2450
2540
|
cursor: c.req.query('cursor') ?? undefined,
|
|
2451
2541
|
});
|
|
2452
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2542
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2453
2543
|
if (!scope)
|
|
2454
2544
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2455
2545
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'entityHistory', input, {
|
|
2456
2546
|
viaVertical: (v) => v.entityHistory(scopeId, input),
|
|
2457
|
-
colocated: () => admin.entityHistory(c.get('actor'), tenantId, scopeId, input),
|
|
2547
|
+
colocated: () => c.var.admin.entityHistory(c.get('actor'), tenantId, scopeId, input),
|
|
2458
2548
|
}, (r) => r.entries.length));
|
|
2459
2549
|
});
|
|
2460
2550
|
app.get('/tenants/:tenantId/scopes/:scopeId/tables/:table', async (c) => {
|
|
@@ -2465,12 +2555,12 @@ export function createControlPlaneApi(options) {
|
|
|
2465
2555
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2466
2556
|
offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
|
|
2467
2557
|
});
|
|
2468
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2558
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2469
2559
|
if (!scope)
|
|
2470
2560
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2471
2561
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'readScopeTable', input, {
|
|
2472
2562
|
viaVertical: (v) => v.readScopeTable(scopeId, input),
|
|
2473
|
-
colocated: () => admin.readScopeTable(c.get('actor'), tenantId, scopeId, input),
|
|
2563
|
+
colocated: () => c.var.admin.readScopeTable(c.get('actor'), tenantId, scopeId, input),
|
|
2474
2564
|
}, (r) => r.rows.length));
|
|
2475
2565
|
});
|
|
2476
2566
|
// The SQL console (#219): one read-only statement, POSTed because SQL does not
|
|
@@ -2482,7 +2572,7 @@ export function createControlPlaneApi(options) {
|
|
|
2482
2572
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2483
2573
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2484
2574
|
const input = queryScopeInput.parse(await c.req.json());
|
|
2485
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2575
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2486
2576
|
if (!scope)
|
|
2487
2577
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2488
2578
|
return c.json(await delegatedRead(
|
|
@@ -2491,7 +2581,7 @@ export function createControlPlaneApi(options) {
|
|
|
2491
2581
|
// statement would answer "someone queried" and nothing a reader needs.
|
|
2492
2582
|
c, tenantId, scopeId, scope, 'queryScope', input, {
|
|
2493
2583
|
viaVertical: (v) => v.queryScope(scopeId, input),
|
|
2494
|
-
colocated: () => admin.queryScope(c.get('actor'), tenantId, scopeId, input),
|
|
2584
|
+
colocated: () => c.var.admin.queryScope(c.get('actor'), tenantId, scopeId, input),
|
|
2495
2585
|
}, (r) => r.rows.length));
|
|
2496
2586
|
});
|
|
2497
2587
|
// The K-35 denial log (#867) — the third of the platform's three logs and the last to
|
|
@@ -2513,12 +2603,12 @@ export function createControlPlaneApi(options) {
|
|
|
2513
2603
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2514
2604
|
}
|
|
2515
2605
|
const filter = denialLogQuery.parse(c.req.query());
|
|
2516
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2606
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2517
2607
|
if (!scope)
|
|
2518
2608
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2519
2609
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'listDenials', filter, {
|
|
2520
2610
|
viaVertical: (v) => v.listDenials(scopeId, filter),
|
|
2521
|
-
colocated: () => admin.listDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2611
|
+
colocated: () => c.var.admin.listDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2522
2612
|
}, (r) => r.length));
|
|
2523
2613
|
});
|
|
2524
2614
|
app.get('/tenants/:tenantId/scopes/:scopeId/denials/summary', async (c) => {
|
|
@@ -2529,12 +2619,12 @@ export function createControlPlaneApi(options) {
|
|
|
2529
2619
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2530
2620
|
}
|
|
2531
2621
|
const filter = denialLogQuery.parse(c.req.query());
|
|
2532
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2622
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2533
2623
|
if (!scope)
|
|
2534
2624
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2535
2625
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'summarizeDenials', filter, {
|
|
2536
2626
|
viaVertical: (v) => v.summarizeDenials(scopeId, filter),
|
|
2537
|
-
colocated: () => admin.summarizeDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2627
|
+
colocated: () => c.var.admin.summarizeDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2538
2628
|
}, (r) => r.buckets.length));
|
|
2539
2629
|
});
|
|
2540
2630
|
// The owner seat (#925). Both reads go to the VERTICAL — the seat lives in its identity
|
|
@@ -2551,7 +2641,7 @@ export function createControlPlaneApi(options) {
|
|
|
2551
2641
|
if (outsideTenant(principal, tenantId)) {
|
|
2552
2642
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2553
2643
|
}
|
|
2554
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2644
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2555
2645
|
if (!scope)
|
|
2556
2646
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2557
2647
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2574,7 +2664,7 @@ export function createControlPlaneApi(options) {
|
|
|
2574
2664
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2575
2665
|
}
|
|
2576
2666
|
const actor = c.get('actor');
|
|
2577
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2667
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2578
2668
|
if (!scope)
|
|
2579
2669
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2580
2670
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2584,7 +2674,7 @@ export function createControlPlaneApi(options) {
|
|
|
2584
2674
|
// the platform has activated but not requiring it — a platform hostname is bound
|
|
2585
2675
|
// `pending` and activated a step later, and that step can fail while the app is fully
|
|
2586
2676
|
// reachable (#294). `failed` is the one state that never routes, so only it is skipped.
|
|
2587
|
-
const bound = (await admin.listHostnames(actor, { scopeId })).filter((h) => h.status !== 'failed');
|
|
2677
|
+
const bound = (await c.var.admin.listHostnames(actor, { scopeId })).filter((h) => h.status !== 'failed');
|
|
2588
2678
|
const pick = (hs) => hs.find((h) => h.surface === 'app' && h.canonical) ?? hs.find((h) => h.surface === 'app') ?? hs[0];
|
|
2589
2679
|
const host = pick(bound.filter((h) => h.status === 'active')) ?? pick(bound);
|
|
2590
2680
|
if (!host) {
|
|
@@ -2618,7 +2708,7 @@ export function createControlPlaneApi(options) {
|
|
|
2618
2708
|
const body = ownerTransferInput.parse(await c.req.json());
|
|
2619
2709
|
const actor = c.get('actor');
|
|
2620
2710
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
2621
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2711
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2622
2712
|
if (!scope)
|
|
2623
2713
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2624
2714
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2628,7 +2718,7 @@ export function createControlPlaneApi(options) {
|
|
|
2628
2718
|
// An abandon (`abandon: true`) is audited the same way, every row marked, so the log tells
|
|
2629
2719
|
// a hand-over closed without finishing from one that finished.
|
|
2630
2720
|
const base = { tenantId, scopeId, operationId, from: body.from, to: body.to, ...(body.abandon ? { abandon: true } : {}) };
|
|
2631
|
-
await admin.recordOwnerTransfer(actor, { ...base, phase: 'intent' });
|
|
2721
|
+
await c.var.admin.recordOwnerTransfer(actor, { ...base, phase: 'intent' });
|
|
2632
2722
|
let moved;
|
|
2633
2723
|
try {
|
|
2634
2724
|
moved = await vertical.transferOwner({ tenantId, scopeId, ...body });
|
|
@@ -2636,7 +2726,7 @@ export function createControlPlaneApi(options) {
|
|
|
2636
2726
|
catch (e) {
|
|
2637
2727
|
const refused = e instanceof ControlPlaneError && e.status === 409;
|
|
2638
2728
|
const error = (e instanceof Error ? e.message : String(e)).slice(0, OWNER_TRANSFER_AUDIT_ERROR_MAX);
|
|
2639
|
-
await admin
|
|
2729
|
+
await c.var.admin
|
|
2640
2730
|
.recordOwnerTransfer(actor, { ...base, phase: refused ? 'refused' : 'failed', error })
|
|
2641
2731
|
.catch(() => undefined);
|
|
2642
2732
|
if (e instanceof ControlPlaneError) {
|
|
@@ -2645,7 +2735,7 @@ export function createControlPlaneApi(options) {
|
|
|
2645
2735
|
throw e;
|
|
2646
2736
|
}
|
|
2647
2737
|
try {
|
|
2648
|
-
await admin.recordOwnerTransfer(actor, {
|
|
2738
|
+
await c.var.admin.recordOwnerTransfer(actor, {
|
|
2649
2739
|
...base,
|
|
2650
2740
|
phase: 'applied',
|
|
2651
2741
|
outcome: moved.outcome,
|
|
@@ -2676,7 +2766,7 @@ export function createControlPlaneApi(options) {
|
|
|
2676
2766
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2677
2767
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2678
2768
|
const input = configureInstanceBody.parse(await c.req.json());
|
|
2679
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2769
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2680
2770
|
if (!scope)
|
|
2681
2771
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2682
2772
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2699,18 +2789,18 @@ export function createControlPlaneApi(options) {
|
|
|
2699
2789
|
// status the transition graph forbids. The graph is enforced below the seam;
|
|
2700
2790
|
// an illegal transition surfaces as a 409.
|
|
2701
2791
|
const transitions = {
|
|
2702
|
-
activate: (a, t, s) => admin.activateScope(a, t, s),
|
|
2703
|
-
suspend: (a, t, s) => admin.suspendScope(a, t, s),
|
|
2704
|
-
unsuspend: (a, t, s) => admin.unsuspendScope(a, t, s),
|
|
2705
|
-
archive: (a, t, s) => admin.archiveScope(a, t, s),
|
|
2706
|
-
unarchive: (a, t, s) => admin.unarchiveScope(a, t, s),
|
|
2792
|
+
activate: (admin, a, t, s) => admin.activateScope(a, t, s),
|
|
2793
|
+
suspend: (admin, a, t, s) => admin.suspendScope(a, t, s),
|
|
2794
|
+
unsuspend: (admin, a, t, s) => admin.unsuspendScope(a, t, s),
|
|
2795
|
+
archive: (admin, a, t, s) => admin.archiveScope(a, t, s),
|
|
2796
|
+
unarchive: (admin, a, t, s) => admin.unarchiveScope(a, t, s),
|
|
2707
2797
|
};
|
|
2708
2798
|
for (const [action, run] of Object.entries(transitions)) {
|
|
2709
2799
|
app.post(`/tenants/:tenantId/scopes/:scopeId/${action}`, async (c) => {
|
|
2710
2800
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2711
2801
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2712
|
-
await run(c.get('actor'), tenantId, scopeId);
|
|
2713
|
-
return c.json(await admin.getScopeRecord(c.get('actor'), tenantId, scopeId));
|
|
2802
|
+
await run(c.var.admin, c.get('actor'), tenantId, scopeId);
|
|
2803
|
+
return c.json(await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId));
|
|
2714
2804
|
});
|
|
2715
2805
|
}
|
|
2716
2806
|
// -- snapshots (preview-and-snapshots.md §3/§9) -----------------------------
|
|
@@ -2723,13 +2813,13 @@ export function createControlPlaneApi(options) {
|
|
|
2723
2813
|
const actor = c.get('actor');
|
|
2724
2814
|
const vertical = await verticalForScope(c, scope);
|
|
2725
2815
|
if (!vertical)
|
|
2726
|
-
return
|
|
2816
|
+
return c.var.host.snapshotScope(actor, tenantId, scope.id, opts);
|
|
2727
2817
|
const snapId = scopeIdSchema.parse(ulid());
|
|
2728
2818
|
// Directory row FIRST, as `provisioning` (K-31's two-phase shape, used as
|
|
2729
2819
|
// intended): a crash between the row and the data copy leaves an inert
|
|
2730
2820
|
// provisioning row — which, carrying provenance and an expiry, the GC sweep
|
|
2731
2821
|
// eventually reaps — never copied data with no record.
|
|
2732
|
-
await
|
|
2822
|
+
await c.var.host.provisionScope(actor, {
|
|
2733
2823
|
tenantId,
|
|
2734
2824
|
scopeId: snapId,
|
|
2735
2825
|
kind: opts.kind ?? 'archive',
|
|
@@ -2740,11 +2830,11 @@ export function createControlPlaneApi(options) {
|
|
|
2740
2830
|
expiresAt: opts.expiresAt,
|
|
2741
2831
|
});
|
|
2742
2832
|
await retryTransient(() => vertical.snapshotScope({ sourceScopeId: scope.id, newScopeId: snapId }));
|
|
2743
|
-
await admin.activateScope(actor, tenantId, snapId);
|
|
2833
|
+
await c.var.admin.activateScope(actor, tenantId, snapId);
|
|
2744
2834
|
// Bound to the SOURCE's current version: source and fork share a deployment, so
|
|
2745
2835
|
// the fork resolves to the DO namespace its bytes actually live in.
|
|
2746
2836
|
if (scope.verticalVersionId) {
|
|
2747
|
-
await admin.bindScopeVersion(actor, tenantId, snapId, scope.verticalVersionId);
|
|
2837
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, snapId, scope.verticalVersionId);
|
|
2748
2838
|
}
|
|
2749
2839
|
return snapId;
|
|
2750
2840
|
};
|
|
@@ -2753,7 +2843,7 @@ export function createControlPlaneApi(options) {
|
|
|
2753
2843
|
app.get('/tenants/:tenantId/scopes/:scopeId/snapshots', async (c) => {
|
|
2754
2844
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2755
2845
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2756
|
-
const scopes = await admin.listScopes(c.get('actor'), { tenantId });
|
|
2846
|
+
const scopes = await c.var.admin.listScopes(c.get('actor'), { tenantId });
|
|
2757
2847
|
return c.json(scopes
|
|
2758
2848
|
.filter((s) => s.forkedFrom === scopeId)
|
|
2759
2849
|
.sort((a, b) => (a.forkedAt < b.forkedAt ? 1 : -1)));
|
|
@@ -2762,12 +2852,12 @@ export function createControlPlaneApi(options) {
|
|
|
2762
2852
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2763
2853
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2764
2854
|
const body = snapshotScopeBody.parse(await c.req.json().catch(() => ({})));
|
|
2765
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2855
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2766
2856
|
if (!scope)
|
|
2767
2857
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2768
2858
|
try {
|
|
2769
2859
|
const snapId = await orchestratedSnapshot(c, tenantId, scope, body);
|
|
2770
|
-
return c.json(await admin.getScopeRecord(c.get('actor'), tenantId, snapId), 201);
|
|
2860
|
+
return c.json(await c.var.admin.getScopeRecord(c.get('actor'), tenantId, snapId), 201);
|
|
2771
2861
|
}
|
|
2772
2862
|
catch (e) {
|
|
2773
2863
|
if (e instanceof ControlPlaneError) {
|
|
@@ -2808,7 +2898,7 @@ export function createControlPlaneApi(options) {
|
|
|
2808
2898
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2809
2899
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2810
2900
|
const actor = c.get('actor');
|
|
2811
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2901
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2812
2902
|
if (!scope)
|
|
2813
2903
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2814
2904
|
if (!scope.forkedFrom) {
|
|
@@ -2821,7 +2911,7 @@ export function createControlPlaneApi(options) {
|
|
|
2821
2911
|
// between the two converges on retry.
|
|
2822
2912
|
const vertical = await verticalForScope(c, scope);
|
|
2823
2913
|
const storageStranded = await deleteScopeStorageOrStrand(vertical, scopeId);
|
|
2824
|
-
await
|
|
2914
|
+
await c.var.host.deleteSnapshot(actor, tenantId, scopeId);
|
|
2825
2915
|
return c.json({ deleted: scopeId, ...(storageStranded ? { storageStranded: true } : {}) });
|
|
2826
2916
|
}
|
|
2827
2917
|
catch (e) {
|
|
@@ -2852,7 +2942,7 @@ export function createControlPlaneApi(options) {
|
|
|
2852
2942
|
`store is global, so a backup would move its data out of that jurisdiction ` +
|
|
2853
2943
|
`(K-32); refused until a per-jurisdiction store exists`);
|
|
2854
2944
|
}
|
|
2855
|
-
const dump = await admin.exportScope(c.get('actor'), tenantId, scope.id);
|
|
2945
|
+
const dump = await c.var.admin.exportScope(c.get('actor'), tenantId, scope.id);
|
|
2856
2946
|
const vertical = await verticalForScope(c, scope);
|
|
2857
2947
|
const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
|
|
2858
2948
|
// Seal classified payloads per subject on the way in (#37). This copy is full-fidelity
|
|
@@ -2875,7 +2965,7 @@ export function createControlPlaneApi(options) {
|
|
|
2875
2965
|
return c.json({ error: 'no backup target configured' }, 501);
|
|
2876
2966
|
// Reads the directory, not the store, for the tenant cross-check: the store is keyed
|
|
2877
2967
|
// by (tenant, scope) but nothing there proves the caller's tenant owns the scope.
|
|
2878
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2968
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2879
2969
|
if (!scope)
|
|
2880
2970
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2881
2971
|
return c.json(await options.scopeBackups.list({ tenantId, scopeId }));
|
|
@@ -2889,7 +2979,7 @@ export function createControlPlaneApi(options) {
|
|
|
2889
2979
|
const capturedAt = c.req.param('capturedAt');
|
|
2890
2980
|
if (!options.scopeBackups)
|
|
2891
2981
|
return c.json({ error: 'no backup target configured' }, 501);
|
|
2892
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2982
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2893
2983
|
if (!scope)
|
|
2894
2984
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2895
2985
|
const dump = await options.scopeBackups.get({ tenantId, scopeId, capturedAt });
|
|
@@ -2909,7 +2999,7 @@ export function createControlPlaneApi(options) {
|
|
|
2909
2999
|
app.post('/tenants/:tenantId/scopes/:scopeId/backups', async (c) => {
|
|
2910
3000
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2911
3001
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2912
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
3002
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2913
3003
|
if (!scope)
|
|
2914
3004
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2915
3005
|
try {
|
|
@@ -2943,10 +3033,10 @@ export function createControlPlaneApi(options) {
|
|
|
2943
3033
|
// A ULID, like every other subject id the spine carries — parsed rather than trusted,
|
|
2944
3034
|
// so a wildcard or an injection attempt is a 400 and never reaches the UPDATE.
|
|
2945
3035
|
const subjectId = dataSubjectIdSchema.parse(c.req.param('subjectId'));
|
|
2946
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
3036
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2947
3037
|
if (!scope)
|
|
2948
3038
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2949
|
-
return c.json(await admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId));
|
|
3039
|
+
return c.json(await c.var.admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId));
|
|
2950
3040
|
});
|
|
2951
3041
|
// -- directory backups (#40) -----------------------------------------------
|
|
2952
3042
|
// The platform's own disaster recovery, on the same store posture as the scope
|
|
@@ -2971,7 +3061,7 @@ export function createControlPlaneApi(options) {
|
|
|
2971
3061
|
if (!options.directoryBackups)
|
|
2972
3062
|
return c.json({ error: 'no directory backup target configured' }, 501);
|
|
2973
3063
|
const result = await backupDirectoryIfDue({
|
|
2974
|
-
admin,
|
|
3064
|
+
admin: c.var.admin,
|
|
2975
3065
|
store: options.directoryBackups,
|
|
2976
3066
|
actor: c.get('actor'),
|
|
2977
3067
|
force: true,
|
|
@@ -3008,14 +3098,14 @@ export function createControlPlaneApi(options) {
|
|
|
3008
3098
|
if (!dump)
|
|
3009
3099
|
return c.json({ error: `no directory backup at ${body.capturedAt}` }, 404);
|
|
3010
3100
|
const actor = c.get('actor');
|
|
3011
|
-
const live = await admin.listTenants(actor, { limit: 1 });
|
|
3101
|
+
const live = await c.var.admin.listTenants(actor, { limit: 1 });
|
|
3012
3102
|
if (live.length > 0 && !body.overwrite) {
|
|
3013
3103
|
return c.json({
|
|
3014
3104
|
error: 'the directory is not empty — a restore REPLACES it, so anything created ' +
|
|
3015
3105
|
'since this copy was taken would be lost; pass overwrite=true to confirm',
|
|
3016
3106
|
}, 409);
|
|
3017
3107
|
}
|
|
3018
|
-
await admin.restoreDirectory(actor, dump);
|
|
3108
|
+
await c.var.admin.restoreDirectory(actor, dump);
|
|
3019
3109
|
return c.json({ capturedAt: dump.capturedAt, tables: dump.tables.length });
|
|
3020
3110
|
});
|
|
3021
3111
|
// Reap an ARCHIVED primary scope (control-plane.md §4.4): free its DO storage —
|
|
@@ -3059,12 +3149,12 @@ export function createControlPlaneApi(options) {
|
|
|
3059
3149
|
if (parsed.data.countOnly !== undefined) {
|
|
3060
3150
|
return c.json({ error: 'countOnly is not honoured here — POST the same body to …/redrain-count for a read-only count' }, 400);
|
|
3061
3151
|
}
|
|
3062
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3152
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3063
3153
|
if (!scope)
|
|
3064
3154
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3065
3155
|
// The instant alone, never `parsed.data`, so the field cannot reach the adapter from
|
|
3066
3156
|
// this door even if the refusal above is ever relaxed.
|
|
3067
|
-
const redrained = await admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3157
|
+
const redrained = await c.var.admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3068
3158
|
drainedBefore: parsed.data.drainedBefore,
|
|
3069
3159
|
});
|
|
3070
3160
|
// `more` rather than silence: the verb reopens a bounded batch, so a caller that posts
|
|
@@ -3103,10 +3193,10 @@ export function createControlPlaneApi(options) {
|
|
|
3103
3193
|
'already shipped to the rebuilt table. Pass the instant the old table stopped receiving.',
|
|
3104
3194
|
}, 400);
|
|
3105
3195
|
}
|
|
3106
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3196
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3107
3197
|
if (!scope)
|
|
3108
3198
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3109
|
-
const redrainable = await admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3199
|
+
const redrainable = await c.var.admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3110
3200
|
drainedBefore: parsed.data.drainedBefore,
|
|
3111
3201
|
countOnly: true,
|
|
3112
3202
|
});
|
|
@@ -3119,7 +3209,7 @@ export function createControlPlaneApi(options) {
|
|
|
3119
3209
|
// Body is optional so the bare `POST …/reap` every existing caller sends still
|
|
3120
3210
|
// parses; `backup` tri-states on purpose (see the ordering comment below).
|
|
3121
3211
|
const { backup: wantsBackup } = reapScopeBody.parse(await c.req.json().catch(() => ({})));
|
|
3122
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3212
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3123
3213
|
if (!scope)
|
|
3124
3214
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3125
3215
|
if (scope.status !== 'archived') {
|
|
@@ -3130,7 +3220,7 @@ export function createControlPlaneApi(options) {
|
|
|
3130
3220
|
// only in `reapScope` after. A serving app always holds a bound name; unbind it first.
|
|
3131
3221
|
// (This route is the interactive per-scope reap; the tenant-teardown route forces past
|
|
3132
3222
|
// the same guard because releasing every name is the point there.)
|
|
3133
|
-
const boundNames = await admin.listHostnames(actor, { scopeId, limit: 1 });
|
|
3223
|
+
const boundNames = await c.var.admin.listHostnames(actor, { scopeId, limit: 1 });
|
|
3134
3224
|
if (boundNames.length > 0) {
|
|
3135
3225
|
return c.json({
|
|
3136
3226
|
error: `scope ${scopeId} still resolves hostname '${boundNames[0].hostname}' — ` +
|
|
@@ -3168,10 +3258,10 @@ export function createControlPlaneApi(options) {
|
|
|
3168
3258
|
// By this point the backup contract has resolved (a copy landed, or the caller
|
|
3169
3259
|
// explicitly declined one), so stranding is a bookkeeping fact, not data loss.
|
|
3170
3260
|
const storageStranded = await deleteScopeStorageOrStrand(vertical, scopeId);
|
|
3171
|
-
await admin.reapScope(actor, tenantId, scopeId, {
|
|
3261
|
+
await c.var.admin.reapScope(actor, tenantId, scopeId, {
|
|
3172
3262
|
...(backup ? { backupRef: backupRefOf(backup) } : {}),
|
|
3173
3263
|
});
|
|
3174
|
-
const reaped = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3264
|
+
const reaped = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3175
3265
|
return c.json({ ...reaped, backup, ...(storageStranded ? { storageStranded: true } : {}) });
|
|
3176
3266
|
}
|
|
3177
3267
|
catch (e) {
|
|
@@ -3191,7 +3281,7 @@ export function createControlPlaneApi(options) {
|
|
|
3191
3281
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3192
3282
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3193
3283
|
const actor = c.get('actor');
|
|
3194
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3284
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3195
3285
|
if (!scope)
|
|
3196
3286
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3197
3287
|
// Residency (K-7/K-32): jurisdiction pins EXECUTION, not just storage. A pull
|
|
@@ -3209,7 +3299,7 @@ export function createControlPlaneApi(options) {
|
|
|
3209
3299
|
// bytes when the host is co-located. When the scope's data lives in a vertical
|
|
3210
3300
|
// deployment, its dump OVERLAYS the (placeholder) tables — audit stays on the
|
|
3211
3301
|
// one canonical path either way.
|
|
3212
|
-
const dump = await admin.exportScope(actor, tenantId, scopeId);
|
|
3302
|
+
const dump = await c.var.admin.exportScope(actor, tenantId, scopeId);
|
|
3213
3303
|
const vertical = await verticalForScope(c, scope);
|
|
3214
3304
|
const tables = vertical ? await vertical.exportScope(scopeId) : dump.tables;
|
|
3215
3305
|
if (full)
|
|
@@ -3248,13 +3338,13 @@ export function createControlPlaneApi(options) {
|
|
|
3248
3338
|
app.get('/tenants/:tenantId/export', async (c) => {
|
|
3249
3339
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3250
3340
|
const actor = c.get('actor');
|
|
3251
|
-
const tenant = await admin.getTenant(actor, tenantId);
|
|
3341
|
+
const tenant = await c.var.admin.getTenant(actor, tenantId);
|
|
3252
3342
|
if (!tenant)
|
|
3253
3343
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
3254
3344
|
// Every scope, tombstones included: an archived or reaped scope is part of the
|
|
3255
3345
|
// tenant's history, and an export that quietly dropped them would misrepresent what
|
|
3256
3346
|
// the tenant was. Their DATA is a different question, handled below.
|
|
3257
|
-
const scopes = await admin.listScopes(actor, { tenantId });
|
|
3347
|
+
const scopes = await c.var.admin.listScopes(actor, { tenantId });
|
|
3258
3348
|
// Residency (K-7/K-32), checked across the WHOLE tenant before anything is read: an
|
|
3259
3349
|
// export lands on a machine outside the platform's control, so one pinned scope
|
|
3260
3350
|
// taints the file. Refused as a unit rather than silently exporting the global
|
|
@@ -3271,18 +3361,18 @@ export function createControlPlaneApi(options) {
|
|
|
3271
3361
|
}
|
|
3272
3362
|
const full = c.req.query('full') === 'true';
|
|
3273
3363
|
try {
|
|
3274
|
-
const orgs = await admin.listOrgs(actor, tenantId);
|
|
3364
|
+
const orgs = await c.var.admin.listOrgs(actor, tenantId);
|
|
3275
3365
|
// Revoked memberships included: K-21 makes a removal a tombstone precisely because
|
|
3276
3366
|
// "was a member until March" is the fact an audit asks for.
|
|
3277
|
-
const members = (await Promise.all(orgs.map((o) => admin.listMembers(actor, tenantId, o.id, { includeRevoked: true })))).flat();
|
|
3367
|
+
const members = (await Promise.all(orgs.map((o) => c.var.admin.listMembers(actor, tenantId, o.id, { includeRevoked: true })))).flat();
|
|
3278
3368
|
const [roles, entitlements, identityLinks, hostnames, stores, blobStores, connections] = await Promise.all([
|
|
3279
|
-
admin.listRoles(actor, { tenantId }),
|
|
3280
|
-
admin.listEntitlements(actor, tenantId),
|
|
3281
|
-
admin.listIdentityLinks(actor, tenantId),
|
|
3282
|
-
admin.listHostnames(actor, { tenantId }),
|
|
3283
|
-
admin.listTenantStores(actor, { tenantId }),
|
|
3284
|
-
admin.listBlobStores(actor, { tenantId }),
|
|
3285
|
-
admin.listConnections(actor, { tenantId }),
|
|
3369
|
+
c.var.admin.listRoles(actor, { tenantId }),
|
|
3370
|
+
c.var.admin.listEntitlements(actor, tenantId),
|
|
3371
|
+
c.var.admin.listIdentityLinks(actor, tenantId),
|
|
3372
|
+
c.var.admin.listHostnames(actor, { tenantId }),
|
|
3373
|
+
c.var.admin.listTenantStores(actor, { tenantId }),
|
|
3374
|
+
c.var.admin.listBlobStores(actor, { tenantId }),
|
|
3375
|
+
c.var.admin.listConnections(actor, { tenantId }),
|
|
3286
3376
|
]);
|
|
3287
3377
|
// Scope DATA, from the same delegation the per-scope export route uses: the
|
|
3288
3378
|
// canonical `exportScope` writes the audit entry and is the bytes when co-located;
|
|
@@ -3296,7 +3386,7 @@ export function createControlPlaneApi(options) {
|
|
|
3296
3386
|
const live = scopes.filter((s) => s.status !== 'reaped');
|
|
3297
3387
|
const data = [];
|
|
3298
3388
|
for (const scope of live) {
|
|
3299
|
-
const dump = await admin.exportScope(actor, tenantId, scope.id);
|
|
3389
|
+
const dump = await c.var.admin.exportScope(actor, tenantId, scope.id);
|
|
3300
3390
|
const vertical = await verticalForScope(c, scope);
|
|
3301
3391
|
const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
|
|
3302
3392
|
data.push({ ...dump, tables: mask ? await maskDump(tables, mask) : tables });
|
|
@@ -3305,7 +3395,7 @@ export function createControlPlaneApi(options) {
|
|
|
3305
3395
|
// customer's Art. 20 data, and it carries staff actor ids and internal action
|
|
3306
3396
|
// names. An escrow or a dispute needs it, which is why break-glass reaches it
|
|
3307
3397
|
// rather than nothing reaching it.
|
|
3308
|
-
const adminLog = full ? await admin.auditLog(actor, { tenantId }) : null;
|
|
3398
|
+
const adminLog = full ? await c.var.admin.auditLog(actor, { tenantId }) : null;
|
|
3309
3399
|
const body = {
|
|
3310
3400
|
tenantId,
|
|
3311
3401
|
capturedAt: new Date().toISOString(),
|
|
@@ -3358,7 +3448,7 @@ export function createControlPlaneApi(options) {
|
|
|
3358
3448
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3359
3449
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3360
3450
|
const actor = c.get('actor');
|
|
3361
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3451
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3362
3452
|
if (!scope)
|
|
3363
3453
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3364
3454
|
const raw = await c.req.json();
|
|
@@ -3386,7 +3476,7 @@ export function createControlPlaneApi(options) {
|
|
|
3386
3476
|
const origin = { tenantId: tenantIdSchema.parse(dump.tenantId), scopeId: scopeIdSchema.parse(dump.scopeId) };
|
|
3387
3477
|
const tables = await openDump(dump.tables, sealerFor(c, origin.tenantId, origin.scopeId));
|
|
3388
3478
|
const landing = { ...dump, tables };
|
|
3389
|
-
await host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
|
|
3479
|
+
await c.var.host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
|
|
3390
3480
|
const vertical = await verticalForScope(c, scope);
|
|
3391
3481
|
// #1742: the recorded OFF positions ride the restore, so the deployment switches them
|
|
3392
3482
|
// off in the replay's own event — its own sweeper cannot land in between.
|
|
@@ -3399,7 +3489,7 @@ export function createControlPlaneApi(options) {
|
|
|
3399
3489
|
// #1674: a backup from before a module was switched off brings it back on; the
|
|
3400
3490
|
// directory's record puts it back off now, not at the next reconcile. Where the
|
|
3401
3491
|
// deployment already did (#1742), this finds it done and audits what it reported.
|
|
3402
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored?.switchedOff });
|
|
3492
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored?.switchedOff });
|
|
3403
3493
|
return c.json({ restored: scopeId, tables: tables.length });
|
|
3404
3494
|
}
|
|
3405
3495
|
catch (e) {
|
|
@@ -3423,13 +3513,13 @@ export function createControlPlaneApi(options) {
|
|
|
3423
3513
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3424
3514
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3425
3515
|
const actor = c.get('actor');
|
|
3426
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3516
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3427
3517
|
if (!scope)
|
|
3428
3518
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3429
3519
|
const vertical = await verticalForScope(c, scope);
|
|
3430
3520
|
return c.json(vertical
|
|
3431
3521
|
? await vertical.appliedMigrations(scopeId)
|
|
3432
|
-
: await admin.scopeAppliedMigrations(actor, tenantId, scopeId));
|
|
3522
|
+
: await c.var.admin.scopeAppliedMigrations(actor, tenantId, scopeId));
|
|
3433
3523
|
});
|
|
3434
3524
|
// #286: the PITR bookmarks a scope recorded before its migration passes — the
|
|
3435
3525
|
// rewind points the deployments UI offers for a backout. Read through the
|
|
@@ -3439,13 +3529,13 @@ export function createControlPlaneApi(options) {
|
|
|
3439
3529
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3440
3530
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3441
3531
|
const actor = c.get('actor');
|
|
3442
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3532
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3443
3533
|
if (!scope)
|
|
3444
3534
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3445
3535
|
const vertical = await verticalForScope(c, scope);
|
|
3446
3536
|
return c.json(vertical
|
|
3447
3537
|
? await vertical.migrationBookmarks(scopeId)
|
|
3448
|
-
: await admin.scopeMigrationBookmarks(actor, tenantId, scopeId));
|
|
3538
|
+
: await c.var.admin.scopeMigrationBookmarks(actor, tenantId, scopeId));
|
|
3449
3539
|
});
|
|
3450
3540
|
// #286's backout: rewind a scope to a pre-migration bookmark — schema AND data,
|
|
3451
3541
|
// discarding every write since. Audited below the seam BEFORE the rewind runs;
|
|
@@ -3459,12 +3549,12 @@ export function createControlPlaneApi(options) {
|
|
|
3459
3549
|
.object({ bookmark: z.string().min(1), force: z.boolean().optional() })
|
|
3460
3550
|
.parse(await c.req.json());
|
|
3461
3551
|
const actor = c.get('actor');
|
|
3462
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3552
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3463
3553
|
if (!scope)
|
|
3464
3554
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3465
3555
|
try {
|
|
3466
3556
|
const vertical = await verticalForScope(c, scope);
|
|
3467
|
-
const result = await admin.rewindScope(actor, tenantId, scopeId, bookmark, {
|
|
3557
|
+
const result = await c.var.admin.rewindScope(actor, tenantId, scopeId, bookmark, {
|
|
3468
3558
|
force,
|
|
3469
3559
|
localApply: !vertical,
|
|
3470
3560
|
});
|
|
@@ -3478,7 +3568,7 @@ export function createControlPlaneApi(options) {
|
|
|
3478
3568
|
// Meanwhile, the deployment that rewound it holds those modules off (#1819). A hold is
|
|
3479
3569
|
// released by that reconcile's re-assert, or earlier by a switch move known to land on the
|
|
3480
3570
|
// rewound storage, such as an operator's ON.
|
|
3481
|
-
await admin.markScopeProvisioned(actor, tenantId, scopeId, null);
|
|
3571
|
+
await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, null);
|
|
3482
3572
|
return c.json(answer);
|
|
3483
3573
|
}
|
|
3484
3574
|
catch (e) {
|
|
@@ -3529,7 +3619,7 @@ export function createControlPlaneApi(options) {
|
|
|
3529
3619
|
if (v.ownerTenant === null) {
|
|
3530
3620
|
return c.json({ error: 'adopt-serving is a private-vertical operation' }, 409);
|
|
3531
3621
|
}
|
|
3532
|
-
const owned = (await admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug })).filter((s) => !s.forkedFrom && s.kind !== 'preview' && s.status === 'active');
|
|
3622
|
+
const owned = (await c.var.admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug })).filter((s) => !s.forkedFrom && s.kind !== 'preview' && s.status === 'active');
|
|
3533
3623
|
const { acknowledge } = adoptServingBody.parse(await c.req.json().catch(() => ({})));
|
|
3534
3624
|
const adopted = [];
|
|
3535
3625
|
const alreadyAdopted = [];
|
|
@@ -3556,6 +3646,13 @@ export function createControlPlaneApi(options) {
|
|
|
3556
3646
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3557
3647
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3558
3648
|
const { vertical, ackMigrations, abandonData, acknowledge } = rebindScopeVerticalBody.parse(await c.req.json());
|
|
3649
|
+
// #977: the TARGET must be a vertical this principal can read — its own tenant's, or a
|
|
3650
|
+
// listed one. The path pin confines whose scope moves; without this, it could move onto
|
|
3651
|
+
// another tenant's private vertical, which a confined caller must not even learn exists
|
|
3652
|
+
// (404, as `GET /verticals/:slug` answers it).
|
|
3653
|
+
if (await notReadable(c.get('principal'), vertical)) {
|
|
3654
|
+
return c.json({ error: `unknown vertical: ${vertical}` }, 404);
|
|
3655
|
+
}
|
|
3559
3656
|
try {
|
|
3560
3657
|
const r = await rebindScopeOntoVertical(c, tenantId, scopeId, vertical, {
|
|
3561
3658
|
ackMigrations,
|
|
@@ -3581,7 +3678,7 @@ export function createControlPlaneApi(options) {
|
|
|
3581
3678
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3582
3679
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3583
3680
|
const versionId = z.string().min(1).parse(c.req.query('versionId'));
|
|
3584
|
-
return c.json({ affected: await admin.bindingImpact(c.get('actor'), tenantId, scopeId, versionId) });
|
|
3681
|
+
return c.json({ affected: await c.var.admin.bindingImpact(c.get('actor'), tenantId, scopeId, versionId) });
|
|
3585
3682
|
});
|
|
3586
3683
|
// Pin a scope to a vertical version (#31; orchestration.md §4). Refuses a
|
|
3587
3684
|
// non-admitted version below the seam — that refusal is the registry's reason to
|
|
@@ -3600,7 +3697,7 @@ export function createControlPlaneApi(options) {
|
|
|
3600
3697
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3601
3698
|
const { versionId, snapshot, acknowledge } = bindScopeVersionBody.parse(await c.req.json());
|
|
3602
3699
|
const actor = c.get('actor');
|
|
3603
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3700
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3604
3701
|
// #1756: the apps in this tenant the bind would break, asked BEFORE the carry and the
|
|
3605
3702
|
// snapshot below, which move data. The host refuses too, whoever calls, but by then a
|
|
3606
3703
|
// refusal would land after the bytes had moved. It refuses what the bind would refuse
|
|
@@ -3621,12 +3718,12 @@ export function createControlPlaneApi(options) {
|
|
|
3621
3718
|
// already did so in the restore's own event; what it reports is audited here.
|
|
3622
3719
|
const reassert = async () => {
|
|
3623
3720
|
if (scope) {
|
|
3624
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: carried?.switchedOff });
|
|
3721
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: carried?.switchedOff });
|
|
3625
3722
|
}
|
|
3626
3723
|
};
|
|
3627
3724
|
// The host's own refusal, should the impact have changed since it was read above (a
|
|
3628
3725
|
// promote landing in between): relayed as the same 409, never as a 500.
|
|
3629
|
-
const bind = (opts) => admin.bindScopeVersion(actor, tenantId, scopeId, versionId, opts).catch(relayHostRefusal);
|
|
3726
|
+
const bind = (opts) => c.var.admin.bindScopeVersion(actor, tenantId, scopeId, versionId, opts).catch(relayHostRefusal);
|
|
3630
3727
|
if (snapshot) {
|
|
3631
3728
|
if (!scope) {
|
|
3632
3729
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
@@ -3638,8 +3735,8 @@ export function createControlPlaneApi(options) {
|
|
|
3638
3735
|
let migrationCrossing = false;
|
|
3639
3736
|
if (scope.vertical && scope.verticalVersionId) {
|
|
3640
3737
|
const [current, incoming] = await Promise.all([
|
|
3641
|
-
admin.getVersion(actor, scope.verticalVersionId, scope.vertical),
|
|
3642
|
-
admin.getVersion(actor, versionId, scope.vertical),
|
|
3738
|
+
c.var.admin.getVersion(actor, scope.verticalVersionId, scope.vertical),
|
|
3739
|
+
c.var.admin.getVersion(actor, versionId, scope.vertical),
|
|
3643
3740
|
]);
|
|
3644
3741
|
migrationCrossing = Boolean(current && incoming && current.migrationDigest !== incoming.migrationDigest);
|
|
3645
3742
|
}
|
|
@@ -3657,12 +3754,12 @@ export function createControlPlaneApi(options) {
|
|
|
3657
3754
|
await bind({ acknowledge, snapshot: true });
|
|
3658
3755
|
}
|
|
3659
3756
|
await reassert();
|
|
3660
|
-
return c.json(await admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3757
|
+
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3661
3758
|
}
|
|
3662
3759
|
await carry();
|
|
3663
3760
|
await bind({ acknowledge });
|
|
3664
3761
|
await reassert();
|
|
3665
|
-
return c.json(await admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3762
|
+
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3666
3763
|
});
|
|
3667
3764
|
// -- instances (K-31) -------------------------------------------------------
|
|
3668
3765
|
// The one place this surface calls OUT rather than sitting over `HostAdmin`, and
|
|
@@ -3679,7 +3776,7 @@ export function createControlPlaneApi(options) {
|
|
|
3679
3776
|
// The install kill-switch: a blocked vertical takes no NEW instances, for anyone
|
|
3680
3777
|
// including its owner. Refused before deployment resolution so the answer is
|
|
3681
3778
|
// uniform whether or not anything is deployed. Existing scopes keep serving.
|
|
3682
|
-
const registered = (await admin.listVerticals(c.get('actor'))).find((v) => v.slug === slug);
|
|
3779
|
+
const registered = (await c.var.admin.listVerticals(c.get('actor'))).find((v) => v.slug === slug);
|
|
3683
3780
|
if (registered?.installsBlocked) {
|
|
3684
3781
|
return c.json({ error: `new installs of vertical '${slug}' are blocked` }, 403);
|
|
3685
3782
|
}
|
|
@@ -3696,11 +3793,11 @@ export function createControlPlaneApi(options) {
|
|
|
3696
3793
|
// (#304). A vertical predating the field ignores it; grant/revoke AFTER provision ride
|
|
3697
3794
|
// a re-provision (this endpoint is idempotent, K-31), meanwhile expiry still enforces
|
|
3698
3795
|
// locally because the projected row carries it.
|
|
3699
|
-
const entitlements = await admin.listEntitlements(c.get('actor'), input.tenantId);
|
|
3796
|
+
const entitlements = await c.var.admin.listEntitlements(c.get('actor'), input.tenantId);
|
|
3700
3797
|
// #406: identity links ride the same authoritative gather — the vertical projects them
|
|
3701
3798
|
// so its auth adapter resolves logins from local storage, and offboarding becomes an
|
|
3702
3799
|
// unlink + re-deliver instead of a source edit + deploy.
|
|
3703
|
-
const identityLinks = (await admin.listIdentityLinks(c.get('actor'), input.tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
3800
|
+
const identityLinks = (await c.var.admin.listIdentityLinks(c.get('actor'), input.tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
3704
3801
|
// #592: connection grants too — tenant-wide rows materialize for the NEW scope, so an
|
|
3705
3802
|
// install provisioned after `grantToConnection` holds the same `connection:<id>` tuple
|
|
3706
3803
|
// as one provisioned before it, and the connector return path works without a human
|
|
@@ -3709,24 +3806,24 @@ export function createControlPlaneApi(options) {
|
|
|
3709
3806
|
// connector has since declared another grant receives it — rather than inheriting the
|
|
3710
3807
|
// gap the tenant's first install was created with.
|
|
3711
3808
|
try {
|
|
3712
|
-
await reconcileConnectionGrants({ admin, actor: c.get('actor'), declared: options.connectorGrants ?? {} }, input.tenantId, slug);
|
|
3809
|
+
await reconcileConnectionGrants({ admin: c.var.admin, actor: c.get('actor'), declared: options.connectorGrants ?? {} }, input.tenantId, slug);
|
|
3713
3810
|
}
|
|
3714
3811
|
catch {
|
|
3715
3812
|
// Best-effort, exactly as on the reconcile route.
|
|
3716
3813
|
}
|
|
3717
|
-
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(c.get('actor'), input.tenantId), slug, input.scopeId);
|
|
3814
|
+
const connectionGrants = connectionGrantsForScope(await c.var.admin.listConnectionGrants(c.get('actor'), input.tenantId), slug, input.scopeId);
|
|
3718
3815
|
// #687: and the public sealing keys of this tenant's live connections for the same
|
|
3719
3816
|
// vertical, so the new install can seal a value TO a connector from its first
|
|
3720
3817
|
// operation — before this, a scope could only receive a connector's answer, never
|
|
3721
3818
|
// hand it something the spine must not carry in the clear.
|
|
3722
|
-
const connectionKeys = await admin.connectionSealingKeys(input.tenantId, slug);
|
|
3819
|
+
const connectionKeys = await c.var.admin.connectionSealingKeys(input.tenantId, slug);
|
|
3723
3820
|
// #301 PR-2: per-tenant relational stores the vertical DECLARED, minted here (before
|
|
3724
3821
|
// the callback — the vertical migrates the store inside the K-31 ready-gate, so it
|
|
3725
3822
|
// must exist and be bound first). Idempotent like the endpoint: a retried provision
|
|
3726
3823
|
// re-resolves the same handles and the binding attach no-ops once present. `[]` for
|
|
3727
3824
|
// every vertical that declares none — the payload is unchanged for them.
|
|
3728
3825
|
const tenantStores = await collectTenantStoreHandles({
|
|
3729
|
-
host:
|
|
3826
|
+
host: c.var.host,
|
|
3730
3827
|
actor: c.get('actor'),
|
|
3731
3828
|
slug,
|
|
3732
3829
|
tenantId: input.tenantId,
|
|
@@ -3736,7 +3833,7 @@ export function createControlPlaneApi(options) {
|
|
|
3736
3833
|
// No handle rides the provision callback (a blob store has no schema to migrate), so
|
|
3737
3834
|
// the effect is purely the ledger row + the attached r2_bucket binding.
|
|
3738
3835
|
await collectBlobStoreHandles({
|
|
3739
|
-
host:
|
|
3836
|
+
host: c.var.host,
|
|
3740
3837
|
actor: c.get('actor'),
|
|
3741
3838
|
slug,
|
|
3742
3839
|
tenantId: input.tenantId,
|
|
@@ -3745,7 +3842,7 @@ export function createControlPlaneApi(options) {
|
|
|
3745
3842
|
// #1742: the recorded OFF positions ride the provision into the seat's own unit. Read
|
|
3746
3843
|
// from the directory whether or not the row exists yet: a brand-new install has no
|
|
3747
3844
|
// record, so nothing is carried and the body is unchanged.
|
|
3748
|
-
const switches = await switchCarryFor(admin, c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId });
|
|
3845
|
+
const switches = await switchCarryFor(c.var.admin, c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId });
|
|
3749
3846
|
try {
|
|
3750
3847
|
// #424 case 2: the binding attach above races Cloudflare script-settings
|
|
3751
3848
|
// propagation, so the vertical's FIRST answer can be a transient 5xx that a
|
|
@@ -3767,17 +3864,17 @@ export function createControlPlaneApi(options) {
|
|
|
3767
3864
|
// is the right trade — a receipt written before a bind would name no version, and
|
|
3768
3865
|
// one guessed from the serving pointer would claim a hook ran against code this
|
|
3769
3866
|
// scope was not running.
|
|
3770
|
-
const provisioned = await admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
3867
|
+
const provisioned = await c.var.admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
3771
3868
|
// #1674: this route is idempotent (K-31), so an already-bound scope that was wiped can
|
|
3772
3869
|
// come back through it, re-seated live by the deployment — and the receipt below means
|
|
3773
3870
|
// no sweep follows. Re-assert the recorded OFF positions before it. Only once the
|
|
3774
3871
|
// directory row exists: a brand-new install's is written after this call (see above),
|
|
3775
3872
|
// has no record to re-assert, and `reassertSystemSwitches` refuses an unknown scope.
|
|
3776
3873
|
if (provisioned) {
|
|
3777
|
-
await admin.reassertSystemSwitches(c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId }, { appliedInUnit: instance.switchedOff });
|
|
3874
|
+
await c.var.admin.reassertSystemSwitches(c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId }, { appliedInUnit: instance.switchedOff });
|
|
3778
3875
|
}
|
|
3779
3876
|
if (provisioned?.verticalVersionId) {
|
|
3780
|
-
await admin.markScopeProvisioned(c.get('actor'), input.tenantId, input.scopeId, provisioned.verticalVersionId);
|
|
3877
|
+
await c.var.admin.markScopeProvisioned(c.get('actor'), input.tenantId, input.scopeId, provisioned.verticalVersionId);
|
|
3781
3878
|
}
|
|
3782
3879
|
return c.json(instance, 201);
|
|
3783
3880
|
}
|
|
@@ -3816,13 +3913,13 @@ export function createControlPlaneApi(options) {
|
|
|
3816
3913
|
// `listVerticals` returns every row (the adapter does not narrow); the transport is
|
|
3817
3914
|
// where per-principal filtering happens — here and on GET /verticals.
|
|
3818
3915
|
const ownerOf = async (actor, slug) => {
|
|
3819
|
-
const v = (await admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3916
|
+
const v = (await host.admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3820
3917
|
return v ? v.ownerTenant : undefined;
|
|
3821
3918
|
};
|
|
3822
3919
|
// The full registry row, for the checks that need more than the owner — whether the
|
|
3823
3920
|
// vertical is PRIVATE (owned + not listed), which is what scopes a builder's prod
|
|
3824
3921
|
// self-serve below.
|
|
3825
|
-
const verticalOf = async (actor, slug) => (await admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3922
|
+
const verticalOf = async (actor, slug) => (await host.admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3826
3923
|
/**
|
|
3827
3924
|
* "This principal is confined, and this vertical is not its tenant's." The one
|
|
3828
3925
|
* predicate every slug-addressed route asks.
|
|
@@ -3846,6 +3943,22 @@ export function createControlPlaneApi(options) {
|
|
|
3846
3943
|
* vertical stays owner-only, and so do the reads narrowed on purpose (migration SQL,
|
|
3847
3944
|
* the prod history).
|
|
3848
3945
|
*/
|
|
3946
|
+
/**
|
|
3947
|
+
* Whether a CONFINED principal may grant its own tenant `key` (#977): some vertical it
|
|
3948
|
+
* can read — its own, or a listed one — declares the key, or is the vertical whose
|
|
3949
|
+
* bare slug it is. The same derivation the dashboard's install makes
|
|
3950
|
+
* (`installEntitlements`), checked here so a credential cannot grant what no install
|
|
3951
|
+
* would. Read through the configured host: a check audits nothing.
|
|
3952
|
+
*/
|
|
3953
|
+
const tenantGrantable = async (p, key) => {
|
|
3954
|
+
const pin = confinedTenant(p);
|
|
3955
|
+
if (pin === null)
|
|
3956
|
+
return true;
|
|
3957
|
+
const visible = (await host.admin.listVerticals(p.actor)).filter((v) => v.ownerTenant === pin || v.listed);
|
|
3958
|
+
return visible.some((v) => v.entitlements && v.entitlements.length > 0
|
|
3959
|
+
? v.entitlements.includes(key)
|
|
3960
|
+
: (v.slug.split('/').pop() || v.slug) === key);
|
|
3961
|
+
};
|
|
3849
3962
|
const notReadable = async (p, slug) => {
|
|
3850
3963
|
const pin = confinedTenant(p);
|
|
3851
3964
|
if (pin === null)
|
|
@@ -3883,7 +3996,7 @@ export function createControlPlaneApi(options) {
|
|
|
3883
3996
|
if (!pin)
|
|
3884
3997
|
return raw;
|
|
3885
3998
|
const actor = c.get('actor');
|
|
3886
|
-
const workspace = (await admin.listTenants(actor)).find((t) => t.slug === pin || t.id === pin);
|
|
3999
|
+
const workspace = (await c.var.admin.listTenants(actor)).find((t) => t.slug === pin || t.id === pin);
|
|
3887
4000
|
if (!workspace || raw.startsWith(`${workspace.slug}/`))
|
|
3888
4001
|
return raw;
|
|
3889
4002
|
if ((await ownerOf(actor, raw)) === workspace.id)
|
|
@@ -3893,7 +4006,7 @@ export function createControlPlaneApi(options) {
|
|
|
3893
4006
|
};
|
|
3894
4007
|
app.get('/verticals', async (c) => {
|
|
3895
4008
|
const page = pageParams(c);
|
|
3896
|
-
const all = await admin.listVerticals(c.get('actor'));
|
|
4009
|
+
const all = await c.var.admin.listVerticals(c.get('actor'));
|
|
3897
4010
|
const p = c.get('principal');
|
|
3898
4011
|
// A builder sees only what it owns; staff see the whole registry. The narrowing
|
|
3899
4012
|
// runs above the adapter, so the page is sliced here — over the narrowed list.
|
|
@@ -3932,12 +4045,12 @@ export function createControlPlaneApi(options) {
|
|
|
3932
4045
|
* it that can drift when a third ref scheme appears.
|
|
3933
4046
|
*/
|
|
3934
4047
|
const tenantServiceRefs = async (actor, tenantId, slug) => {
|
|
3935
|
-
const all = (await admin.listVerticals(actor)).filter((v) => v.ownerTenant === tenantId);
|
|
4048
|
+
const all = (await host.admin.listVerticals(actor)).filter((v) => v.ownerTenant === tenantId);
|
|
3936
4049
|
const considered = slug === undefined ? all : all.filter((v) => v.slug === slug);
|
|
3937
4050
|
const verticalsTruncated = considered.length > MAX_SERVICE_REF_VERTICALS;
|
|
3938
4051
|
const entries = [];
|
|
3939
4052
|
for (const v of considered.slice(0, MAX_SERVICE_REF_VERTICALS)) {
|
|
3940
|
-
const versions = await admin.listVersions(actor, v.slug);
|
|
4053
|
+
const versions = await host.admin.listVersions(actor, v.slug);
|
|
3941
4054
|
const labelOf = new Map(versions.map((ver) => [ver.id, ver.version]));
|
|
3942
4055
|
// The stable serving script — where real traffic lands. Its version is the
|
|
3943
4056
|
// registry row's own servingVersionId: authoritative, not the scope-derived
|
|
@@ -4016,10 +4129,10 @@ export function createControlPlaneApi(options) {
|
|
|
4016
4129
|
return c.json({ error: 'forbidden' }, 403);
|
|
4017
4130
|
input.ownerTenant = p.tenantId;
|
|
4018
4131
|
}
|
|
4019
|
-
await admin.registerVertical(c.get('actor'), input);
|
|
4132
|
+
await c.var.admin.registerVertical(c.get('actor'), input);
|
|
4020
4133
|
// Idempotent on the slug (a conflicting re-register throws below the seam), so
|
|
4021
4134
|
// read back rather than echo the request.
|
|
4022
|
-
const registered = (await admin.listVerticals(c.get('actor'))).find((v) => v.slug === input.slug);
|
|
4135
|
+
const registered = (await c.var.admin.listVerticals(c.get('actor'))).find((v) => v.slug === input.slug);
|
|
4023
4136
|
return c.json(registered, 201);
|
|
4024
4137
|
});
|
|
4025
4138
|
app.get('/verticals/:slug/versions', async (c) => {
|
|
@@ -4031,7 +4144,7 @@ export function createControlPlaneApi(options) {
|
|
|
4031
4144
|
return c.json({ error: 'not found' }, 404);
|
|
4032
4145
|
}
|
|
4033
4146
|
const page = pageParams(c);
|
|
4034
|
-
const entries = await admin.listVersions(c.get('actor'), slug, page);
|
|
4147
|
+
const entries = await c.var.admin.listVersions(c.get('actor'), slug, page);
|
|
4035
4148
|
return c.json(pageOf(entries, page.limit, (v) => v.id));
|
|
4036
4149
|
});
|
|
4037
4150
|
app.post('/verticals/:slug/versions', async (c) => {
|
|
@@ -4077,8 +4190,8 @@ export function createControlPlaneApi(options) {
|
|
|
4077
4190
|
return refuse(registry.error.issues[0], ['registry']);
|
|
4078
4191
|
}
|
|
4079
4192
|
}
|
|
4080
|
-
await admin.publishVersion(c.get('actor'), input);
|
|
4081
|
-
const version = await admin.getVersion(c.get('actor'), input.id, slug);
|
|
4193
|
+
await c.var.admin.publishVersion(c.get('actor'), input);
|
|
4194
|
+
const version = await c.var.admin.getVersion(c.get('actor'), input.id, slug);
|
|
4082
4195
|
return c.json(version, 201);
|
|
4083
4196
|
});
|
|
4084
4197
|
// The declared permission registry (D-39, #336) that ships inside one version's manifest:
|
|
@@ -4093,7 +4206,7 @@ export function createControlPlaneApi(options) {
|
|
|
4093
4206
|
if (await notReadable(p, slug)) {
|
|
4094
4207
|
return c.json({ error: 'not found' }, 404);
|
|
4095
4208
|
}
|
|
4096
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4209
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4097
4210
|
const registry = json ? (storedDeployManifest.parse(JSON.parse(json)).registry ?? null) : null;
|
|
4098
4211
|
return c.json({ registry });
|
|
4099
4212
|
});
|
|
@@ -4112,7 +4225,7 @@ export function createControlPlaneApi(options) {
|
|
|
4112
4225
|
return c.json({ error: 'not found' }, 404);
|
|
4113
4226
|
}
|
|
4114
4227
|
// `versionMigrations` refuses a version of another vertical, so `base` cannot reach one.
|
|
4115
|
-
const migrationsOf = (id) => admin.versionMigrations(c.get('actor'), slug, id);
|
|
4228
|
+
const migrationsOf = (id) => c.var.admin.versionMigrations(c.get('actor'), slug, id);
|
|
4116
4229
|
const base = c.req.query('base');
|
|
4117
4230
|
const [incoming, baseline] = await Promise.all([
|
|
4118
4231
|
migrationsOf(c.req.param('id')),
|
|
@@ -4133,7 +4246,7 @@ export function createControlPlaneApi(options) {
|
|
|
4133
4246
|
if (await notReadable(p, slug)) {
|
|
4134
4247
|
return c.json({ error: 'not found' }, 404);
|
|
4135
4248
|
}
|
|
4136
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4249
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4137
4250
|
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
4138
4251
|
// #1232: freshness rides the same read — the manifest is already in hand, and a
|
|
4139
4252
|
// sibling route would cost a second full parse for a field sitting beside this one.
|
|
@@ -4156,7 +4269,7 @@ export function createControlPlaneApi(options) {
|
|
|
4156
4269
|
if (await notReadable(p, slug)) {
|
|
4157
4270
|
return c.json({ error: 'not found' }, 404);
|
|
4158
4271
|
}
|
|
4159
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4272
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4160
4273
|
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
4161
4274
|
return c.json({
|
|
4162
4275
|
declaredEvents: parsed?.declaredEvents ?? null,
|
|
@@ -4184,7 +4297,7 @@ export function createControlPlaneApi(options) {
|
|
|
4184
4297
|
if (await notReadable(p, slug)) {
|
|
4185
4298
|
return c.json({ error: 'not found' }, 404);
|
|
4186
4299
|
}
|
|
4187
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4300
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4188
4301
|
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
4189
4302
|
// #1321 rides this read for the reason the freshness field rides the schedules
|
|
4190
4303
|
// one: the manifest is already parsed, and the field sits beside the model the
|
|
@@ -4203,22 +4316,22 @@ export function createControlPlaneApi(options) {
|
|
|
4203
4316
|
if (await notReadable(p, slug)) {
|
|
4204
4317
|
return c.json({ error: 'not found' }, 404);
|
|
4205
4318
|
}
|
|
4206
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4319
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4207
4320
|
const assets = json ? (storedDeployManifest.parse(JSON.parse(json)).assets ?? null) : null;
|
|
4208
4321
|
return c.json({ assets });
|
|
4209
4322
|
});
|
|
4210
4323
|
app.post('/verticals/:slug/versions/:id/admit', async (c) => {
|
|
4211
4324
|
const slug = c.req.param('slug');
|
|
4212
4325
|
const id = c.req.param('id');
|
|
4213
|
-
await admin.admitVersion(c.get('actor'), id);
|
|
4214
|
-
return c.json(await admin.getVersion(c.get('actor'), id, slug));
|
|
4326
|
+
await c.var.admin.admitVersion(c.get('actor'), id);
|
|
4327
|
+
return c.json(await c.var.admin.getVersion(c.get('actor'), id, slug));
|
|
4215
4328
|
});
|
|
4216
4329
|
app.post('/verticals/:slug/versions/:id/reject', async (c) => {
|
|
4217
4330
|
const slug = c.req.param('slug');
|
|
4218
4331
|
const id = c.req.param('id');
|
|
4219
4332
|
const { note } = rejectVersionBody.parse(await c.req.json());
|
|
4220
|
-
await admin.rejectVersion(c.get('actor'), id, note);
|
|
4221
|
-
return c.json(await admin.getVersion(c.get('actor'), id, slug));
|
|
4333
|
+
await c.var.admin.rejectVersion(c.get('actor'), id, note);
|
|
4334
|
+
return c.json(await c.var.admin.getVersion(c.get('actor'), id, slug));
|
|
4222
4335
|
});
|
|
4223
4336
|
// A builder REQUESTS publication of a vertical it owns (marketplace-publish.md §5) — any owner
|
|
4224
4337
|
// may ask; the staff `listing` flip below is the review gate. Owner-checked (like promote).
|
|
@@ -4228,7 +4341,7 @@ export function createControlPlaneApi(options) {
|
|
|
4228
4341
|
if (await notOwned(p, slug)) {
|
|
4229
4342
|
return c.json({ error: 'not found' }, 404);
|
|
4230
4343
|
}
|
|
4231
|
-
await admin.requestPublish(c.get('actor'), slug);
|
|
4344
|
+
await c.var.admin.requestPublish(c.get('actor'), slug);
|
|
4232
4345
|
return c.json({ slug, requested: true });
|
|
4233
4346
|
});
|
|
4234
4347
|
// Publish/unpublish a vertical to the PUBLIC marketplace (marketplace-publish.md §5). Staff
|
|
@@ -4237,7 +4350,7 @@ export function createControlPlaneApi(options) {
|
|
|
4237
4350
|
app.post('/verticals/:slug/listing', async (c) => {
|
|
4238
4351
|
const slug = c.req.param('slug');
|
|
4239
4352
|
const { listed } = z.object({ listed: z.boolean() }).parse(await c.req.json());
|
|
4240
|
-
await admin.setVerticalListed(c.get('actor'), slug, listed);
|
|
4353
|
+
await c.var.admin.setVerticalListed(c.get('actor'), slug, listed);
|
|
4241
4354
|
return c.json({ slug, listed });
|
|
4242
4355
|
});
|
|
4243
4356
|
// The install kill-switch (staff-only — not in BUILDER_ROUTES, so a builder is
|
|
@@ -4246,7 +4359,7 @@ export function createControlPlaneApi(options) {
|
|
|
4246
4359
|
app.post('/verticals/:slug/install-block', async (c) => {
|
|
4247
4360
|
const slug = c.req.param('slug');
|
|
4248
4361
|
const { blocked } = z.object({ blocked: z.boolean() }).parse(await c.req.json());
|
|
4249
|
-
await admin.setVerticalInstallsBlocked(c.get('actor'), slug, blocked);
|
|
4362
|
+
await c.var.admin.setVerticalInstallsBlocked(c.get('actor'), slug, blocked);
|
|
4250
4363
|
return c.json({ slug, installsBlocked: blocked });
|
|
4251
4364
|
});
|
|
4252
4365
|
// Grant/revoke the TENANT-PROVISIONER capability (#412) — whether this vertical's scopes
|
|
@@ -4256,7 +4369,7 @@ export function createControlPlaneApi(options) {
|
|
|
4256
4369
|
app.post('/verticals/:slug/tenant-provisioner', async (c) => {
|
|
4257
4370
|
const slug = c.req.param('slug');
|
|
4258
4371
|
const { granted } = z.object({ granted: z.boolean() }).parse(await c.req.json());
|
|
4259
|
-
await admin.setVerticalTenantProvisioner(c.get('actor'), slug, granted);
|
|
4372
|
+
await c.var.admin.setVerticalTenantProvisioner(c.get('actor'), slug, granted);
|
|
4260
4373
|
return c.json({ slug, tenantProvisioner: granted });
|
|
4261
4374
|
});
|
|
4262
4375
|
// Grant/revoke the EMAIL-SENDER capability (#303) — whether this vertical's scopes may POST to
|
|
@@ -4266,7 +4379,7 @@ export function createControlPlaneApi(options) {
|
|
|
4266
4379
|
app.post('/verticals/:slug/email-sender', async (c) => {
|
|
4267
4380
|
const slug = c.req.param('slug');
|
|
4268
4381
|
const { granted } = z.object({ granted: z.boolean() }).parse(await c.req.json());
|
|
4269
|
-
await admin.setVerticalEmailSender(c.get('actor'), slug, granted);
|
|
4382
|
+
await c.var.admin.setVerticalEmailSender(c.get('actor'), slug, granted);
|
|
4270
4383
|
return c.json({ slug, emailSender: granted });
|
|
4271
4384
|
});
|
|
4272
4385
|
// Delete a vertical + its versions and channels. Refused below the seam while any
|
|
@@ -4283,7 +4396,7 @@ export function createControlPlaneApi(options) {
|
|
|
4283
4396
|
// existence-hiding reflex the registry's other per-slug reads use (K-3).
|
|
4284
4397
|
if (await notOwned(c.get('principal'), slug))
|
|
4285
4398
|
return c.json({ error: 'not found' }, 404);
|
|
4286
|
-
await admin.deleteVertical(c.get('actor'), slug);
|
|
4399
|
+
await c.var.admin.deleteVertical(c.get('actor'), slug);
|
|
4287
4400
|
return c.json({ slug, deleted: true });
|
|
4288
4401
|
});
|
|
4289
4402
|
app.get('/verticals/:slug/channels', async (c) => {
|
|
@@ -4293,7 +4406,7 @@ export function createControlPlaneApi(options) {
|
|
|
4293
4406
|
return c.json({ error: 'not found' }, 404);
|
|
4294
4407
|
}
|
|
4295
4408
|
const page = pageParams(c);
|
|
4296
|
-
const entries = await admin.listChannels(c.get('actor'), slug, page);
|
|
4409
|
+
const entries = await c.var.admin.listChannels(c.get('actor'), slug, page);
|
|
4297
4410
|
return c.json(pageOf(entries, page.limit, (ch) => ch.channel));
|
|
4298
4411
|
});
|
|
4299
4412
|
// The promotion timeline (newest first) — what a rollback UI picks a target from.
|
|
@@ -4311,7 +4424,7 @@ export function createControlPlaneApi(options) {
|
|
|
4311
4424
|
return c.json({ error: 'not found' }, 404);
|
|
4312
4425
|
}
|
|
4313
4426
|
const page = pageParams(c);
|
|
4314
|
-
const entries = await admin.listChannelHistory(c.get('actor'), slug, channel, page);
|
|
4427
|
+
const entries = await c.var.admin.listChannelHistory(c.get('actor'), slug, channel, page);
|
|
4315
4428
|
return c.json(pageOf(entries, page.limit, (e) => e.id));
|
|
4316
4429
|
});
|
|
4317
4430
|
/**
|
|
@@ -4326,20 +4439,20 @@ export function createControlPlaneApi(options) {
|
|
|
4326
4439
|
* the caller surfaces it — and leaves `servingVersionId` trailing the channel:
|
|
4327
4440
|
* visible, and retried by promoting again.
|
|
4328
4441
|
*/
|
|
4329
|
-
const serveVersionInPlace = async (actor, slug, versionId) => {
|
|
4442
|
+
const serveVersionInPlace = async (admin, actor, slug, versionId) => {
|
|
4330
4443
|
if (!options.deployVertical || !options.fetchVerticalModules)
|
|
4331
4444
|
return; // not configured: pre-#286 behavior
|
|
4332
|
-
const version = await admin.getVersion(actor, versionId, slug);
|
|
4445
|
+
const version = await host.admin.getVersion(actor, versionId, slug);
|
|
4333
4446
|
if (!version?.deploymentRef) {
|
|
4334
4447
|
throw new ControlPlaneError(502, `version ${versionId} has no archive script to serve from`);
|
|
4335
4448
|
}
|
|
4336
4449
|
const archiveRef = version.deploymentRef;
|
|
4337
|
-
const manifestJson = await admin.versionManifest(actor, slug, versionId);
|
|
4450
|
+
const manifestJson = await host.admin.versionManifest(actor, slug, versionId);
|
|
4338
4451
|
if (!manifestJson) {
|
|
4339
4452
|
throw new ControlPlaneError(502, `version ${versionId} retained no manifest — pushed pre-#286; push it again to serve in place`);
|
|
4340
4453
|
}
|
|
4341
4454
|
const manifest = storedDeployManifest.parse(JSON.parse(manifestJson));
|
|
4342
|
-
const serving = await admin.verticalServing(actor, slug);
|
|
4455
|
+
const serving = await host.admin.verticalServing(actor, slug);
|
|
4343
4456
|
const ref = serving?.ref ?? stableDeploymentRefFor(slug);
|
|
4344
4457
|
const modules = await options.fetchVerticalModules(version.deploymentRef);
|
|
4345
4458
|
// #301 PR-2: every serving upload re-derives the per-tenant store D1 bindings from
|
|
@@ -4349,11 +4462,11 @@ export function createControlPlaneApi(options) {
|
|
|
4349
4462
|
// tenant's store. Platform-granted after the §4 sandbox check, like injectSecrets:
|
|
4350
4463
|
// the builder never declared these and never named the ids.
|
|
4351
4464
|
const storeBindings = [
|
|
4352
|
-
...tenantStoreBindings(await admin.listTenantStores(actor, { vertical: slug })),
|
|
4465
|
+
...tenantStoreBindings(await host.admin.listTenantStores(actor, { vertical: slug })),
|
|
4353
4466
|
// #473: the per-tenant blob-store r2_bucket bindings, re-derived from the ledger on
|
|
4354
4467
|
// every serving upload for the same reason — an upload replaces the script's binding
|
|
4355
4468
|
// set, so a re-deploy must never drop a tenant's attachment bucket.
|
|
4356
|
-
...blobStoreBindings(await admin.listBlobStores(actor, { vertical: slug })),
|
|
4469
|
+
...blobStoreBindings(await host.admin.listBlobStores(actor, { vertical: slug })),
|
|
4357
4470
|
].map((b) => b.type === 'd1'
|
|
4358
4471
|
? { type: 'd1', name: b.name, id: b.id }
|
|
4359
4472
|
: { type: 'r2_bucket', name: b.name, bucket_name: b.bucketName });
|
|
@@ -4438,7 +4551,7 @@ export function createControlPlaneApi(options) {
|
|
|
4438
4551
|
try {
|
|
4439
4552
|
// Every tenant with an install that can still serve. Archived/reaped scopes are
|
|
4440
4553
|
// excluded — minting a bucket for storage that is gone is pure waste.
|
|
4441
|
-
const scopes = await admin.listScopes(actor, {
|
|
4554
|
+
const scopes = await c.var.admin.listScopes(actor, {
|
|
4442
4555
|
vertical: slug,
|
|
4443
4556
|
status: ['provisioning', 'active', 'suspended'],
|
|
4444
4557
|
});
|
|
@@ -4446,7 +4559,7 @@ export function createControlPlaneApi(options) {
|
|
|
4446
4559
|
if (tenantIds.length === 0)
|
|
4447
4560
|
return { minted: [] };
|
|
4448
4561
|
const minted = await backfillDeclaredStores({
|
|
4449
|
-
host:
|
|
4562
|
+
host: c.var.host,
|
|
4450
4563
|
actor,
|
|
4451
4564
|
slug,
|
|
4452
4565
|
tenantIds,
|
|
@@ -4474,22 +4587,22 @@ export function createControlPlaneApi(options) {
|
|
|
4474
4587
|
* costs a read, a missing one a silent stall.
|
|
4475
4588
|
*/
|
|
4476
4589
|
const promoteImpactOf = async (actor, slug, channel, versionId) => {
|
|
4477
|
-
const gate = await admin.promotionImpact(actor, slug, channel, versionId);
|
|
4590
|
+
const gate = await host.admin.promotionImpact(actor, slug, channel, versionId);
|
|
4478
4591
|
if (channel !== 'prod')
|
|
4479
4592
|
return gate;
|
|
4480
4593
|
const v = await verticalOf(actor, slug);
|
|
4481
4594
|
if (!v || v.ownerTenant === null || v.listed)
|
|
4482
4595
|
return gate;
|
|
4483
|
-
const incoming = await admin.getVersion(actor, versionId, slug);
|
|
4596
|
+
const incoming = await host.admin.getVersion(actor, versionId, slug);
|
|
4484
4597
|
if (!incoming?.deploymentRef || incoming.admission !== 'admitted')
|
|
4485
4598
|
return gate;
|
|
4486
|
-
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);
|
|
4599
|
+
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);
|
|
4487
4600
|
const seen = new Set(gate.map((b) => `${b.scopeId}|${b.type}`));
|
|
4488
4601
|
const out = [...gate];
|
|
4489
4602
|
for (const s of lagging) {
|
|
4490
4603
|
// After the adopt it runs the promoted version, served in place: that pointer move, from
|
|
4491
4604
|
// what it runs now, is the move to judge.
|
|
4492
|
-
for (const b of await admin.bindingImpact(actor, s.tenantId, s.id, versionId)) {
|
|
4605
|
+
for (const b of await host.admin.bindingImpact(actor, s.tenantId, s.id, versionId)) {
|
|
4493
4606
|
const key = `${b.scopeId}|${b.type}`;
|
|
4494
4607
|
if (!seen.has(key))
|
|
4495
4608
|
(seen.add(key), out.push(b));
|
|
@@ -4575,7 +4688,7 @@ export function createControlPlaneApi(options) {
|
|
|
4575
4688
|
// and refuses a non-admitted version. Both are enforced below the seam and
|
|
4576
4689
|
// surface as a 4xx through mapError, not a 500.
|
|
4577
4690
|
try {
|
|
4578
|
-
await admin.promoteVersion(c.get('actor'), slug, channel, versionId, acknowledge);
|
|
4691
|
+
await c.var.admin.promoteVersion(c.get('actor'), slug, channel, versionId, acknowledge);
|
|
4579
4692
|
}
|
|
4580
4693
|
catch (err) {
|
|
4581
4694
|
if (!isExportBreakRefusal(err))
|
|
@@ -4596,7 +4709,7 @@ export function createControlPlaneApi(options) {
|
|
|
4596
4709
|
let adopted = { refused: [] };
|
|
4597
4710
|
if (channel === 'prod') {
|
|
4598
4711
|
try {
|
|
4599
|
-
await serveVersionInPlace(c.get('actor'), slug, versionId);
|
|
4712
|
+
await serveVersionInPlace(c.var.admin, c.get('actor'), slug, versionId);
|
|
4600
4713
|
// Adopt any still-legacy owned scope onto the serving script and advance every
|
|
4601
4714
|
// owned scope's version — the rebind the host cascade delegated to us for a
|
|
4602
4715
|
// dispatch-backed vertical (#321), in the correct order (serve → adopt → rebind),
|
|
@@ -4643,7 +4756,7 @@ export function createControlPlaneApi(options) {
|
|
|
4643
4756
|
: {}),
|
|
4644
4757
|
}, 409);
|
|
4645
4758
|
}
|
|
4646
|
-
const promoted = (await admin.listChannels(c.get('actor'), slug)).find((ch) => ch.channel === channel);
|
|
4759
|
+
const promoted = (await c.var.admin.listChannels(c.get('actor'), slug)).find((ch) => ch.channel === channel);
|
|
4647
4760
|
// The store backfill rides the response only when it has something to say (#825), so
|
|
4648
4761
|
// nothing changes for the overwhelming majority of promotes — and when a store WAS
|
|
4649
4762
|
// minted, or could not be, the builder reads it at the terminal rather than finding out
|
|
@@ -4733,7 +4846,7 @@ export function createControlPlaneApi(options) {
|
|
|
4733
4846
|
ownerTenant = p.tenantId;
|
|
4734
4847
|
}
|
|
4735
4848
|
else if (pin) {
|
|
4736
|
-
const workspace = (await admin.listTenants(c.get('actor'))).find((t) => t.slug === pin || t.id === pin);
|
|
4849
|
+
const workspace = (await c.var.admin.listTenants(c.get('actor'))).find((t) => t.slug === pin || t.id === pin);
|
|
4737
4850
|
if (!workspace)
|
|
4738
4851
|
return c.json({ error: `unknown workspace '${pin}'` }, 404);
|
|
4739
4852
|
// Back-compat: a BARE slug already registered as the pinned tenant's (a staff
|
|
@@ -4772,7 +4885,7 @@ export function createControlPlaneApi(options) {
|
|
|
4772
4885
|
// scoping is also what keeps existence-hiding intact — a builder is never told about
|
|
4773
4886
|
// a foreign private slug.
|
|
4774
4887
|
if (form.get('allowFork') !== '1') {
|
|
4775
|
-
const registry = await admin.listVerticals(c.get('actor'));
|
|
4888
|
+
const registry = await c.var.admin.listVerticals(c.get('actor'));
|
|
4776
4889
|
const tail = slug.split('/').pop();
|
|
4777
4890
|
const siblings = registry.some((v) => v.slug === slug)
|
|
4778
4891
|
? []
|
|
@@ -4920,7 +5033,7 @@ export function createControlPlaneApi(options) {
|
|
|
4920
5033
|
// (or admitted, for a PRIVATE vertical — the registry's self-admit rule). The owner
|
|
4921
5034
|
// was resolved with the slug above: the builder's tenant, the pinned workspace, or
|
|
4922
5035
|
// the preserved existing owner for an unpinned staff push.
|
|
4923
|
-
await admin.registerVertical(c.get('actor'), {
|
|
5036
|
+
await c.var.admin.registerVertical(c.get('actor'), {
|
|
4924
5037
|
slug,
|
|
4925
5038
|
name: manifest.name ?? slug,
|
|
4926
5039
|
source: 'cli',
|
|
@@ -4946,7 +5059,7 @@ export function createControlPlaneApi(options) {
|
|
|
4946
5059
|
// hostname-binding picker, never behavior. Not part of any admission digest.
|
|
4947
5060
|
...(manifest.surfaces ? { surfaces: manifest.surfaces } : {}),
|
|
4948
5061
|
});
|
|
4949
|
-
await admin.publishVersion(c.get('actor'), {
|
|
5062
|
+
await c.var.admin.publishVersion(c.get('actor'), {
|
|
4950
5063
|
id,
|
|
4951
5064
|
verticalSlug: slug,
|
|
4952
5065
|
version: manifest.version,
|
|
@@ -4972,14 +5085,14 @@ export function createControlPlaneApi(options) {
|
|
|
4972
5085
|
// one push depend on every other tenant's routing rows parsing cleanly — a
|
|
4973
5086
|
// malformed cert-validation blob on an unrelated domain took the whole deploy down
|
|
4974
5087
|
// with a blank 500, after the version had already been published.
|
|
4975
|
-
const bound = await admin.listHostnames(c.get('actor'), { verticalSlug: slug });
|
|
5088
|
+
const bound = await c.var.admin.listHostnames(c.get('actor'), { verticalSlug: slug });
|
|
4976
5089
|
for (const h of bound) {
|
|
4977
5090
|
if (!declared.has(h.surface)) {
|
|
4978
5091
|
warnings.push(`hostname '${h.hostname}' is bound to surface '${h.surface}', which this version no longer declares`);
|
|
4979
5092
|
}
|
|
4980
5093
|
}
|
|
4981
5094
|
}
|
|
4982
|
-
const version = await admin.getVersion(c.get('actor'), id, slug);
|
|
5095
|
+
const version = await c.var.admin.getVersion(c.get('actor'), id, slug);
|
|
4983
5096
|
return c.json({ ...version, ...(warnings.length ? { warnings } : {}) }, 201);
|
|
4984
5097
|
});
|
|
4985
5098
|
// -- where this platform runs (ops ergonomics) -----------------------------
|
|
@@ -5258,7 +5371,7 @@ export function createControlPlaneApi(options) {
|
|
|
5258
5371
|
* nothing. `listScopes`, not `getScopeRecord`: a dashboard read must not write an access
|
|
5259
5372
|
* row per page view.
|
|
5260
5373
|
*/
|
|
5261
|
-
const appServices = async (actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
|
|
5374
|
+
const appServices = async (actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await host.admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
|
|
5262
5375
|
app.get('/observability/tenant-request-volume', async (c) => {
|
|
5263
5376
|
if (!options.observability?.tenantRequestVolume) {
|
|
5264
5377
|
return c.json({ error: 'request aggregation is not configured on this control plane' }, 501);
|
|
@@ -5359,7 +5472,7 @@ export function createControlPlaneApi(options) {
|
|
|
5359
5472
|
.parse({ hours: c.req.query('hours'), limit: c.req.query('limit') });
|
|
5360
5473
|
// Deployed versions only: a version with no `deploymentRef` has never run, so there is
|
|
5361
5474
|
// nothing it could have been observed reaching.
|
|
5362
|
-
const versions = (await admin.listVersions(c.get('actor'), slug)).filter((v) => v.deploymentRef);
|
|
5475
|
+
const versions = (await c.var.admin.listVersions(c.get('actor'), slug)).filter((v) => v.deploymentRef);
|
|
5363
5476
|
// Cap the fan-out — each service is another backend query. Truncating the SERVICE list
|
|
5364
5477
|
// is exactly the silent-drop #859 calls a defect, so it is reported, not swallowed.
|
|
5365
5478
|
const MAX_SERVICES = 20;
|
|
@@ -5422,7 +5535,7 @@ export function createControlPlaneApi(options) {
|
|
|
5422
5535
|
return c.json({ error: 'push tokens are not configured on this control plane' }, 501);
|
|
5423
5536
|
}
|
|
5424
5537
|
const { tenantId } = z.object({ tenantId: tenantIdSchema }).parse(await c.req.json());
|
|
5425
|
-
const tenant = await admin.getTenant(c.get('actor'), tenantId);
|
|
5538
|
+
const tenant = await c.var.admin.getTenant(c.get('actor'), tenantId);
|
|
5426
5539
|
if (!tenant)
|
|
5427
5540
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
5428
5541
|
const token = await mintPushToken(options.pushTokenSecret, {
|
|
@@ -5443,12 +5556,22 @@ export function createControlPlaneApi(options) {
|
|
|
5443
5556
|
if (!options.tenantTokenSecret) {
|
|
5444
5557
|
return c.json({ error: 'tenant tokens are not configured on this control plane' }, 501);
|
|
5445
5558
|
}
|
|
5446
|
-
const { tenantId } = z
|
|
5559
|
+
const { tenantId, principal, impersonation } = z
|
|
5560
|
+
.object({
|
|
5561
|
+
tenantId: tenantIdSchema,
|
|
5562
|
+
// #977: the signed-in person the token will act for — attribution on every admin
|
|
5563
|
+
// row it writes, never reach. Optional: a token for background work names nobody.
|
|
5564
|
+
principal: principalIdSchema.optional(),
|
|
5565
|
+
// K-42: the staff member acting as `principal`. Meaningless without the person.
|
|
5566
|
+
impersonation: impersonationStamp.optional(),
|
|
5567
|
+
})
|
|
5568
|
+
.refine((b) => !b.impersonation || b.principal, { message: 'impersonation names no principal' })
|
|
5569
|
+
.parse(await c.req.json());
|
|
5447
5570
|
// Unlike the push-token mint, this does NOT require the tenant to exist: a token
|
|
5448
5571
|
// for a tenant with no directory row reaches nothing except creating that row
|
|
5449
5572
|
// (`POST /tenants`, body-pinned to the same id), which is the sign-up bootstrap.
|
|
5450
5573
|
// See the mint's docblock in tenant-token.ts.
|
|
5451
|
-
const token = await mintTenantToken(options.tenantTokenSecret, { tenantId });
|
|
5574
|
+
const token = await mintTenantToken(options.tenantTokenSecret, { tenantId, principal, impersonation });
|
|
5452
5575
|
return c.json({ token }, 201);
|
|
5453
5576
|
});
|
|
5454
5577
|
// -- the hostname map (§4.7, K-26) -----------------------------------------
|
|
@@ -5466,7 +5589,7 @@ export function createControlPlaneApi(options) {
|
|
|
5466
5589
|
const p = c.get('principal');
|
|
5467
5590
|
const pin = confinedTenant(p);
|
|
5468
5591
|
const filter = pin ? { tenantId: pin } : {};
|
|
5469
|
-
return (await admin.listHostnames(c.get('actor'), filter)).find((h) => h.hostname === name.toLowerCase());
|
|
5592
|
+
return (await c.var.admin.listHostnames(c.get('actor'), filter)).find((h) => h.hostname === name.toLowerCase());
|
|
5470
5593
|
};
|
|
5471
5594
|
app.get('/hostnames', async (c) => {
|
|
5472
5595
|
const p = c.get('principal');
|
|
@@ -5483,7 +5606,7 @@ export function createControlPlaneApi(options) {
|
|
|
5483
5606
|
if (hostnamePin)
|
|
5484
5607
|
filter.tenantId = hostnamePin;
|
|
5485
5608
|
const page = pageParams(c);
|
|
5486
|
-
const entries = await admin.listHostnames(c.get('actor'), { ...filter, ...page });
|
|
5609
|
+
const entries = await c.var.admin.listHostnames(c.get('actor'), { ...filter, ...page });
|
|
5487
5610
|
return c.json(pageOf(entries, page.limit, (h) => h.hostname));
|
|
5488
5611
|
});
|
|
5489
5612
|
// Which base domains ride the platform wildcard cert (go straight to `active`);
|
|
@@ -5501,7 +5624,7 @@ export function createControlPlaneApi(options) {
|
|
|
5501
5624
|
* Failures are swallowed to a `failed`/`pending` note rather than thrown: a bind that
|
|
5502
5625
|
* recorded the row must still return 201, and the reconcile sweep retries.
|
|
5503
5626
|
*/
|
|
5504
|
-
const runIssuance = async (actor, row) => {
|
|
5627
|
+
const runIssuance = async (admin, actor, row) => {
|
|
5505
5628
|
if (!custom(row.hostname)) {
|
|
5506
5629
|
// Platform mint: rides *.<base> — no per-hostname CF object, immediately servable.
|
|
5507
5630
|
// A mint carrying issuance relics (a CF id, publish-these-records rows) was born
|
|
@@ -5549,7 +5672,7 @@ export function createControlPlaneApi(options) {
|
|
|
5549
5672
|
});
|
|
5550
5673
|
}
|
|
5551
5674
|
};
|
|
5552
|
-
const hostnameRow = async (c, name) => (await admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5675
|
+
const hostnameRow = async (c, name) => (await c.var.admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5553
5676
|
app.post('/hostnames', async (c) => {
|
|
5554
5677
|
const p = c.get('principal');
|
|
5555
5678
|
const input = bindHostnameBody.parse(await c.req.json());
|
|
@@ -5578,13 +5701,13 @@ export function createControlPlaneApi(options) {
|
|
|
5578
5701
|
if (bad)
|
|
5579
5702
|
return c.json({ error: bad }, 422);
|
|
5580
5703
|
}
|
|
5581
|
-
await admin.bindHostname(c.get('actor'), input);
|
|
5582
|
-
const bound = (await admin.listHostnames(c.get('actor'), { scopeId: input.scopeId })).find((h) => h.hostname === input.hostname);
|
|
5704
|
+
await c.var.admin.bindHostname(c.get('actor'), input);
|
|
5705
|
+
const bound = (await c.var.admin.listHostnames(c.get('actor'), { scopeId: input.scopeId })).find((h) => h.hostname === input.hostname);
|
|
5583
5706
|
if (bound)
|
|
5584
|
-
await runIssuance(c.get('actor'), bound);
|
|
5707
|
+
await runIssuance(c.var.admin, c.get('actor'), bound);
|
|
5585
5708
|
// Re-read so the response carries the post-issuance status + DNS records the caller
|
|
5586
5709
|
// (dashboard / CLI) renders — a custom bind comes back `verifying` with records.
|
|
5587
|
-
const out = (await admin.listHostnames(c.get('actor'), { scopeId: input.scopeId })).find((h) => h.hostname === input.hostname);
|
|
5710
|
+
const out = (await c.var.admin.listHostnames(c.get('actor'), { scopeId: input.scopeId })).find((h) => h.hostname === input.hostname);
|
|
5588
5711
|
return c.json(out ?? bound, 201);
|
|
5589
5712
|
});
|
|
5590
5713
|
// Re-poll a custom hostname's issuance ("check again" in the dashboard). Tenant-narrowed
|
|
@@ -5598,7 +5721,7 @@ export function createControlPlaneApi(options) {
|
|
|
5598
5721
|
const row = await hostnameRow(c, name);
|
|
5599
5722
|
if (!row)
|
|
5600
5723
|
return c.json({ error: `unknown hostname: ${name.toLowerCase()}` }, 404);
|
|
5601
|
-
await runIssuance(c.get('actor'), row);
|
|
5724
|
+
await runIssuance(c.var.admin, c.get('actor'), row);
|
|
5602
5725
|
return c.json(await hostnameRow(c, name));
|
|
5603
5726
|
});
|
|
5604
5727
|
app.patch('/hostnames/:hostname/status', async (c) => {
|
|
@@ -5609,8 +5732,8 @@ export function createControlPlaneApi(options) {
|
|
|
5609
5732
|
if (confinedTenant(c.get('principal')) !== null && !(await tenantHostname(c, name))) {
|
|
5610
5733
|
return c.json({ error: `unknown hostname: ${name.toLowerCase()}` }, 404);
|
|
5611
5734
|
}
|
|
5612
|
-
await admin.setHostnameStatus(c.get('actor'), name, status, note);
|
|
5613
|
-
const row = (await admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5735
|
+
await c.var.admin.setHostnameStatus(c.get('actor'), name, status, note);
|
|
5736
|
+
const row = (await c.var.admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5614
5737
|
return c.json(row);
|
|
5615
5738
|
});
|
|
5616
5739
|
// Unbind (hard-delete) a hostname row — what the orphan cleanup uses on rows
|
|
@@ -5631,7 +5754,7 @@ export function createControlPlaneApi(options) {
|
|
|
5631
5754
|
if (row?.customHostnameId && options.provisionHostname) {
|
|
5632
5755
|
await options.provisionHostname.remove(row.customHostnameId).catch(() => { });
|
|
5633
5756
|
}
|
|
5634
|
-
await admin.unbindHostname(c.get('actor'), name);
|
|
5757
|
+
await c.var.admin.unbindHostname(c.get('actor'), name);
|
|
5635
5758
|
return c.json({ deleted: name.toLowerCase() });
|
|
5636
5759
|
});
|
|
5637
5760
|
// -- per-PR previews (preview-and-snapshots.md §2/§9) ----------------------
|
|
@@ -5677,23 +5800,23 @@ export function createControlPlaneApi(options) {
|
|
|
5677
5800
|
const vertical = await verticalForScope(c, preview);
|
|
5678
5801
|
if (vertical)
|
|
5679
5802
|
await vertical.deleteScope({ scopeId: preview.id });
|
|
5680
|
-
await
|
|
5803
|
+
await c.var.host.deleteSnapshot(c.get('actor'), preview.tenantId, preview.id);
|
|
5681
5804
|
};
|
|
5682
5805
|
/** The preview-login seam (#1704, `preview-auth.ts`): the directory, and each issuer's deployment. */
|
|
5683
5806
|
const previewAuthDeps = (c) => ({
|
|
5684
|
-
admin,
|
|
5807
|
+
admin: c.var.admin,
|
|
5685
5808
|
actor: c.get('actor'),
|
|
5686
5809
|
issuerClient: (scope) => verticalForScope(c, scope),
|
|
5687
5810
|
});
|
|
5688
5811
|
/** Given a base hostname `<label>.<domain>`, mint (or find) the preview's `--<tag>`
|
|
5689
5812
|
* hostname `<label>--<tag>.<domain>` bound to `previewId`. Non-canonical, so it never
|
|
5690
5813
|
* demotes the prod surface. Shared by the fork and clean-room paths. */
|
|
5691
|
-
const bindPreviewHostname = async (actor, baseHostname, tenantId, previewId, tag, surface) => {
|
|
5814
|
+
const bindPreviewHostname = async (admin, actor, baseHostname, tenantId, previewId, tag, surface) => {
|
|
5692
5815
|
const parsed = parseHostname(baseHostname);
|
|
5693
5816
|
if (!parsed)
|
|
5694
5817
|
throw new ControlPlaneError(400, `'${baseHostname}' is not a hostname a preview can be minted beside`);
|
|
5695
5818
|
const hostname = withLabel(parsed, `${parsed.label}${RESERVED_LABEL_SEPARATOR}${tag}`);
|
|
5696
|
-
const existing = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5819
|
+
const existing = (await host.admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5697
5820
|
if (!existing) {
|
|
5698
5821
|
await admin.bindHostname(actor, {
|
|
5699
5822
|
hostname,
|
|
@@ -5704,9 +5827,9 @@ export function createControlPlaneApi(options) {
|
|
|
5704
5827
|
canonical: false,
|
|
5705
5828
|
});
|
|
5706
5829
|
}
|
|
5707
|
-
const bound = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5830
|
+
const bound = (await host.admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5708
5831
|
if (bound)
|
|
5709
|
-
await runIssuance(actor, bound); // platform mint rides the wildcard cert → active
|
|
5832
|
+
await runIssuance(admin, actor, bound); // platform mint rides the wildcard cert → active
|
|
5710
5833
|
return hostname;
|
|
5711
5834
|
};
|
|
5712
5835
|
/** The base hostname a preview's `--<tag>` URL is derived from. A FORK inherits the
|
|
@@ -5715,7 +5838,7 @@ export function createControlPlaneApi(options) {
|
|
|
5715
5838
|
* the same scheme provisioning mints (`callout-sesamy.global.substrat.run`). */
|
|
5716
5839
|
const previewBaseHostname = async (actor, source, tenantId, slug, surface) => {
|
|
5717
5840
|
if (source) {
|
|
5718
|
-
const own = await admin.listHostnames(actor, { scopeId: source.id });
|
|
5841
|
+
const own = await host.admin.listHostnames(actor, { scopeId: source.id });
|
|
5719
5842
|
const src = own.find((h) => h.canonical && h.surface === surface) ??
|
|
5720
5843
|
own.find((h) => h.canonical) ??
|
|
5721
5844
|
own[0];
|
|
@@ -5737,7 +5860,7 @@ export function createControlPlaneApi(options) {
|
|
|
5737
5860
|
// bare apex and stranded clean-room previews on a hostname that never resolves.
|
|
5738
5861
|
const baseDomain = [...platformBaseDomains].sort((a, b) => a.length - b.length)[0];
|
|
5739
5862
|
const jurisdiction = 'global';
|
|
5740
|
-
const tenant = await admin.getTenant(actor, tenantId);
|
|
5863
|
+
const tenant = await host.admin.getTenant(actor, tenantId);
|
|
5741
5864
|
const handle = tenant?.slug ?? tenantId;
|
|
5742
5865
|
return `${slug.split('/').at(-1)}-${handle}.${jurisdiction}.${baseDomain}`;
|
|
5743
5866
|
};
|
|
@@ -5753,12 +5876,12 @@ export function createControlPlaneApi(options) {
|
|
|
5753
5876
|
// prod build) while we report success. That is worse than a failure: it sends a reviewer
|
|
5754
5877
|
// to redo correct work. So refuse to return success for a URL that would serve other code.
|
|
5755
5878
|
const assertServesBoundVersion = async (previewId) => {
|
|
5756
|
-
const bound = await admin.getVersion(actor, opts.versionId, slug);
|
|
5879
|
+
const bound = await c.var.admin.getVersion(actor, opts.versionId, slug);
|
|
5757
5880
|
// A co-located / embedded vertical has no per-version dispatch script (deploymentRef
|
|
5758
5881
|
// null) and routes via the static fallback — nothing to compare, so nothing to guard.
|
|
5759
5882
|
if (!bound?.deploymentRef)
|
|
5760
5883
|
return;
|
|
5761
|
-
const rec = await admin.getScopeRecord(actor, tenantId, previewId);
|
|
5884
|
+
const rec = await c.var.admin.getScopeRecord(actor, tenantId, previewId);
|
|
5762
5885
|
const effectiveRef = rec?.servingRef ?? bound.deploymentRef;
|
|
5763
5886
|
if (effectiveRef !== bound.deploymentRef) {
|
|
5764
5887
|
throw new ControlPlaneError(500, `preview ${previewId} bound version ${opts.versionId} but would route to '${effectiveRef}' ` +
|
|
@@ -5784,7 +5907,7 @@ export function createControlPlaneApi(options) {
|
|
|
5784
5907
|
// every push is its own script, so the rebind first CARRIES the preview's data into
|
|
5785
5908
|
// the new version's script (#1710). The preview's data then follows it from push to
|
|
5786
5909
|
// push, and each push's migrations run forward over it (§4's rehearsal case).
|
|
5787
|
-
const existing = (await admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, opts.tag));
|
|
5910
|
+
const existing = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, opts.tag));
|
|
5788
5911
|
// A preview only HAS data once its two-phase create finished: the directory row lands
|
|
5789
5912
|
// first as `provisioning` (K-31), the fork's export→restore runs, and `activateScope`
|
|
5790
5913
|
// is the last step. So a row still at `provisioning` is a create that DIED mid-fork —
|
|
@@ -5898,20 +6021,20 @@ export function createControlPlaneApi(options) {
|
|
|
5898
6021
|
// Heal a preview provisioned before #527: clear any inherited serving_ref so routing
|
|
5899
6022
|
// follows the bound version (its per-version script), not the prod serving script.
|
|
5900
6023
|
if (existing.servingRef)
|
|
5901
|
-
await admin.setScopeServingRef(actor, tenantId, existing.id, null);
|
|
5902
|
-
await admin.bindScopeVersion(actor, tenantId, existing.id, opts.versionId);
|
|
6024
|
+
await c.var.admin.setScopeServingRef(actor, tenantId, existing.id, null);
|
|
6025
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, existing.id, opts.versionId);
|
|
5903
6026
|
// #1674: re-assert any recorded OFF in the script the preview now routes to.
|
|
5904
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId: existing.id }, { appliedInUnit: carried?.switchedOff });
|
|
6027
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId: existing.id }, { appliedInUnit: carried?.switchedOff });
|
|
5905
6028
|
// Renew (or clear) the preview's GC deadline so a reused preview does not silently die.
|
|
5906
|
-
await admin.setScopeExpiresAt(actor, tenantId, existing.id, expiresAt);
|
|
5907
|
-
const hostname = await bindPreviewHostname(actor, baseHostname, tenantId, existing.id, opts.tag, surface);
|
|
6029
|
+
await c.var.admin.setScopeExpiresAt(actor, tenantId, existing.id, expiresAt);
|
|
6030
|
+
const hostname = await bindPreviewHostname(c.var.admin, actor, baseHostname, tenantId, existing.id, opts.tag, surface);
|
|
5908
6031
|
await assertServesBoundVersion(existing.id);
|
|
5909
6032
|
// The fork it was made from, not whichever prod scope this call picked: a reuse is the
|
|
5910
6033
|
// same preview. Every push is a new version with an EMPTY config store, and the carry
|
|
5911
6034
|
// above moves the scope's data but not the vertical's config store, so the login is
|
|
5912
6035
|
// wired again on every reuse, never assumed to have survived.
|
|
5913
6036
|
const parent = existing.forkedFrom
|
|
5914
|
-
? ((await admin.getScopeRecord(actor, tenantId, existing.forkedFrom)) ?? null)
|
|
6037
|
+
? ((await c.var.admin.getScopeRecord(actor, tenantId, existing.forkedFrom)) ?? null)
|
|
5915
6038
|
: null;
|
|
5916
6039
|
const auth = await wireLogin(existing.id, hostname, parent);
|
|
5917
6040
|
return {
|
|
@@ -5981,7 +6104,7 @@ export function createControlPlaneApi(options) {
|
|
|
5981
6104
|
if (!target) {
|
|
5982
6105
|
throw new ControlPlaneError(501, 'preview needs dispatch resolution for the PR version');
|
|
5983
6106
|
}
|
|
5984
|
-
const canonical = await admin.exportScope(actor, tenantId, source.id);
|
|
6107
|
+
const canonical = await c.var.admin.exportScope(actor, tenantId, source.id);
|
|
5985
6108
|
const tables = sourceClient ? await sourceClient.exportScope(source.id) : canonical.tables;
|
|
5986
6109
|
// Directory row FIRST as `provisioning` (K-31 two-phase): a crash before the data
|
|
5987
6110
|
// copy leaves an inert row that — carrying `forkedFrom` + `expiresAt` — the GC sweep
|
|
@@ -6028,10 +6151,10 @@ export function createControlPlaneApi(options) {
|
|
|
6028
6151
|
// seats, so a provision that throws can leave a `provisioning` row too. A provision
|
|
6029
6152
|
// that never wrote one (the tag's slug taken by a racing create) marks nothing: the
|
|
6030
6153
|
// write below targets this create's own id, which does not exist, and is swallowed.
|
|
6031
|
-
await
|
|
6154
|
+
await c.var.host.provisionScope(actor, row);
|
|
6032
6155
|
if (restore)
|
|
6033
6156
|
await restoreOrRecord(restore);
|
|
6034
|
-
await admin.activateScope(actor, tenantId, previewId);
|
|
6157
|
+
await c.var.admin.activateScope(actor, tenantId, previewId);
|
|
6035
6158
|
}
|
|
6036
6159
|
catch (e) {
|
|
6037
6160
|
// Expire the row this create left behind, so the next create reaps it at once rather
|
|
@@ -6039,7 +6162,7 @@ export function createControlPlaneApi(options) {
|
|
|
6039
6162
|
// CLI's retry after a known-dead attempt (#1918). The row stays until then, or until
|
|
6040
6163
|
// the GC sweep's next pass reaps it as expired; the #559 ops-failure row outlives both.
|
|
6041
6164
|
// Best-effort: if this write fails too, the age bound still frees the tag.
|
|
6042
|
-
await admin
|
|
6165
|
+
await c.var.admin
|
|
6043
6166
|
.setScopeExpiresAt(actor, tenantId, previewId, new Date().toISOString())
|
|
6044
6167
|
.catch(() => undefined);
|
|
6045
6168
|
throw e;
|
|
@@ -6047,8 +6170,8 @@ export function createControlPlaneApi(options) {
|
|
|
6047
6170
|
// Bind the PR version. A private vertical's push self-admitted, so this is accepted; a
|
|
6048
6171
|
// preview scope also admits a pending version (#513), which is what a clean-room rehearsal
|
|
6049
6172
|
// of not-yet-admitted code needs.
|
|
6050
|
-
await admin.bindScopeVersion(actor, tenantId, previewId, opts.versionId);
|
|
6051
|
-
const hostname = await bindPreviewHostname(actor, baseHostname, tenantId, previewId, opts.tag, surface);
|
|
6173
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, previewId, opts.versionId);
|
|
6174
|
+
const hostname = await bindPreviewHostname(c.var.admin, actor, baseHostname, tenantId, previewId, opts.tag, surface);
|
|
6052
6175
|
await assertServesBoundVersion(previewId);
|
|
6053
6176
|
const auth = await wireLogin(previewId, hostname, source);
|
|
6054
6177
|
return {
|
|
@@ -6095,7 +6218,7 @@ export function createControlPlaneApi(options) {
|
|
|
6095
6218
|
// Everything else forks the tenant's own active, non-fork scope for this vertical.
|
|
6096
6219
|
let source = null;
|
|
6097
6220
|
if (!body.empty) {
|
|
6098
|
-
const owned = (await admin.listScopes(actor, { tenantId, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
6221
|
+
const owned = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
6099
6222
|
source = body.sourceScopeId
|
|
6100
6223
|
? (owned.find((s) => s.id === body.sourceScopeId) ?? null)
|
|
6101
6224
|
: owned.length === 1
|
|
@@ -6127,12 +6250,12 @@ export function createControlPlaneApi(options) {
|
|
|
6127
6250
|
return c.json({ error: gate.error }, gate.status);
|
|
6128
6251
|
const { tenantId, slug } = gate;
|
|
6129
6252
|
const actor = c.get('actor');
|
|
6130
|
-
const previews = (await admin.listScopes(actor, { tenantId, vertical: slug }))
|
|
6253
|
+
const previews = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug }))
|
|
6131
6254
|
.filter((s) => s.kind === 'preview')
|
|
6132
6255
|
.sort((a, b) => ((a.forkedAt ?? '') < (b.forkedAt ?? '') ? 1 : -1));
|
|
6133
6256
|
const out = [];
|
|
6134
6257
|
for (const s of previews) {
|
|
6135
|
-
const hosts = await admin.listHostnames(actor, { scopeId: s.id });
|
|
6258
|
+
const hosts = await c.var.admin.listHostnames(actor, { scopeId: s.id });
|
|
6136
6259
|
out.push({
|
|
6137
6260
|
scopeId: s.id,
|
|
6138
6261
|
tag: s.slug?.split('--').at(-1) ?? null,
|
|
@@ -6154,7 +6277,7 @@ export function createControlPlaneApi(options) {
|
|
|
6154
6277
|
const { tenantId, slug } = gate;
|
|
6155
6278
|
const tag = c.req.param('tag');
|
|
6156
6279
|
const actor = c.get('actor');
|
|
6157
|
-
const preview = (await admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, tag));
|
|
6280
|
+
const preview = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, tag));
|
|
6158
6281
|
// Idempotent: an already-reaped (or never-created) preview is a no-op success, so a
|
|
6159
6282
|
// PR-close job never fails because the preview was already gone.
|
|
6160
6283
|
if (!preview)
|
|
@@ -6183,7 +6306,7 @@ export function createControlPlaneApi(options) {
|
|
|
6183
6306
|
source: c.req.query('source'),
|
|
6184
6307
|
});
|
|
6185
6308
|
const page = pageParams(c);
|
|
6186
|
-
const entries = await admin.listRoles(c.get('actor'), { ...filter, ...page });
|
|
6309
|
+
const entries = await c.var.admin.listRoles(c.get('actor'), { ...filter, ...page });
|
|
6187
6310
|
// Composite sort key (tenant_id, role_key) — the `|` join is the documented
|
|
6188
6311
|
// cursor shape (scope-host.ts listRoles).
|
|
6189
6312
|
return c.json(pageOf(entries, page.limit, (r) => `${r.tenantId}|${r.key}`));
|
|
@@ -6209,7 +6332,7 @@ export function createControlPlaneApi(options) {
|
|
|
6209
6332
|
// The whole node comes from the path. Pinning only the tenant would pin
|
|
6210
6333
|
// nothing — see the schema's note: a node carrying a scope is written to that
|
|
6211
6334
|
// scope's DO directly, with no tenant cross-check below.
|
|
6212
|
-
await admin.assignRole(c.get('actor'), {
|
|
6335
|
+
await c.var.admin.assignRole(c.get('actor'), {
|
|
6213
6336
|
principalId: body.principalId,
|
|
6214
6337
|
roleKey: body.roleKey,
|
|
6215
6338
|
node: { tenantId, scopeId: null },
|
|
@@ -6222,7 +6345,7 @@ export function createControlPlaneApi(options) {
|
|
|
6222
6345
|
// Tombstones rather than deletes (K-21) and is idempotent — unassigning what
|
|
6223
6346
|
// was never assigned is a silent no-op, so a retry is safe. Node from the
|
|
6224
6347
|
// path, for the same reason as the assign above.
|
|
6225
|
-
await admin.unassignRole(c.get('actor'), {
|
|
6348
|
+
await c.var.admin.unassignRole(c.get('actor'), {
|
|
6226
6349
|
principalId: body.principalId,
|
|
6227
6350
|
roleKey: body.roleKey,
|
|
6228
6351
|
node: { tenantId, scopeId: null },
|
|
@@ -6262,11 +6385,11 @@ export function createControlPlaneApi(options) {
|
|
|
6262
6385
|
const body = systemSwitchBody.parse(await c.req.json());
|
|
6263
6386
|
const actor = c.get('actor');
|
|
6264
6387
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6265
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6388
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6266
6389
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6267
6390
|
}
|
|
6268
6391
|
const input = { moduleId: body.moduleId, node: { tenantId, scopeId }, reason: body.reason };
|
|
6269
|
-
return c.json(to === 'off' ? await admin.revokeFromSystem(actor, input) : await admin.restoreToSystem(actor, input));
|
|
6392
|
+
return c.json(to === 'off' ? await c.var.admin.revokeFromSystem(actor, input) : await c.var.admin.restoreToSystem(actor, input));
|
|
6270
6393
|
};
|
|
6271
6394
|
app.delete('/tenants/:tenantId/scopes/:scopeId/system-grants', switchScheduleRoute('off'));
|
|
6272
6395
|
app.post('/tenants/:tenantId/scopes/:scopeId/system-grants', switchScheduleRoute('on'));
|
|
@@ -6283,10 +6406,10 @@ export function createControlPlaneApi(options) {
|
|
|
6283
6406
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
6284
6407
|
const actor = c.get('actor');
|
|
6285
6408
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6286
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6409
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6287
6410
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6288
6411
|
}
|
|
6289
|
-
return c.json(await admin.systemGrantsStatus(actor, { tenantId, scopeId }));
|
|
6412
|
+
return c.json(await c.var.admin.systemGrantsStatus(actor, { tenantId, scopeId }));
|
|
6290
6413
|
});
|
|
6291
6414
|
// The fleet read (#1674): every scope with a module switched off, from the directory's
|
|
6292
6415
|
// record of the switch, so it needs no walk of every scope's store. The record is not the
|
|
@@ -6312,7 +6435,7 @@ export function createControlPlaneApi(options) {
|
|
|
6312
6435
|
order: c.req.query('order'),
|
|
6313
6436
|
});
|
|
6314
6437
|
const { position, ...rest } = q;
|
|
6315
|
-
const entries = await admin.listSystemSwitches(c.get('actor'), {
|
|
6438
|
+
const entries = await c.var.admin.listSystemSwitches(c.get('actor'), {
|
|
6316
6439
|
...rest,
|
|
6317
6440
|
...(position === 'all' ? {} : { position }),
|
|
6318
6441
|
});
|
|
@@ -6341,11 +6464,11 @@ export function createControlPlaneApi(options) {
|
|
|
6341
6464
|
const body = peerSwitchBody.parse(await c.req.json());
|
|
6342
6465
|
const actor = c.get('actor');
|
|
6343
6466
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6344
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6467
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6345
6468
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6346
6469
|
}
|
|
6347
6470
|
const input = { vertical: body.vertical, node: { tenantId, scopeId }, reason: body.reason };
|
|
6348
|
-
return c.json(to === 'off' ? await admin.revokeFromPeer(actor, input) : await admin.restoreToPeer(actor, input));
|
|
6471
|
+
return c.json(to === 'off' ? await c.var.admin.revokeFromPeer(actor, input) : await c.var.admin.restoreToPeer(actor, input));
|
|
6349
6472
|
};
|
|
6350
6473
|
app.delete('/tenants/:tenantId/scopes/:scopeId/peer-grants', switchPeerRoute('off'));
|
|
6351
6474
|
app.post('/tenants/:tenantId/scopes/:scopeId/peer-grants', switchPeerRoute('on'));
|
|
@@ -6359,22 +6482,22 @@ export function createControlPlaneApi(options) {
|
|
|
6359
6482
|
if (pin !== null && pin !== tenantId)
|
|
6360
6483
|
return c.json({ error: 'forbidden' }, 403);
|
|
6361
6484
|
const actor = c.get('actor');
|
|
6362
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6485
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6363
6486
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6364
6487
|
}
|
|
6365
|
-
return c.json(await admin.peerGrantsStatus(actor, { tenantId, scopeId }));
|
|
6488
|
+
return c.json(await c.var.admin.peerGrantsStatus(actor, { tenantId, scopeId }));
|
|
6366
6489
|
});
|
|
6367
6490
|
// Orgs: the portal-customer grouping (§4.1). Creating one mints no permission —
|
|
6368
6491
|
// members reach what the org was GRANTED, and granting stays off this surface.
|
|
6369
6492
|
app.post('/tenants/:tenantId/orgs', async (c) => {
|
|
6370
6493
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
6371
6494
|
const body = tenantOrgBody.parse(await c.req.json());
|
|
6372
|
-
await admin.createOrg(c.get('actor'), { ...body, tenantId });
|
|
6495
|
+
await c.var.admin.createOrg(c.get('actor'), { ...body, tenantId });
|
|
6373
6496
|
return c.body(null, 201);
|
|
6374
6497
|
});
|
|
6375
6498
|
app.get('/tenants/:tenantId/orgs', async (c) => {
|
|
6376
6499
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
6377
|
-
return c.json(await admin.listOrgs(c.get('actor'), tenantId));
|
|
6500
|
+
return c.json(await c.var.admin.listOrgs(c.get('actor'), tenantId));
|
|
6378
6501
|
});
|
|
6379
6502
|
// -- the admin log (§4.4/§4.5) ---------------------------------------------
|
|
6380
6503
|
app.get('/admin-log', async (c) => {
|
|
@@ -6389,7 +6512,7 @@ export function createControlPlaneApi(options) {
|
|
|
6389
6512
|
cursor: c.req.query('cursor'),
|
|
6390
6513
|
order: c.req.query('order'),
|
|
6391
6514
|
});
|
|
6392
|
-
const entries = await admin.auditLog(c.get('actor'), filter);
|
|
6515
|
+
const entries = await c.var.admin.auditLog(c.get('actor'), filter);
|
|
6393
6516
|
// The cursor IS the last entry's id (ULID order is chronological), so the
|
|
6394
6517
|
// page carries its own continuation and the console never assembles one.
|
|
6395
6518
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
@@ -6415,7 +6538,7 @@ export function createControlPlaneApi(options) {
|
|
|
6415
6538
|
cursor: c.req.query('cursor'),
|
|
6416
6539
|
order: c.req.query('order'),
|
|
6417
6540
|
});
|
|
6418
|
-
const entries = await admin.listOpsFailures(c.get('actor'), filter);
|
|
6541
|
+
const entries = await c.var.admin.listOpsFailures(c.get('actor'), filter);
|
|
6419
6542
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
6420
6543
|
});
|
|
6421
6544
|
// -- sweep runs (#1232): the durable record of what a sweep pass touched -------
|
|
@@ -6437,7 +6560,7 @@ export function createControlPlaneApi(options) {
|
|
|
6437
6560
|
cursor: c.req.query('cursor'),
|
|
6438
6561
|
order: c.req.query('order'),
|
|
6439
6562
|
});
|
|
6440
|
-
const entries = await admin.listSweepRuns(c.get('actor'), filter);
|
|
6563
|
+
const entries = await c.var.admin.listSweepRuns(c.get('actor'), filter);
|
|
6441
6564
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
6442
6565
|
});
|
|
6443
6566
|
// -- the replay lever (#1705 PR 3): move a consumer's watermark on one edge ----
|
|
@@ -6462,10 +6585,10 @@ export function createControlPlaneApi(options) {
|
|
|
6462
6585
|
const move = importCursorMove.parse(raw);
|
|
6463
6586
|
const actor = c.get('actor');
|
|
6464
6587
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6465
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6588
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6466
6589
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6467
6590
|
}
|
|
6468
|
-
return c.json(await admin.moveImportCursor(actor, tenantId, scopeId, move));
|
|
6591
|
+
return c.json(await c.var.admin.moveImportCursor(actor, tenantId, scopeId, move));
|
|
6469
6592
|
});
|
|
6470
6593
|
// -- edge health (#1705 PR 3): where each cross-vertical edge of one tenant stands -
|
|
6471
6594
|
// Read live, through the phase's own reach, so it cannot disagree with the next pass about
|
|
@@ -6478,7 +6601,7 @@ export function createControlPlaneApi(options) {
|
|
|
6478
6601
|
if (pin !== null && pin !== tenantId)
|
|
6479
6602
|
return c.json({ error: 'forbidden' }, 403);
|
|
6480
6603
|
const focus = c.req.query('scopeId');
|
|
6481
|
-
return c.json(await crossVerticalHealth(host, {
|
|
6604
|
+
return c.json(await crossVerticalHealth(c.var.host, {
|
|
6482
6605
|
actor: c.get('actor'),
|
|
6483
6606
|
tenantId,
|
|
6484
6607
|
...(focus ? { focus: scopeIdSchema.parse(focus) } : {}),
|
|
@@ -6496,7 +6619,7 @@ export function createControlPlaneApi(options) {
|
|
|
6496
6619
|
code: c.req.query('code'),
|
|
6497
6620
|
limit: c.req.query('limit'),
|
|
6498
6621
|
});
|
|
6499
|
-
const entries = await admin.listIssues(c.get('actor'), filter);
|
|
6622
|
+
const entries = await c.var.admin.listIssues(c.get('actor'), filter);
|
|
6500
6623
|
// No cursor, deliberately — see issuesQuery. `entries` alone, so a client
|
|
6501
6624
|
// never walks a continuation that cannot exist.
|
|
6502
6625
|
return c.json({ entries });
|
|
@@ -6506,7 +6629,7 @@ export function createControlPlaneApi(options) {
|
|
|
6506
6629
|
// rather than an invented row.
|
|
6507
6630
|
app.put('/issues/status', async (c) => {
|
|
6508
6631
|
const input = issueStatusUpdate.parse(await c.req.json());
|
|
6509
|
-
const updated = await admin.setIssueStatus(c.get('actor'), input.fingerprint, input.status);
|
|
6632
|
+
const updated = await c.var.admin.setIssueStatus(c.get('actor'), input.fingerprint, input.status);
|
|
6510
6633
|
if (!updated)
|
|
6511
6634
|
return c.json({ error: 'unknown issue fingerprint' }, 404);
|
|
6512
6635
|
return c.json(updated);
|
|
@@ -6527,7 +6650,7 @@ export function createControlPlaneApi(options) {
|
|
|
6527
6650
|
cursor: c.req.query('cursor'),
|
|
6528
6651
|
order: c.req.query('order'),
|
|
6529
6652
|
});
|
|
6530
|
-
const entries = await admin.listModelUsage(c.get('actor'), filter);
|
|
6653
|
+
const entries = await c.var.admin.listModelUsage(c.get('actor'), filter);
|
|
6531
6654
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
6532
6655
|
});
|
|
6533
6656
|
// Folded per (tenant, vertical, model) with the platform's margin applied at read
|
|
@@ -6541,7 +6664,7 @@ export function createControlPlaneApi(options) {
|
|
|
6541
6664
|
const now = new Date();
|
|
6542
6665
|
const since = q.since ?? new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
|
|
6543
6666
|
const until = q.until ?? now.toISOString();
|
|
6544
|
-
const summary = await admin.summarizeModelUsage(c.get('actor'), { ...(q.tenantId ? { tenantId: q.tenantId } : {}), since, until }, options.modelMarginPercent ?? DEFAULT_MODEL_MARGIN_PERCENT);
|
|
6667
|
+
const summary = await c.var.admin.summarizeModelUsage(c.get('actor'), { ...(q.tenantId ? { tenantId: q.tenantId } : {}), since, until }, options.modelMarginPercent ?? DEFAULT_MODEL_MARGIN_PERCENT);
|
|
6545
6668
|
return c.json(summary);
|
|
6546
6669
|
});
|
|
6547
6670
|
return app;
|