@substrat-run/control-plane-api 0.130.1 → 0.132.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +11 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +498 -343
- 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/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +10 -2
- package/dist/cf-observability.js.map +1 -1
- package/dist/observability.d.ts +6 -3
- package/dist/observability.d.ts.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/dist/vertical-client.d.ts +3 -0
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +4 -1
- package/dist/vertical-client.js.map +1 -1
- package/package.json +5 -5
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,79 @@ 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: 'GET', re: /\/tenants\/[^/]+\/connections\/calls$/, pin: 'path' },
|
|
512
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/connections$/, pin: 'path' },
|
|
513
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/connections\/[^/]+$/, pin: 'path' },
|
|
514
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/activity$/, pin: 'path' },
|
|
515
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/credential$/, pin: 'path' },
|
|
516
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/connections\/[^/]+\/verify$/, pin: 'path' },
|
|
517
|
+
// Members: identity links mirrored from the dashboard, orgs, role assignments.
|
|
518
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/identities$/, pin: 'path' },
|
|
519
|
+
{ method: 'PUT', re: /\/tenants\/[^/]+\/identities$/, pin: 'path' },
|
|
520
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/identities\/[^/]+$/, pin: 'path' },
|
|
521
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/orgs$/, pin: 'path' },
|
|
522
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/orgs$/, pin: 'path' },
|
|
523
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
|
|
524
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/role-assignments$/, pin: 'path' },
|
|
525
|
+
// An install's entitlement. The handler narrows WHICH key a tenant credential may
|
|
526
|
+
// grant (`tenantGrantableEntitlement`): the pin alone would let it grant itself any.
|
|
527
|
+
{ method: 'PUT', re: /\/tenants\/[^/]+\/entitlements\/[^/]+$/, pin: 'path' },
|
|
528
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/cross-vertical\/edges$/, pin: 'path' },
|
|
529
|
+
// Scopes: provision, read, and the lifecycle acts the Data and Deployments tabs offer.
|
|
530
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes$/, pin: 'path' },
|
|
531
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+$/, pin: 'path' },
|
|
532
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/scopes\/[^/]+$/, pin: 'path' },
|
|
533
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/binding-impact$/, pin: 'path' },
|
|
534
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/bookmarks$/, pin: 'path' },
|
|
535
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/cause$/, pin: 'path' },
|
|
536
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/dead-letters$/, pin: 'path' },
|
|
537
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/denials$/, pin: 'path' },
|
|
538
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/denials\/summary$/, pin: 'path' },
|
|
539
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/effects$/, pin: 'path' },
|
|
540
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/export$/, pin: 'path' },
|
|
541
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/facets$/, pin: 'path' },
|
|
542
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/history$/, pin: 'path' },
|
|
543
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/intents$/, pin: 'path' },
|
|
544
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/invocation$/, pin: 'path' },
|
|
545
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/migrations$/, pin: 'path' },
|
|
546
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/owner-seat$/, pin: 'path' },
|
|
547
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
548
|
+
// The peer switch (#1706): on is POST, off is DELETE, on the tenant's own scope.
|
|
549
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
550
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
551
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/snapshots$/, pin: 'path' },
|
|
552
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/tables$/, pin: 'path' },
|
|
553
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/tables\/[^/]+$/, pin: 'path' },
|
|
554
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/activate$/, pin: 'path' },
|
|
555
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/archive$/, pin: 'path' },
|
|
556
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/configure$/, pin: 'path' },
|
|
557
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/import-cursor$/, pin: 'path' },
|
|
558
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/lifecycle-flow$/, pin: 'path' },
|
|
559
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/owner-claim$/, pin: 'path' },
|
|
560
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/query$/, pin: 'path' },
|
|
561
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/reap$/, pin: 'path' },
|
|
562
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/rebind-vertical$/, pin: 'path' },
|
|
563
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/restore$/, pin: 'path' },
|
|
564
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/rewind$/, pin: 'path' },
|
|
565
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/snapshots$/, pin: 'path' },
|
|
566
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/suspend$/, pin: 'path' },
|
|
567
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/version$/, pin: 'path' },
|
|
504
568
|
// Create the tenant's own directory row (idempotent) — `id` is the pin.
|
|
505
569
|
{ method: 'POST', re: /\/tenants$/, pin: 'body' },
|
|
506
570
|
// Provision an app + materialize its instance.
|
|
@@ -627,7 +691,6 @@ const namedTenants = (c) => {
|
|
|
627
691
|
*/
|
|
628
692
|
export function createControlPlaneApi(options) {
|
|
629
693
|
const { host, authenticate, authenticateBuilder, authenticateTenantService } = options;
|
|
630
|
-
const admin = host.admin;
|
|
631
694
|
const app = new Hono();
|
|
632
695
|
// The CLI version advisory (#971), stamped on EVERY response — including the 401 the
|
|
633
696
|
// auth middleware below answers with, since a CLI too old to authenticate is exactly
|
|
@@ -659,7 +722,12 @@ export function createControlPlaneApi(options) {
|
|
|
659
722
|
if (authenticateTenantService) {
|
|
660
723
|
const tenant = await authenticateTenantService(c.req.raw);
|
|
661
724
|
if (tenant) {
|
|
662
|
-
principal = {
|
|
725
|
+
principal = {
|
|
726
|
+
kind: 'tenant',
|
|
727
|
+
actor: tenant.actor,
|
|
728
|
+
tenantId: tenant.tenantId,
|
|
729
|
+
...(tenant.onBehalfOf ? { onBehalfOf: tenant.onBehalfOf } : {}),
|
|
730
|
+
};
|
|
663
731
|
}
|
|
664
732
|
}
|
|
665
733
|
const staff = principal ? null : await authenticate(c.req.raw);
|
|
@@ -680,6 +748,14 @@ export function createControlPlaneApi(options) {
|
|
|
680
748
|
return c.json({ error: 'unauthenticated' }, 401);
|
|
681
749
|
c.set('principal', principal);
|
|
682
750
|
c.set('actor', principal.actor);
|
|
751
|
+
// #977: a tenant credential minted for a person writes through a view that names
|
|
752
|
+
// them on every admin row. Only the credential's own claim can supply the person —
|
|
753
|
+
// never a header or a body — and only the staff mint writes that claim.
|
|
754
|
+
const view = principal.kind === 'tenant' && principal.onBehalfOf && host.attributed
|
|
755
|
+
? host.attributed(principal.onBehalfOf)
|
|
756
|
+
: host;
|
|
757
|
+
c.set('host', view);
|
|
758
|
+
c.set('admin', view.admin);
|
|
683
759
|
await next();
|
|
684
760
|
});
|
|
685
761
|
// Confine a BUILDER to the vertical-management surface, fail-CLOSED (builder-plane.md
|
|
@@ -839,7 +915,7 @@ export function createControlPlaneApi(options) {
|
|
|
839
915
|
// regex a message. A caller with no throw in hand leaves the columns null,
|
|
840
916
|
// which reads as "nobody classified this" rather than a guess nobody made.
|
|
841
917
|
const attributed = cause === NO_CAUSE ? undefined : attributeFailure(cause);
|
|
842
|
-
void admin
|
|
918
|
+
void host.admin
|
|
843
919
|
.recordOpsFailure({
|
|
844
920
|
...entry,
|
|
845
921
|
origin: entry.origin ?? attributed?.origin ?? null,
|
|
@@ -927,7 +1003,7 @@ export function createControlPlaneApi(options) {
|
|
|
927
1003
|
// -- tenant registry (§4.1) ------------------------------------------------
|
|
928
1004
|
app.get('/tenants', async (c) => {
|
|
929
1005
|
const page = pageParams(c);
|
|
930
|
-
const entries = await admin.listTenants(c.get('actor'), page);
|
|
1006
|
+
const entries = await c.var.admin.listTenants(c.get('actor'), page);
|
|
931
1007
|
return c.json(pageOf(entries, page.limit, (t) => t.id));
|
|
932
1008
|
});
|
|
933
1009
|
app.post('/tenants', async (c) => {
|
|
@@ -936,14 +1012,14 @@ export function createControlPlaneApi(options) {
|
|
|
936
1012
|
// (from the manager scope's directory row) — never caller-supplied. A direct staff
|
|
937
1013
|
// create is first-class by definition, so force it null even if a body carries it,
|
|
938
1014
|
// so the field can't be forged into a false ownership relationship.
|
|
939
|
-
await admin.createTenant(c.get('actor'), { ...input, provisionedByTenant: null });
|
|
1015
|
+
await c.var.admin.createTenant(c.get('actor'), { ...input, provisionedByTenant: null });
|
|
940
1016
|
// Idempotent (§4.1): re-creating an existing tenant is a no-op, not an error,
|
|
941
1017
|
// 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);
|
|
1018
|
+
return c.json(await c.var.admin.getTenant(c.get('actor'), input.id), 201);
|
|
943
1019
|
});
|
|
944
1020
|
app.get('/tenants/:tenantId', async (c) => {
|
|
945
1021
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
946
|
-
const tenant = await admin.getTenant(c.get('actor'), tenantId);
|
|
1022
|
+
const tenant = await c.var.admin.getTenant(c.get('actor'), tenantId);
|
|
947
1023
|
if (!tenant)
|
|
948
1024
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
949
1025
|
return c.json(tenant);
|
|
@@ -955,8 +1031,8 @@ export function createControlPlaneApi(options) {
|
|
|
955
1031
|
app.patch('/tenants/:tenantId', async (c) => {
|
|
956
1032
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
957
1033
|
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));
|
|
1034
|
+
await c.var.admin.setTenantName(c.get('actor'), tenantId, name);
|
|
1035
|
+
return c.json(await c.var.admin.getTenant(c.get('actor'), tenantId));
|
|
960
1036
|
});
|
|
961
1037
|
app.patch('/tenants/:tenantId/status', async (c) => {
|
|
962
1038
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
@@ -964,8 +1040,8 @@ export function createControlPlaneApi(options) {
|
|
|
964
1040
|
// The live weapon (§7): `suspended` fails getScope closed for EVERY scope
|
|
965
1041
|
// under the tenant. The blast radius is the console's to show; the audit row
|
|
966
1042
|
// 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));
|
|
1043
|
+
await c.var.admin.setTenantStatus(c.get('actor'), tenantId, status);
|
|
1044
|
+
return c.json(await c.var.admin.getTenant(c.get('actor'), tenantId));
|
|
969
1045
|
});
|
|
970
1046
|
// Reap a DELETING tenant now (control-plane.md §4.8) — the staff "reap now" that skips
|
|
971
1047
|
// the grace window, the tenant analogue of the scope reap route below. Refuses a tenant
|
|
@@ -980,7 +1056,7 @@ export function createControlPlaneApi(options) {
|
|
|
980
1056
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
981
1057
|
const actor = c.get('actor');
|
|
982
1058
|
const { backup: wantsBackup } = reapScopeBody.parse(await c.req.json().catch(() => ({})));
|
|
983
|
-
const tenant = await admin.getTenant(actor, tenantId);
|
|
1059
|
+
const tenant = await c.var.admin.getTenant(actor, tenantId);
|
|
984
1060
|
if (!tenant)
|
|
985
1061
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
986
1062
|
if (tenant.status !== 'deleting') {
|
|
@@ -989,11 +1065,11 @@ export function createControlPlaneApi(options) {
|
|
|
989
1065
|
}, 409);
|
|
990
1066
|
}
|
|
991
1067
|
try {
|
|
992
|
-
for (const scope of await admin.listScopes(actor, { tenantId })) {
|
|
1068
|
+
for (const scope of await c.var.admin.listScopes(actor, { tenantId })) {
|
|
993
1069
|
if (scope.status === 'reaped')
|
|
994
1070
|
continue;
|
|
995
1071
|
if (scope.status !== 'archived')
|
|
996
|
-
await admin.archiveScope(actor, tenantId, scope.id);
|
|
1072
|
+
await c.var.admin.archiveScope(actor, tenantId, scope.id);
|
|
997
1073
|
// A backup here is OPT-IN, the inverse of the per-scope reap's default (#493).
|
|
998
1074
|
// A scope reap is operational cleanup, so leaving a copy is the safe default; a
|
|
999
1075
|
// TENANT reap is the deletion of a customer, and §4.8 exists partly to serve an
|
|
@@ -1003,16 +1079,16 @@ export function createControlPlaneApi(options) {
|
|
|
1003
1079
|
const backup = wantsBackup === true ? await backupScope(c, tenantId, scope) : null;
|
|
1004
1080
|
const vertical = await verticalForScope(c, scope);
|
|
1005
1081
|
if (vertical)
|
|
1006
|
-
await vertical.deleteScope({ scopeId: scope.id });
|
|
1082
|
+
await vertical.deleteScope({ tenantId, scopeId: scope.id });
|
|
1007
1083
|
// Tenant teardown reaps every scope and releases every name by design — force past
|
|
1008
1084
|
// the bound-hostname guard (which fences the interactive per-scope reap route below).
|
|
1009
|
-
await admin.reapScope(actor, tenantId, scope.id, {
|
|
1085
|
+
await c.var.admin.reapScope(actor, tenantId, scope.id, {
|
|
1010
1086
|
force: true,
|
|
1011
1087
|
...(backup ? { backupRef: backupRefOf(backup) } : {}),
|
|
1012
1088
|
});
|
|
1013
1089
|
}
|
|
1014
|
-
await admin.reapTenant(actor, tenantId);
|
|
1015
|
-
return c.json(await admin.getTenant(actor, tenantId));
|
|
1090
|
+
await c.var.admin.reapTenant(actor, tenantId);
|
|
1091
|
+
return c.json(await c.var.admin.getTenant(actor, tenantId));
|
|
1016
1092
|
}
|
|
1017
1093
|
catch (e) {
|
|
1018
1094
|
if (e instanceof ControlPlaneError) {
|
|
@@ -1022,20 +1098,35 @@ export function createControlPlaneApi(options) {
|
|
|
1022
1098
|
}
|
|
1023
1099
|
});
|
|
1024
1100
|
// -- 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')))));
|
|
1101
|
+
app.get('/tenants/:tenantId/entitlements', async (c) => c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantIdSchema.parse(c.req.param('tenantId')))));
|
|
1026
1102
|
app.put('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
1027
1103
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1028
1104
|
// The body is the plan half (#33) and optional — a bodyless PUT is the
|
|
1029
1105
|
// pre-widening bare flag grant, and PATCH semantics in the store mean it
|
|
1030
1106
|
// preserves whatever plan fields the grant already carries.
|
|
1031
1107
|
const plan = entitlementGrantInput.parse(await c.req.json().catch(() => ({})));
|
|
1032
|
-
|
|
1033
|
-
|
|
1108
|
+
const key = c.req.param('key');
|
|
1109
|
+
const p = c.get('principal');
|
|
1110
|
+
if (p.kind === 'tenant') {
|
|
1111
|
+
// #977: a tenant credential may grant its own tenant only what an INSTALL grants —
|
|
1112
|
+
// a key some vertical it can see declares (or that vertical's bare slug, the key a
|
|
1113
|
+
// vertical declaring none gates on). Anything else is the platform's to grant:
|
|
1114
|
+
// `builder`, a plan-gated feature, a key no vertical asked for. And never the plan
|
|
1115
|
+
// half: an expiry, a quota or a plan name is pricing, which is not the tenant's.
|
|
1116
|
+
if (plan.expiresAt != null || plan.quota != null || plan.plan != null) {
|
|
1117
|
+
return c.json({ error: 'a tenant credential grants entitlements without plan fields' }, 403);
|
|
1118
|
+
}
|
|
1119
|
+
if (!(await tenantGrantable(p, key))) {
|
|
1120
|
+
return c.json({ error: `entitlement '${key}' is not declared by a vertical this tenant can install` }, 403);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
await c.var.admin.grantEntitlement(c.get('actor'), tenantId, key, plan);
|
|
1124
|
+
return c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantId));
|
|
1034
1125
|
});
|
|
1035
1126
|
app.delete('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
1036
1127
|
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));
|
|
1128
|
+
await c.var.admin.revokeEntitlement(c.get('actor'), tenantId, c.req.param('key'));
|
|
1129
|
+
return c.json(await c.var.admin.listEntitlements(c.get('actor'), tenantId));
|
|
1039
1130
|
});
|
|
1040
1131
|
// -- the meters (§5, #38) --------------------------------------------------
|
|
1041
1132
|
// Meters 1 and 2, read fleet-wide or narrowed with `?tenantId=`. Meters 3 and 4 have
|
|
@@ -1049,7 +1140,7 @@ export function createControlPlaneApi(options) {
|
|
|
1049
1140
|
app.get('/meters', async (c) => {
|
|
1050
1141
|
const raw = c.req.query('tenantId');
|
|
1051
1142
|
const tenantId = raw ? tenantIdSchema.parse(raw) : undefined;
|
|
1052
|
-
return c.json(await admin.readMeters(c.get('actor'), tenantId ? { tenantId } : undefined));
|
|
1143
|
+
return c.json(await c.var.admin.readMeters(c.get('actor'), tenantId ? { tenantId } : undefined));
|
|
1053
1144
|
});
|
|
1054
1145
|
// Storage, read on demand (#1524): one tenant's scope-database sizes and their sum.
|
|
1055
1146
|
// Staff-only like `/meters` (on neither allowlist, so a builder or a tenant token is
|
|
@@ -1076,7 +1167,7 @@ export function createControlPlaneApi(options) {
|
|
|
1076
1167
|
limit: c.req.query('limit') || undefined,
|
|
1077
1168
|
});
|
|
1078
1169
|
const actor = c.get('actor');
|
|
1079
|
-
const scopes = await admin.listScopes(actor, { tenantId: q.tenantId });
|
|
1170
|
+
const scopes = await c.var.admin.listScopes(actor, { tenantId: q.tenantId });
|
|
1080
1171
|
return c.json(await readStoragePage({
|
|
1081
1172
|
tenantId: q.tenantId,
|
|
1082
1173
|
readAt: new Date().toISOString(),
|
|
@@ -1085,13 +1176,13 @@ export function createControlPlaneApi(options) {
|
|
|
1085
1176
|
limit: q.limit,
|
|
1086
1177
|
read: async (scope) => {
|
|
1087
1178
|
if (!scope.vertical)
|
|
1088
|
-
return admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1179
|
+
return c.var.admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1089
1180
|
const vertical = await verticalForScope(c, scope);
|
|
1090
1181
|
if (vertical)
|
|
1091
1182
|
return vertical.databaseSize(scope.id);
|
|
1092
1183
|
if (delegatesToVerticals)
|
|
1093
1184
|
throw new Error(`no deployment resolves for vertical ${scope.vertical}`);
|
|
1094
|
-
return admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1185
|
+
return c.var.admin.scopeDatabaseSize(actor, q.tenantId, scope.id);
|
|
1095
1186
|
},
|
|
1096
1187
|
}));
|
|
1097
1188
|
});
|
|
@@ -1106,8 +1197,8 @@ export function createControlPlaneApi(options) {
|
|
|
1106
1197
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1107
1198
|
const actor = c.get('actor');
|
|
1108
1199
|
const [tenantStores, blobStores] = await Promise.all([
|
|
1109
|
-
admin.listTenantStores(actor, { tenantId }),
|
|
1110
|
-
admin.listBlobStores(actor, { tenantId }),
|
|
1200
|
+
c.var.admin.listTenantStores(actor, { tenantId }),
|
|
1201
|
+
c.var.admin.listBlobStores(actor, { tenantId }),
|
|
1111
1202
|
]);
|
|
1112
1203
|
return c.json({ tenantStores, blobStores });
|
|
1113
1204
|
});
|
|
@@ -1124,26 +1215,26 @@ export function createControlPlaneApi(options) {
|
|
|
1124
1215
|
const link = mirrorIdentityBody.parse(await c.req.json());
|
|
1125
1216
|
// The pool must exist before a link can land in it (central topology, K-23);
|
|
1126
1217
|
// 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 });
|
|
1218
|
+
await c.var.admin.registerIdentityPool(c.get('actor'), { provider: link.provider, topology: 'central', tenantId: null });
|
|
1219
|
+
await c.var.admin.linkIdentity(c.get('actor'), { ...link, tenantId });
|
|
1129
1220
|
return c.body(null, 204);
|
|
1130
1221
|
});
|
|
1131
1222
|
app.delete('/tenants/:tenantId/identities/:principal', async (c) => {
|
|
1132
1223
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1133
|
-
await admin.unlinkIdentity(c.get('actor'), tenantId, principalIdSchema.parse(c.req.param('principal')));
|
|
1224
|
+
await c.var.admin.unlinkIdentity(c.get('actor'), tenantId, principalIdSchema.parse(c.req.param('principal')));
|
|
1134
1225
|
return c.body(null, 204);
|
|
1135
1226
|
});
|
|
1136
1227
|
// The read half of the mirror seam (#406): what the directory currently links for a
|
|
1137
1228
|
// tenant — the console's offboarding view, and the way to verify an unlink landed.
|
|
1138
1229
|
app.get('/tenants/:tenantId/identities', async (c) => {
|
|
1139
1230
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1140
|
-
return c.json(await admin.listIdentityLinks(c.get('actor'), tenantId));
|
|
1231
|
+
return c.json(await c.var.admin.listIdentityLinks(c.get('actor'), tenantId));
|
|
1141
1232
|
});
|
|
1142
1233
|
// The tenant's live connection grants (#592) — the readable "what may this connection
|
|
1143
1234
|
// invoke" (connections.md §6.2.4 Q2), and the rows provision/reconcile deliver from.
|
|
1144
1235
|
app.get('/tenants/:tenantId/connection-grants', async (c) => {
|
|
1145
1236
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1146
|
-
return c.json(await admin.listConnectionGrants(c.get('actor'), tenantId));
|
|
1237
|
+
return c.json(await c.var.admin.listConnectionGrants(c.get('actor'), tenantId));
|
|
1147
1238
|
});
|
|
1148
1239
|
// -- the connection store, tenant-scoped (connections.md §3.5) -------------
|
|
1149
1240
|
//
|
|
@@ -1162,7 +1253,7 @@ export function createControlPlaneApi(options) {
|
|
|
1162
1253
|
provider: c.req.query('provider'),
|
|
1163
1254
|
includeRevoked: c.req.query('includeRevoked') === '1' ? true : undefined,
|
|
1164
1255
|
});
|
|
1165
|
-
return c.json(await admin.listConnections(c.get('actor'), filter));
|
|
1256
|
+
return c.json(await c.var.admin.listConnections(c.get('actor'), filter));
|
|
1166
1257
|
});
|
|
1167
1258
|
// -- connection health, fleet-wide (#1690) -----------------------------------
|
|
1168
1259
|
//
|
|
@@ -1198,7 +1289,7 @@ export function createControlPlaneApi(options) {
|
|
|
1198
1289
|
order: c.req.query('order'),
|
|
1199
1290
|
});
|
|
1200
1291
|
const now = new Date();
|
|
1201
|
-
const rows = await admin.listConnections(c.get('actor'), {
|
|
1292
|
+
const rows = await c.var.admin.listConnections(c.get('actor'), {
|
|
1202
1293
|
...(q.tenantId ? { tenantId: q.tenantId } : {}),
|
|
1203
1294
|
...(q.provider ? { provider: q.provider } : {}),
|
|
1204
1295
|
});
|
|
@@ -1243,7 +1334,7 @@ export function createControlPlaneApi(options) {
|
|
|
1243
1334
|
const since = new Date(now.getTime() - CONNECTOR_DEAD_LETTER_WINDOW_DAYS * 86_400_000).toISOString();
|
|
1244
1335
|
const providers = q.provider ? [q.provider] : [...new Set(rows.map((r) => r.provider))].sort();
|
|
1245
1336
|
const deadLetters = await Promise.all(providers.map(async (provider) => {
|
|
1246
|
-
const found = await admin.listOpsFailures(c.get('actor'), {
|
|
1337
|
+
const found = await c.var.admin.listOpsFailures(c.get('actor'), {
|
|
1247
1338
|
operation: `intent.${connectorDispatchKind(provider)}`,
|
|
1248
1339
|
...(q.tenantId ? { tenantId: q.tenantId } : {}),
|
|
1249
1340
|
since,
|
|
@@ -1290,6 +1381,37 @@ export function createControlPlaneApi(options) {
|
|
|
1290
1381
|
const buckets = await options.observability.connectorCallsSeries(input);
|
|
1291
1382
|
return c.json({ hours: input.hours, buckets });
|
|
1292
1383
|
});
|
|
1384
|
+
// The same dataset, with the tenant predicate enforced by the reader. The tenant
|
|
1385
|
+
// credential can reach only this path, never the fleet-wide sibling above.
|
|
1386
|
+
app.get('/tenants/:tenantId/connections/calls', async (c) => {
|
|
1387
|
+
if (!options.observability?.connectorCallsSeries) {
|
|
1388
|
+
return c.json({ error: 'connector-call analytics are not configured on this control plane' }, 501);
|
|
1389
|
+
}
|
|
1390
|
+
const input = z.object({
|
|
1391
|
+
tenantId: tenantIdSchema,
|
|
1392
|
+
hours: z.coerce.number().int().min(1).max(72).default(24),
|
|
1393
|
+
provider: z.string().regex(/^[A-Za-z0-9_\-.]{1,64}$/).optional(),
|
|
1394
|
+
since: z.string().datetime({ offset: true }).optional(),
|
|
1395
|
+
until: z.string().datetime({ offset: true }).optional(),
|
|
1396
|
+
}).parse({
|
|
1397
|
+
tenantId: c.req.param('tenantId'),
|
|
1398
|
+
hours: c.req.query('hours'),
|
|
1399
|
+
provider: c.req.query('provider') || undefined,
|
|
1400
|
+
since: c.req.query('since'),
|
|
1401
|
+
until: c.req.query('until'),
|
|
1402
|
+
});
|
|
1403
|
+
try {
|
|
1404
|
+
resolveObservabilityWindow(input);
|
|
1405
|
+
}
|
|
1406
|
+
catch (e) {
|
|
1407
|
+
throw new ControlPlaneError(400, e.message);
|
|
1408
|
+
}
|
|
1409
|
+
if (input.since !== undefined && !options.observability.absoluteTenantWindows) {
|
|
1410
|
+
throw new ControlPlaneError(501, 'absolute tenant telemetry windows are not supported by this backend');
|
|
1411
|
+
}
|
|
1412
|
+
const buckets = await options.observability.connectorCallsSeries(input);
|
|
1413
|
+
return c.json({ hours: input.hours, buckets });
|
|
1414
|
+
});
|
|
1293
1415
|
// -- platform-request backlog, fleet-wide (#1690 §2) -----------------------
|
|
1294
1416
|
//
|
|
1295
1417
|
// "Is the platform's own intent drain keeping up" — composed from the SAME ops-failure
|
|
@@ -1312,7 +1434,7 @@ export function createControlPlaneApi(options) {
|
|
|
1312
1434
|
// per provider that has (or had) a live connection anywhere in the fleet — the same
|
|
1313
1435
|
// derivation the dead-letter count above uses, since that kind only exists for a
|
|
1314
1436
|
// provider some connection actually names.
|
|
1315
|
-
const providers = [...new Set((await admin.listConnections(c.get('actor'), { includeRevoked: true })).map((r) => r.provider))];
|
|
1437
|
+
const providers = [...new Set((await c.var.admin.listConnections(c.get('actor'), { includeRevoked: true })).map((r) => r.provider))];
|
|
1316
1438
|
const kinds = [
|
|
1317
1439
|
PROVISION_SIBLING_KIND,
|
|
1318
1440
|
ARCHIVE_SCOPE_KIND,
|
|
@@ -1325,7 +1447,7 @@ export function createControlPlaneApi(options) {
|
|
|
1325
1447
|
// The drain's newest pass is independent of the failure counts, so it rides alongside them.
|
|
1326
1448
|
const [counts, [lastPass]] = await Promise.all([
|
|
1327
1449
|
Promise.all(kinds.map(async (kind) => {
|
|
1328
|
-
const found = await admin.listOpsFailures(c.get('actor'), {
|
|
1450
|
+
const found = await c.var.admin.listOpsFailures(c.get('actor'), {
|
|
1329
1451
|
operation: `intent.${kind}`,
|
|
1330
1452
|
since,
|
|
1331
1453
|
limit: PLATFORM_REQUEST_BACKLOG_CAP + 1,
|
|
@@ -1335,7 +1457,7 @@ export function createControlPlaneApi(options) {
|
|
|
1335
1457
|
capped: found.length > PLATFORM_REQUEST_BACKLOG_CAP,
|
|
1336
1458
|
};
|
|
1337
1459
|
})),
|
|
1338
|
-
admin.listSweepRuns(c.get('actor'), { kind: 'platform-request', unit: 'fleet', limit: 1 }),
|
|
1460
|
+
c.var.admin.listSweepRuns(c.get('actor'), { kind: 'platform-request', unit: 'fleet', limit: 1 }),
|
|
1339
1461
|
]);
|
|
1340
1462
|
const body = {
|
|
1341
1463
|
total: counts.reduce((sum, k) => sum + k.count, 0),
|
|
@@ -1363,7 +1485,7 @@ export function createControlPlaneApi(options) {
|
|
|
1363
1485
|
return c.json({ error: 'body tenantId disagrees with the route tenant' }, 400);
|
|
1364
1486
|
}
|
|
1365
1487
|
try {
|
|
1366
|
-
return c.json(await relayConnectionUpsert(host, c.get('actor'), { ...body, tenantId }, { probeCandidate }));
|
|
1488
|
+
return c.json(await relayConnectionUpsert(c.var.host, c.get('actor'), { ...body, tenantId }, { probeCandidate }));
|
|
1367
1489
|
}
|
|
1368
1490
|
catch (err) {
|
|
1369
1491
|
if (err instanceof ConnectionRelayError && err.status < 500) {
|
|
@@ -1385,12 +1507,12 @@ export function createControlPlaneApi(options) {
|
|
|
1385
1507
|
app.delete('/tenants/:tenantId/connections/:id', async (c) => {
|
|
1386
1508
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1387
1509
|
const id = c.req.param('id');
|
|
1388
|
-
const rows = await admin.listConnections(c.get('actor'), { tenantId, includeRevoked: true });
|
|
1510
|
+
const rows = await c.var.admin.listConnections(c.get('actor'), { tenantId, includeRevoked: true });
|
|
1389
1511
|
const row = rows.find((r) => r.id === id);
|
|
1390
1512
|
if (!row)
|
|
1391
1513
|
return c.json({ error: 'unknown connection' }, 404);
|
|
1392
1514
|
if (row.status !== 'revoked')
|
|
1393
|
-
await admin.revokeConnection(c.get('actor'), row.id);
|
|
1515
|
+
await c.var.admin.revokeConnection(c.get('actor'), row.id);
|
|
1394
1516
|
return c.body(null, 204);
|
|
1395
1517
|
});
|
|
1396
1518
|
/**
|
|
@@ -1408,7 +1530,7 @@ export function createControlPlaneApi(options) {
|
|
|
1408
1530
|
const inspectableConnection = async (c) => {
|
|
1409
1531
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1410
1532
|
const id = c.req.param('id');
|
|
1411
|
-
const rows = await admin.listConnections(c.get('actor'), { tenantId });
|
|
1533
|
+
const rows = await c.var.admin.listConnections(c.get('actor'), { tenantId });
|
|
1412
1534
|
return rows.find((r) => r.id === id);
|
|
1413
1535
|
};
|
|
1414
1536
|
/**
|
|
@@ -1433,7 +1555,7 @@ export function createControlPlaneApi(options) {
|
|
|
1433
1555
|
if (!probe) {
|
|
1434
1556
|
return c.json({ error: `no probe registered for provider '${row.provider}'` }, 501);
|
|
1435
1557
|
}
|
|
1436
|
-
return c.json(connectionProbe.parse(await probe(host, row)));
|
|
1558
|
+
return c.json(connectionProbe.parse(await probe(c.var.host, row)));
|
|
1437
1559
|
});
|
|
1438
1560
|
/**
|
|
1439
1561
|
* **What the connection has done** (#605) — the connector's dispatch ledger, projected.
|
|
@@ -1457,7 +1579,7 @@ export function createControlPlaneApi(options) {
|
|
|
1457
1579
|
}
|
|
1458
1580
|
const source = connectionActivitySource.catch('ledger').parse(c.req.query('source'));
|
|
1459
1581
|
const live = c.req.query('live') === '1';
|
|
1460
|
-
return c.json(connectionActivity.parse(await activity(host, row, { live, source })));
|
|
1582
|
+
return c.json(connectionActivity.parse(await activity(c.var.host, row, { live, source })));
|
|
1461
1583
|
});
|
|
1462
1584
|
/**
|
|
1463
1585
|
* **The stored credential, reduced** (#605) — identifiers whole, secrets masked.
|
|
@@ -1476,7 +1598,7 @@ export function createControlPlaneApi(options) {
|
|
|
1476
1598
|
if (!credential) {
|
|
1477
1599
|
return c.json({ error: `no credential view registered for provider '${row.provider}'` }, 501);
|
|
1478
1600
|
}
|
|
1479
|
-
return c.json(connectionCredential.parse(await credential(host, row)));
|
|
1601
|
+
return c.json(connectionCredential.parse(await credential(c.var.host, row)));
|
|
1480
1602
|
});
|
|
1481
1603
|
// -- the scope directory (§3.2/§4.2) ---------------------------------------
|
|
1482
1604
|
app.get('/scopes', async (c) => {
|
|
@@ -1489,7 +1611,7 @@ export function createControlPlaneApi(options) {
|
|
|
1489
1611
|
vertical: c.req.query('vertical'),
|
|
1490
1612
|
});
|
|
1491
1613
|
const page = pageParams(c);
|
|
1492
|
-
const entries = await admin.listScopes(c.get('actor'), { ...filter, ...page });
|
|
1614
|
+
const entries = await c.var.admin.listScopes(c.get('actor'), { ...filter, ...page });
|
|
1493
1615
|
return c.json(pageOf(entries, page.limit, (s) => s.id));
|
|
1494
1616
|
});
|
|
1495
1617
|
// -- fleet migration progress (kernel-design §5.3, #49) ---------------------
|
|
@@ -1501,7 +1623,7 @@ export function createControlPlaneApi(options) {
|
|
|
1501
1623
|
// an unfiltered read is meaningful only where one deployment runs everything.
|
|
1502
1624
|
app.get('/fleet/migrations', async (c) => {
|
|
1503
1625
|
const filter = fleetMigrationsQuery.parse({ vertical: c.req.query('vertical') });
|
|
1504
|
-
const scopes = await admin.listScopes(c.get('actor'), {
|
|
1626
|
+
const scopes = await c.var.admin.listScopes(c.get('actor'), {
|
|
1505
1627
|
status: ['active', 'provisioning'],
|
|
1506
1628
|
...(filter.vertical ? { vertical: filter.vertical } : {}),
|
|
1507
1629
|
});
|
|
@@ -1509,8 +1631,8 @@ export function createControlPlaneApi(options) {
|
|
|
1509
1631
|
});
|
|
1510
1632
|
app.post('/scopes', async (c) => {
|
|
1511
1633
|
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);
|
|
1634
|
+
await c.var.host.provisionScope(c.get('actor'), input);
|
|
1635
|
+
const record = await c.var.admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
1514
1636
|
return c.json(record, 201);
|
|
1515
1637
|
});
|
|
1516
1638
|
// Add a SIBLING scope — a new "site" — to an app the tenant already runs (multi-scope
|
|
@@ -1538,7 +1660,7 @@ export function createControlPlaneApi(options) {
|
|
|
1538
1660
|
// The provisioning sequence lives in one place — `provisionSiblingScope` — shared with the
|
|
1539
1661
|
// platform-intent drain's `provision-sibling` handler (platform-intents.md B2).
|
|
1540
1662
|
try {
|
|
1541
|
-
const result = await provisionSiblingScope({ host, actor, resolveVerticalForScope: (scope) => verticalForScope(
|
|
1663
|
+
const result = await provisionSiblingScope({ host: c.var.host, actor, resolveVerticalForScope: (scope) => verticalForScope(c, scope) }, {
|
|
1542
1664
|
tenantId,
|
|
1543
1665
|
parentScopeId: input.parentScopeId,
|
|
1544
1666
|
scopeId: input.scopeId,
|
|
@@ -1548,7 +1670,7 @@ export function createControlPlaneApi(options) {
|
|
|
1548
1670
|
});
|
|
1549
1671
|
if (!result.ok)
|
|
1550
1672
|
return c.json({ error: result.error }, result.status);
|
|
1551
|
-
return c.json(await admin.getScopeRecord(actor, tenantId, input.scopeId), 201);
|
|
1673
|
+
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, input.scopeId), 201);
|
|
1552
1674
|
}
|
|
1553
1675
|
catch (e) {
|
|
1554
1676
|
if (e instanceof ControlPlaneError)
|
|
@@ -1564,7 +1686,7 @@ export function createControlPlaneApi(options) {
|
|
|
1564
1686
|
if (outsideTenant(p, tenantId)) {
|
|
1565
1687
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
1566
1688
|
}
|
|
1567
|
-
const record = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
1689
|
+
const record = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
1568
1690
|
// Absent, or present under another tenant — indistinguishable on purpose (K-3).
|
|
1569
1691
|
if (!record)
|
|
1570
1692
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
@@ -1594,8 +1716,8 @@ export function createControlPlaneApi(options) {
|
|
|
1594
1716
|
* protect a subject's data is itself a thing an incident asks about.
|
|
1595
1717
|
*/
|
|
1596
1718
|
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),
|
|
1719
|
+
seal: (items) => c.var.admin.sealSubjectPayloads(c.get('actor'), tenantId, scopeId, items),
|
|
1720
|
+
open: (items) => c.var.admin.openSubjectPayloads(c.get('actor'), tenantId, scopeId, items),
|
|
1599
1721
|
});
|
|
1600
1722
|
const verticalForScope = async (c, scope) => (await deploymentForScope(c, scope))?.client;
|
|
1601
1723
|
/**
|
|
@@ -1637,7 +1759,7 @@ export function createControlPlaneApi(options) {
|
|
|
1637
1759
|
// under its bare spelling. Retry once under the registry id; anything else still
|
|
1638
1760
|
// misses and surfaces through `diagnoseUnboundScope` as before.
|
|
1639
1761
|
if (scope.tenantId && !scope.vertical.includes('/') && (await ownerOf(actor, scope.vertical)) === undefined) {
|
|
1640
|
-
const tenant = await admin.getTenant(actor, scope.tenantId).catch(() => null);
|
|
1762
|
+
const tenant = await c.var.admin.getTenant(actor, scope.tenantId).catch(() => null);
|
|
1641
1763
|
const prefixed = tenant ? `${tenant.slug}/${scope.vertical}` : null;
|
|
1642
1764
|
if (prefixed && (await ownerOf(actor, prefixed)) !== undefined)
|
|
1643
1765
|
return bySlug(prefixed);
|
|
@@ -1660,7 +1782,7 @@ export function createControlPlaneApi(options) {
|
|
|
1660
1782
|
const slug = scope.vertical;
|
|
1661
1783
|
if (!slug)
|
|
1662
1784
|
return 'scope has no vertical bound — nothing to deliver to';
|
|
1663
|
-
const versions = await admin.listVersions(actor, slug).catch(() => []);
|
|
1785
|
+
const versions = await host.admin.listVersions(actor, slug).catch(() => []);
|
|
1664
1786
|
const boundId = scope.verticalVersionId;
|
|
1665
1787
|
if (boundId && !versions.some((v) => v.id === boundId)) {
|
|
1666
1788
|
return (`scope is bound to version '${boundId}', which is not among vertical '${slug}'’s ${versions.length} ` +
|
|
@@ -1673,7 +1795,7 @@ export function createControlPlaneApi(options) {
|
|
|
1673
1795
|
`pushes are on different slugs (a lineage fork). Push under '${slug}' by setting \`substrat.slug\`, ` +
|
|
1674
1796
|
`or rebind the install to the slug the versions live under.`);
|
|
1675
1797
|
}
|
|
1676
|
-
const serving = await admin.verticalServing(actor, slug).catch(() => null);
|
|
1798
|
+
const serving = await host.admin.verticalServing(actor, slug).catch(() => null);
|
|
1677
1799
|
if (!serving) {
|
|
1678
1800
|
return `vertical '${slug}' has ${versions.length} version(s) but none is promoted to a serving channel — promote one to prod.`;
|
|
1679
1801
|
}
|
|
@@ -1689,7 +1811,7 @@ export function createControlPlaneApi(options) {
|
|
|
1689
1811
|
const refuseOnBreaks = async (actor, tenantId, scopeId, versionId, opts) => {
|
|
1690
1812
|
if (opts.acknowledge?.exportBreak)
|
|
1691
1813
|
return;
|
|
1692
|
-
const breaks = await admin.bindingImpact(actor, tenantId, scopeId, versionId, opts.servingRef !== undefined ? { servingRef: opts.servingRef } : undefined);
|
|
1814
|
+
const breaks = await host.admin.bindingImpact(actor, tenantId, scopeId, versionId, opts.servingRef !== undefined ? { servingRef: opts.servingRef } : undefined);
|
|
1693
1815
|
if (breaks.length > 0)
|
|
1694
1816
|
throw new ExportBreakRefused(breaks);
|
|
1695
1817
|
};
|
|
@@ -1724,14 +1846,14 @@ export function createControlPlaneApi(options) {
|
|
|
1724
1846
|
* and `exact` when the platform exported them itself rather than a caller supplying them. */
|
|
1725
1847
|
source) => {
|
|
1726
1848
|
return retryTransient(async () => dest.restoreScope(tenantId, scopeId, tables, {
|
|
1727
|
-
...(await switchCarryFor(admin, actor, { tenantId, scopeId })),
|
|
1849
|
+
...(await switchCarryFor(host.admin, actor, { tenantId, scopeId })),
|
|
1728
1850
|
sourceScopeId: source.scopeId,
|
|
1729
1851
|
exact: source.exact,
|
|
1730
1852
|
}));
|
|
1731
1853
|
};
|
|
1732
1854
|
const adoptScopeOntoServing = async (c, tenantId, scopeId, opts = {}) => {
|
|
1733
1855
|
const actor = c.get('actor');
|
|
1734
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1856
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1735
1857
|
if (!scope) {
|
|
1736
1858
|
throw new ControlPlaneError(404, `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
1737
1859
|
}
|
|
@@ -1742,13 +1864,13 @@ export function createControlPlaneApi(options) {
|
|
|
1742
1864
|
// #1674 (Copilot review): an adopt that flipped routing and then failed to re-assert is
|
|
1743
1865
|
// retried HERE, so the retry re-asserts too — else a store that lost its OFF marker in
|
|
1744
1866
|
// the copy would stay on for good. Cheap and idempotent when nothing is owed.
|
|
1745
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1867
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1746
1868
|
return { servingRef: scope.servingRef, alreadyAdopted: true };
|
|
1747
1869
|
}
|
|
1748
1870
|
if (!scope.vertical) {
|
|
1749
1871
|
throw new ControlPlaneError(409, 'scope has no vertical — nothing to adopt onto');
|
|
1750
1872
|
}
|
|
1751
|
-
const serving = await admin.verticalServing(actor, scope.vertical);
|
|
1873
|
+
const serving = await c.var.admin.verticalServing(actor, scope.vertical);
|
|
1752
1874
|
if (!serving) {
|
|
1753
1875
|
throw new ControlPlaneError(409, `vertical '${scope.vertical}' has no serving script yet — promote a version to prod first`);
|
|
1754
1876
|
}
|
|
@@ -1767,15 +1889,15 @@ export function createControlPlaneApi(options) {
|
|
|
1767
1889
|
// #1742: the recorded OFF positions ride the restore, applied in the replay's own event.
|
|
1768
1890
|
const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump, { scopeId, exact: true });
|
|
1769
1891
|
// Data landed — only now flip routing and move the version pointer.
|
|
1770
|
-
await admin
|
|
1892
|
+
await c.var.admin
|
|
1771
1893
|
.setScopeServingRef(actor, tenantId, scopeId, serving.ref, { acknowledge: opts.acknowledge })
|
|
1772
1894
|
.catch(relayHostRefusal);
|
|
1773
|
-
await admin
|
|
1895
|
+
await c.var.admin
|
|
1774
1896
|
.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, { acknowledge: opts.acknowledge })
|
|
1775
1897
|
.catch(relayHostRefusal);
|
|
1776
1898
|
// #1674: the scope now routes to a different store, so put the directory's recorded
|
|
1777
1899
|
// OFF positions back there — cheap and idempotent when the dump or the restore did.
|
|
1778
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
1900
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
1779
1901
|
return { servingRef: serving.ref, tables: restored.tables };
|
|
1780
1902
|
};
|
|
1781
1903
|
/**
|
|
@@ -1798,13 +1920,13 @@ export function createControlPlaneApi(options) {
|
|
|
1798
1920
|
acknowledge) => {
|
|
1799
1921
|
const actor = c.get('actor');
|
|
1800
1922
|
const none = { refused: [] };
|
|
1801
|
-
const serving = await admin.verticalServing(actor, slug);
|
|
1923
|
+
const serving = await c.var.admin.verticalServing(actor, slug);
|
|
1802
1924
|
if (!serving)
|
|
1803
1925
|
return none; // embedded / not dispatch-backed — the host cascade handled rebinds
|
|
1804
1926
|
const v = await verticalOf(actor, slug);
|
|
1805
1927
|
if (!v || v.ownerTenant === null || v.listed)
|
|
1806
1928
|
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');
|
|
1929
|
+
const owned = (await c.var.admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
1808
1930
|
// An install refused for what it would break is left where it is, and the rest still move:
|
|
1809
1931
|
// one lagging app must not keep every other install's pointer, or the store backfill after
|
|
1810
1932
|
// this, from happening. The caller answers with what was refused.
|
|
@@ -1822,7 +1944,7 @@ export function createControlPlaneApi(options) {
|
|
|
1822
1944
|
// Already on the serving script (born there, or adopted earlier): routing is
|
|
1823
1945
|
// pinned to servingRef, so advancing the version pointer only affects Update
|
|
1824
1946
|
// offers. Snapshot on a migration-digest crossing (fork-before-promote, §4).
|
|
1825
|
-
await admin.bindScopeVersion(actor, s.tenantId, s.id, versionId, { snapshot: true, acknowledge });
|
|
1947
|
+
await c.var.admin.bindScopeVersion(actor, s.tenantId, s.id, versionId, { snapshot: true, acknowledge });
|
|
1826
1948
|
}
|
|
1827
1949
|
}
|
|
1828
1950
|
catch (e) {
|
|
@@ -1854,7 +1976,7 @@ export function createControlPlaneApi(options) {
|
|
|
1854
1976
|
*/
|
|
1855
1977
|
const rebindScopeOntoVertical = async (c, tenantId, scopeId, target, opts) => {
|
|
1856
1978
|
const actor = c.get('actor');
|
|
1857
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1979
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
1858
1980
|
if (!scope) {
|
|
1859
1981
|
throw new ControlPlaneError(404, `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
1860
1982
|
}
|
|
@@ -1864,13 +1986,13 @@ export function createControlPlaneApi(options) {
|
|
|
1864
1986
|
if (!scope.vertical) {
|
|
1865
1987
|
throw new ControlPlaneError(409, 'scope has no vertical — nothing to rebind');
|
|
1866
1988
|
}
|
|
1867
|
-
const serving = await admin.verticalServing(actor, target);
|
|
1989
|
+
const serving = await c.var.admin.verticalServing(actor, target);
|
|
1868
1990
|
if (!serving) {
|
|
1869
1991
|
throw new ControlPlaneError(409, `vertical '${target}' has no serving script yet — promote a version to prod first`);
|
|
1870
1992
|
}
|
|
1871
1993
|
if (scope.vertical === target && scope.servingRef === serving.ref) {
|
|
1872
1994
|
// #1674: a rebind retried after its re-assert failed lands here; re-assert, as adopt does.
|
|
1873
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1995
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId });
|
|
1874
1996
|
return { servingRef: serving.ref, versionId: serving.versionId, alreadyBound: true };
|
|
1875
1997
|
}
|
|
1876
1998
|
// #1756: within one lineage this is an adopt, and judged as one before anything moves. A
|
|
@@ -1885,8 +2007,8 @@ export function createControlPlaneApi(options) {
|
|
|
1885
2007
|
// protect — the target provisions its own schema from scratch. The source
|
|
1886
2008
|
// script's copy is untouched and remains the backout, same as a carried rebind.
|
|
1887
2009
|
// 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);
|
|
2010
|
+
await c.var.admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref, move).catch(relayHostRefusal);
|
|
2011
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, move).catch(relayHostRefusal);
|
|
1890
2012
|
return { servingRef: serving.ref, versionId: serving.versionId, dataAbandoned: true };
|
|
1891
2013
|
}
|
|
1892
2014
|
// The frontier gate. Digest equality proves the target's migration set is exactly
|
|
@@ -1894,9 +2016,9 @@ export function createControlPlaneApi(options) {
|
|
|
1894
2016
|
// brought — the crossing is then no riskier than an ordinary version bind. Anything
|
|
1895
2017
|
// else (differing digests, or a scope with no bound version to compare) needs the
|
|
1896
2018
|
// operator's explicit acknowledgement.
|
|
1897
|
-
const targetV = await admin.getVersion(actor, serving.versionId, target);
|
|
2019
|
+
const targetV = await c.var.admin.getVersion(actor, serving.versionId, target);
|
|
1898
2020
|
const currentV = scope.verticalVersionId
|
|
1899
|
-
? await admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
2021
|
+
? await c.var.admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
1900
2022
|
: undefined;
|
|
1901
2023
|
const digestsMatch = Boolean(currentV && targetV && currentV.migrationDigest === targetV.migrationDigest);
|
|
1902
2024
|
if (!digestsMatch && !opts.ackMigrations) {
|
|
@@ -1914,10 +2036,10 @@ export function createControlPlaneApi(options) {
|
|
|
1914
2036
|
// `bindScopeVersion` rewrites `scopes.vertical` from the version row, audited. No
|
|
1915
2037
|
// extra snapshot here (adopt-serving's precedent): the source script's copy is the
|
|
1916
2038
|
// 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);
|
|
2039
|
+
await c.var.admin.setScopeServingRef(actor, tenantId, scopeId, serving.ref, move).catch(relayHostRefusal);
|
|
2040
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, scopeId, serving.versionId, move).catch(relayHostRefusal);
|
|
1919
2041
|
// #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 });
|
|
2042
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored.switchedOff });
|
|
1921
2043
|
return { servingRef: serving.ref, versionId: serving.versionId, tables: restored.tables };
|
|
1922
2044
|
};
|
|
1923
2045
|
/**
|
|
@@ -1960,7 +2082,7 @@ export function createControlPlaneApi(options) {
|
|
|
1960
2082
|
if (!scope.vertical)
|
|
1961
2083
|
return null;
|
|
1962
2084
|
const actor = c.get('actor');
|
|
1963
|
-
const incoming = await admin.getVersion(actor, versionId, scope.vertical);
|
|
2085
|
+
const incoming = await c.var.admin.getVersion(actor, versionId, scope.vertical);
|
|
1964
2086
|
// The bind refuses a version that is not admitted, except onto a preview. Check that
|
|
1965
2087
|
// BEFORE the copy. Otherwise unreviewed code would receive the scope's data through its
|
|
1966
2088
|
// own restore handler, only for the bind to refuse afterwards.
|
|
@@ -1974,7 +2096,7 @@ export function createControlPlaneApi(options) {
|
|
|
1974
2096
|
return null;
|
|
1975
2097
|
const to = incoming.deploymentRef ?? null;
|
|
1976
2098
|
const bound = scope.verticalVersionId
|
|
1977
|
-
? await admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
2099
|
+
? await c.var.admin.getVersion(actor, scope.verticalVersionId, scope.vertical)
|
|
1978
2100
|
: undefined;
|
|
1979
2101
|
const from = scope.servingRef ?? bound?.deploymentRef ?? null;
|
|
1980
2102
|
if (!to || !from || from === to)
|
|
@@ -2005,12 +2127,12 @@ export function createControlPlaneApi(options) {
|
|
|
2005
2127
|
app.get('/tenants/:tenantId/scopes/:scopeId/tables', async (c) => {
|
|
2006
2128
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2007
2129
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2008
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2130
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2009
2131
|
if (!scope)
|
|
2010
2132
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2011
2133
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'listScopeTables', null, {
|
|
2012
2134
|
viaVertical: (v) => v.listScopeTables(scopeId),
|
|
2013
|
-
colocated: () => admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2135
|
+
colocated: () => c.var.admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2014
2136
|
}, (r) => r.length));
|
|
2015
2137
|
});
|
|
2016
2138
|
// Scope health (#321, criterion #3). The silent failure the field report chased was an
|
|
@@ -2039,21 +2161,21 @@ export function createControlPlaneApi(options) {
|
|
|
2039
2161
|
if (outsideTenant(principal, tenantId)) {
|
|
2040
2162
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2041
2163
|
}
|
|
2042
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2164
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2043
2165
|
if (!scope)
|
|
2044
2166
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2045
2167
|
// Health reads the scope the same way the Data view does, so it leaves the same
|
|
2046
2168
|
// row: an auditor should not be able to tell which route reached the tables.
|
|
2047
2169
|
const tables = await delegatedRead(c, tenantId, scopeId, scope, 'listScopeTables', null, {
|
|
2048
2170
|
viaVertical: (v) => v.listScopeTables(scopeId),
|
|
2049
|
-
colocated: () => admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2171
|
+
colocated: () => c.var.admin.listScopeTables(c.get('actor'), tenantId, scopeId),
|
|
2050
2172
|
}, (r) => r.length);
|
|
2051
2173
|
const roles = tables.find((t) => t.name === '_substrat_roles');
|
|
2052
2174
|
const roleCount = roles ? roles.rowCount : null;
|
|
2053
2175
|
const roleProjectionEmpty = scope.status === 'active' && roleCount === 0;
|
|
2054
2176
|
const missingStores = scope.vertical
|
|
2055
2177
|
? await missingStoresForTenant({
|
|
2056
|
-
host:
|
|
2178
|
+
host: c.var.host,
|
|
2057
2179
|
actor: c.get('actor'),
|
|
2058
2180
|
slug: scope.vertical,
|
|
2059
2181
|
tenantId,
|
|
@@ -2096,7 +2218,7 @@ export function createControlPlaneApi(options) {
|
|
|
2096
2218
|
status: c.req.query('status'),
|
|
2097
2219
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2098
2220
|
});
|
|
2099
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2221
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2100
2222
|
if (!scope)
|
|
2101
2223
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2102
2224
|
// Same delegation ladder as the table reads: the intents live in the DO of the scope's BOUND
|
|
@@ -2104,7 +2226,7 @@ export function createControlPlaneApi(options) {
|
|
|
2104
2226
|
const vertical = await verticalForScope(c, scope);
|
|
2105
2227
|
return c.json(vertical
|
|
2106
2228
|
? await vertical.listPlatformRequestHistory(tenantId, scopeId, filter)
|
|
2107
|
-
: await host.listPlatformRequestHistory(tenantId, scopeId, filter));
|
|
2229
|
+
: await c.var.host.listPlatformRequestHistory(tenantId, scopeId, filter));
|
|
2108
2230
|
});
|
|
2109
2231
|
// #332: re-provision a scope stuck at "roles projected, zero tuples" — the enforcement flip
|
|
2110
2232
|
// switched on against an empty tuple table, so every login denies and the owner is locked out
|
|
@@ -2117,7 +2239,7 @@ export function createControlPlaneApi(options) {
|
|
|
2117
2239
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2118
2240
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2119
2241
|
const actor = c.get('actor');
|
|
2120
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2242
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2121
2243
|
if (!scope)
|
|
2122
2244
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2123
2245
|
const p = c.get('principal');
|
|
@@ -2131,10 +2253,10 @@ export function createControlPlaneApi(options) {
|
|
|
2131
2253
|
if (!reached)
|
|
2132
2254
|
return c.json({ error: await diagnoseUnboundScope(c.get('actor'), scope) }, 501);
|
|
2133
2255
|
const vertical = reached.client;
|
|
2134
|
-
const entitlements = await admin.listEntitlements(actor, tenantId);
|
|
2256
|
+
const entitlements = await c.var.admin.listEntitlements(actor, tenantId);
|
|
2135
2257
|
// #406: re-gathered and re-delivered like entitlements, so a reconcile also repairs a
|
|
2136
2258
|
// 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);
|
|
2259
|
+
const identityLinks = (await c.var.admin.listIdentityLinks(actor, tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
2138
2260
|
// #592: connection grants ride the same authoritative gather — the back-fill for a
|
|
2139
2261
|
// scope provisioned before `grantToConnection` ran, and how a revoked connection's
|
|
2140
2262
|
// grants stop being delivered (they are absent from the directory's live rows).
|
|
@@ -2148,18 +2270,18 @@ export function createControlPlaneApi(options) {
|
|
|
2148
2270
|
// Best-effort: healing reaches the directory and must never take the reconcile down
|
|
2149
2271
|
// with it. A failure here leaves exactly the behaviour that shipped before it existed.
|
|
2150
2272
|
try {
|
|
2151
|
-
await reconcileConnectionGrants({ admin, actor, declared: options.connectorGrants ?? {} }, tenantId, scope.vertical);
|
|
2273
|
+
await reconcileConnectionGrants({ admin: c.var.admin, actor, declared: options.connectorGrants ?? {} }, tenantId, scope.vertical);
|
|
2152
2274
|
}
|
|
2153
2275
|
catch {
|
|
2154
2276
|
// The next reconcile tries again; the read-back still shows what is missing.
|
|
2155
2277
|
}
|
|
2156
|
-
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(actor, tenantId), scope.vertical, scopeId);
|
|
2278
|
+
const connectionGrants = connectionGrantsForScope(await c.var.admin.listConnectionGrants(actor, tenantId), scope.vertical, scopeId);
|
|
2157
2279
|
// #687: connection sealing keys ride the same gather — the back-fill for a scope
|
|
2158
2280
|
// provisioned before its connection existed, and the channel a connection made AFTER
|
|
2159
2281
|
// provision rides. Minted on first ask, so this is also how a connection older than
|
|
2160
2282
|
// the feature acquires a keypair without being reconnected.
|
|
2161
2283
|
const connectionKeys = scope.vertical
|
|
2162
|
-
? await admin.connectionSealingKeys(tenantId, scope.vertical)
|
|
2284
|
+
? await c.var.admin.connectionSealingKeys(tenantId, scope.vertical)
|
|
2163
2285
|
: [];
|
|
2164
2286
|
// #825: the BACKFILL seam for per-tenant stores. Minting lived only in the
|
|
2165
2287
|
// tenant-creation lifecycle, so a tenant created before its vertical declared a
|
|
@@ -2213,7 +2335,7 @@ export function createControlPlaneApi(options) {
|
|
|
2213
2335
|
// need a second run to discover the other.
|
|
2214
2336
|
const tenantStores = scope.vertical
|
|
2215
2337
|
? ((await mintBestEffort('tenant-stores', () => collectTenantStoreHandles({
|
|
2216
|
-
host:
|
|
2338
|
+
host: c.var.host,
|
|
2217
2339
|
actor,
|
|
2218
2340
|
slug: scope.vertical,
|
|
2219
2341
|
tenantId,
|
|
@@ -2222,7 +2344,7 @@ export function createControlPlaneApi(options) {
|
|
|
2222
2344
|
: [];
|
|
2223
2345
|
if (scope.vertical) {
|
|
2224
2346
|
await mintBestEffort('blob-stores', () => collectBlobStoreHandles({
|
|
2225
|
-
host:
|
|
2347
|
+
host: c.var.host,
|
|
2226
2348
|
actor,
|
|
2227
2349
|
slug: scope.vertical,
|
|
2228
2350
|
tenantId,
|
|
@@ -2237,12 +2359,12 @@ export function createControlPlaneApi(options) {
|
|
|
2237
2359
|
// one before. Best-effort: without it the receipt falls back to the bound version, the
|
|
2238
2360
|
// pre-#1653 answer, which costs at most one more reconcile.
|
|
2239
2361
|
const serving = reached.via === 'serving-script' && scope.vertical
|
|
2240
|
-
? await admin.verticalServing(actor, scope.vertical).catch(() => null)
|
|
2362
|
+
? await c.var.admin.verticalServing(actor, scope.vertical).catch(() => null)
|
|
2241
2363
|
: null;
|
|
2242
2364
|
const ranAs = versionReachedAt(reached.via, scope, serving);
|
|
2243
2365
|
try {
|
|
2244
2366
|
// #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({
|
|
2367
|
+
const result = await reconcileThenReassert(c.var.admin, actor, { tenantId, scopeId }, (carry) => vertical.reconcileInstance({
|
|
2246
2368
|
tenantId,
|
|
2247
2369
|
scopeId,
|
|
2248
2370
|
entitlements,
|
|
@@ -2261,7 +2383,7 @@ export function createControlPlaneApi(options) {
|
|
|
2261
2383
|
// button means nothing to the thing that watches. Both name it with
|
|
2262
2384
|
// `versionReachedAt`, from the deployment each actually reached.
|
|
2263
2385
|
if (ranAs) {
|
|
2264
|
-
await admin.markScopeProvisioned(actor, tenantId, scopeId, ranAs);
|
|
2386
|
+
await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, ranAs);
|
|
2265
2387
|
}
|
|
2266
2388
|
// The reconcile succeeded; a store that did not mint rides back as a diagnosis
|
|
2267
2389
|
// (`substrat scope provision` prints it, and `/health` keeps reporting the gap
|
|
@@ -2295,14 +2417,14 @@ export function createControlPlaneApi(options) {
|
|
|
2295
2417
|
until: c.req.query('until') ?? undefined,
|
|
2296
2418
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2297
2419
|
});
|
|
2298
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2420
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2299
2421
|
if (!scope)
|
|
2300
2422
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2301
2423
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'facetEvents', input, {
|
|
2302
2424
|
// #1762: only the vertical's answer needs the guard. The co-located read runs
|
|
2303
2425
|
// this platform's own kernel, which withholds by construction.
|
|
2304
2426
|
viaVertical: async (v) => withholdIfPredatesRule(input, await v.facetEvents(scopeId, input)),
|
|
2305
|
-
colocated: () => admin.facetEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2427
|
+
colocated: () => c.var.admin.facetEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2306
2428
|
},
|
|
2307
2429
|
// The buckets RETURNED, not `total`: the row says how much this read handed
|
|
2308
2430
|
// back, and a capped facet hands back fewer than it counted.
|
|
@@ -2343,7 +2465,7 @@ export function createControlPlaneApi(options) {
|
|
|
2343
2465
|
if (!vertical)
|
|
2344
2466
|
return run.colocated();
|
|
2345
2467
|
const answer = await run.viaVertical(vertical);
|
|
2346
|
-
await admin.recordDelegatedRead(c.get('actor'), {
|
|
2468
|
+
await c.var.admin.recordDelegatedRead(c.get('actor'), {
|
|
2347
2469
|
method,
|
|
2348
2470
|
tenantId,
|
|
2349
2471
|
scopeId,
|
|
@@ -2364,12 +2486,12 @@ export function createControlPlaneApi(options) {
|
|
|
2364
2486
|
eventId: c.req.query('eventId'),
|
|
2365
2487
|
maxDepth: c.req.query('maxDepth') ? Number(c.req.query('maxDepth')) : undefined,
|
|
2366
2488
|
});
|
|
2367
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2489
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2368
2490
|
if (!scope)
|
|
2369
2491
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2370
2492
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'eventCause', input, {
|
|
2371
2493
|
viaVertical: (v) => v.eventCause(scopeId, input),
|
|
2372
|
-
colocated: () => admin.eventCause(c.get('actor'), tenantId, scopeId, input),
|
|
2494
|
+
colocated: () => c.var.admin.eventCause(c.get('actor'), tenantId, scopeId, input),
|
|
2373
2495
|
}, (r) => r.chain.length));
|
|
2374
2496
|
});
|
|
2375
2497
|
// #1237 forward: what one event set off. Delegated like its backwards twin.
|
|
@@ -2380,12 +2502,12 @@ export function createControlPlaneApi(options) {
|
|
|
2380
2502
|
eventId: c.req.query('eventId'),
|
|
2381
2503
|
maxNodes: c.req.query('maxNodes') ? Number(c.req.query('maxNodes')) : undefined,
|
|
2382
2504
|
});
|
|
2383
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2505
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2384
2506
|
if (!scope)
|
|
2385
2507
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2386
2508
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'eventEffects', input, {
|
|
2387
2509
|
viaVertical: (v) => v.eventEffects(scopeId, input),
|
|
2388
|
-
colocated: () => admin.eventEffects(c.get('actor'), tenantId, scopeId, input),
|
|
2510
|
+
colocated: () => c.var.admin.eventEffects(c.get('actor'), tenantId, scopeId, input),
|
|
2389
2511
|
}, (r) => r.count));
|
|
2390
2512
|
});
|
|
2391
2513
|
// #1237: everything one call emitted — the siblings neither walk reaches. Delegated
|
|
@@ -2397,12 +2519,12 @@ export function createControlPlaneApi(options) {
|
|
|
2397
2519
|
invocationId: c.req.query('invocationId'),
|
|
2398
2520
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2399
2521
|
});
|
|
2400
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2522
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2401
2523
|
if (!scope)
|
|
2402
2524
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2403
2525
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'invocationEvents', input, {
|
|
2404
2526
|
viaVertical: (v) => v.invocationEvents(scopeId, input),
|
|
2405
|
-
colocated: () => admin.invocationEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2527
|
+
colocated: () => c.var.admin.invocationEvents(c.get('actor'), tenantId, scopeId, input),
|
|
2406
2528
|
}, (r) => r.events.length));
|
|
2407
2529
|
});
|
|
2408
2530
|
// #1525: every delivery in the scope that gave up — the list the walks can only reach
|
|
@@ -2414,12 +2536,12 @@ export function createControlPlaneApi(options) {
|
|
|
2414
2536
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2415
2537
|
cursor: c.req.query('cursor') ?? undefined,
|
|
2416
2538
|
});
|
|
2417
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2539
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2418
2540
|
if (!scope)
|
|
2419
2541
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2420
2542
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'deadLetters', input, {
|
|
2421
2543
|
viaVertical: (v) => v.deadLetters(scopeId, input),
|
|
2422
|
-
colocated: () => admin.deadLetters(c.get('actor'), tenantId, scopeId, input),
|
|
2544
|
+
colocated: () => c.var.admin.deadLetters(c.get('actor'), tenantId, scopeId, input),
|
|
2423
2545
|
}, (r) => r.entries.length));
|
|
2424
2546
|
});
|
|
2425
2547
|
// #1744: one entity's declared lifecycle replayed over the scope's outbox — the process
|
|
@@ -2429,12 +2551,12 @@ export function createControlPlaneApi(options) {
|
|
|
2429
2551
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2430
2552
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2431
2553
|
const input = lifecycleFlowInput.parse(await c.req.json());
|
|
2432
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2554
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2433
2555
|
if (!scope)
|
|
2434
2556
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2435
2557
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'lifecycleFlow', input, {
|
|
2436
2558
|
viaVertical: (v) => v.lifecycleFlow(scopeId, input),
|
|
2437
|
-
colocated: () => admin.lifecycleFlow(c.get('actor'), tenantId, scopeId, input),
|
|
2559
|
+
colocated: () => c.var.admin.lifecycleFlow(c.get('actor'), tenantId, scopeId, input),
|
|
2438
2560
|
}, (r) => r.observation.events));
|
|
2439
2561
|
});
|
|
2440
2562
|
// #1235: one record's event history — the per-entity pivot of the audit spine.
|
|
@@ -2449,12 +2571,12 @@ export function createControlPlaneApi(options) {
|
|
|
2449
2571
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2450
2572
|
cursor: c.req.query('cursor') ?? undefined,
|
|
2451
2573
|
});
|
|
2452
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2574
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2453
2575
|
if (!scope)
|
|
2454
2576
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2455
2577
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'entityHistory', input, {
|
|
2456
2578
|
viaVertical: (v) => v.entityHistory(scopeId, input),
|
|
2457
|
-
colocated: () => admin.entityHistory(c.get('actor'), tenantId, scopeId, input),
|
|
2579
|
+
colocated: () => c.var.admin.entityHistory(c.get('actor'), tenantId, scopeId, input),
|
|
2458
2580
|
}, (r) => r.entries.length));
|
|
2459
2581
|
});
|
|
2460
2582
|
app.get('/tenants/:tenantId/scopes/:scopeId/tables/:table', async (c) => {
|
|
@@ -2465,12 +2587,12 @@ export function createControlPlaneApi(options) {
|
|
|
2465
2587
|
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
2466
2588
|
offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
|
|
2467
2589
|
});
|
|
2468
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2590
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2469
2591
|
if (!scope)
|
|
2470
2592
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2471
2593
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'readScopeTable', input, {
|
|
2472
2594
|
viaVertical: (v) => v.readScopeTable(scopeId, input),
|
|
2473
|
-
colocated: () => admin.readScopeTable(c.get('actor'), tenantId, scopeId, input),
|
|
2595
|
+
colocated: () => c.var.admin.readScopeTable(c.get('actor'), tenantId, scopeId, input),
|
|
2474
2596
|
}, (r) => r.rows.length));
|
|
2475
2597
|
});
|
|
2476
2598
|
// The SQL console (#219): one read-only statement, POSTed because SQL does not
|
|
@@ -2482,7 +2604,7 @@ export function createControlPlaneApi(options) {
|
|
|
2482
2604
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2483
2605
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2484
2606
|
const input = queryScopeInput.parse(await c.req.json());
|
|
2485
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2607
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2486
2608
|
if (!scope)
|
|
2487
2609
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2488
2610
|
return c.json(await delegatedRead(
|
|
@@ -2491,7 +2613,7 @@ export function createControlPlaneApi(options) {
|
|
|
2491
2613
|
// statement would answer "someone queried" and nothing a reader needs.
|
|
2492
2614
|
c, tenantId, scopeId, scope, 'queryScope', input, {
|
|
2493
2615
|
viaVertical: (v) => v.queryScope(scopeId, input),
|
|
2494
|
-
colocated: () => admin.queryScope(c.get('actor'), tenantId, scopeId, input),
|
|
2616
|
+
colocated: () => c.var.admin.queryScope(c.get('actor'), tenantId, scopeId, input),
|
|
2495
2617
|
}, (r) => r.rows.length));
|
|
2496
2618
|
});
|
|
2497
2619
|
// The K-35 denial log (#867) — the third of the platform's three logs and the last to
|
|
@@ -2513,12 +2635,12 @@ export function createControlPlaneApi(options) {
|
|
|
2513
2635
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2514
2636
|
}
|
|
2515
2637
|
const filter = denialLogQuery.parse(c.req.query());
|
|
2516
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2638
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2517
2639
|
if (!scope)
|
|
2518
2640
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2519
2641
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'listDenials', filter, {
|
|
2520
2642
|
viaVertical: (v) => v.listDenials(scopeId, filter),
|
|
2521
|
-
colocated: () => admin.listDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2643
|
+
colocated: () => c.var.admin.listDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2522
2644
|
}, (r) => r.length));
|
|
2523
2645
|
});
|
|
2524
2646
|
app.get('/tenants/:tenantId/scopes/:scopeId/denials/summary', async (c) => {
|
|
@@ -2529,12 +2651,12 @@ export function createControlPlaneApi(options) {
|
|
|
2529
2651
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2530
2652
|
}
|
|
2531
2653
|
const filter = denialLogQuery.parse(c.req.query());
|
|
2532
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2654
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2533
2655
|
if (!scope)
|
|
2534
2656
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2535
2657
|
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'summarizeDenials', filter, {
|
|
2536
2658
|
viaVertical: (v) => v.summarizeDenials(scopeId, filter),
|
|
2537
|
-
colocated: () => admin.summarizeDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2659
|
+
colocated: () => c.var.admin.summarizeDenials(c.get('actor'), tenantId, scopeId, filter),
|
|
2538
2660
|
}, (r) => r.buckets.length));
|
|
2539
2661
|
});
|
|
2540
2662
|
// The owner seat (#925). Both reads go to the VERTICAL — the seat lives in its identity
|
|
@@ -2551,7 +2673,7 @@ export function createControlPlaneApi(options) {
|
|
|
2551
2673
|
if (outsideTenant(principal, tenantId)) {
|
|
2552
2674
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2553
2675
|
}
|
|
2554
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2676
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2555
2677
|
if (!scope)
|
|
2556
2678
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2557
2679
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2574,7 +2696,7 @@ export function createControlPlaneApi(options) {
|
|
|
2574
2696
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2575
2697
|
}
|
|
2576
2698
|
const actor = c.get('actor');
|
|
2577
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2699
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2578
2700
|
if (!scope)
|
|
2579
2701
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2580
2702
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2584,7 +2706,7 @@ export function createControlPlaneApi(options) {
|
|
|
2584
2706
|
// the platform has activated but not requiring it — a platform hostname is bound
|
|
2585
2707
|
// `pending` and activated a step later, and that step can fail while the app is fully
|
|
2586
2708
|
// 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');
|
|
2709
|
+
const bound = (await c.var.admin.listHostnames(actor, { scopeId })).filter((h) => h.status !== 'failed');
|
|
2588
2710
|
const pick = (hs) => hs.find((h) => h.surface === 'app' && h.canonical) ?? hs.find((h) => h.surface === 'app') ?? hs[0];
|
|
2589
2711
|
const host = pick(bound.filter((h) => h.status === 'active')) ?? pick(bound);
|
|
2590
2712
|
if (!host) {
|
|
@@ -2618,7 +2740,7 @@ export function createControlPlaneApi(options) {
|
|
|
2618
2740
|
const body = ownerTransferInput.parse(await c.req.json());
|
|
2619
2741
|
const actor = c.get('actor');
|
|
2620
2742
|
// 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);
|
|
2743
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2622
2744
|
if (!scope)
|
|
2623
2745
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2624
2746
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2628,7 +2750,7 @@ export function createControlPlaneApi(options) {
|
|
|
2628
2750
|
// An abandon (`abandon: true`) is audited the same way, every row marked, so the log tells
|
|
2629
2751
|
// a hand-over closed without finishing from one that finished.
|
|
2630
2752
|
const base = { tenantId, scopeId, operationId, from: body.from, to: body.to, ...(body.abandon ? { abandon: true } : {}) };
|
|
2631
|
-
await admin.recordOwnerTransfer(actor, { ...base, phase: 'intent' });
|
|
2753
|
+
await c.var.admin.recordOwnerTransfer(actor, { ...base, phase: 'intent' });
|
|
2632
2754
|
let moved;
|
|
2633
2755
|
try {
|
|
2634
2756
|
moved = await vertical.transferOwner({ tenantId, scopeId, ...body });
|
|
@@ -2636,7 +2758,7 @@ export function createControlPlaneApi(options) {
|
|
|
2636
2758
|
catch (e) {
|
|
2637
2759
|
const refused = e instanceof ControlPlaneError && e.status === 409;
|
|
2638
2760
|
const error = (e instanceof Error ? e.message : String(e)).slice(0, OWNER_TRANSFER_AUDIT_ERROR_MAX);
|
|
2639
|
-
await admin
|
|
2761
|
+
await c.var.admin
|
|
2640
2762
|
.recordOwnerTransfer(actor, { ...base, phase: refused ? 'refused' : 'failed', error })
|
|
2641
2763
|
.catch(() => undefined);
|
|
2642
2764
|
if (e instanceof ControlPlaneError) {
|
|
@@ -2645,7 +2767,7 @@ export function createControlPlaneApi(options) {
|
|
|
2645
2767
|
throw e;
|
|
2646
2768
|
}
|
|
2647
2769
|
try {
|
|
2648
|
-
await admin.recordOwnerTransfer(actor, {
|
|
2770
|
+
await c.var.admin.recordOwnerTransfer(actor, {
|
|
2649
2771
|
...base,
|
|
2650
2772
|
phase: 'applied',
|
|
2651
2773
|
outcome: moved.outcome,
|
|
@@ -2676,7 +2798,7 @@ export function createControlPlaneApi(options) {
|
|
|
2676
2798
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2677
2799
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2678
2800
|
const input = configureInstanceBody.parse(await c.req.json());
|
|
2679
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2801
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2680
2802
|
if (!scope)
|
|
2681
2803
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2682
2804
|
const vertical = await verticalForScope(c, scope);
|
|
@@ -2699,18 +2821,18 @@ export function createControlPlaneApi(options) {
|
|
|
2699
2821
|
// status the transition graph forbids. The graph is enforced below the seam;
|
|
2700
2822
|
// an illegal transition surfaces as a 409.
|
|
2701
2823
|
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),
|
|
2824
|
+
activate: (admin, a, t, s) => admin.activateScope(a, t, s),
|
|
2825
|
+
suspend: (admin, a, t, s) => admin.suspendScope(a, t, s),
|
|
2826
|
+
unsuspend: (admin, a, t, s) => admin.unsuspendScope(a, t, s),
|
|
2827
|
+
archive: (admin, a, t, s) => admin.archiveScope(a, t, s),
|
|
2828
|
+
unarchive: (admin, a, t, s) => admin.unarchiveScope(a, t, s),
|
|
2707
2829
|
};
|
|
2708
2830
|
for (const [action, run] of Object.entries(transitions)) {
|
|
2709
2831
|
app.post(`/tenants/:tenantId/scopes/:scopeId/${action}`, async (c) => {
|
|
2710
2832
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2711
2833
|
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));
|
|
2834
|
+
await run(c.var.admin, c.get('actor'), tenantId, scopeId);
|
|
2835
|
+
return c.json(await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId));
|
|
2714
2836
|
});
|
|
2715
2837
|
}
|
|
2716
2838
|
// -- snapshots (preview-and-snapshots.md §3/§9) -----------------------------
|
|
@@ -2723,13 +2845,13 @@ export function createControlPlaneApi(options) {
|
|
|
2723
2845
|
const actor = c.get('actor');
|
|
2724
2846
|
const vertical = await verticalForScope(c, scope);
|
|
2725
2847
|
if (!vertical)
|
|
2726
|
-
return
|
|
2848
|
+
return c.var.host.snapshotScope(actor, tenantId, scope.id, opts);
|
|
2727
2849
|
const snapId = scopeIdSchema.parse(ulid());
|
|
2728
2850
|
// Directory row FIRST, as `provisioning` (K-31's two-phase shape, used as
|
|
2729
2851
|
// intended): a crash between the row and the data copy leaves an inert
|
|
2730
2852
|
// provisioning row — which, carrying provenance and an expiry, the GC sweep
|
|
2731
2853
|
// eventually reaps — never copied data with no record.
|
|
2732
|
-
await
|
|
2854
|
+
await c.var.host.provisionScope(actor, {
|
|
2733
2855
|
tenantId,
|
|
2734
2856
|
scopeId: snapId,
|
|
2735
2857
|
kind: opts.kind ?? 'archive',
|
|
@@ -2740,11 +2862,11 @@ export function createControlPlaneApi(options) {
|
|
|
2740
2862
|
expiresAt: opts.expiresAt,
|
|
2741
2863
|
});
|
|
2742
2864
|
await retryTransient(() => vertical.snapshotScope({ sourceScopeId: scope.id, newScopeId: snapId }));
|
|
2743
|
-
await admin.activateScope(actor, tenantId, snapId);
|
|
2865
|
+
await c.var.admin.activateScope(actor, tenantId, snapId);
|
|
2744
2866
|
// Bound to the SOURCE's current version: source and fork share a deployment, so
|
|
2745
2867
|
// the fork resolves to the DO namespace its bytes actually live in.
|
|
2746
2868
|
if (scope.verticalVersionId) {
|
|
2747
|
-
await admin.bindScopeVersion(actor, tenantId, snapId, scope.verticalVersionId);
|
|
2869
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, snapId, scope.verticalVersionId);
|
|
2748
2870
|
}
|
|
2749
2871
|
return snapId;
|
|
2750
2872
|
};
|
|
@@ -2753,7 +2875,7 @@ export function createControlPlaneApi(options) {
|
|
|
2753
2875
|
app.get('/tenants/:tenantId/scopes/:scopeId/snapshots', async (c) => {
|
|
2754
2876
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2755
2877
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2756
|
-
const scopes = await admin.listScopes(c.get('actor'), { tenantId });
|
|
2878
|
+
const scopes = await c.var.admin.listScopes(c.get('actor'), { tenantId });
|
|
2757
2879
|
return c.json(scopes
|
|
2758
2880
|
.filter((s) => s.forkedFrom === scopeId)
|
|
2759
2881
|
.sort((a, b) => (a.forkedAt < b.forkedAt ? 1 : -1)));
|
|
@@ -2762,12 +2884,12 @@ export function createControlPlaneApi(options) {
|
|
|
2762
2884
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2763
2885
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2764
2886
|
const body = snapshotScopeBody.parse(await c.req.json().catch(() => ({})));
|
|
2765
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2887
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2766
2888
|
if (!scope)
|
|
2767
2889
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2768
2890
|
try {
|
|
2769
2891
|
const snapId = await orchestratedSnapshot(c, tenantId, scope, body);
|
|
2770
|
-
return c.json(await admin.getScopeRecord(c.get('actor'), tenantId, snapId), 201);
|
|
2892
|
+
return c.json(await c.var.admin.getScopeRecord(c.get('actor'), tenantId, snapId), 201);
|
|
2771
2893
|
}
|
|
2772
2894
|
catch (e) {
|
|
2773
2895
|
if (e instanceof ControlPlaneError) {
|
|
@@ -2791,11 +2913,11 @@ export function createControlPlaneApi(options) {
|
|
|
2791
2913
|
* 5xx, a timeout) still aborts — an implemented wipe that failed is a retry, not a
|
|
2792
2914
|
* shrug. Returns true when the storage was stranded.
|
|
2793
2915
|
*/
|
|
2794
|
-
const deleteScopeStorageOrStrand = async (vertical, scopeId) => {
|
|
2916
|
+
const deleteScopeStorageOrStrand = async (vertical, tenantId, scopeId) => {
|
|
2795
2917
|
if (!vertical)
|
|
2796
2918
|
return false;
|
|
2797
2919
|
try {
|
|
2798
|
-
await vertical.deleteScope({ scopeId });
|
|
2920
|
+
await vertical.deleteScope({ tenantId, scopeId });
|
|
2799
2921
|
return false;
|
|
2800
2922
|
}
|
|
2801
2923
|
catch (e) {
|
|
@@ -2808,7 +2930,7 @@ export function createControlPlaneApi(options) {
|
|
|
2808
2930
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2809
2931
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2810
2932
|
const actor = c.get('actor');
|
|
2811
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2933
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2812
2934
|
if (!scope)
|
|
2813
2935
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2814
2936
|
if (!scope.forkedFrom) {
|
|
@@ -2820,8 +2942,8 @@ export function createControlPlaneApi(options) {
|
|
|
2820
2942
|
// storage-before-row ordering deleteSnapshot itself keeps, so a crash
|
|
2821
2943
|
// between the two converges on retry.
|
|
2822
2944
|
const vertical = await verticalForScope(c, scope);
|
|
2823
|
-
const storageStranded = await deleteScopeStorageOrStrand(vertical, scopeId);
|
|
2824
|
-
await
|
|
2945
|
+
const storageStranded = await deleteScopeStorageOrStrand(vertical, tenantId, scopeId);
|
|
2946
|
+
await c.var.host.deleteSnapshot(actor, tenantId, scopeId);
|
|
2825
2947
|
return c.json({ deleted: scopeId, ...(storageStranded ? { storageStranded: true } : {}) });
|
|
2826
2948
|
}
|
|
2827
2949
|
catch (e) {
|
|
@@ -2852,7 +2974,7 @@ export function createControlPlaneApi(options) {
|
|
|
2852
2974
|
`store is global, so a backup would move its data out of that jurisdiction ` +
|
|
2853
2975
|
`(K-32); refused until a per-jurisdiction store exists`);
|
|
2854
2976
|
}
|
|
2855
|
-
const dump = await admin.exportScope(c.get('actor'), tenantId, scope.id);
|
|
2977
|
+
const dump = await c.var.admin.exportScope(c.get('actor'), tenantId, scope.id);
|
|
2856
2978
|
const vertical = await verticalForScope(c, scope);
|
|
2857
2979
|
const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
|
|
2858
2980
|
// Seal classified payloads per subject on the way in (#37). This copy is full-fidelity
|
|
@@ -2875,7 +2997,7 @@ export function createControlPlaneApi(options) {
|
|
|
2875
2997
|
return c.json({ error: 'no backup target configured' }, 501);
|
|
2876
2998
|
// Reads the directory, not the store, for the tenant cross-check: the store is keyed
|
|
2877
2999
|
// 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);
|
|
3000
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2879
3001
|
if (!scope)
|
|
2880
3002
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2881
3003
|
return c.json(await options.scopeBackups.list({ tenantId, scopeId }));
|
|
@@ -2889,7 +3011,7 @@ export function createControlPlaneApi(options) {
|
|
|
2889
3011
|
const capturedAt = c.req.param('capturedAt');
|
|
2890
3012
|
if (!options.scopeBackups)
|
|
2891
3013
|
return c.json({ error: 'no backup target configured' }, 501);
|
|
2892
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
3014
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2893
3015
|
if (!scope)
|
|
2894
3016
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2895
3017
|
const dump = await options.scopeBackups.get({ tenantId, scopeId, capturedAt });
|
|
@@ -2909,7 +3031,7 @@ export function createControlPlaneApi(options) {
|
|
|
2909
3031
|
app.post('/tenants/:tenantId/scopes/:scopeId/backups', async (c) => {
|
|
2910
3032
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
2911
3033
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
2912
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
3034
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2913
3035
|
if (!scope)
|
|
2914
3036
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2915
3037
|
try {
|
|
@@ -2943,10 +3065,10 @@ export function createControlPlaneApi(options) {
|
|
|
2943
3065
|
// A ULID, like every other subject id the spine carries — parsed rather than trusted,
|
|
2944
3066
|
// so a wildcard or an injection attempt is a 400 and never reaches the UPDATE.
|
|
2945
3067
|
const subjectId = dataSubjectIdSchema.parse(c.req.param('subjectId'));
|
|
2946
|
-
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
3068
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
2947
3069
|
if (!scope)
|
|
2948
3070
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
2949
|
-
return c.json(await admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId));
|
|
3071
|
+
return c.json(await c.var.admin.shredSubject(c.get('actor'), tenantId, scopeId, subjectId));
|
|
2950
3072
|
});
|
|
2951
3073
|
// -- directory backups (#40) -----------------------------------------------
|
|
2952
3074
|
// The platform's own disaster recovery, on the same store posture as the scope
|
|
@@ -2971,7 +3093,7 @@ export function createControlPlaneApi(options) {
|
|
|
2971
3093
|
if (!options.directoryBackups)
|
|
2972
3094
|
return c.json({ error: 'no directory backup target configured' }, 501);
|
|
2973
3095
|
const result = await backupDirectoryIfDue({
|
|
2974
|
-
admin,
|
|
3096
|
+
admin: c.var.admin,
|
|
2975
3097
|
store: options.directoryBackups,
|
|
2976
3098
|
actor: c.get('actor'),
|
|
2977
3099
|
force: true,
|
|
@@ -3008,14 +3130,14 @@ export function createControlPlaneApi(options) {
|
|
|
3008
3130
|
if (!dump)
|
|
3009
3131
|
return c.json({ error: `no directory backup at ${body.capturedAt}` }, 404);
|
|
3010
3132
|
const actor = c.get('actor');
|
|
3011
|
-
const live = await admin.listTenants(actor, { limit: 1 });
|
|
3133
|
+
const live = await c.var.admin.listTenants(actor, { limit: 1 });
|
|
3012
3134
|
if (live.length > 0 && !body.overwrite) {
|
|
3013
3135
|
return c.json({
|
|
3014
3136
|
error: 'the directory is not empty — a restore REPLACES it, so anything created ' +
|
|
3015
3137
|
'since this copy was taken would be lost; pass overwrite=true to confirm',
|
|
3016
3138
|
}, 409);
|
|
3017
3139
|
}
|
|
3018
|
-
await admin.restoreDirectory(actor, dump);
|
|
3140
|
+
await c.var.admin.restoreDirectory(actor, dump);
|
|
3019
3141
|
return c.json({ capturedAt: dump.capturedAt, tables: dump.tables.length });
|
|
3020
3142
|
});
|
|
3021
3143
|
// Reap an ARCHIVED primary scope (control-plane.md §4.4): free its DO storage —
|
|
@@ -3059,12 +3181,12 @@ export function createControlPlaneApi(options) {
|
|
|
3059
3181
|
if (parsed.data.countOnly !== undefined) {
|
|
3060
3182
|
return c.json({ error: 'countOnly is not honoured here — POST the same body to …/redrain-count for a read-only count' }, 400);
|
|
3061
3183
|
}
|
|
3062
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3184
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3063
3185
|
if (!scope)
|
|
3064
3186
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3065
3187
|
// The instant alone, never `parsed.data`, so the field cannot reach the adapter from
|
|
3066
3188
|
// this door even if the refusal above is ever relaxed.
|
|
3067
|
-
const redrained = await admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3189
|
+
const redrained = await c.var.admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3068
3190
|
drainedBefore: parsed.data.drainedBefore,
|
|
3069
3191
|
});
|
|
3070
3192
|
// `more` rather than silence: the verb reopens a bounded batch, so a caller that posts
|
|
@@ -3103,10 +3225,10 @@ export function createControlPlaneApi(options) {
|
|
|
3103
3225
|
'already shipped to the rebuilt table. Pass the instant the old table stopped receiving.',
|
|
3104
3226
|
}, 400);
|
|
3105
3227
|
}
|
|
3106
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3228
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3107
3229
|
if (!scope)
|
|
3108
3230
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3109
|
-
const redrainable = await admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3231
|
+
const redrainable = await c.var.admin.redrainEvents(actor, tenantId, scopeId, {
|
|
3110
3232
|
drainedBefore: parsed.data.drainedBefore,
|
|
3111
3233
|
countOnly: true,
|
|
3112
3234
|
});
|
|
@@ -3119,7 +3241,7 @@ export function createControlPlaneApi(options) {
|
|
|
3119
3241
|
// Body is optional so the bare `POST …/reap` every existing caller sends still
|
|
3120
3242
|
// parses; `backup` tri-states on purpose (see the ordering comment below).
|
|
3121
3243
|
const { backup: wantsBackup } = reapScopeBody.parse(await c.req.json().catch(() => ({})));
|
|
3122
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3244
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3123
3245
|
if (!scope)
|
|
3124
3246
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3125
3247
|
if (scope.status !== 'archived') {
|
|
@@ -3130,7 +3252,7 @@ export function createControlPlaneApi(options) {
|
|
|
3130
3252
|
// only in `reapScope` after. A serving app always holds a bound name; unbind it first.
|
|
3131
3253
|
// (This route is the interactive per-scope reap; the tenant-teardown route forces past
|
|
3132
3254
|
// the same guard because releasing every name is the point there.)
|
|
3133
|
-
const boundNames = await admin.listHostnames(actor, { scopeId, limit: 1 });
|
|
3255
|
+
const boundNames = await c.var.admin.listHostnames(actor, { scopeId, limit: 1 });
|
|
3134
3256
|
if (boundNames.length > 0) {
|
|
3135
3257
|
return c.json({
|
|
3136
3258
|
error: `scope ${scopeId} still resolves hostname '${boundNames[0].hostname}' — ` +
|
|
@@ -3167,11 +3289,11 @@ export function createControlPlaneApi(options) {
|
|
|
3167
3289
|
const vertical = await verticalForScope(c, scope);
|
|
3168
3290
|
// By this point the backup contract has resolved (a copy landed, or the caller
|
|
3169
3291
|
// explicitly declined one), so stranding is a bookkeeping fact, not data loss.
|
|
3170
|
-
const storageStranded = await deleteScopeStorageOrStrand(vertical, scopeId);
|
|
3171
|
-
await admin.reapScope(actor, tenantId, scopeId, {
|
|
3292
|
+
const storageStranded = await deleteScopeStorageOrStrand(vertical, tenantId, scopeId);
|
|
3293
|
+
await c.var.admin.reapScope(actor, tenantId, scopeId, {
|
|
3172
3294
|
...(backup ? { backupRef: backupRefOf(backup) } : {}),
|
|
3173
3295
|
});
|
|
3174
|
-
const reaped = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3296
|
+
const reaped = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3175
3297
|
return c.json({ ...reaped, backup, ...(storageStranded ? { storageStranded: true } : {}) });
|
|
3176
3298
|
}
|
|
3177
3299
|
catch (e) {
|
|
@@ -3191,7 +3313,7 @@ export function createControlPlaneApi(options) {
|
|
|
3191
3313
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3192
3314
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3193
3315
|
const actor = c.get('actor');
|
|
3194
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3316
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3195
3317
|
if (!scope)
|
|
3196
3318
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3197
3319
|
// Residency (K-7/K-32): jurisdiction pins EXECUTION, not just storage. A pull
|
|
@@ -3209,7 +3331,7 @@ export function createControlPlaneApi(options) {
|
|
|
3209
3331
|
// bytes when the host is co-located. When the scope's data lives in a vertical
|
|
3210
3332
|
// deployment, its dump OVERLAYS the (placeholder) tables — audit stays on the
|
|
3211
3333
|
// one canonical path either way.
|
|
3212
|
-
const dump = await admin.exportScope(actor, tenantId, scopeId);
|
|
3334
|
+
const dump = await c.var.admin.exportScope(actor, tenantId, scopeId);
|
|
3213
3335
|
const vertical = await verticalForScope(c, scope);
|
|
3214
3336
|
const tables = vertical ? await vertical.exportScope(scopeId) : dump.tables;
|
|
3215
3337
|
if (full)
|
|
@@ -3248,13 +3370,13 @@ export function createControlPlaneApi(options) {
|
|
|
3248
3370
|
app.get('/tenants/:tenantId/export', async (c) => {
|
|
3249
3371
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3250
3372
|
const actor = c.get('actor');
|
|
3251
|
-
const tenant = await admin.getTenant(actor, tenantId);
|
|
3373
|
+
const tenant = await c.var.admin.getTenant(actor, tenantId);
|
|
3252
3374
|
if (!tenant)
|
|
3253
3375
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
3254
3376
|
// Every scope, tombstones included: an archived or reaped scope is part of the
|
|
3255
3377
|
// tenant's history, and an export that quietly dropped them would misrepresent what
|
|
3256
3378
|
// the tenant was. Their DATA is a different question, handled below.
|
|
3257
|
-
const scopes = await admin.listScopes(actor, { tenantId });
|
|
3379
|
+
const scopes = await c.var.admin.listScopes(actor, { tenantId });
|
|
3258
3380
|
// Residency (K-7/K-32), checked across the WHOLE tenant before anything is read: an
|
|
3259
3381
|
// export lands on a machine outside the platform's control, so one pinned scope
|
|
3260
3382
|
// taints the file. Refused as a unit rather than silently exporting the global
|
|
@@ -3271,18 +3393,18 @@ export function createControlPlaneApi(options) {
|
|
|
3271
3393
|
}
|
|
3272
3394
|
const full = c.req.query('full') === 'true';
|
|
3273
3395
|
try {
|
|
3274
|
-
const orgs = await admin.listOrgs(actor, tenantId);
|
|
3396
|
+
const orgs = await c.var.admin.listOrgs(actor, tenantId);
|
|
3275
3397
|
// Revoked memberships included: K-21 makes a removal a tombstone precisely because
|
|
3276
3398
|
// "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();
|
|
3399
|
+
const members = (await Promise.all(orgs.map((o) => c.var.admin.listMembers(actor, tenantId, o.id, { includeRevoked: true })))).flat();
|
|
3278
3400
|
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 }),
|
|
3401
|
+
c.var.admin.listRoles(actor, { tenantId }),
|
|
3402
|
+
c.var.admin.listEntitlements(actor, tenantId),
|
|
3403
|
+
c.var.admin.listIdentityLinks(actor, tenantId),
|
|
3404
|
+
c.var.admin.listHostnames(actor, { tenantId }),
|
|
3405
|
+
c.var.admin.listTenantStores(actor, { tenantId }),
|
|
3406
|
+
c.var.admin.listBlobStores(actor, { tenantId }),
|
|
3407
|
+
c.var.admin.listConnections(actor, { tenantId }),
|
|
3286
3408
|
]);
|
|
3287
3409
|
// Scope DATA, from the same delegation the per-scope export route uses: the
|
|
3288
3410
|
// canonical `exportScope` writes the audit entry and is the bytes when co-located;
|
|
@@ -3296,7 +3418,7 @@ export function createControlPlaneApi(options) {
|
|
|
3296
3418
|
const live = scopes.filter((s) => s.status !== 'reaped');
|
|
3297
3419
|
const data = [];
|
|
3298
3420
|
for (const scope of live) {
|
|
3299
|
-
const dump = await admin.exportScope(actor, tenantId, scope.id);
|
|
3421
|
+
const dump = await c.var.admin.exportScope(actor, tenantId, scope.id);
|
|
3300
3422
|
const vertical = await verticalForScope(c, scope);
|
|
3301
3423
|
const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
|
|
3302
3424
|
data.push({ ...dump, tables: mask ? await maskDump(tables, mask) : tables });
|
|
@@ -3305,7 +3427,7 @@ export function createControlPlaneApi(options) {
|
|
|
3305
3427
|
// customer's Art. 20 data, and it carries staff actor ids and internal action
|
|
3306
3428
|
// names. An escrow or a dispute needs it, which is why break-glass reaches it
|
|
3307
3429
|
// rather than nothing reaching it.
|
|
3308
|
-
const adminLog = full ? await admin.auditLog(actor, { tenantId }) : null;
|
|
3430
|
+
const adminLog = full ? await c.var.admin.auditLog(actor, { tenantId }) : null;
|
|
3309
3431
|
const body = {
|
|
3310
3432
|
tenantId,
|
|
3311
3433
|
capturedAt: new Date().toISOString(),
|
|
@@ -3358,7 +3480,7 @@ export function createControlPlaneApi(options) {
|
|
|
3358
3480
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3359
3481
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3360
3482
|
const actor = c.get('actor');
|
|
3361
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3483
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3362
3484
|
if (!scope)
|
|
3363
3485
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3364
3486
|
const raw = await c.req.json();
|
|
@@ -3386,7 +3508,7 @@ export function createControlPlaneApi(options) {
|
|
|
3386
3508
|
const origin = { tenantId: tenantIdSchema.parse(dump.tenantId), scopeId: scopeIdSchema.parse(dump.scopeId) };
|
|
3387
3509
|
const tables = await openDump(dump.tables, sealerFor(c, origin.tenantId, origin.scopeId));
|
|
3388
3510
|
const landing = { ...dump, tables };
|
|
3389
|
-
await host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
|
|
3511
|
+
await c.var.host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
|
|
3390
3512
|
const vertical = await verticalForScope(c, scope);
|
|
3391
3513
|
// #1742: the recorded OFF positions ride the restore, so the deployment switches them
|
|
3392
3514
|
// off in the replay's own event — its own sweeper cannot land in between.
|
|
@@ -3399,7 +3521,7 @@ export function createControlPlaneApi(options) {
|
|
|
3399
3521
|
// #1674: a backup from before a module was switched off brings it back on; the
|
|
3400
3522
|
// directory's record puts it back off now, not at the next reconcile. Where the
|
|
3401
3523
|
// deployment already did (#1742), this finds it done and audits what it reported.
|
|
3402
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored?.switchedOff });
|
|
3524
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: restored?.switchedOff });
|
|
3403
3525
|
return c.json({ restored: scopeId, tables: tables.length });
|
|
3404
3526
|
}
|
|
3405
3527
|
catch (e) {
|
|
@@ -3423,13 +3545,13 @@ export function createControlPlaneApi(options) {
|
|
|
3423
3545
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3424
3546
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3425
3547
|
const actor = c.get('actor');
|
|
3426
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3548
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3427
3549
|
if (!scope)
|
|
3428
3550
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3429
3551
|
const vertical = await verticalForScope(c, scope);
|
|
3430
3552
|
return c.json(vertical
|
|
3431
3553
|
? await vertical.appliedMigrations(scopeId)
|
|
3432
|
-
: await admin.scopeAppliedMigrations(actor, tenantId, scopeId));
|
|
3554
|
+
: await c.var.admin.scopeAppliedMigrations(actor, tenantId, scopeId));
|
|
3433
3555
|
});
|
|
3434
3556
|
// #286: the PITR bookmarks a scope recorded before its migration passes — the
|
|
3435
3557
|
// rewind points the deployments UI offers for a backout. Read through the
|
|
@@ -3439,13 +3561,13 @@ export function createControlPlaneApi(options) {
|
|
|
3439
3561
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3440
3562
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3441
3563
|
const actor = c.get('actor');
|
|
3442
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3564
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3443
3565
|
if (!scope)
|
|
3444
3566
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3445
3567
|
const vertical = await verticalForScope(c, scope);
|
|
3446
3568
|
return c.json(vertical
|
|
3447
3569
|
? await vertical.migrationBookmarks(scopeId)
|
|
3448
|
-
: await admin.scopeMigrationBookmarks(actor, tenantId, scopeId));
|
|
3570
|
+
: await c.var.admin.scopeMigrationBookmarks(actor, tenantId, scopeId));
|
|
3449
3571
|
});
|
|
3450
3572
|
// #286's backout: rewind a scope to a pre-migration bookmark — schema AND data,
|
|
3451
3573
|
// discarding every write since. Audited below the seam BEFORE the rewind runs;
|
|
@@ -3459,12 +3581,12 @@ export function createControlPlaneApi(options) {
|
|
|
3459
3581
|
.object({ bookmark: z.string().min(1), force: z.boolean().optional() })
|
|
3460
3582
|
.parse(await c.req.json());
|
|
3461
3583
|
const actor = c.get('actor');
|
|
3462
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3584
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3463
3585
|
if (!scope)
|
|
3464
3586
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3465
3587
|
try {
|
|
3466
3588
|
const vertical = await verticalForScope(c, scope);
|
|
3467
|
-
const result = await admin.rewindScope(actor, tenantId, scopeId, bookmark, {
|
|
3589
|
+
const result = await c.var.admin.rewindScope(actor, tenantId, scopeId, bookmark, {
|
|
3468
3590
|
force,
|
|
3469
3591
|
localApply: !vertical,
|
|
3470
3592
|
});
|
|
@@ -3478,7 +3600,7 @@ export function createControlPlaneApi(options) {
|
|
|
3478
3600
|
// Meanwhile, the deployment that rewound it holds those modules off (#1819). A hold is
|
|
3479
3601
|
// released by that reconcile's re-assert, or earlier by a switch move known to land on the
|
|
3480
3602
|
// rewound storage, such as an operator's ON.
|
|
3481
|
-
await admin.markScopeProvisioned(actor, tenantId, scopeId, null);
|
|
3603
|
+
await c.var.admin.markScopeProvisioned(actor, tenantId, scopeId, null);
|
|
3482
3604
|
return c.json(answer);
|
|
3483
3605
|
}
|
|
3484
3606
|
catch (e) {
|
|
@@ -3529,7 +3651,7 @@ export function createControlPlaneApi(options) {
|
|
|
3529
3651
|
if (v.ownerTenant === null) {
|
|
3530
3652
|
return c.json({ error: 'adopt-serving is a private-vertical operation' }, 409);
|
|
3531
3653
|
}
|
|
3532
|
-
const owned = (await admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug })).filter((s) => !s.forkedFrom && s.kind !== 'preview' && s.status === 'active');
|
|
3654
|
+
const owned = (await c.var.admin.listScopes(actor, { tenantId: v.ownerTenant, vertical: slug })).filter((s) => !s.forkedFrom && s.kind !== 'preview' && s.status === 'active');
|
|
3533
3655
|
const { acknowledge } = adoptServingBody.parse(await c.req.json().catch(() => ({})));
|
|
3534
3656
|
const adopted = [];
|
|
3535
3657
|
const alreadyAdopted = [];
|
|
@@ -3556,6 +3678,13 @@ export function createControlPlaneApi(options) {
|
|
|
3556
3678
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3557
3679
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3558
3680
|
const { vertical, ackMigrations, abandonData, acknowledge } = rebindScopeVerticalBody.parse(await c.req.json());
|
|
3681
|
+
// #977: the TARGET must be a vertical this principal can read — its own tenant's, or a
|
|
3682
|
+
// listed one. The path pin confines whose scope moves; without this, it could move onto
|
|
3683
|
+
// another tenant's private vertical, which a confined caller must not even learn exists
|
|
3684
|
+
// (404, as `GET /verticals/:slug` answers it).
|
|
3685
|
+
if (await notReadable(c.get('principal'), vertical)) {
|
|
3686
|
+
return c.json({ error: `unknown vertical: ${vertical}` }, 404);
|
|
3687
|
+
}
|
|
3559
3688
|
try {
|
|
3560
3689
|
const r = await rebindScopeOntoVertical(c, tenantId, scopeId, vertical, {
|
|
3561
3690
|
ackMigrations,
|
|
@@ -3581,7 +3710,7 @@ export function createControlPlaneApi(options) {
|
|
|
3581
3710
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3582
3711
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3583
3712
|
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) });
|
|
3713
|
+
return c.json({ affected: await c.var.admin.bindingImpact(c.get('actor'), tenantId, scopeId, versionId) });
|
|
3585
3714
|
});
|
|
3586
3715
|
// Pin a scope to a vertical version (#31; orchestration.md §4). Refuses a
|
|
3587
3716
|
// non-admitted version below the seam — that refusal is the registry's reason to
|
|
@@ -3600,7 +3729,7 @@ export function createControlPlaneApi(options) {
|
|
|
3600
3729
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3601
3730
|
const { versionId, snapshot, acknowledge } = bindScopeVersionBody.parse(await c.req.json());
|
|
3602
3731
|
const actor = c.get('actor');
|
|
3603
|
-
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3732
|
+
const scope = await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3604
3733
|
// #1756: the apps in this tenant the bind would break, asked BEFORE the carry and the
|
|
3605
3734
|
// snapshot below, which move data. The host refuses too, whoever calls, but by then a
|
|
3606
3735
|
// refusal would land after the bytes had moved. It refuses what the bind would refuse
|
|
@@ -3621,12 +3750,12 @@ export function createControlPlaneApi(options) {
|
|
|
3621
3750
|
// already did so in the restore's own event; what it reports is audited here.
|
|
3622
3751
|
const reassert = async () => {
|
|
3623
3752
|
if (scope) {
|
|
3624
|
-
await admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: carried?.switchedOff });
|
|
3753
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId }, { appliedInUnit: carried?.switchedOff });
|
|
3625
3754
|
}
|
|
3626
3755
|
};
|
|
3627
3756
|
// The host's own refusal, should the impact have changed since it was read above (a
|
|
3628
3757
|
// 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);
|
|
3758
|
+
const bind = (opts) => c.var.admin.bindScopeVersion(actor, tenantId, scopeId, versionId, opts).catch(relayHostRefusal);
|
|
3630
3759
|
if (snapshot) {
|
|
3631
3760
|
if (!scope) {
|
|
3632
3761
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
@@ -3638,8 +3767,8 @@ export function createControlPlaneApi(options) {
|
|
|
3638
3767
|
let migrationCrossing = false;
|
|
3639
3768
|
if (scope.vertical && scope.verticalVersionId) {
|
|
3640
3769
|
const [current, incoming] = await Promise.all([
|
|
3641
|
-
admin.getVersion(actor, scope.verticalVersionId, scope.vertical),
|
|
3642
|
-
admin.getVersion(actor, versionId, scope.vertical),
|
|
3770
|
+
c.var.admin.getVersion(actor, scope.verticalVersionId, scope.vertical),
|
|
3771
|
+
c.var.admin.getVersion(actor, versionId, scope.vertical),
|
|
3643
3772
|
]);
|
|
3644
3773
|
migrationCrossing = Boolean(current && incoming && current.migrationDigest !== incoming.migrationDigest);
|
|
3645
3774
|
}
|
|
@@ -3657,12 +3786,12 @@ export function createControlPlaneApi(options) {
|
|
|
3657
3786
|
await bind({ acknowledge, snapshot: true });
|
|
3658
3787
|
}
|
|
3659
3788
|
await reassert();
|
|
3660
|
-
return c.json(await admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3789
|
+
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3661
3790
|
}
|
|
3662
3791
|
await carry();
|
|
3663
3792
|
await bind({ acknowledge });
|
|
3664
3793
|
await reassert();
|
|
3665
|
-
return c.json(await admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3794
|
+
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, scopeId));
|
|
3666
3795
|
});
|
|
3667
3796
|
// -- instances (K-31) -------------------------------------------------------
|
|
3668
3797
|
// The one place this surface calls OUT rather than sitting over `HostAdmin`, and
|
|
@@ -3679,7 +3808,7 @@ export function createControlPlaneApi(options) {
|
|
|
3679
3808
|
// The install kill-switch: a blocked vertical takes no NEW instances, for anyone
|
|
3680
3809
|
// including its owner. Refused before deployment resolution so the answer is
|
|
3681
3810
|
// 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);
|
|
3811
|
+
const registered = (await c.var.admin.listVerticals(c.get('actor'))).find((v) => v.slug === slug);
|
|
3683
3812
|
if (registered?.installsBlocked) {
|
|
3684
3813
|
return c.json({ error: `new installs of vertical '${slug}' are blocked` }, 403);
|
|
3685
3814
|
}
|
|
@@ -3696,11 +3825,11 @@ export function createControlPlaneApi(options) {
|
|
|
3696
3825
|
// (#304). A vertical predating the field ignores it; grant/revoke AFTER provision ride
|
|
3697
3826
|
// a re-provision (this endpoint is idempotent, K-31), meanwhile expiry still enforces
|
|
3698
3827
|
// locally because the projected row carries it.
|
|
3699
|
-
const entitlements = await admin.listEntitlements(c.get('actor'), input.tenantId);
|
|
3828
|
+
const entitlements = await c.var.admin.listEntitlements(c.get('actor'), input.tenantId);
|
|
3700
3829
|
// #406: identity links ride the same authoritative gather — the vertical projects them
|
|
3701
3830
|
// so its auth adapter resolves logins from local storage, and offboarding becomes an
|
|
3702
3831
|
// 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);
|
|
3832
|
+
const identityLinks = (await c.var.admin.listIdentityLinks(c.get('actor'), input.tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
3704
3833
|
// #592: connection grants too — tenant-wide rows materialize for the NEW scope, so an
|
|
3705
3834
|
// install provisioned after `grantToConnection` holds the same `connection:<id>` tuple
|
|
3706
3835
|
// as one provisioned before it, and the connector return path works without a human
|
|
@@ -3709,24 +3838,24 @@ export function createControlPlaneApi(options) {
|
|
|
3709
3838
|
// connector has since declared another grant receives it — rather than inheriting the
|
|
3710
3839
|
// gap the tenant's first install was created with.
|
|
3711
3840
|
try {
|
|
3712
|
-
await reconcileConnectionGrants({ admin, actor: c.get('actor'), declared: options.connectorGrants ?? {} }, input.tenantId, slug);
|
|
3841
|
+
await reconcileConnectionGrants({ admin: c.var.admin, actor: c.get('actor'), declared: options.connectorGrants ?? {} }, input.tenantId, slug);
|
|
3713
3842
|
}
|
|
3714
3843
|
catch {
|
|
3715
3844
|
// Best-effort, exactly as on the reconcile route.
|
|
3716
3845
|
}
|
|
3717
|
-
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(c.get('actor'), input.tenantId), slug, input.scopeId);
|
|
3846
|
+
const connectionGrants = connectionGrantsForScope(await c.var.admin.listConnectionGrants(c.get('actor'), input.tenantId), slug, input.scopeId);
|
|
3718
3847
|
// #687: and the public sealing keys of this tenant's live connections for the same
|
|
3719
3848
|
// vertical, so the new install can seal a value TO a connector from its first
|
|
3720
3849
|
// operation — before this, a scope could only receive a connector's answer, never
|
|
3721
3850
|
// hand it something the spine must not carry in the clear.
|
|
3722
|
-
const connectionKeys = await admin.connectionSealingKeys(input.tenantId, slug);
|
|
3851
|
+
const connectionKeys = await c.var.admin.connectionSealingKeys(input.tenantId, slug);
|
|
3723
3852
|
// #301 PR-2: per-tenant relational stores the vertical DECLARED, minted here (before
|
|
3724
3853
|
// the callback — the vertical migrates the store inside the K-31 ready-gate, so it
|
|
3725
3854
|
// must exist and be bound first). Idempotent like the endpoint: a retried provision
|
|
3726
3855
|
// re-resolves the same handles and the binding attach no-ops once present. `[]` for
|
|
3727
3856
|
// every vertical that declares none — the payload is unchanged for them.
|
|
3728
3857
|
const tenantStores = await collectTenantStoreHandles({
|
|
3729
|
-
host:
|
|
3858
|
+
host: c.var.host,
|
|
3730
3859
|
actor: c.get('actor'),
|
|
3731
3860
|
slug,
|
|
3732
3861
|
tenantId: input.tenantId,
|
|
@@ -3736,7 +3865,7 @@ export function createControlPlaneApi(options) {
|
|
|
3736
3865
|
// No handle rides the provision callback (a blob store has no schema to migrate), so
|
|
3737
3866
|
// the effect is purely the ledger row + the attached r2_bucket binding.
|
|
3738
3867
|
await collectBlobStoreHandles({
|
|
3739
|
-
host:
|
|
3868
|
+
host: c.var.host,
|
|
3740
3869
|
actor: c.get('actor'),
|
|
3741
3870
|
slug,
|
|
3742
3871
|
tenantId: input.tenantId,
|
|
@@ -3745,7 +3874,7 @@ export function createControlPlaneApi(options) {
|
|
|
3745
3874
|
// #1742: the recorded OFF positions ride the provision into the seat's own unit. Read
|
|
3746
3875
|
// from the directory whether or not the row exists yet: a brand-new install has no
|
|
3747
3876
|
// 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 });
|
|
3877
|
+
const switches = await switchCarryFor(c.var.admin, c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId });
|
|
3749
3878
|
try {
|
|
3750
3879
|
// #424 case 2: the binding attach above races Cloudflare script-settings
|
|
3751
3880
|
// propagation, so the vertical's FIRST answer can be a transient 5xx that a
|
|
@@ -3767,17 +3896,17 @@ export function createControlPlaneApi(options) {
|
|
|
3767
3896
|
// is the right trade — a receipt written before a bind would name no version, and
|
|
3768
3897
|
// one guessed from the serving pointer would claim a hook ran against code this
|
|
3769
3898
|
// scope was not running.
|
|
3770
|
-
const provisioned = await admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
3899
|
+
const provisioned = await c.var.admin.getScopeRecord(c.get('actor'), input.tenantId, input.scopeId);
|
|
3771
3900
|
// #1674: this route is idempotent (K-31), so an already-bound scope that was wiped can
|
|
3772
3901
|
// come back through it, re-seated live by the deployment — and the receipt below means
|
|
3773
3902
|
// no sweep follows. Re-assert the recorded OFF positions before it. Only once the
|
|
3774
3903
|
// directory row exists: a brand-new install's is written after this call (see above),
|
|
3775
3904
|
// has no record to re-assert, and `reassertSystemSwitches` refuses an unknown scope.
|
|
3776
3905
|
if (provisioned) {
|
|
3777
|
-
await admin.reassertSystemSwitches(c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId }, { appliedInUnit: instance.switchedOff });
|
|
3906
|
+
await c.var.admin.reassertSystemSwitches(c.get('actor'), { tenantId: input.tenantId, scopeId: input.scopeId }, { appliedInUnit: instance.switchedOff });
|
|
3778
3907
|
}
|
|
3779
3908
|
if (provisioned?.verticalVersionId) {
|
|
3780
|
-
await admin.markScopeProvisioned(c.get('actor'), input.tenantId, input.scopeId, provisioned.verticalVersionId);
|
|
3909
|
+
await c.var.admin.markScopeProvisioned(c.get('actor'), input.tenantId, input.scopeId, provisioned.verticalVersionId);
|
|
3781
3910
|
}
|
|
3782
3911
|
return c.json(instance, 201);
|
|
3783
3912
|
}
|
|
@@ -3816,13 +3945,13 @@ export function createControlPlaneApi(options) {
|
|
|
3816
3945
|
// `listVerticals` returns every row (the adapter does not narrow); the transport is
|
|
3817
3946
|
// where per-principal filtering happens — here and on GET /verticals.
|
|
3818
3947
|
const ownerOf = async (actor, slug) => {
|
|
3819
|
-
const v = (await admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3948
|
+
const v = (await host.admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3820
3949
|
return v ? v.ownerTenant : undefined;
|
|
3821
3950
|
};
|
|
3822
3951
|
// The full registry row, for the checks that need more than the owner — whether the
|
|
3823
3952
|
// vertical is PRIVATE (owned + not listed), which is what scopes a builder's prod
|
|
3824
3953
|
// self-serve below.
|
|
3825
|
-
const verticalOf = async (actor, slug) => (await admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3954
|
+
const verticalOf = async (actor, slug) => (await host.admin.listVerticals(actor)).find((x) => x.slug === slug);
|
|
3826
3955
|
/**
|
|
3827
3956
|
* "This principal is confined, and this vertical is not its tenant's." The one
|
|
3828
3957
|
* predicate every slug-addressed route asks.
|
|
@@ -3846,6 +3975,22 @@ export function createControlPlaneApi(options) {
|
|
|
3846
3975
|
* vertical stays owner-only, and so do the reads narrowed on purpose (migration SQL,
|
|
3847
3976
|
* the prod history).
|
|
3848
3977
|
*/
|
|
3978
|
+
/**
|
|
3979
|
+
* Whether a CONFINED principal may grant its own tenant `key` (#977): some vertical it
|
|
3980
|
+
* can read — its own, or a listed one — declares the key, or is the vertical whose
|
|
3981
|
+
* bare slug it is. The same derivation the dashboard's install makes
|
|
3982
|
+
* (`installEntitlements`), checked here so a credential cannot grant what no install
|
|
3983
|
+
* would. Read through the configured host: a check audits nothing.
|
|
3984
|
+
*/
|
|
3985
|
+
const tenantGrantable = async (p, key) => {
|
|
3986
|
+
const pin = confinedTenant(p);
|
|
3987
|
+
if (pin === null)
|
|
3988
|
+
return true;
|
|
3989
|
+
const visible = (await host.admin.listVerticals(p.actor)).filter((v) => v.ownerTenant === pin || v.listed);
|
|
3990
|
+
return visible.some((v) => v.entitlements && v.entitlements.length > 0
|
|
3991
|
+
? v.entitlements.includes(key)
|
|
3992
|
+
: (v.slug.split('/').pop() || v.slug) === key);
|
|
3993
|
+
};
|
|
3849
3994
|
const notReadable = async (p, slug) => {
|
|
3850
3995
|
const pin = confinedTenant(p);
|
|
3851
3996
|
if (pin === null)
|
|
@@ -3883,7 +4028,7 @@ export function createControlPlaneApi(options) {
|
|
|
3883
4028
|
if (!pin)
|
|
3884
4029
|
return raw;
|
|
3885
4030
|
const actor = c.get('actor');
|
|
3886
|
-
const workspace = (await admin.listTenants(actor)).find((t) => t.slug === pin || t.id === pin);
|
|
4031
|
+
const workspace = (await c.var.admin.listTenants(actor)).find((t) => t.slug === pin || t.id === pin);
|
|
3887
4032
|
if (!workspace || raw.startsWith(`${workspace.slug}/`))
|
|
3888
4033
|
return raw;
|
|
3889
4034
|
if ((await ownerOf(actor, raw)) === workspace.id)
|
|
@@ -3893,7 +4038,7 @@ export function createControlPlaneApi(options) {
|
|
|
3893
4038
|
};
|
|
3894
4039
|
app.get('/verticals', async (c) => {
|
|
3895
4040
|
const page = pageParams(c);
|
|
3896
|
-
const all = await admin.listVerticals(c.get('actor'));
|
|
4041
|
+
const all = await c.var.admin.listVerticals(c.get('actor'));
|
|
3897
4042
|
const p = c.get('principal');
|
|
3898
4043
|
// A builder sees only what it owns; staff see the whole registry. The narrowing
|
|
3899
4044
|
// runs above the adapter, so the page is sliced here — over the narrowed list.
|
|
@@ -3932,12 +4077,12 @@ export function createControlPlaneApi(options) {
|
|
|
3932
4077
|
* it that can drift when a third ref scheme appears.
|
|
3933
4078
|
*/
|
|
3934
4079
|
const tenantServiceRefs = async (actor, tenantId, slug) => {
|
|
3935
|
-
const all = (await admin.listVerticals(actor)).filter((v) => v.ownerTenant === tenantId);
|
|
4080
|
+
const all = (await host.admin.listVerticals(actor)).filter((v) => v.ownerTenant === tenantId);
|
|
3936
4081
|
const considered = slug === undefined ? all : all.filter((v) => v.slug === slug);
|
|
3937
4082
|
const verticalsTruncated = considered.length > MAX_SERVICE_REF_VERTICALS;
|
|
3938
4083
|
const entries = [];
|
|
3939
4084
|
for (const v of considered.slice(0, MAX_SERVICE_REF_VERTICALS)) {
|
|
3940
|
-
const versions = await admin.listVersions(actor, v.slug);
|
|
4085
|
+
const versions = await host.admin.listVersions(actor, v.slug);
|
|
3941
4086
|
const labelOf = new Map(versions.map((ver) => [ver.id, ver.version]));
|
|
3942
4087
|
// The stable serving script — where real traffic lands. Its version is the
|
|
3943
4088
|
// registry row's own servingVersionId: authoritative, not the scope-derived
|
|
@@ -4016,10 +4161,10 @@ export function createControlPlaneApi(options) {
|
|
|
4016
4161
|
return c.json({ error: 'forbidden' }, 403);
|
|
4017
4162
|
input.ownerTenant = p.tenantId;
|
|
4018
4163
|
}
|
|
4019
|
-
await admin.registerVertical(c.get('actor'), input);
|
|
4164
|
+
await c.var.admin.registerVertical(c.get('actor'), input);
|
|
4020
4165
|
// Idempotent on the slug (a conflicting re-register throws below the seam), so
|
|
4021
4166
|
// read back rather than echo the request.
|
|
4022
|
-
const registered = (await admin.listVerticals(c.get('actor'))).find((v) => v.slug === input.slug);
|
|
4167
|
+
const registered = (await c.var.admin.listVerticals(c.get('actor'))).find((v) => v.slug === input.slug);
|
|
4023
4168
|
return c.json(registered, 201);
|
|
4024
4169
|
});
|
|
4025
4170
|
app.get('/verticals/:slug/versions', async (c) => {
|
|
@@ -4031,7 +4176,7 @@ export function createControlPlaneApi(options) {
|
|
|
4031
4176
|
return c.json({ error: 'not found' }, 404);
|
|
4032
4177
|
}
|
|
4033
4178
|
const page = pageParams(c);
|
|
4034
|
-
const entries = await admin.listVersions(c.get('actor'), slug, page);
|
|
4179
|
+
const entries = await c.var.admin.listVersions(c.get('actor'), slug, page);
|
|
4035
4180
|
return c.json(pageOf(entries, page.limit, (v) => v.id));
|
|
4036
4181
|
});
|
|
4037
4182
|
app.post('/verticals/:slug/versions', async (c) => {
|
|
@@ -4077,8 +4222,8 @@ export function createControlPlaneApi(options) {
|
|
|
4077
4222
|
return refuse(registry.error.issues[0], ['registry']);
|
|
4078
4223
|
}
|
|
4079
4224
|
}
|
|
4080
|
-
await admin.publishVersion(c.get('actor'), input);
|
|
4081
|
-
const version = await admin.getVersion(c.get('actor'), input.id, slug);
|
|
4225
|
+
await c.var.admin.publishVersion(c.get('actor'), input);
|
|
4226
|
+
const version = await c.var.admin.getVersion(c.get('actor'), input.id, slug);
|
|
4082
4227
|
return c.json(version, 201);
|
|
4083
4228
|
});
|
|
4084
4229
|
// The declared permission registry (D-39, #336) that ships inside one version's manifest:
|
|
@@ -4093,7 +4238,7 @@ export function createControlPlaneApi(options) {
|
|
|
4093
4238
|
if (await notReadable(p, slug)) {
|
|
4094
4239
|
return c.json({ error: 'not found' }, 404);
|
|
4095
4240
|
}
|
|
4096
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4241
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4097
4242
|
const registry = json ? (storedDeployManifest.parse(JSON.parse(json)).registry ?? null) : null;
|
|
4098
4243
|
return c.json({ registry });
|
|
4099
4244
|
});
|
|
@@ -4112,7 +4257,7 @@ export function createControlPlaneApi(options) {
|
|
|
4112
4257
|
return c.json({ error: 'not found' }, 404);
|
|
4113
4258
|
}
|
|
4114
4259
|
// `versionMigrations` refuses a version of another vertical, so `base` cannot reach one.
|
|
4115
|
-
const migrationsOf = (id) => admin.versionMigrations(c.get('actor'), slug, id);
|
|
4260
|
+
const migrationsOf = (id) => c.var.admin.versionMigrations(c.get('actor'), slug, id);
|
|
4116
4261
|
const base = c.req.query('base');
|
|
4117
4262
|
const [incoming, baseline] = await Promise.all([
|
|
4118
4263
|
migrationsOf(c.req.param('id')),
|
|
@@ -4133,7 +4278,7 @@ export function createControlPlaneApi(options) {
|
|
|
4133
4278
|
if (await notReadable(p, slug)) {
|
|
4134
4279
|
return c.json({ error: 'not found' }, 404);
|
|
4135
4280
|
}
|
|
4136
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4281
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4137
4282
|
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
4138
4283
|
// #1232: freshness rides the same read — the manifest is already in hand, and a
|
|
4139
4284
|
// sibling route would cost a second full parse for a field sitting beside this one.
|
|
@@ -4156,7 +4301,7 @@ export function createControlPlaneApi(options) {
|
|
|
4156
4301
|
if (await notReadable(p, slug)) {
|
|
4157
4302
|
return c.json({ error: 'not found' }, 404);
|
|
4158
4303
|
}
|
|
4159
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4304
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4160
4305
|
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
4161
4306
|
return c.json({
|
|
4162
4307
|
declaredEvents: parsed?.declaredEvents ?? null,
|
|
@@ -4184,7 +4329,7 @@ export function createControlPlaneApi(options) {
|
|
|
4184
4329
|
if (await notReadable(p, slug)) {
|
|
4185
4330
|
return c.json({ error: 'not found' }, 404);
|
|
4186
4331
|
}
|
|
4187
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4332
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4188
4333
|
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
4189
4334
|
// #1321 rides this read for the reason the freshness field rides the schedules
|
|
4190
4335
|
// one: the manifest is already parsed, and the field sits beside the model the
|
|
@@ -4203,22 +4348,22 @@ export function createControlPlaneApi(options) {
|
|
|
4203
4348
|
if (await notReadable(p, slug)) {
|
|
4204
4349
|
return c.json({ error: 'not found' }, 404);
|
|
4205
4350
|
}
|
|
4206
|
-
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4351
|
+
const json = await c.var.admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
4207
4352
|
const assets = json ? (storedDeployManifest.parse(JSON.parse(json)).assets ?? null) : null;
|
|
4208
4353
|
return c.json({ assets });
|
|
4209
4354
|
});
|
|
4210
4355
|
app.post('/verticals/:slug/versions/:id/admit', async (c) => {
|
|
4211
4356
|
const slug = c.req.param('slug');
|
|
4212
4357
|
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));
|
|
4358
|
+
await c.var.admin.admitVersion(c.get('actor'), id);
|
|
4359
|
+
return c.json(await c.var.admin.getVersion(c.get('actor'), id, slug));
|
|
4215
4360
|
});
|
|
4216
4361
|
app.post('/verticals/:slug/versions/:id/reject', async (c) => {
|
|
4217
4362
|
const slug = c.req.param('slug');
|
|
4218
4363
|
const id = c.req.param('id');
|
|
4219
4364
|
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));
|
|
4365
|
+
await c.var.admin.rejectVersion(c.get('actor'), id, note);
|
|
4366
|
+
return c.json(await c.var.admin.getVersion(c.get('actor'), id, slug));
|
|
4222
4367
|
});
|
|
4223
4368
|
// A builder REQUESTS publication of a vertical it owns (marketplace-publish.md §5) — any owner
|
|
4224
4369
|
// may ask; the staff `listing` flip below is the review gate. Owner-checked (like promote).
|
|
@@ -4228,7 +4373,7 @@ export function createControlPlaneApi(options) {
|
|
|
4228
4373
|
if (await notOwned(p, slug)) {
|
|
4229
4374
|
return c.json({ error: 'not found' }, 404);
|
|
4230
4375
|
}
|
|
4231
|
-
await admin.requestPublish(c.get('actor'), slug);
|
|
4376
|
+
await c.var.admin.requestPublish(c.get('actor'), slug);
|
|
4232
4377
|
return c.json({ slug, requested: true });
|
|
4233
4378
|
});
|
|
4234
4379
|
// Publish/unpublish a vertical to the PUBLIC marketplace (marketplace-publish.md §5). Staff
|
|
@@ -4237,7 +4382,7 @@ export function createControlPlaneApi(options) {
|
|
|
4237
4382
|
app.post('/verticals/:slug/listing', async (c) => {
|
|
4238
4383
|
const slug = c.req.param('slug');
|
|
4239
4384
|
const { listed } = z.object({ listed: z.boolean() }).parse(await c.req.json());
|
|
4240
|
-
await admin.setVerticalListed(c.get('actor'), slug, listed);
|
|
4385
|
+
await c.var.admin.setVerticalListed(c.get('actor'), slug, listed);
|
|
4241
4386
|
return c.json({ slug, listed });
|
|
4242
4387
|
});
|
|
4243
4388
|
// The install kill-switch (staff-only — not in BUILDER_ROUTES, so a builder is
|
|
@@ -4246,7 +4391,7 @@ export function createControlPlaneApi(options) {
|
|
|
4246
4391
|
app.post('/verticals/:slug/install-block', async (c) => {
|
|
4247
4392
|
const slug = c.req.param('slug');
|
|
4248
4393
|
const { blocked } = z.object({ blocked: z.boolean() }).parse(await c.req.json());
|
|
4249
|
-
await admin.setVerticalInstallsBlocked(c.get('actor'), slug, blocked);
|
|
4394
|
+
await c.var.admin.setVerticalInstallsBlocked(c.get('actor'), slug, blocked);
|
|
4250
4395
|
return c.json({ slug, installsBlocked: blocked });
|
|
4251
4396
|
});
|
|
4252
4397
|
// Grant/revoke the TENANT-PROVISIONER capability (#412) — whether this vertical's scopes
|
|
@@ -4256,7 +4401,7 @@ export function createControlPlaneApi(options) {
|
|
|
4256
4401
|
app.post('/verticals/:slug/tenant-provisioner', async (c) => {
|
|
4257
4402
|
const slug = c.req.param('slug');
|
|
4258
4403
|
const { granted } = z.object({ granted: z.boolean() }).parse(await c.req.json());
|
|
4259
|
-
await admin.setVerticalTenantProvisioner(c.get('actor'), slug, granted);
|
|
4404
|
+
await c.var.admin.setVerticalTenantProvisioner(c.get('actor'), slug, granted);
|
|
4260
4405
|
return c.json({ slug, tenantProvisioner: granted });
|
|
4261
4406
|
});
|
|
4262
4407
|
// Grant/revoke the EMAIL-SENDER capability (#303) — whether this vertical's scopes may POST to
|
|
@@ -4266,7 +4411,7 @@ export function createControlPlaneApi(options) {
|
|
|
4266
4411
|
app.post('/verticals/:slug/email-sender', async (c) => {
|
|
4267
4412
|
const slug = c.req.param('slug');
|
|
4268
4413
|
const { granted } = z.object({ granted: z.boolean() }).parse(await c.req.json());
|
|
4269
|
-
await admin.setVerticalEmailSender(c.get('actor'), slug, granted);
|
|
4414
|
+
await c.var.admin.setVerticalEmailSender(c.get('actor'), slug, granted);
|
|
4270
4415
|
return c.json({ slug, emailSender: granted });
|
|
4271
4416
|
});
|
|
4272
4417
|
// Delete a vertical + its versions and channels. Refused below the seam while any
|
|
@@ -4283,7 +4428,7 @@ export function createControlPlaneApi(options) {
|
|
|
4283
4428
|
// existence-hiding reflex the registry's other per-slug reads use (K-3).
|
|
4284
4429
|
if (await notOwned(c.get('principal'), slug))
|
|
4285
4430
|
return c.json({ error: 'not found' }, 404);
|
|
4286
|
-
await admin.deleteVertical(c.get('actor'), slug);
|
|
4431
|
+
await c.var.admin.deleteVertical(c.get('actor'), slug);
|
|
4287
4432
|
return c.json({ slug, deleted: true });
|
|
4288
4433
|
});
|
|
4289
4434
|
app.get('/verticals/:slug/channels', async (c) => {
|
|
@@ -4293,7 +4438,7 @@ export function createControlPlaneApi(options) {
|
|
|
4293
4438
|
return c.json({ error: 'not found' }, 404);
|
|
4294
4439
|
}
|
|
4295
4440
|
const page = pageParams(c);
|
|
4296
|
-
const entries = await admin.listChannels(c.get('actor'), slug, page);
|
|
4441
|
+
const entries = await c.var.admin.listChannels(c.get('actor'), slug, page);
|
|
4297
4442
|
return c.json(pageOf(entries, page.limit, (ch) => ch.channel));
|
|
4298
4443
|
});
|
|
4299
4444
|
// The promotion timeline (newest first) — what a rollback UI picks a target from.
|
|
@@ -4311,7 +4456,7 @@ export function createControlPlaneApi(options) {
|
|
|
4311
4456
|
return c.json({ error: 'not found' }, 404);
|
|
4312
4457
|
}
|
|
4313
4458
|
const page = pageParams(c);
|
|
4314
|
-
const entries = await admin.listChannelHistory(c.get('actor'), slug, channel, page);
|
|
4459
|
+
const entries = await c.var.admin.listChannelHistory(c.get('actor'), slug, channel, page);
|
|
4315
4460
|
return c.json(pageOf(entries, page.limit, (e) => e.id));
|
|
4316
4461
|
});
|
|
4317
4462
|
/**
|
|
@@ -4326,20 +4471,20 @@ export function createControlPlaneApi(options) {
|
|
|
4326
4471
|
* the caller surfaces it — and leaves `servingVersionId` trailing the channel:
|
|
4327
4472
|
* visible, and retried by promoting again.
|
|
4328
4473
|
*/
|
|
4329
|
-
const serveVersionInPlace = async (actor, slug, versionId) => {
|
|
4474
|
+
const serveVersionInPlace = async (admin, actor, slug, versionId) => {
|
|
4330
4475
|
if (!options.deployVertical || !options.fetchVerticalModules)
|
|
4331
4476
|
return; // not configured: pre-#286 behavior
|
|
4332
|
-
const version = await admin.getVersion(actor, versionId, slug);
|
|
4477
|
+
const version = await host.admin.getVersion(actor, versionId, slug);
|
|
4333
4478
|
if (!version?.deploymentRef) {
|
|
4334
4479
|
throw new ControlPlaneError(502, `version ${versionId} has no archive script to serve from`);
|
|
4335
4480
|
}
|
|
4336
4481
|
const archiveRef = version.deploymentRef;
|
|
4337
|
-
const manifestJson = await admin.versionManifest(actor, slug, versionId);
|
|
4482
|
+
const manifestJson = await host.admin.versionManifest(actor, slug, versionId);
|
|
4338
4483
|
if (!manifestJson) {
|
|
4339
4484
|
throw new ControlPlaneError(502, `version ${versionId} retained no manifest — pushed pre-#286; push it again to serve in place`);
|
|
4340
4485
|
}
|
|
4341
4486
|
const manifest = storedDeployManifest.parse(JSON.parse(manifestJson));
|
|
4342
|
-
const serving = await admin.verticalServing(actor, slug);
|
|
4487
|
+
const serving = await host.admin.verticalServing(actor, slug);
|
|
4343
4488
|
const ref = serving?.ref ?? stableDeploymentRefFor(slug);
|
|
4344
4489
|
const modules = await options.fetchVerticalModules(version.deploymentRef);
|
|
4345
4490
|
// #301 PR-2: every serving upload re-derives the per-tenant store D1 bindings from
|
|
@@ -4349,11 +4494,11 @@ export function createControlPlaneApi(options) {
|
|
|
4349
4494
|
// tenant's store. Platform-granted after the §4 sandbox check, like injectSecrets:
|
|
4350
4495
|
// the builder never declared these and never named the ids.
|
|
4351
4496
|
const storeBindings = [
|
|
4352
|
-
...tenantStoreBindings(await admin.listTenantStores(actor, { vertical: slug })),
|
|
4497
|
+
...tenantStoreBindings(await host.admin.listTenantStores(actor, { vertical: slug })),
|
|
4353
4498
|
// #473: the per-tenant blob-store r2_bucket bindings, re-derived from the ledger on
|
|
4354
4499
|
// every serving upload for the same reason — an upload replaces the script's binding
|
|
4355
4500
|
// set, so a re-deploy must never drop a tenant's attachment bucket.
|
|
4356
|
-
...blobStoreBindings(await admin.listBlobStores(actor, { vertical: slug })),
|
|
4501
|
+
...blobStoreBindings(await host.admin.listBlobStores(actor, { vertical: slug })),
|
|
4357
4502
|
].map((b) => b.type === 'd1'
|
|
4358
4503
|
? { type: 'd1', name: b.name, id: b.id }
|
|
4359
4504
|
: { type: 'r2_bucket', name: b.name, bucket_name: b.bucketName });
|
|
@@ -4438,7 +4583,7 @@ export function createControlPlaneApi(options) {
|
|
|
4438
4583
|
try {
|
|
4439
4584
|
// Every tenant with an install that can still serve. Archived/reaped scopes are
|
|
4440
4585
|
// excluded — minting a bucket for storage that is gone is pure waste.
|
|
4441
|
-
const scopes = await admin.listScopes(actor, {
|
|
4586
|
+
const scopes = await c.var.admin.listScopes(actor, {
|
|
4442
4587
|
vertical: slug,
|
|
4443
4588
|
status: ['provisioning', 'active', 'suspended'],
|
|
4444
4589
|
});
|
|
@@ -4446,7 +4591,7 @@ export function createControlPlaneApi(options) {
|
|
|
4446
4591
|
if (tenantIds.length === 0)
|
|
4447
4592
|
return { minted: [] };
|
|
4448
4593
|
const minted = await backfillDeclaredStores({
|
|
4449
|
-
host:
|
|
4594
|
+
host: c.var.host,
|
|
4450
4595
|
actor,
|
|
4451
4596
|
slug,
|
|
4452
4597
|
tenantIds,
|
|
@@ -4474,22 +4619,22 @@ export function createControlPlaneApi(options) {
|
|
|
4474
4619
|
* costs a read, a missing one a silent stall.
|
|
4475
4620
|
*/
|
|
4476
4621
|
const promoteImpactOf = async (actor, slug, channel, versionId) => {
|
|
4477
|
-
const gate = await admin.promotionImpact(actor, slug, channel, versionId);
|
|
4622
|
+
const gate = await host.admin.promotionImpact(actor, slug, channel, versionId);
|
|
4478
4623
|
if (channel !== 'prod')
|
|
4479
4624
|
return gate;
|
|
4480
4625
|
const v = await verticalOf(actor, slug);
|
|
4481
4626
|
if (!v || v.ownerTenant === null || v.listed)
|
|
4482
4627
|
return gate;
|
|
4483
|
-
const incoming = await admin.getVersion(actor, versionId, slug);
|
|
4628
|
+
const incoming = await host.admin.getVersion(actor, versionId, slug);
|
|
4484
4629
|
if (!incoming?.deploymentRef || incoming.admission !== 'admitted')
|
|
4485
4630
|
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);
|
|
4631
|
+
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
4632
|
const seen = new Set(gate.map((b) => `${b.scopeId}|${b.type}`));
|
|
4488
4633
|
const out = [...gate];
|
|
4489
4634
|
for (const s of lagging) {
|
|
4490
4635
|
// After the adopt it runs the promoted version, served in place: that pointer move, from
|
|
4491
4636
|
// what it runs now, is the move to judge.
|
|
4492
|
-
for (const b of await admin.bindingImpact(actor, s.tenantId, s.id, versionId)) {
|
|
4637
|
+
for (const b of await host.admin.bindingImpact(actor, s.tenantId, s.id, versionId)) {
|
|
4493
4638
|
const key = `${b.scopeId}|${b.type}`;
|
|
4494
4639
|
if (!seen.has(key))
|
|
4495
4640
|
(seen.add(key), out.push(b));
|
|
@@ -4575,7 +4720,7 @@ export function createControlPlaneApi(options) {
|
|
|
4575
4720
|
// and refuses a non-admitted version. Both are enforced below the seam and
|
|
4576
4721
|
// surface as a 4xx through mapError, not a 500.
|
|
4577
4722
|
try {
|
|
4578
|
-
await admin.promoteVersion(c.get('actor'), slug, channel, versionId, acknowledge);
|
|
4723
|
+
await c.var.admin.promoteVersion(c.get('actor'), slug, channel, versionId, acknowledge);
|
|
4579
4724
|
}
|
|
4580
4725
|
catch (err) {
|
|
4581
4726
|
if (!isExportBreakRefusal(err))
|
|
@@ -4596,7 +4741,7 @@ export function createControlPlaneApi(options) {
|
|
|
4596
4741
|
let adopted = { refused: [] };
|
|
4597
4742
|
if (channel === 'prod') {
|
|
4598
4743
|
try {
|
|
4599
|
-
await serveVersionInPlace(c.get('actor'), slug, versionId);
|
|
4744
|
+
await serveVersionInPlace(c.var.admin, c.get('actor'), slug, versionId);
|
|
4600
4745
|
// Adopt any still-legacy owned scope onto the serving script and advance every
|
|
4601
4746
|
// owned scope's version — the rebind the host cascade delegated to us for a
|
|
4602
4747
|
// dispatch-backed vertical (#321), in the correct order (serve → adopt → rebind),
|
|
@@ -4643,7 +4788,7 @@ export function createControlPlaneApi(options) {
|
|
|
4643
4788
|
: {}),
|
|
4644
4789
|
}, 409);
|
|
4645
4790
|
}
|
|
4646
|
-
const promoted = (await admin.listChannels(c.get('actor'), slug)).find((ch) => ch.channel === channel);
|
|
4791
|
+
const promoted = (await c.var.admin.listChannels(c.get('actor'), slug)).find((ch) => ch.channel === channel);
|
|
4647
4792
|
// The store backfill rides the response only when it has something to say (#825), so
|
|
4648
4793
|
// nothing changes for the overwhelming majority of promotes — and when a store WAS
|
|
4649
4794
|
// minted, or could not be, the builder reads it at the terminal rather than finding out
|
|
@@ -4733,7 +4878,7 @@ export function createControlPlaneApi(options) {
|
|
|
4733
4878
|
ownerTenant = p.tenantId;
|
|
4734
4879
|
}
|
|
4735
4880
|
else if (pin) {
|
|
4736
|
-
const workspace = (await admin.listTenants(c.get('actor'))).find((t) => t.slug === pin || t.id === pin);
|
|
4881
|
+
const workspace = (await c.var.admin.listTenants(c.get('actor'))).find((t) => t.slug === pin || t.id === pin);
|
|
4737
4882
|
if (!workspace)
|
|
4738
4883
|
return c.json({ error: `unknown workspace '${pin}'` }, 404);
|
|
4739
4884
|
// Back-compat: a BARE slug already registered as the pinned tenant's (a staff
|
|
@@ -4772,7 +4917,7 @@ export function createControlPlaneApi(options) {
|
|
|
4772
4917
|
// scoping is also what keeps existence-hiding intact — a builder is never told about
|
|
4773
4918
|
// a foreign private slug.
|
|
4774
4919
|
if (form.get('allowFork') !== '1') {
|
|
4775
|
-
const registry = await admin.listVerticals(c.get('actor'));
|
|
4920
|
+
const registry = await c.var.admin.listVerticals(c.get('actor'));
|
|
4776
4921
|
const tail = slug.split('/').pop();
|
|
4777
4922
|
const siblings = registry.some((v) => v.slug === slug)
|
|
4778
4923
|
? []
|
|
@@ -4920,7 +5065,7 @@ export function createControlPlaneApi(options) {
|
|
|
4920
5065
|
// (or admitted, for a PRIVATE vertical — the registry's self-admit rule). The owner
|
|
4921
5066
|
// was resolved with the slug above: the builder's tenant, the pinned workspace, or
|
|
4922
5067
|
// the preserved existing owner for an unpinned staff push.
|
|
4923
|
-
await admin.registerVertical(c.get('actor'), {
|
|
5068
|
+
await c.var.admin.registerVertical(c.get('actor'), {
|
|
4924
5069
|
slug,
|
|
4925
5070
|
name: manifest.name ?? slug,
|
|
4926
5071
|
source: 'cli',
|
|
@@ -4946,7 +5091,7 @@ export function createControlPlaneApi(options) {
|
|
|
4946
5091
|
// hostname-binding picker, never behavior. Not part of any admission digest.
|
|
4947
5092
|
...(manifest.surfaces ? { surfaces: manifest.surfaces } : {}),
|
|
4948
5093
|
});
|
|
4949
|
-
await admin.publishVersion(c.get('actor'), {
|
|
5094
|
+
await c.var.admin.publishVersion(c.get('actor'), {
|
|
4950
5095
|
id,
|
|
4951
5096
|
verticalSlug: slug,
|
|
4952
5097
|
version: manifest.version,
|
|
@@ -4972,14 +5117,14 @@ export function createControlPlaneApi(options) {
|
|
|
4972
5117
|
// one push depend on every other tenant's routing rows parsing cleanly — a
|
|
4973
5118
|
// malformed cert-validation blob on an unrelated domain took the whole deploy down
|
|
4974
5119
|
// with a blank 500, after the version had already been published.
|
|
4975
|
-
const bound = await admin.listHostnames(c.get('actor'), { verticalSlug: slug });
|
|
5120
|
+
const bound = await c.var.admin.listHostnames(c.get('actor'), { verticalSlug: slug });
|
|
4976
5121
|
for (const h of bound) {
|
|
4977
5122
|
if (!declared.has(h.surface)) {
|
|
4978
5123
|
warnings.push(`hostname '${h.hostname}' is bound to surface '${h.surface}', which this version no longer declares`);
|
|
4979
5124
|
}
|
|
4980
5125
|
}
|
|
4981
5126
|
}
|
|
4982
|
-
const version = await admin.getVersion(c.get('actor'), id, slug);
|
|
5127
|
+
const version = await c.var.admin.getVersion(c.get('actor'), id, slug);
|
|
4983
5128
|
return c.json({ ...version, ...(warnings.length ? { warnings } : {}) }, 201);
|
|
4984
5129
|
});
|
|
4985
5130
|
// -- where this platform runs (ops ergonomics) -----------------------------
|
|
@@ -5258,7 +5403,7 @@ export function createControlPlaneApi(options) {
|
|
|
5258
5403
|
* nothing. `listScopes`, not `getScopeRecord`: a dashboard read must not write an access
|
|
5259
5404
|
* row per page view.
|
|
5260
5405
|
*/
|
|
5261
|
-
const appServices = async (actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
|
|
5406
|
+
const appServices = async (actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await host.admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
|
|
5262
5407
|
app.get('/observability/tenant-request-volume', async (c) => {
|
|
5263
5408
|
if (!options.observability?.tenantRequestVolume) {
|
|
5264
5409
|
return c.json({ error: 'request aggregation is not configured on this control plane' }, 501);
|
|
@@ -5359,7 +5504,7 @@ export function createControlPlaneApi(options) {
|
|
|
5359
5504
|
.parse({ hours: c.req.query('hours'), limit: c.req.query('limit') });
|
|
5360
5505
|
// Deployed versions only: a version with no `deploymentRef` has never run, so there is
|
|
5361
5506
|
// nothing it could have been observed reaching.
|
|
5362
|
-
const versions = (await admin.listVersions(c.get('actor'), slug)).filter((v) => v.deploymentRef);
|
|
5507
|
+
const versions = (await c.var.admin.listVersions(c.get('actor'), slug)).filter((v) => v.deploymentRef);
|
|
5363
5508
|
// Cap the fan-out — each service is another backend query. Truncating the SERVICE list
|
|
5364
5509
|
// is exactly the silent-drop #859 calls a defect, so it is reported, not swallowed.
|
|
5365
5510
|
const MAX_SERVICES = 20;
|
|
@@ -5422,7 +5567,7 @@ export function createControlPlaneApi(options) {
|
|
|
5422
5567
|
return c.json({ error: 'push tokens are not configured on this control plane' }, 501);
|
|
5423
5568
|
}
|
|
5424
5569
|
const { tenantId } = z.object({ tenantId: tenantIdSchema }).parse(await c.req.json());
|
|
5425
|
-
const tenant = await admin.getTenant(c.get('actor'), tenantId);
|
|
5570
|
+
const tenant = await c.var.admin.getTenant(c.get('actor'), tenantId);
|
|
5426
5571
|
if (!tenant)
|
|
5427
5572
|
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
5428
5573
|
const token = await mintPushToken(options.pushTokenSecret, {
|
|
@@ -5443,12 +5588,22 @@ export function createControlPlaneApi(options) {
|
|
|
5443
5588
|
if (!options.tenantTokenSecret) {
|
|
5444
5589
|
return c.json({ error: 'tenant tokens are not configured on this control plane' }, 501);
|
|
5445
5590
|
}
|
|
5446
|
-
const { tenantId } = z
|
|
5591
|
+
const { tenantId, principal, impersonation } = z
|
|
5592
|
+
.object({
|
|
5593
|
+
tenantId: tenantIdSchema,
|
|
5594
|
+
// #977: the signed-in person the token will act for — attribution on every admin
|
|
5595
|
+
// row it writes, never reach. Optional: a token for background work names nobody.
|
|
5596
|
+
principal: principalIdSchema.optional(),
|
|
5597
|
+
// K-42: the staff member acting as `principal`. Meaningless without the person.
|
|
5598
|
+
impersonation: impersonationStamp.optional(),
|
|
5599
|
+
})
|
|
5600
|
+
.refine((b) => !b.impersonation || b.principal, { message: 'impersonation names no principal' })
|
|
5601
|
+
.parse(await c.req.json());
|
|
5447
5602
|
// Unlike the push-token mint, this does NOT require the tenant to exist: a token
|
|
5448
5603
|
// for a tenant with no directory row reaches nothing except creating that row
|
|
5449
5604
|
// (`POST /tenants`, body-pinned to the same id), which is the sign-up bootstrap.
|
|
5450
5605
|
// See the mint's docblock in tenant-token.ts.
|
|
5451
|
-
const token = await mintTenantToken(options.tenantTokenSecret, { tenantId });
|
|
5606
|
+
const token = await mintTenantToken(options.tenantTokenSecret, { tenantId, principal, impersonation });
|
|
5452
5607
|
return c.json({ token }, 201);
|
|
5453
5608
|
});
|
|
5454
5609
|
// -- the hostname map (§4.7, K-26) -----------------------------------------
|
|
@@ -5466,7 +5621,7 @@ export function createControlPlaneApi(options) {
|
|
|
5466
5621
|
const p = c.get('principal');
|
|
5467
5622
|
const pin = confinedTenant(p);
|
|
5468
5623
|
const filter = pin ? { tenantId: pin } : {};
|
|
5469
|
-
return (await admin.listHostnames(c.get('actor'), filter)).find((h) => h.hostname === name.toLowerCase());
|
|
5624
|
+
return (await c.var.admin.listHostnames(c.get('actor'), filter)).find((h) => h.hostname === name.toLowerCase());
|
|
5470
5625
|
};
|
|
5471
5626
|
app.get('/hostnames', async (c) => {
|
|
5472
5627
|
const p = c.get('principal');
|
|
@@ -5483,7 +5638,7 @@ export function createControlPlaneApi(options) {
|
|
|
5483
5638
|
if (hostnamePin)
|
|
5484
5639
|
filter.tenantId = hostnamePin;
|
|
5485
5640
|
const page = pageParams(c);
|
|
5486
|
-
const entries = await admin.listHostnames(c.get('actor'), { ...filter, ...page });
|
|
5641
|
+
const entries = await c.var.admin.listHostnames(c.get('actor'), { ...filter, ...page });
|
|
5487
5642
|
return c.json(pageOf(entries, page.limit, (h) => h.hostname));
|
|
5488
5643
|
});
|
|
5489
5644
|
// Which base domains ride the platform wildcard cert (go straight to `active`);
|
|
@@ -5501,7 +5656,7 @@ export function createControlPlaneApi(options) {
|
|
|
5501
5656
|
* Failures are swallowed to a `failed`/`pending` note rather than thrown: a bind that
|
|
5502
5657
|
* recorded the row must still return 201, and the reconcile sweep retries.
|
|
5503
5658
|
*/
|
|
5504
|
-
const runIssuance = async (actor, row) => {
|
|
5659
|
+
const runIssuance = async (admin, actor, row) => {
|
|
5505
5660
|
if (!custom(row.hostname)) {
|
|
5506
5661
|
// Platform mint: rides *.<base> — no per-hostname CF object, immediately servable.
|
|
5507
5662
|
// A mint carrying issuance relics (a CF id, publish-these-records rows) was born
|
|
@@ -5549,7 +5704,7 @@ export function createControlPlaneApi(options) {
|
|
|
5549
5704
|
});
|
|
5550
5705
|
}
|
|
5551
5706
|
};
|
|
5552
|
-
const hostnameRow = async (c, name) => (await admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5707
|
+
const hostnameRow = async (c, name) => (await c.var.admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5553
5708
|
app.post('/hostnames', async (c) => {
|
|
5554
5709
|
const p = c.get('principal');
|
|
5555
5710
|
const input = bindHostnameBody.parse(await c.req.json());
|
|
@@ -5578,13 +5733,13 @@ export function createControlPlaneApi(options) {
|
|
|
5578
5733
|
if (bad)
|
|
5579
5734
|
return c.json({ error: bad }, 422);
|
|
5580
5735
|
}
|
|
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);
|
|
5736
|
+
await c.var.admin.bindHostname(c.get('actor'), input);
|
|
5737
|
+
const bound = (await c.var.admin.listHostnames(c.get('actor'), { scopeId: input.scopeId })).find((h) => h.hostname === input.hostname);
|
|
5583
5738
|
if (bound)
|
|
5584
|
-
await runIssuance(c.get('actor'), bound);
|
|
5739
|
+
await runIssuance(c.var.admin, c.get('actor'), bound);
|
|
5585
5740
|
// Re-read so the response carries the post-issuance status + DNS records the caller
|
|
5586
5741
|
// (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);
|
|
5742
|
+
const out = (await c.var.admin.listHostnames(c.get('actor'), { scopeId: input.scopeId })).find((h) => h.hostname === input.hostname);
|
|
5588
5743
|
return c.json(out ?? bound, 201);
|
|
5589
5744
|
});
|
|
5590
5745
|
// Re-poll a custom hostname's issuance ("check again" in the dashboard). Tenant-narrowed
|
|
@@ -5598,7 +5753,7 @@ export function createControlPlaneApi(options) {
|
|
|
5598
5753
|
const row = await hostnameRow(c, name);
|
|
5599
5754
|
if (!row)
|
|
5600
5755
|
return c.json({ error: `unknown hostname: ${name.toLowerCase()}` }, 404);
|
|
5601
|
-
await runIssuance(c.get('actor'), row);
|
|
5756
|
+
await runIssuance(c.var.admin, c.get('actor'), row);
|
|
5602
5757
|
return c.json(await hostnameRow(c, name));
|
|
5603
5758
|
});
|
|
5604
5759
|
app.patch('/hostnames/:hostname/status', async (c) => {
|
|
@@ -5609,8 +5764,8 @@ export function createControlPlaneApi(options) {
|
|
|
5609
5764
|
if (confinedTenant(c.get('principal')) !== null && !(await tenantHostname(c, name))) {
|
|
5610
5765
|
return c.json({ error: `unknown hostname: ${name.toLowerCase()}` }, 404);
|
|
5611
5766
|
}
|
|
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());
|
|
5767
|
+
await c.var.admin.setHostnameStatus(c.get('actor'), name, status, note);
|
|
5768
|
+
const row = (await c.var.admin.listHostnames(c.get('actor'), {})).find((h) => h.hostname === name.toLowerCase());
|
|
5614
5769
|
return c.json(row);
|
|
5615
5770
|
});
|
|
5616
5771
|
// Unbind (hard-delete) a hostname row — what the orphan cleanup uses on rows
|
|
@@ -5631,7 +5786,7 @@ export function createControlPlaneApi(options) {
|
|
|
5631
5786
|
if (row?.customHostnameId && options.provisionHostname) {
|
|
5632
5787
|
await options.provisionHostname.remove(row.customHostnameId).catch(() => { });
|
|
5633
5788
|
}
|
|
5634
|
-
await admin.unbindHostname(c.get('actor'), name);
|
|
5789
|
+
await c.var.admin.unbindHostname(c.get('actor'), name);
|
|
5635
5790
|
return c.json({ deleted: name.toLowerCase() });
|
|
5636
5791
|
});
|
|
5637
5792
|
// -- per-PR previews (preview-and-snapshots.md §2/§9) ----------------------
|
|
@@ -5676,24 +5831,24 @@ export function createControlPlaneApi(options) {
|
|
|
5676
5831
|
await retireClientsOfReapedScope(previewAuthDeps(c), preview);
|
|
5677
5832
|
const vertical = await verticalForScope(c, preview);
|
|
5678
5833
|
if (vertical)
|
|
5679
|
-
await vertical.deleteScope({ scopeId: preview.id });
|
|
5680
|
-
await
|
|
5834
|
+
await vertical.deleteScope({ tenantId: preview.tenantId, scopeId: preview.id });
|
|
5835
|
+
await c.var.host.deleteSnapshot(c.get('actor'), preview.tenantId, preview.id);
|
|
5681
5836
|
};
|
|
5682
5837
|
/** The preview-login seam (#1704, `preview-auth.ts`): the directory, and each issuer's deployment. */
|
|
5683
5838
|
const previewAuthDeps = (c) => ({
|
|
5684
|
-
admin,
|
|
5839
|
+
admin: c.var.admin,
|
|
5685
5840
|
actor: c.get('actor'),
|
|
5686
5841
|
issuerClient: (scope) => verticalForScope(c, scope),
|
|
5687
5842
|
});
|
|
5688
5843
|
/** Given a base hostname `<label>.<domain>`, mint (or find) the preview's `--<tag>`
|
|
5689
5844
|
* hostname `<label>--<tag>.<domain>` bound to `previewId`. Non-canonical, so it never
|
|
5690
5845
|
* demotes the prod surface. Shared by the fork and clean-room paths. */
|
|
5691
|
-
const bindPreviewHostname = async (actor, baseHostname, tenantId, previewId, tag, surface) => {
|
|
5846
|
+
const bindPreviewHostname = async (admin, actor, baseHostname, tenantId, previewId, tag, surface) => {
|
|
5692
5847
|
const parsed = parseHostname(baseHostname);
|
|
5693
5848
|
if (!parsed)
|
|
5694
5849
|
throw new ControlPlaneError(400, `'${baseHostname}' is not a hostname a preview can be minted beside`);
|
|
5695
5850
|
const hostname = withLabel(parsed, `${parsed.label}${RESERVED_LABEL_SEPARATOR}${tag}`);
|
|
5696
|
-
const existing = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5851
|
+
const existing = (await host.admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5697
5852
|
if (!existing) {
|
|
5698
5853
|
await admin.bindHostname(actor, {
|
|
5699
5854
|
hostname,
|
|
@@ -5704,9 +5859,9 @@ export function createControlPlaneApi(options) {
|
|
|
5704
5859
|
canonical: false,
|
|
5705
5860
|
});
|
|
5706
5861
|
}
|
|
5707
|
-
const bound = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5862
|
+
const bound = (await host.admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
5708
5863
|
if (bound)
|
|
5709
|
-
await runIssuance(actor, bound); // platform mint rides the wildcard cert → active
|
|
5864
|
+
await runIssuance(admin, actor, bound); // platform mint rides the wildcard cert → active
|
|
5710
5865
|
return hostname;
|
|
5711
5866
|
};
|
|
5712
5867
|
/** The base hostname a preview's `--<tag>` URL is derived from. A FORK inherits the
|
|
@@ -5715,7 +5870,7 @@ export function createControlPlaneApi(options) {
|
|
|
5715
5870
|
* the same scheme provisioning mints (`callout-sesamy.global.substrat.run`). */
|
|
5716
5871
|
const previewBaseHostname = async (actor, source, tenantId, slug, surface) => {
|
|
5717
5872
|
if (source) {
|
|
5718
|
-
const own = await admin.listHostnames(actor, { scopeId: source.id });
|
|
5873
|
+
const own = await host.admin.listHostnames(actor, { scopeId: source.id });
|
|
5719
5874
|
const src = own.find((h) => h.canonical && h.surface === surface) ??
|
|
5720
5875
|
own.find((h) => h.canonical) ??
|
|
5721
5876
|
own[0];
|
|
@@ -5737,7 +5892,7 @@ export function createControlPlaneApi(options) {
|
|
|
5737
5892
|
// bare apex and stranded clean-room previews on a hostname that never resolves.
|
|
5738
5893
|
const baseDomain = [...platformBaseDomains].sort((a, b) => a.length - b.length)[0];
|
|
5739
5894
|
const jurisdiction = 'global';
|
|
5740
|
-
const tenant = await admin.getTenant(actor, tenantId);
|
|
5895
|
+
const tenant = await host.admin.getTenant(actor, tenantId);
|
|
5741
5896
|
const handle = tenant?.slug ?? tenantId;
|
|
5742
5897
|
return `${slug.split('/').at(-1)}-${handle}.${jurisdiction}.${baseDomain}`;
|
|
5743
5898
|
};
|
|
@@ -5753,12 +5908,12 @@ export function createControlPlaneApi(options) {
|
|
|
5753
5908
|
// prod build) while we report success. That is worse than a failure: it sends a reviewer
|
|
5754
5909
|
// to redo correct work. So refuse to return success for a URL that would serve other code.
|
|
5755
5910
|
const assertServesBoundVersion = async (previewId) => {
|
|
5756
|
-
const bound = await admin.getVersion(actor, opts.versionId, slug);
|
|
5911
|
+
const bound = await c.var.admin.getVersion(actor, opts.versionId, slug);
|
|
5757
5912
|
// A co-located / embedded vertical has no per-version dispatch script (deploymentRef
|
|
5758
5913
|
// null) and routes via the static fallback — nothing to compare, so nothing to guard.
|
|
5759
5914
|
if (!bound?.deploymentRef)
|
|
5760
5915
|
return;
|
|
5761
|
-
const rec = await admin.getScopeRecord(actor, tenantId, previewId);
|
|
5916
|
+
const rec = await c.var.admin.getScopeRecord(actor, tenantId, previewId);
|
|
5762
5917
|
const effectiveRef = rec?.servingRef ?? bound.deploymentRef;
|
|
5763
5918
|
if (effectiveRef !== bound.deploymentRef) {
|
|
5764
5919
|
throw new ControlPlaneError(500, `preview ${previewId} bound version ${opts.versionId} but would route to '${effectiveRef}' ` +
|
|
@@ -5784,7 +5939,7 @@ export function createControlPlaneApi(options) {
|
|
|
5784
5939
|
// every push is its own script, so the rebind first CARRIES the preview's data into
|
|
5785
5940
|
// the new version's script (#1710). The preview's data then follows it from push to
|
|
5786
5941
|
// 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));
|
|
5942
|
+
const existing = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, opts.tag));
|
|
5788
5943
|
// A preview only HAS data once its two-phase create finished: the directory row lands
|
|
5789
5944
|
// first as `provisioning` (K-31), the fork's export→restore runs, and `activateScope`
|
|
5790
5945
|
// is the last step. So a row still at `provisioning` is a create that DIED mid-fork —
|
|
@@ -5898,20 +6053,20 @@ export function createControlPlaneApi(options) {
|
|
|
5898
6053
|
// Heal a preview provisioned before #527: clear any inherited serving_ref so routing
|
|
5899
6054
|
// follows the bound version (its per-version script), not the prod serving script.
|
|
5900
6055
|
if (existing.servingRef)
|
|
5901
|
-
await admin.setScopeServingRef(actor, tenantId, existing.id, null);
|
|
5902
|
-
await admin.bindScopeVersion(actor, tenantId, existing.id, opts.versionId);
|
|
6056
|
+
await c.var.admin.setScopeServingRef(actor, tenantId, existing.id, null);
|
|
6057
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, existing.id, opts.versionId);
|
|
5903
6058
|
// #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 });
|
|
6059
|
+
await c.var.admin.reassertSystemSwitches(actor, { tenantId, scopeId: existing.id }, { appliedInUnit: carried?.switchedOff });
|
|
5905
6060
|
// 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);
|
|
6061
|
+
await c.var.admin.setScopeExpiresAt(actor, tenantId, existing.id, expiresAt);
|
|
6062
|
+
const hostname = await bindPreviewHostname(c.var.admin, actor, baseHostname, tenantId, existing.id, opts.tag, surface);
|
|
5908
6063
|
await assertServesBoundVersion(existing.id);
|
|
5909
6064
|
// The fork it was made from, not whichever prod scope this call picked: a reuse is the
|
|
5910
6065
|
// same preview. Every push is a new version with an EMPTY config store, and the carry
|
|
5911
6066
|
// above moves the scope's data but not the vertical's config store, so the login is
|
|
5912
6067
|
// wired again on every reuse, never assumed to have survived.
|
|
5913
6068
|
const parent = existing.forkedFrom
|
|
5914
|
-
? ((await admin.getScopeRecord(actor, tenantId, existing.forkedFrom)) ?? null)
|
|
6069
|
+
? ((await c.var.admin.getScopeRecord(actor, tenantId, existing.forkedFrom)) ?? null)
|
|
5915
6070
|
: null;
|
|
5916
6071
|
const auth = await wireLogin(existing.id, hostname, parent);
|
|
5917
6072
|
return {
|
|
@@ -5981,7 +6136,7 @@ export function createControlPlaneApi(options) {
|
|
|
5981
6136
|
if (!target) {
|
|
5982
6137
|
throw new ControlPlaneError(501, 'preview needs dispatch resolution for the PR version');
|
|
5983
6138
|
}
|
|
5984
|
-
const canonical = await admin.exportScope(actor, tenantId, source.id);
|
|
6139
|
+
const canonical = await c.var.admin.exportScope(actor, tenantId, source.id);
|
|
5985
6140
|
const tables = sourceClient ? await sourceClient.exportScope(source.id) : canonical.tables;
|
|
5986
6141
|
// Directory row FIRST as `provisioning` (K-31 two-phase): a crash before the data
|
|
5987
6142
|
// copy leaves an inert row that — carrying `forkedFrom` + `expiresAt` — the GC sweep
|
|
@@ -6028,10 +6183,10 @@ export function createControlPlaneApi(options) {
|
|
|
6028
6183
|
// seats, so a provision that throws can leave a `provisioning` row too. A provision
|
|
6029
6184
|
// that never wrote one (the tag's slug taken by a racing create) marks nothing: the
|
|
6030
6185
|
// write below targets this create's own id, which does not exist, and is swallowed.
|
|
6031
|
-
await
|
|
6186
|
+
await c.var.host.provisionScope(actor, row);
|
|
6032
6187
|
if (restore)
|
|
6033
6188
|
await restoreOrRecord(restore);
|
|
6034
|
-
await admin.activateScope(actor, tenantId, previewId);
|
|
6189
|
+
await c.var.admin.activateScope(actor, tenantId, previewId);
|
|
6035
6190
|
}
|
|
6036
6191
|
catch (e) {
|
|
6037
6192
|
// Expire the row this create left behind, so the next create reaps it at once rather
|
|
@@ -6039,7 +6194,7 @@ export function createControlPlaneApi(options) {
|
|
|
6039
6194
|
// CLI's retry after a known-dead attempt (#1918). The row stays until then, or until
|
|
6040
6195
|
// the GC sweep's next pass reaps it as expired; the #559 ops-failure row outlives both.
|
|
6041
6196
|
// Best-effort: if this write fails too, the age bound still frees the tag.
|
|
6042
|
-
await admin
|
|
6197
|
+
await c.var.admin
|
|
6043
6198
|
.setScopeExpiresAt(actor, tenantId, previewId, new Date().toISOString())
|
|
6044
6199
|
.catch(() => undefined);
|
|
6045
6200
|
throw e;
|
|
@@ -6047,8 +6202,8 @@ export function createControlPlaneApi(options) {
|
|
|
6047
6202
|
// Bind the PR version. A private vertical's push self-admitted, so this is accepted; a
|
|
6048
6203
|
// preview scope also admits a pending version (#513), which is what a clean-room rehearsal
|
|
6049
6204
|
// 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);
|
|
6205
|
+
await c.var.admin.bindScopeVersion(actor, tenantId, previewId, opts.versionId);
|
|
6206
|
+
const hostname = await bindPreviewHostname(c.var.admin, actor, baseHostname, tenantId, previewId, opts.tag, surface);
|
|
6052
6207
|
await assertServesBoundVersion(previewId);
|
|
6053
6208
|
const auth = await wireLogin(previewId, hostname, source);
|
|
6054
6209
|
return {
|
|
@@ -6095,7 +6250,7 @@ export function createControlPlaneApi(options) {
|
|
|
6095
6250
|
// Everything else forks the tenant's own active, non-fork scope for this vertical.
|
|
6096
6251
|
let source = null;
|
|
6097
6252
|
if (!body.empty) {
|
|
6098
|
-
const owned = (await admin.listScopes(actor, { tenantId, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
6253
|
+
const owned = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug, status: ['active'] })).filter((s) => !s.forkedFrom && s.kind !== 'preview');
|
|
6099
6254
|
source = body.sourceScopeId
|
|
6100
6255
|
? (owned.find((s) => s.id === body.sourceScopeId) ?? null)
|
|
6101
6256
|
: owned.length === 1
|
|
@@ -6127,12 +6282,12 @@ export function createControlPlaneApi(options) {
|
|
|
6127
6282
|
return c.json({ error: gate.error }, gate.status);
|
|
6128
6283
|
const { tenantId, slug } = gate;
|
|
6129
6284
|
const actor = c.get('actor');
|
|
6130
|
-
const previews = (await admin.listScopes(actor, { tenantId, vertical: slug }))
|
|
6285
|
+
const previews = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug }))
|
|
6131
6286
|
.filter((s) => s.kind === 'preview')
|
|
6132
6287
|
.sort((a, b) => ((a.forkedAt ?? '') < (b.forkedAt ?? '') ? 1 : -1));
|
|
6133
6288
|
const out = [];
|
|
6134
6289
|
for (const s of previews) {
|
|
6135
|
-
const hosts = await admin.listHostnames(actor, { scopeId: s.id });
|
|
6290
|
+
const hosts = await c.var.admin.listHostnames(actor, { scopeId: s.id });
|
|
6136
6291
|
out.push({
|
|
6137
6292
|
scopeId: s.id,
|
|
6138
6293
|
tag: s.slug?.split('--').at(-1) ?? null,
|
|
@@ -6154,7 +6309,7 @@ export function createControlPlaneApi(options) {
|
|
|
6154
6309
|
const { tenantId, slug } = gate;
|
|
6155
6310
|
const tag = c.req.param('tag');
|
|
6156
6311
|
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));
|
|
6312
|
+
const preview = (await c.var.admin.listScopes(actor, { tenantId, vertical: slug })).find((s) => s.kind === 'preview' && s.slug === previewSlug(slug, tag));
|
|
6158
6313
|
// Idempotent: an already-reaped (or never-created) preview is a no-op success, so a
|
|
6159
6314
|
// PR-close job never fails because the preview was already gone.
|
|
6160
6315
|
if (!preview)
|
|
@@ -6183,7 +6338,7 @@ export function createControlPlaneApi(options) {
|
|
|
6183
6338
|
source: c.req.query('source'),
|
|
6184
6339
|
});
|
|
6185
6340
|
const page = pageParams(c);
|
|
6186
|
-
const entries = await admin.listRoles(c.get('actor'), { ...filter, ...page });
|
|
6341
|
+
const entries = await c.var.admin.listRoles(c.get('actor'), { ...filter, ...page });
|
|
6187
6342
|
// Composite sort key (tenant_id, role_key) — the `|` join is the documented
|
|
6188
6343
|
// cursor shape (scope-host.ts listRoles).
|
|
6189
6344
|
return c.json(pageOf(entries, page.limit, (r) => `${r.tenantId}|${r.key}`));
|
|
@@ -6209,7 +6364,7 @@ export function createControlPlaneApi(options) {
|
|
|
6209
6364
|
// The whole node comes from the path. Pinning only the tenant would pin
|
|
6210
6365
|
// nothing — see the schema's note: a node carrying a scope is written to that
|
|
6211
6366
|
// scope's DO directly, with no tenant cross-check below.
|
|
6212
|
-
await admin.assignRole(c.get('actor'), {
|
|
6367
|
+
await c.var.admin.assignRole(c.get('actor'), {
|
|
6213
6368
|
principalId: body.principalId,
|
|
6214
6369
|
roleKey: body.roleKey,
|
|
6215
6370
|
node: { tenantId, scopeId: null },
|
|
@@ -6222,7 +6377,7 @@ export function createControlPlaneApi(options) {
|
|
|
6222
6377
|
// Tombstones rather than deletes (K-21) and is idempotent — unassigning what
|
|
6223
6378
|
// was never assigned is a silent no-op, so a retry is safe. Node from the
|
|
6224
6379
|
// path, for the same reason as the assign above.
|
|
6225
|
-
await admin.unassignRole(c.get('actor'), {
|
|
6380
|
+
await c.var.admin.unassignRole(c.get('actor'), {
|
|
6226
6381
|
principalId: body.principalId,
|
|
6227
6382
|
roleKey: body.roleKey,
|
|
6228
6383
|
node: { tenantId, scopeId: null },
|
|
@@ -6262,11 +6417,11 @@ export function createControlPlaneApi(options) {
|
|
|
6262
6417
|
const body = systemSwitchBody.parse(await c.req.json());
|
|
6263
6418
|
const actor = c.get('actor');
|
|
6264
6419
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6265
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6420
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6266
6421
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6267
6422
|
}
|
|
6268
6423
|
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));
|
|
6424
|
+
return c.json(to === 'off' ? await c.var.admin.revokeFromSystem(actor, input) : await c.var.admin.restoreToSystem(actor, input));
|
|
6270
6425
|
};
|
|
6271
6426
|
app.delete('/tenants/:tenantId/scopes/:scopeId/system-grants', switchScheduleRoute('off'));
|
|
6272
6427
|
app.post('/tenants/:tenantId/scopes/:scopeId/system-grants', switchScheduleRoute('on'));
|
|
@@ -6283,10 +6438,10 @@ export function createControlPlaneApi(options) {
|
|
|
6283
6438
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
6284
6439
|
const actor = c.get('actor');
|
|
6285
6440
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6286
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6441
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6287
6442
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6288
6443
|
}
|
|
6289
|
-
return c.json(await admin.systemGrantsStatus(actor, { tenantId, scopeId }));
|
|
6444
|
+
return c.json(await c.var.admin.systemGrantsStatus(actor, { tenantId, scopeId }));
|
|
6290
6445
|
});
|
|
6291
6446
|
// The fleet read (#1674): every scope with a module switched off, from the directory's
|
|
6292
6447
|
// record of the switch, so it needs no walk of every scope's store. The record is not the
|
|
@@ -6312,7 +6467,7 @@ export function createControlPlaneApi(options) {
|
|
|
6312
6467
|
order: c.req.query('order'),
|
|
6313
6468
|
});
|
|
6314
6469
|
const { position, ...rest } = q;
|
|
6315
|
-
const entries = await admin.listSystemSwitches(c.get('actor'), {
|
|
6470
|
+
const entries = await c.var.admin.listSystemSwitches(c.get('actor'), {
|
|
6316
6471
|
...rest,
|
|
6317
6472
|
...(position === 'all' ? {} : { position }),
|
|
6318
6473
|
});
|
|
@@ -6341,11 +6496,11 @@ export function createControlPlaneApi(options) {
|
|
|
6341
6496
|
const body = peerSwitchBody.parse(await c.req.json());
|
|
6342
6497
|
const actor = c.get('actor');
|
|
6343
6498
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6344
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6499
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6345
6500
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6346
6501
|
}
|
|
6347
6502
|
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));
|
|
6503
|
+
return c.json(to === 'off' ? await c.var.admin.revokeFromPeer(actor, input) : await c.var.admin.restoreToPeer(actor, input));
|
|
6349
6504
|
};
|
|
6350
6505
|
app.delete('/tenants/:tenantId/scopes/:scopeId/peer-grants', switchPeerRoute('off'));
|
|
6351
6506
|
app.post('/tenants/:tenantId/scopes/:scopeId/peer-grants', switchPeerRoute('on'));
|
|
@@ -6359,22 +6514,22 @@ export function createControlPlaneApi(options) {
|
|
|
6359
6514
|
if (pin !== null && pin !== tenantId)
|
|
6360
6515
|
return c.json({ error: 'forbidden' }, 403);
|
|
6361
6516
|
const actor = c.get('actor');
|
|
6362
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6517
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6363
6518
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6364
6519
|
}
|
|
6365
|
-
return c.json(await admin.peerGrantsStatus(actor, { tenantId, scopeId }));
|
|
6520
|
+
return c.json(await c.var.admin.peerGrantsStatus(actor, { tenantId, scopeId }));
|
|
6366
6521
|
});
|
|
6367
6522
|
// Orgs: the portal-customer grouping (§4.1). Creating one mints no permission —
|
|
6368
6523
|
// members reach what the org was GRANTED, and granting stays off this surface.
|
|
6369
6524
|
app.post('/tenants/:tenantId/orgs', async (c) => {
|
|
6370
6525
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
6371
6526
|
const body = tenantOrgBody.parse(await c.req.json());
|
|
6372
|
-
await admin.createOrg(c.get('actor'), { ...body, tenantId });
|
|
6527
|
+
await c.var.admin.createOrg(c.get('actor'), { ...body, tenantId });
|
|
6373
6528
|
return c.body(null, 201);
|
|
6374
6529
|
});
|
|
6375
6530
|
app.get('/tenants/:tenantId/orgs', async (c) => {
|
|
6376
6531
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
6377
|
-
return c.json(await admin.listOrgs(c.get('actor'), tenantId));
|
|
6532
|
+
return c.json(await c.var.admin.listOrgs(c.get('actor'), tenantId));
|
|
6378
6533
|
});
|
|
6379
6534
|
// -- the admin log (§4.4/§4.5) ---------------------------------------------
|
|
6380
6535
|
app.get('/admin-log', async (c) => {
|
|
@@ -6389,7 +6544,7 @@ export function createControlPlaneApi(options) {
|
|
|
6389
6544
|
cursor: c.req.query('cursor'),
|
|
6390
6545
|
order: c.req.query('order'),
|
|
6391
6546
|
});
|
|
6392
|
-
const entries = await admin.auditLog(c.get('actor'), filter);
|
|
6547
|
+
const entries = await c.var.admin.auditLog(c.get('actor'), filter);
|
|
6393
6548
|
// The cursor IS the last entry's id (ULID order is chronological), so the
|
|
6394
6549
|
// page carries its own continuation and the console never assembles one.
|
|
6395
6550
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
@@ -6415,7 +6570,7 @@ export function createControlPlaneApi(options) {
|
|
|
6415
6570
|
cursor: c.req.query('cursor'),
|
|
6416
6571
|
order: c.req.query('order'),
|
|
6417
6572
|
});
|
|
6418
|
-
const entries = await admin.listOpsFailures(c.get('actor'), filter);
|
|
6573
|
+
const entries = await c.var.admin.listOpsFailures(c.get('actor'), filter);
|
|
6419
6574
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
6420
6575
|
});
|
|
6421
6576
|
// -- sweep runs (#1232): the durable record of what a sweep pass touched -------
|
|
@@ -6437,7 +6592,7 @@ export function createControlPlaneApi(options) {
|
|
|
6437
6592
|
cursor: c.req.query('cursor'),
|
|
6438
6593
|
order: c.req.query('order'),
|
|
6439
6594
|
});
|
|
6440
|
-
const entries = await admin.listSweepRuns(c.get('actor'), filter);
|
|
6595
|
+
const entries = await c.var.admin.listSweepRuns(c.get('actor'), filter);
|
|
6441
6596
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
6442
6597
|
});
|
|
6443
6598
|
// -- the replay lever (#1705 PR 3): move a consumer's watermark on one edge ----
|
|
@@ -6462,10 +6617,10 @@ export function createControlPlaneApi(options) {
|
|
|
6462
6617
|
const move = importCursorMove.parse(raw);
|
|
6463
6618
|
const actor = c.get('actor');
|
|
6464
6619
|
// K-3 first, so a scope of another tenant reads as absent before anything is reached.
|
|
6465
|
-
if (!(await admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6620
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId))) {
|
|
6466
6621
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
6467
6622
|
}
|
|
6468
|
-
return c.json(await admin.moveImportCursor(actor, tenantId, scopeId, move));
|
|
6623
|
+
return c.json(await c.var.admin.moveImportCursor(actor, tenantId, scopeId, move));
|
|
6469
6624
|
});
|
|
6470
6625
|
// -- edge health (#1705 PR 3): where each cross-vertical edge of one tenant stands -
|
|
6471
6626
|
// Read live, through the phase's own reach, so it cannot disagree with the next pass about
|
|
@@ -6478,7 +6633,7 @@ export function createControlPlaneApi(options) {
|
|
|
6478
6633
|
if (pin !== null && pin !== tenantId)
|
|
6479
6634
|
return c.json({ error: 'forbidden' }, 403);
|
|
6480
6635
|
const focus = c.req.query('scopeId');
|
|
6481
|
-
return c.json(await crossVerticalHealth(host, {
|
|
6636
|
+
return c.json(await crossVerticalHealth(c.var.host, {
|
|
6482
6637
|
actor: c.get('actor'),
|
|
6483
6638
|
tenantId,
|
|
6484
6639
|
...(focus ? { focus: scopeIdSchema.parse(focus) } : {}),
|
|
@@ -6496,7 +6651,7 @@ export function createControlPlaneApi(options) {
|
|
|
6496
6651
|
code: c.req.query('code'),
|
|
6497
6652
|
limit: c.req.query('limit'),
|
|
6498
6653
|
});
|
|
6499
|
-
const entries = await admin.listIssues(c.get('actor'), filter);
|
|
6654
|
+
const entries = await c.var.admin.listIssues(c.get('actor'), filter);
|
|
6500
6655
|
// No cursor, deliberately — see issuesQuery. `entries` alone, so a client
|
|
6501
6656
|
// never walks a continuation that cannot exist.
|
|
6502
6657
|
return c.json({ entries });
|
|
@@ -6506,7 +6661,7 @@ export function createControlPlaneApi(options) {
|
|
|
6506
6661
|
// rather than an invented row.
|
|
6507
6662
|
app.put('/issues/status', async (c) => {
|
|
6508
6663
|
const input = issueStatusUpdate.parse(await c.req.json());
|
|
6509
|
-
const updated = await admin.setIssueStatus(c.get('actor'), input.fingerprint, input.status);
|
|
6664
|
+
const updated = await c.var.admin.setIssueStatus(c.get('actor'), input.fingerprint, input.status);
|
|
6510
6665
|
if (!updated)
|
|
6511
6666
|
return c.json({ error: 'unknown issue fingerprint' }, 404);
|
|
6512
6667
|
return c.json(updated);
|
|
@@ -6527,7 +6682,7 @@ export function createControlPlaneApi(options) {
|
|
|
6527
6682
|
cursor: c.req.query('cursor'),
|
|
6528
6683
|
order: c.req.query('order'),
|
|
6529
6684
|
});
|
|
6530
|
-
const entries = await admin.listModelUsage(c.get('actor'), filter);
|
|
6685
|
+
const entries = await c.var.admin.listModelUsage(c.get('actor'), filter);
|
|
6531
6686
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
6532
6687
|
});
|
|
6533
6688
|
// Folded per (tenant, vertical, model) with the platform's margin applied at read
|
|
@@ -6541,7 +6696,7 @@ export function createControlPlaneApi(options) {
|
|
|
6541
6696
|
const now = new Date();
|
|
6542
6697
|
const since = q.since ?? new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
|
|
6543
6698
|
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);
|
|
6699
|
+
const summary = await c.var.admin.summarizeModelUsage(c.get('actor'), { ...(q.tenantId ? { tenantId: q.tenantId } : {}), since, until }, options.modelMarginPercent ?? DEFAULT_MODEL_MARGIN_PERCENT);
|
|
6545
6700
|
return c.json(summary);
|
|
6546
6701
|
});
|
|
6547
6702
|
return app;
|