@substrat-run/control-plane-api 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +61 -17
- package/dist/api.js.map +1 -1
- package/dist/auth.d.ts +9 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -1
- package/dist/custom-hostnames.d.ts +5 -4
- package/dist/custom-hostnames.d.ts.map +1 -1
- package/dist/custom-hostnames.js +18 -5
- package/dist/custom-hostnames.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/platform-drain.d.ts +54 -0
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +185 -1
- package/dist/platform-drain.js.map +1 -1
- package/dist/vertical-client.d.ts +9 -0
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +23 -6
- package/dist/vertical-client.js.map +1 -1
- package/package.json +5 -5
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AA2B5B,OAAO,KAAK,EAAE,eAAe,EAA4B,MAAM,yBAAyB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AA2B5B,OAAO,KAAK,EAAE,eAAe,EAA4B,MAAM,yBAAyB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAe3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAGtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC3C;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChG;;;;;;;;;OASG;IACH,sBAAsB,CAAC,EAAE,CACvB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,eAAe,KACnB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACpF;;;;;OAKG;IACH,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,sBAAsB,CAAC;IAC9C;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,qBAAqB,CAAC;IAC5C;;;;OAIG;IACH,YAAY,EAAE,iBAAiB,CAAC;IAChC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,WAAW,CAAC;IAClC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,yBAAyB,CAAC;IAC9C;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAKD,KAAK,IAAI,GAAG;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAAC;AA4J7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAgoEhG"}
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { adminAction, channelName, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, registerVerticalInput, scopeDump, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, z, } from '@substrat-run/contracts';
|
|
3
3
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
4
|
+
import { TENANT_HEADER } from './auth.js';
|
|
4
5
|
import { ControlPlaneError } from './client.js';
|
|
5
6
|
import { provisionSiblingScope } from './platform-drain.js';
|
|
6
7
|
import { mapError } from './errors.js';
|
|
@@ -500,8 +501,7 @@ export function createControlPlaneApi(options) {
|
|
|
500
501
|
// prod-channel/static (a scope with no bound version), then to reading this host's own
|
|
501
502
|
// scope DB directly (a co-located host, or the contract tests — data is right here).
|
|
502
503
|
const verticalForScope = async (c, scope) => {
|
|
503
|
-
|
|
504
|
-
if (!slug)
|
|
504
|
+
if (!scope.vertical)
|
|
505
505
|
return undefined;
|
|
506
506
|
const actor = c.get('actor');
|
|
507
507
|
// A scope on the stable serving script (#286) is reached THERE — that script holds
|
|
@@ -511,12 +511,29 @@ export function createControlPlaneApi(options) {
|
|
|
511
511
|
if (serving)
|
|
512
512
|
return serving;
|
|
513
513
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
const bySlug = async (slug) => {
|
|
515
|
+
if (scope.verticalVersionId && options.resolveVerticalVersion) {
|
|
516
|
+
const bound = await options.resolveVerticalVersion(slug, scope.verticalVersionId, actor);
|
|
517
|
+
if (bound)
|
|
518
|
+
return bound;
|
|
519
|
+
}
|
|
520
|
+
return options.verticals?.[slug] ?? (await options.resolveVertical?.(slug, actor));
|
|
521
|
+
};
|
|
522
|
+
const direct = await bySlug(scope.vertical);
|
|
523
|
+
if (direct)
|
|
524
|
+
return direct;
|
|
525
|
+
// Miss path only (#417) — a resolved scope never pays for these registry reads: a
|
|
526
|
+
// scope bound to a BARE slug that is not registered, while the owning tenant's
|
|
527
|
+
// prefixed registration of the same name exists, is addressing the prefixed lineage
|
|
528
|
+
// under its bare spelling. Retry once under the registry id; anything else still
|
|
529
|
+
// misses and surfaces through `diagnoseUnboundScope` as before.
|
|
530
|
+
if (scope.tenantId && !scope.vertical.includes('/') && (await ownerOf(actor, scope.vertical)) === undefined) {
|
|
531
|
+
const tenant = await admin.getTenant(actor, scope.tenantId).catch(() => null);
|
|
532
|
+
const prefixed = tenant ? `${tenant.slug}/${scope.vertical}` : null;
|
|
533
|
+
if (prefixed && (await ownerOf(actor, prefixed)) !== undefined)
|
|
534
|
+
return bySlug(prefixed);
|
|
535
|
+
}
|
|
536
|
+
return undefined;
|
|
520
537
|
};
|
|
521
538
|
/**
|
|
522
539
|
* Why a scope's vertical did not resolve to a running deployment — an ACTIONABLE 501
|
|
@@ -1075,7 +1092,7 @@ export function createControlPlaneApi(options) {
|
|
|
1075
1092
|
// re-run resumes from there (each adopt is data-first and retry-safe).
|
|
1076
1093
|
app.post('/verticals/:slug/adopt-serving', async (c) => {
|
|
1077
1094
|
const p = c.get('principal');
|
|
1078
|
-
const slug =
|
|
1095
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1079
1096
|
const actor = c.get('actor');
|
|
1080
1097
|
if (p.kind === 'builder') {
|
|
1081
1098
|
const v = await verticalOf(actor, slug);
|
|
@@ -1241,6 +1258,33 @@ export function createControlPlaneApi(options) {
|
|
|
1241
1258
|
// slug carrying any OTHER tenant's prefix yields a two-slash id that fails
|
|
1242
1259
|
// `verticalSlug` validation / the ownership check downstream — fail-closed.
|
|
1243
1260
|
const effectiveSlug = (p, raw) => p.kind === 'builder' ? (raw.startsWith(`${p.tenantSlug}/`) ? raw : `${p.tenantSlug}/${raw}`) : raw;
|
|
1261
|
+
// The ONE (bare slug, tenant context) → registry id resolution for the routes that
|
|
1262
|
+
// ADDRESS an existing vertical (#417). A builder's context is its auth (`effectiveSlug`,
|
|
1263
|
+
// unchanged). STAFF acting for a workspace — the CLI over a service token, the
|
|
1264
|
+
// dashboard's tenant-narrowed seam — name it via the same `x-substrat-tenant` header a
|
|
1265
|
+
// builder session uses, and the prefix is formed exactly as a pinned push forms it: a
|
|
1266
|
+
// bare slug the pinned workspace owns stays itself (staff hand-registrations predate
|
|
1267
|
+
// prefixed claims), otherwise `<workspace.slug>/<slug>` — but only when that row
|
|
1268
|
+
// EXISTS. Addressing never redirects to a lineage that is not there: a platform-owned
|
|
1269
|
+
// bare slug under an irrelevant pin, and an unknown pin, resolve to the raw slug —
|
|
1270
|
+
// exactly the pre-#417 behavior. Registration and deploy keep their own CLAIM
|
|
1271
|
+
// semantics; this helper only addresses lineages, it never decides where one lands.
|
|
1272
|
+
const resolveVerticalId = async (c, raw) => {
|
|
1273
|
+
const p = c.get('principal');
|
|
1274
|
+
if (p.kind === 'builder')
|
|
1275
|
+
return effectiveSlug(p, raw);
|
|
1276
|
+
const pin = c.req.header(TENANT_HEADER)?.trim();
|
|
1277
|
+
if (!pin)
|
|
1278
|
+
return raw;
|
|
1279
|
+
const actor = c.get('actor');
|
|
1280
|
+
const workspace = (await admin.listTenants(actor)).find((t) => t.slug === pin || t.id === pin);
|
|
1281
|
+
if (!workspace || raw.startsWith(`${workspace.slug}/`))
|
|
1282
|
+
return raw;
|
|
1283
|
+
if ((await ownerOf(actor, raw)) === workspace.id)
|
|
1284
|
+
return raw;
|
|
1285
|
+
const prefixed = `${workspace.slug}/${raw}`;
|
|
1286
|
+
return (await ownerOf(actor, prefixed)) !== undefined ? prefixed : raw;
|
|
1287
|
+
};
|
|
1244
1288
|
app.get('/verticals', async (c) => {
|
|
1245
1289
|
const all = await admin.listVerticals(c.get('actor'));
|
|
1246
1290
|
const p = c.get('principal');
|
|
@@ -1269,7 +1313,7 @@ export function createControlPlaneApi(options) {
|
|
|
1269
1313
|
});
|
|
1270
1314
|
app.get('/verticals/:slug/versions', async (c) => {
|
|
1271
1315
|
const p = c.get('principal');
|
|
1272
|
-
const slug =
|
|
1316
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1273
1317
|
// A builder reading a vertical it does not own gets 404 — indistinguishable from
|
|
1274
1318
|
// absent, the same fail-closed reflex K-3 uses for a cross-tenant scope.
|
|
1275
1319
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
@@ -1286,7 +1330,7 @@ export function createControlPlaneApi(options) {
|
|
|
1286
1330
|
return c.json({ error: 'verticalSlug does not match the path' }, 400);
|
|
1287
1331
|
}
|
|
1288
1332
|
const p = c.get('principal');
|
|
1289
|
-
const slug =
|
|
1333
|
+
const slug = await resolveVerticalId(c, input.verticalSlug);
|
|
1290
1334
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1291
1335
|
return c.json({ error: 'forbidden' }, 403);
|
|
1292
1336
|
}
|
|
@@ -1303,7 +1347,7 @@ export function createControlPlaneApi(options) {
|
|
|
1303
1347
|
// permission-diff checkpoint stays the human gate — this route only reads.
|
|
1304
1348
|
app.get('/verticals/:slug/versions/:id/registry', async (c) => {
|
|
1305
1349
|
const p = c.get('principal');
|
|
1306
|
-
const slug =
|
|
1350
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1307
1351
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1308
1352
|
return c.json({ error: 'not found' }, 404);
|
|
1309
1353
|
}
|
|
@@ -1328,7 +1372,7 @@ export function createControlPlaneApi(options) {
|
|
|
1328
1372
|
// may ask; the staff `listing` flip below is the review gate. Owner-checked (like promote).
|
|
1329
1373
|
app.post('/verticals/:slug/publish-request', async (c) => {
|
|
1330
1374
|
const p = c.get('principal');
|
|
1331
|
-
const slug =
|
|
1375
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1332
1376
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1333
1377
|
return c.json({ error: 'not found' }, 404);
|
|
1334
1378
|
}
|
|
@@ -1363,7 +1407,7 @@ export function createControlPlaneApi(options) {
|
|
|
1363
1407
|
});
|
|
1364
1408
|
app.get('/verticals/:slug/channels', async (c) => {
|
|
1365
1409
|
const p = c.get('principal');
|
|
1366
|
-
const slug =
|
|
1410
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1367
1411
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1368
1412
|
return c.json({ error: 'not found' }, 404);
|
|
1369
1413
|
}
|
|
@@ -1374,7 +1418,7 @@ export function createControlPlaneApi(options) {
|
|
|
1374
1418
|
// history, and a foreign slug 404s indistinguishably from an absent one.
|
|
1375
1419
|
app.get('/verticals/:slug/channels/:channel/history', async (c) => {
|
|
1376
1420
|
const p = c.get('principal');
|
|
1377
|
-
const slug =
|
|
1421
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1378
1422
|
const channel = channelName.parse(c.req.param('channel'));
|
|
1379
1423
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1380
1424
|
return c.json({ error: 'not found' }, 404);
|
|
@@ -1446,7 +1490,7 @@ export function createControlPlaneApi(options) {
|
|
|
1446
1490
|
};
|
|
1447
1491
|
app.post('/verticals/:slug/channels/:channel/promote', async (c) => {
|
|
1448
1492
|
const p = c.get('principal');
|
|
1449
|
-
const slug =
|
|
1493
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
1450
1494
|
const channel = channelName.parse(c.req.param('channel'));
|
|
1451
1495
|
if (p.kind === 'builder') {
|
|
1452
1496
|
// A builder promotes only verticals it owns — and prod only while the vertical
|
|
@@ -2012,7 +2056,7 @@ export function createControlPlaneApi(options) {
|
|
|
2012
2056
|
// makes a self-serve act; a listed vertical's prod is staff-gated (marketplace-publish.md §2).
|
|
2013
2057
|
const previewVertical = async (c) => {
|
|
2014
2058
|
const p = c.get('principal');
|
|
2015
|
-
const slug =
|
|
2059
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
2016
2060
|
const v = await verticalOf(c.get('actor'), slug);
|
|
2017
2061
|
if (!v)
|
|
2018
2062
|
return { error: `unknown vertical '${slug}'`, status: 404 };
|