@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 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;AAEtD,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,CA0lEhG"}
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
- const slug = scope.vertical;
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
- if (scope.verticalVersionId && options.resolveVerticalVersion) {
515
- const bound = await options.resolveVerticalVersion(slug, scope.verticalVersionId, actor);
516
- if (bound)
517
- return bound;
518
- }
519
- return options.verticals?.[slug] ?? (await options.resolveVertical?.(slug, actor));
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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, input.verticalSlug);
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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, c.req.param('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 = effectiveSlug(p, c.req.param('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 };