@substrat-run/control-plane-api 0.39.0 → 0.41.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 +85 -22
- package/dist/api.js.map +1 -1
- package/dist/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +4 -1
- package/dist/cf-observability.js.map +1 -1
- package/dist/custom-hostnames.d.ts +18 -2
- package/dist/custom-hostnames.d.ts.map +1 -1
- package/dist/custom-hostnames.js +35 -5
- package/dist/custom-hostnames.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/observability.d.ts +7 -1
- package/dist/observability.d.ts.map +1 -1
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +18 -1
- package/dist/platform-drain.js.map +1 -1
- package/dist/tenant-stores.d.ts +20 -4
- package/dist/tenant-stores.d.ts.map +1 -1
- package/dist/tenant-stores.js +59 -2
- package/dist/tenant-stores.js.map +1 -1
- package/dist/wfp.d.ts +26 -9
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +15 -7
- package/dist/wfp.js.map +1 -1
- package/package.json +4 -4
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;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AA6B5B,OAAO,KAAK,EAGV,eAAe,EAIhB,MAAM,yBAAyB,CAAC;AACjC,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;AAQtD,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;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;;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;AAsM7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CA02EhG"}
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
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';
|
|
2
|
+
import { adminAction, channelName, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, pageOf, 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
4
|
import { TENANT_HEADER } from './auth.js';
|
|
5
5
|
import { ControlPlaneError } from './client.js';
|
|
@@ -7,7 +7,7 @@ import { provisionSiblingScope } from './platform-drain.js';
|
|
|
7
7
|
import { mapError } from './errors.js';
|
|
8
8
|
import { maskDump } from './mask.js';
|
|
9
9
|
import { assertSandboxContract, deployManifest, storedDeployManifest, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
|
|
10
|
-
import { collectTenantStoreHandles, tenantStoreBindings } from './tenant-stores.js';
|
|
10
|
+
import { blobStoreBindings, collectBlobStoreHandles, collectTenantStoreHandles, tenantStoreBindings, } from './tenant-stores.js';
|
|
11
11
|
import { mintPushToken, pushActorFor } from './push-token.js';
|
|
12
12
|
import { isCustomHostname, validateBindableHostname, } from './custom-hostnames.js';
|
|
13
13
|
// -- request schemas ---------------------------------------------------------
|
|
@@ -70,6 +70,29 @@ const listHostnamesQuery = z.object({
|
|
|
70
70
|
tenantId: tenantIdSchema.optional(),
|
|
71
71
|
scopeId: scopeIdSchema.optional(),
|
|
72
72
|
});
|
|
73
|
+
// Every GET list below shares ONE pagination convention (contracts pagination.ts):
|
|
74
|
+
// keyset over the list's own sort key, `limit` DEFAULTED at this egress (the kernel
|
|
75
|
+
// reads stay unbounded for in-process callers — the admin-log precedent, generalized),
|
|
76
|
+
// `{ entries, nextCursor }` out. The cursor is the last entry's sort key verbatim.
|
|
77
|
+
const pageParams = (c) => listPageQuery.parse({
|
|
78
|
+
limit: c.req.query('limit'),
|
|
79
|
+
cursor: c.req.query('cursor'),
|
|
80
|
+
order: c.req.query('order'),
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Page an already-materialized asc-sorted list — for the one read whose principal
|
|
84
|
+
* narrowing runs ABOVE the adapter (`GET /verticals`), where a pushed-down page
|
|
85
|
+
* would terminate early. Same cursor semantics as the keyset reads.
|
|
86
|
+
*/
|
|
87
|
+
const pageSlice = (rows, page, key) => {
|
|
88
|
+
const found = page.cursor === undefined ? 0 : rows.findIndex((r) => key(r) > page.cursor);
|
|
89
|
+
const from = found === -1 ? rows.length : found;
|
|
90
|
+
const entries = rows.slice(from, from + page.limit);
|
|
91
|
+
return {
|
|
92
|
+
entries,
|
|
93
|
+
nextCursor: from + page.limit < rows.length ? key(entries[entries.length - 1]) : null,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
73
96
|
/** Repeatable query params arrive as `?status=active&status=suspended`. */
|
|
74
97
|
const listScopesQuery = z.object({
|
|
75
98
|
tenantId: tenantIdSchema.optional(),
|
|
@@ -151,15 +174,14 @@ const auditLogQuery = z.object({
|
|
|
151
174
|
action: z.array(adminAction).optional(),
|
|
152
175
|
since: z.string().optional(),
|
|
153
176
|
until: z.string().optional(),
|
|
154
|
-
// Defaulted, not merely capped:
|
|
155
|
-
// retention decision — it is the
|
|
156
|
-
// it only grows. An unbounded
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
order: z.enum(['asc', 'desc']).optional(),
|
|
177
|
+
// The shared page fields (contracts pagination.ts). Defaulted, not merely capped:
|
|
178
|
+
// the admin log is append-only and never swept (the retention decision — it is the
|
|
179
|
+
// compliance witness, control-plane.md §4.4/§4.8), so it only grows. An unbounded
|
|
180
|
+
// `GET /admin-log` would dump the whole table; a default page keeps the external
|
|
181
|
+
// read bounded while `nextCursor` still walks the entire log. The KERNEL call stays
|
|
182
|
+
// deliberately unbounded (an in-process caller that wants everything asks for
|
|
183
|
+
// everything) — only the HTTP egress vectors are bounded by default.
|
|
184
|
+
...listPageQuery.shape,
|
|
163
185
|
});
|
|
164
186
|
/**
|
|
165
187
|
* The audited HTTP surface over `HostAdmin` (control-plane.md §4.5).
|
|
@@ -307,7 +329,11 @@ export function createControlPlaneApi(options) {
|
|
|
307
329
|
return c.json(body, status);
|
|
308
330
|
});
|
|
309
331
|
// -- tenant registry (§4.1) ------------------------------------------------
|
|
310
|
-
app.get('/tenants', async (c) =>
|
|
332
|
+
app.get('/tenants', async (c) => {
|
|
333
|
+
const page = pageParams(c);
|
|
334
|
+
const entries = await admin.listTenants(c.get('actor'), page);
|
|
335
|
+
return c.json(pageOf(entries, page.limit, (t) => t.id));
|
|
336
|
+
});
|
|
311
337
|
app.post('/tenants', async (c) => {
|
|
312
338
|
const input = createTenantInput.parse(await c.req.json());
|
|
313
339
|
await admin.createTenant(c.get('actor'), input);
|
|
@@ -436,7 +462,9 @@ export function createControlPlaneApi(options) {
|
|
|
436
462
|
status: c.req.queries('status'),
|
|
437
463
|
vertical: c.req.query('vertical'),
|
|
438
464
|
});
|
|
439
|
-
|
|
465
|
+
const page = pageParams(c);
|
|
466
|
+
const entries = await admin.listScopes(c.get('actor'), { ...filter, ...page });
|
|
467
|
+
return c.json(pageOf(entries, page.limit, (s) => s.id));
|
|
440
468
|
});
|
|
441
469
|
// -- fleet migration progress (kernel-design §5.3, #49) ---------------------
|
|
442
470
|
// The ops-console view: "release N: X/Y migrated, P pending, F failed",
|
|
@@ -1343,6 +1371,16 @@ export function createControlPlaneApi(options) {
|
|
|
1343
1371
|
tenantId: input.tenantId,
|
|
1344
1372
|
patchBindings: options.patchScriptBindings,
|
|
1345
1373
|
});
|
|
1374
|
+
// #473: per-tenant BLOB stores the vertical declared, minted + bound the same way.
|
|
1375
|
+
// No handle rides the provision callback (a blob store has no schema to migrate), so
|
|
1376
|
+
// the effect is purely the ledger row + the attached r2_bucket binding.
|
|
1377
|
+
await collectBlobStoreHandles({
|
|
1378
|
+
host: options.host,
|
|
1379
|
+
actor: c.get('actor'),
|
|
1380
|
+
slug,
|
|
1381
|
+
tenantId: input.tenantId,
|
|
1382
|
+
patchBindings: options.patchScriptBindings,
|
|
1383
|
+
});
|
|
1346
1384
|
try {
|
|
1347
1385
|
// #424 case 2: the binding attach above races Cloudflare script-settings
|
|
1348
1386
|
// propagation, so the vertical's FIRST answer can be a transient 5xx that a
|
|
@@ -1440,10 +1478,13 @@ export function createControlPlaneApi(options) {
|
|
|
1440
1478
|
return (await ownerOf(actor, prefixed)) !== undefined ? prefixed : raw;
|
|
1441
1479
|
};
|
|
1442
1480
|
app.get('/verticals', async (c) => {
|
|
1481
|
+
const page = pageParams(c);
|
|
1443
1482
|
const all = await admin.listVerticals(c.get('actor'));
|
|
1444
1483
|
const p = c.get('principal');
|
|
1445
|
-
// A builder sees only what it owns; staff see the whole registry.
|
|
1446
|
-
|
|
1484
|
+
// A builder sees only what it owns; staff see the whole registry. The narrowing
|
|
1485
|
+
// runs above the adapter, so the page is sliced here — over the narrowed list.
|
|
1486
|
+
const visible = p.kind === 'builder' ? all.filter((v) => v.ownerTenant === p.tenantId) : all;
|
|
1487
|
+
return c.json(pageSlice(visible, page, (v) => v.slug));
|
|
1447
1488
|
});
|
|
1448
1489
|
app.post('/verticals', async (c) => {
|
|
1449
1490
|
const input = registerVerticalInput.parse(await c.req.json());
|
|
@@ -1473,7 +1514,9 @@ export function createControlPlaneApi(options) {
|
|
|
1473
1514
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1474
1515
|
return c.json({ error: 'not found' }, 404);
|
|
1475
1516
|
}
|
|
1476
|
-
|
|
1517
|
+
const page = pageParams(c);
|
|
1518
|
+
const entries = await admin.listVersions(c.get('actor'), slug, page);
|
|
1519
|
+
return c.json(pageOf(entries, page.limit, (v) => v.id));
|
|
1477
1520
|
});
|
|
1478
1521
|
app.post('/verticals/:slug/versions', async (c) => {
|
|
1479
1522
|
const input = publishVersionInput.parse(await c.req.json());
|
|
@@ -1575,7 +1618,9 @@ export function createControlPlaneApi(options) {
|
|
|
1575
1618
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1576
1619
|
return c.json({ error: 'not found' }, 404);
|
|
1577
1620
|
}
|
|
1578
|
-
|
|
1621
|
+
const page = pageParams(c);
|
|
1622
|
+
const entries = await admin.listChannels(c.get('actor'), slug, page);
|
|
1623
|
+
return c.json(pageOf(entries, page.limit, (ch) => ch.channel));
|
|
1579
1624
|
});
|
|
1580
1625
|
// The promotion timeline (newest first) — what a rollback UI picks a target from.
|
|
1581
1626
|
// Owner-narrowed like the channel read above: a builder sees only its own verticals'
|
|
@@ -1587,7 +1632,9 @@ export function createControlPlaneApi(options) {
|
|
|
1587
1632
|
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
1588
1633
|
return c.json({ error: 'not found' }, 404);
|
|
1589
1634
|
}
|
|
1590
|
-
|
|
1635
|
+
const page = pageParams(c);
|
|
1636
|
+
const entries = await admin.listChannelHistory(c.get('actor'), slug, channel, page);
|
|
1637
|
+
return c.json(pageOf(entries, page.limit, (e) => e.id));
|
|
1591
1638
|
});
|
|
1592
1639
|
/**
|
|
1593
1640
|
* Serve a version IN PLACE (#286): upload its bundle onto the vertical's ONE stable
|
|
@@ -1622,7 +1669,15 @@ export function createControlPlaneApi(options) {
|
|
|
1622
1669
|
// happened to carry) is what makes a re-deploy structurally unable to drop a
|
|
1623
1670
|
// tenant's store. Platform-granted after the §4 sandbox check, like injectSecrets:
|
|
1624
1671
|
// the builder never declared these and never named the ids.
|
|
1625
|
-
const storeBindings =
|
|
1672
|
+
const storeBindings = [
|
|
1673
|
+
...tenantStoreBindings(await admin.listTenantStores(actor, { vertical: slug })),
|
|
1674
|
+
// #473: the per-tenant blob-store r2_bucket bindings, re-derived from the ledger on
|
|
1675
|
+
// every serving upload for the same reason — an upload replaces the script's binding
|
|
1676
|
+
// set, so a re-deploy must never drop a tenant's attachment bucket.
|
|
1677
|
+
...blobStoreBindings(await admin.listBlobStores(actor, { vertical: slug })),
|
|
1678
|
+
].map((b) => b.type === 'd1'
|
|
1679
|
+
? { type: 'd1', name: b.name, id: b.id }
|
|
1680
|
+
: { type: 'r2_bucket', name: b.name, bucket_name: b.bucketName });
|
|
1626
1681
|
await options.deployVertical(ref, {
|
|
1627
1682
|
entry: manifest.entry,
|
|
1628
1683
|
compatibilityDate: manifest.compatibilityDate,
|
|
@@ -1937,12 +1992,14 @@ export function createControlPlaneApi(options) {
|
|
|
1937
1992
|
.object({
|
|
1938
1993
|
service: z.string().min(1).max(200).optional(),
|
|
1939
1994
|
level: z.enum(['log', 'info', 'warn', 'error', 'debug']).optional(),
|
|
1995
|
+
search: z.string().min(1).max(200).optional(),
|
|
1940
1996
|
hours: z.coerce.number().int().min(1).max(72).default(1),
|
|
1941
1997
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
1942
1998
|
})
|
|
1943
1999
|
.parse({
|
|
1944
2000
|
service: c.req.query('service') || undefined,
|
|
1945
2001
|
level: c.req.query('level') || undefined,
|
|
2002
|
+
search: c.req.query('search') || undefined,
|
|
1946
2003
|
hours: c.req.query('hours'),
|
|
1947
2004
|
limit: c.req.query('limit'),
|
|
1948
2005
|
});
|
|
@@ -1996,7 +2053,9 @@ export function createControlPlaneApi(options) {
|
|
|
1996
2053
|
// (scopeId) but never widen; a foreign tenantId in the query loses silently.
|
|
1997
2054
|
if (p.kind === 'builder')
|
|
1998
2055
|
filter.tenantId = p.tenantId;
|
|
1999
|
-
|
|
2056
|
+
const page = pageParams(c);
|
|
2057
|
+
const entries = await admin.listHostnames(c.get('actor'), { ...filter, ...page });
|
|
2058
|
+
return c.json(pageOf(entries, page.limit, (h) => h.hostname));
|
|
2000
2059
|
});
|
|
2001
2060
|
// Which base domains ride the platform wildcard cert (go straight to `active`);
|
|
2002
2061
|
// everything else is a custom domain that walks Cloudflare-for-SaaS issuance (§4.7).
|
|
@@ -2349,7 +2408,11 @@ export function createControlPlaneApi(options) {
|
|
|
2349
2408
|
tenantId: c.req.query('tenantId'),
|
|
2350
2409
|
source: c.req.query('source'),
|
|
2351
2410
|
});
|
|
2352
|
-
|
|
2411
|
+
const page = pageParams(c);
|
|
2412
|
+
const entries = await admin.listRoles(c.get('actor'), { ...filter, ...page });
|
|
2413
|
+
// Composite sort key (tenant_id, role_key) — the `|` join is the documented
|
|
2414
|
+
// cursor shape (scope-host.ts listRoles).
|
|
2415
|
+
return c.json(pageOf(entries, page.limit, (r) => `${r.tenantId}|${r.key}`));
|
|
2353
2416
|
});
|
|
2354
2417
|
// -- the admin log (§4.4/§4.5) ---------------------------------------------
|
|
2355
2418
|
app.get('/admin-log', async (c) => {
|
|
@@ -2367,7 +2430,7 @@ export function createControlPlaneApi(options) {
|
|
|
2367
2430
|
const entries = await admin.auditLog(c.get('actor'), filter);
|
|
2368
2431
|
// The cursor IS the last entry's id (ULID order is chronological), so the
|
|
2369
2432
|
// page carries its own continuation and the console never assembles one.
|
|
2370
|
-
return c.json(
|
|
2433
|
+
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
2371
2434
|
});
|
|
2372
2435
|
return app;
|
|
2373
2436
|
}
|