@substrat-run/control-plane-api 0.125.0 → 0.127.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/aggregate-reads.d.ts +67 -0
- package/dist/aggregate-reads.d.ts.map +1 -0
- package/dist/aggregate-reads.js +186 -0
- package/dist/aggregate-reads.js.map +1 -0
- package/dist/aggregate-source.d.ts +125 -0
- package/dist/aggregate-source.d.ts.map +1 -0
- package/dist/aggregate-source.js +134 -0
- package/dist/aggregate-source.js.map +1 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +63 -18
- package/dist/api.js.map +1 -1
- package/dist/cf-observability.d.ts +7 -0
- package/dist/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +316 -214
- package/dist/cf-observability.js.map +1 -1
- package/dist/cube-store-sql.d.ts +34 -0
- package/dist/cube-store-sql.d.ts.map +1 -0
- package/dist/cube-store-sql.js +74 -0
- package/dist/cube-store-sql.js.map +1 -0
- package/dist/deploy.d.ts +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +1 -1
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/observability.d.ts +16 -0
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js.map +1 -1
- package/dist/platform-entry.d.ts +40 -0
- package/dist/platform-entry.d.ts.map +1 -0
- package/dist/platform-entry.generated.d.ts +12 -0
- package/dist/platform-entry.generated.d.ts.map +1 -0
- package/dist/platform-entry.generated.js +14 -0
- package/dist/platform-entry.generated.js.map +1 -0
- package/dist/platform-entry.js +53 -0
- package/dist/platform-entry.js.map +1 -0
- package/dist/tenant-request-query.d.ts +21 -0
- package/dist/tenant-request-query.d.ts.map +1 -1
- package/dist/tenant-request-query.js +27 -0
- package/dist/tenant-request-query.js.map +1 -1
- package/dist/vertical-client.d.ts +6 -1
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +17 -3
- package/dist/vertical-client.js.map +1 -1
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +5 -1
- package/dist/wfp.js.map +1 -1
- package/package.json +7 -7
package/dist/api.js
CHANGED
|
@@ -17,7 +17,7 @@ import { mapError } from './errors.js';
|
|
|
17
17
|
import { maskDump, maskRecords } from './mask.js';
|
|
18
18
|
import { createPseudonymizer } from './pseudonymize.js';
|
|
19
19
|
import { openDump, sealDump } from './seal.js';
|
|
20
|
-
import { assertSandboxContract, deployManifest, storedDeployManifest, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
|
|
20
|
+
import { assertSandboxContract, deployManifest, storedDeployManifest, pushedPermissionRegistry, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
|
|
21
21
|
/**
|
|
22
22
|
* Answer with a problem document (#113 phase 4).
|
|
23
23
|
*
|
|
@@ -34,7 +34,7 @@ function problem(c, answer) {
|
|
|
34
34
|
import { backfillDeclaredStores, blobStoreBindings, collectBlobStoreHandles, collectTenantStoreHandles, missingStoresForTenant, tenantStoreBindings, } from './tenant-stores.js';
|
|
35
35
|
import { mintPushToken, pushActorFor } from './push-token.js';
|
|
36
36
|
import { TENANT_SERIES_SCOPE_CAP, REQUEST_FACET_KEYS } from './observability.js';
|
|
37
|
-
import { parseTenantRequestQuery, parseTenantWindowQuery, refineTenantWindow } from './tenant-request-query.js';
|
|
37
|
+
import { parseTenantRequestQuery, parseTenantWindowQuery, refineTenantWindow, scriptFamiliesOfScopes } from './tenant-request-query.js';
|
|
38
38
|
import { namespacesForScript } from './do-namespaces.js';
|
|
39
39
|
import { backupDirectoryIfDue } from './directory-backup.js';
|
|
40
40
|
import { STORAGE_PAGE_DEFAULT, STORAGE_PAGE_MAX, readStoragePage } from './storage-meter.js';
|
|
@@ -1719,8 +1719,15 @@ export function createControlPlaneApi(options) {
|
|
|
1719
1719
|
* as it stood before the failed attempt. Every caller still re-asserts after it (the
|
|
1720
1720
|
* fallback), passing what this reports.
|
|
1721
1721
|
*/
|
|
1722
|
-
const restoreCarryingSwitches = async (actor, dest, tenantId, scopeId, tables
|
|
1723
|
-
|
|
1722
|
+
const restoreCarryingSwitches = async (actor, dest, tenantId, scopeId, tables,
|
|
1723
|
+
/** #1869: the scope `tables` were captured from — `scopeId` itself unless the copy moves —
|
|
1724
|
+
* and `exact` when the platform exported them itself rather than a caller supplying them. */
|
|
1725
|
+
source) => {
|
|
1726
|
+
return retryTransient(async () => dest.restoreScope(tenantId, scopeId, tables, {
|
|
1727
|
+
...(await switchCarryFor(admin, actor, { tenantId, scopeId })),
|
|
1728
|
+
sourceScopeId: source.scopeId,
|
|
1729
|
+
exact: source.exact,
|
|
1730
|
+
}));
|
|
1724
1731
|
};
|
|
1725
1732
|
const adoptScopeOntoServing = async (c, tenantId, scopeId, opts = {}) => {
|
|
1726
1733
|
const actor = c.get('actor');
|
|
@@ -1758,7 +1765,7 @@ export function createControlPlaneApi(options) {
|
|
|
1758
1765
|
}
|
|
1759
1766
|
const dump = await source.exportScope(scopeId);
|
|
1760
1767
|
// #1742: the recorded OFF positions ride the restore, applied in the replay's own event.
|
|
1761
|
-
const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump);
|
|
1768
|
+
const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump, { scopeId, exact: true });
|
|
1762
1769
|
// Data landed — only now flip routing and move the version pointer.
|
|
1763
1770
|
await admin
|
|
1764
1771
|
.setScopeServingRef(actor, tenantId, scopeId, serving.ref, { acknowledge: opts.acknowledge })
|
|
@@ -1902,7 +1909,7 @@ export function createControlPlaneApi(options) {
|
|
|
1902
1909
|
throw new ControlPlaneError(501, 'rebind-vertical needs dispatch resolution for both ends');
|
|
1903
1910
|
}
|
|
1904
1911
|
const dump = await source.exportScope(scopeId);
|
|
1905
|
-
const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump); // #1742, as adopt
|
|
1912
|
+
const restored = await restoreCarryingSwitches(actor, dest, tenantId, scopeId, dump, { scopeId, exact: true }); // #1742, as adopt
|
|
1906
1913
|
// Data landed on the target script — only now flip routing and cross the pointer.
|
|
1907
1914
|
// `bindScopeVersion` rewrites `scopes.vertical` from the version row, audited. No
|
|
1908
1915
|
// extra snapshot here (adopt-serving's precedent): the source script's copy is the
|
|
@@ -1989,7 +1996,10 @@ export function createControlPlaneApi(options) {
|
|
|
1989
1996
|
}
|
|
1990
1997
|
const dump = await retryTransient(() => source.exportScope(scope.id));
|
|
1991
1998
|
// #1742: the recorded OFF positions ride the restore, as on adopt and rebind.
|
|
1992
|
-
const restored = await restoreCarryingSwitches(actor, dest, scope.tenantId, scope.id, dump
|
|
1999
|
+
const restored = await restoreCarryingSwitches(actor, dest, scope.tenantId, scope.id, dump, {
|
|
2000
|
+
scopeId: scope.id,
|
|
2001
|
+
exact: true,
|
|
2002
|
+
});
|
|
1993
2003
|
return { from, to, tables: restored.tables, switchedOff: restored.switchedOff };
|
|
1994
2004
|
};
|
|
1995
2005
|
app.get('/tenants/:tenantId/scopes/:scopeId/tables', async (c) => {
|
|
@@ -3262,6 +3272,8 @@ export function createControlPlaneApi(options) {
|
|
|
3262
3272
|
// a vertical deployment, the same dump is then loaded THERE — the bytes the router
|
|
3263
3273
|
// actually serves. (Yes, that stores the dump twice in delegated mode; the
|
|
3264
3274
|
// placeholder copy is the price of one canonical audited path, same as export.)
|
|
3275
|
+
/** #1869: the optional re-point hint a restore body may carry beside the dump. */
|
|
3276
|
+
const restoreSourceHint = z.object({ sourceScopeId: scopeIdSchema.optional() });
|
|
3265
3277
|
app.post('/tenants/:tenantId/scopes/:scopeId/restore', async (c) => {
|
|
3266
3278
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3267
3279
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
@@ -3269,7 +3281,12 @@ export function createControlPlaneApi(options) {
|
|
|
3269
3281
|
const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3270
3282
|
if (!scope)
|
|
3271
3283
|
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3272
|
-
const
|
|
3284
|
+
const raw = await c.req.json();
|
|
3285
|
+
const dump = scopeDump.parse(raw);
|
|
3286
|
+
// #1869: where the uploaded file says it came from, when the caller had to set the dump's
|
|
3287
|
+
// own `scopeId` to something else. A hint for the grant re-point ONLY: the dump's
|
|
3288
|
+
// `tenantId`/`scopeId` below still choose the keys that open its sealed payloads.
|
|
3289
|
+
const { sourceScopeId: sourceHint } = restoreSourceHint.parse(raw);
|
|
3273
3290
|
// A backup with no tables is not a scope dump — name that plainly rather than letting
|
|
3274
3291
|
// the empty replay reach the checker and surface as a bare `internal error` (#321).
|
|
3275
3292
|
if (dump.tables.length === 0) {
|
|
@@ -3289,11 +3306,16 @@ export function createControlPlaneApi(options) {
|
|
|
3289
3306
|
const origin = { tenantId: tenantIdSchema.parse(dump.tenantId), scopeId: scopeIdSchema.parse(dump.scopeId) };
|
|
3290
3307
|
const tables = await openDump(dump.tables, sealerFor(c, origin.tenantId, origin.scopeId));
|
|
3291
3308
|
const landing = { ...dump, tables };
|
|
3292
|
-
await host.restoreScope(actor, tenantId, scopeId, landing);
|
|
3309
|
+
await host.restoreScope(actor, tenantId, scopeId, landing, sourceHint ? { sourceScopeId: sourceHint } : undefined);
|
|
3293
3310
|
const vertical = await verticalForScope(c, scope);
|
|
3294
3311
|
// #1742: the recorded OFF positions ride the restore, so the deployment switches them
|
|
3295
3312
|
// off in the replay's own event — its own sweeper cannot land in between.
|
|
3296
|
-
const restored = vertical
|
|
3313
|
+
const restored = vertical
|
|
3314
|
+
? await restoreCarryingSwitches(actor, vertical, tenantId, scopeId, tables, {
|
|
3315
|
+
scopeId: sourceHint ?? origin.scopeId,
|
|
3316
|
+
exact: false,
|
|
3317
|
+
})
|
|
3318
|
+
: undefined;
|
|
3297
3319
|
// #1674: a backup from before a module was switched off brings it back on; the
|
|
3298
3320
|
// directory's record puts it back off now, not at the next reconcile. Where the
|
|
3299
3321
|
// deployment already did (#1742), this finds it done and audits what it reported.
|
|
@@ -3962,10 +3984,17 @@ export function createControlPlaneApi(options) {
|
|
|
3962
3984
|
catch {
|
|
3963
3985
|
return c.json({ error: 'manifestJson is not valid JSON' }, 400);
|
|
3964
3986
|
}
|
|
3987
|
+
const refuse = (issue, under = []) => c.json({ error: `manifestJson is not a deploy manifest: ${[...under, ...issue.path].join('.') || '(root)'}: ${issue.message}` }, 400);
|
|
3965
3988
|
const checked = storedDeployManifest.safeParse(raw);
|
|
3966
|
-
if (!checked.success)
|
|
3967
|
-
|
|
3968
|
-
|
|
3989
|
+
if (!checked.success)
|
|
3990
|
+
return refuse(checked.error.issues[0]);
|
|
3991
|
+
// #1869: a registry handed to this route now is NEW, so it answers to the push-time rule on
|
|
3992
|
+
// entity-grant shapes (no kernel namespace as an entity type), as `deployManifest` does.
|
|
3993
|
+
// An absent registry stays accepted, as above.
|
|
3994
|
+
if (checked.data.registry) {
|
|
3995
|
+
const registry = pushedPermissionRegistry.safeParse(checked.data.registry);
|
|
3996
|
+
if (!registry.success)
|
|
3997
|
+
return refuse(registry.error.issues[0], ['registry']);
|
|
3969
3998
|
}
|
|
3970
3999
|
}
|
|
3971
4000
|
await admin.publishVersion(c.get('actor'), input);
|
|
@@ -5121,7 +5150,9 @@ export function createControlPlaneApi(options) {
|
|
|
5121
5150
|
until: c.req.query('until') || undefined,
|
|
5122
5151
|
limit: c.req.query('limit'),
|
|
5123
5152
|
});
|
|
5124
|
-
|
|
5153
|
+
// #1877: scoped to the app's own scripts — the scan and trust boundary.
|
|
5154
|
+
const services = await appServices(c.get('actor'), input.tenantId, input.scopeId, input.vertical);
|
|
5155
|
+
return c.json(await options.observability.tenantLogs({ ...input, services }));
|
|
5125
5156
|
});
|
|
5126
5157
|
// #1746: the per-request record, read at the tenant grain. The three routes take ONE
|
|
5127
5158
|
// query — tenant, window, facet filters (`tenant-request-query.ts`) — so the histogram,
|
|
@@ -5138,6 +5169,16 @@ export function createControlPlaneApi(options) {
|
|
|
5138
5169
|
throw new ControlPlaneError(400, 'tenantId is required');
|
|
5139
5170
|
return tenantId;
|
|
5140
5171
|
};
|
|
5172
|
+
/**
|
|
5173
|
+
* #1877: the script families a tenant's app — or, with no scope named, its apps — run
|
|
5174
|
+
* as (`scriptFamiliesOfScopes`). Every tenant log read is scoped to these: a query then
|
|
5175
|
+
* reads one vertical's scripts instead of the whole account's, and a line counts only if
|
|
5176
|
+
* one of them wrote it. From the directory, never the caller, and within the tenant: a
|
|
5177
|
+
* scope id that is not this tenant's resolves no family, and a read over none answers
|
|
5178
|
+
* nothing. `listScopes`, not `getScopeRecord`: a dashboard read must not write an access
|
|
5179
|
+
* row per page view.
|
|
5180
|
+
*/
|
|
5181
|
+
const appServices = async (actor, tenantId, scopeId, vertical) => scriptFamiliesOfScopes(await admin.listScopes(actor, { tenantId: tenantIdSchema.parse(tenantId) }), { scopeId, vertical });
|
|
5141
5182
|
app.get('/observability/tenant-request-volume', async (c) => {
|
|
5142
5183
|
if (!options.observability?.tenantRequestVolume) {
|
|
5143
5184
|
return c.json({ error: 'request aggregation is not configured on this control plane' }, 501);
|
|
@@ -5146,7 +5187,8 @@ export function createControlPlaneApi(options) {
|
|
|
5146
5187
|
// A bucket COUNT: zooming in keeps it and narrows the window, so the bucket shrinks.
|
|
5147
5188
|
// 90 is the design's histogram; the bounds keep a chart drawable and a query cheap.
|
|
5148
5189
|
const buckets = z.coerce.number().int().min(10).max(240).default(90).parse(c.req.query('buckets'));
|
|
5149
|
-
|
|
5190
|
+
const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
|
|
5191
|
+
return c.json(await options.observability.tenantRequestVolume({ ...scope, services, buckets }));
|
|
5150
5192
|
});
|
|
5151
5193
|
app.get('/observability/tenant-request-facets', async (c) => {
|
|
5152
5194
|
if (!options.observability?.tenantRequestFacets) {
|
|
@@ -5157,7 +5199,8 @@ export function createControlPlaneApi(options) {
|
|
|
5157
5199
|
// that shows three asks for three.
|
|
5158
5200
|
const asked = (c.req.queries('facet') ?? []).filter((k) => k.length > 0);
|
|
5159
5201
|
const keys = asked.length === 0 ? undefined : z.array(z.enum(REQUEST_FACET_KEYS)).parse([...new Set(asked)]);
|
|
5160
|
-
|
|
5202
|
+
const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
|
|
5203
|
+
return c.json(await options.observability.tenantRequestFacets({ ...scope, services, ...(keys ? { keys } : {}) }));
|
|
5161
5204
|
});
|
|
5162
5205
|
// #1747: the tenant's `ctx.log` lines grouped by the template they were written from.
|
|
5163
5206
|
app.get('/observability/tenant-log-patterns', async (c) => {
|
|
@@ -5185,6 +5228,7 @@ export function createControlPlaneApi(options) {
|
|
|
5185
5228
|
buckets: narrow.buckets,
|
|
5186
5229
|
...(narrow.level.length ? { level: narrow.level } : {}),
|
|
5187
5230
|
...(narrow.operation.length ? { operation: narrow.operation } : {}),
|
|
5231
|
+
services: await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical),
|
|
5188
5232
|
}));
|
|
5189
5233
|
});
|
|
5190
5234
|
app.get('/observability/tenant-requests', async (c) => {
|
|
@@ -5193,7 +5237,8 @@ export function createControlPlaneApi(options) {
|
|
|
5193
5237
|
}
|
|
5194
5238
|
const scope = parseTenantRequestQuery(requestTenantOf(c), requestQueryOf(c));
|
|
5195
5239
|
const limit = z.coerce.number().int().min(1).max(200).default(50).parse(c.req.query('limit'));
|
|
5196
|
-
|
|
5240
|
+
const services = await appServices(c.get('actor'), scope.tenantId, scope.scopeId, scope.vertical);
|
|
5241
|
+
return c.json(await options.observability.tenantRequests({ ...scope, services, limit }));
|
|
5197
5242
|
});
|
|
5198
5243
|
/**
|
|
5199
5244
|
* Observed-vs-declared outbound egress for a vertical's deployed versions (#859, D-46).
|
|
@@ -5836,7 +5881,7 @@ export function createControlPlaneApi(options) {
|
|
|
5836
5881
|
// there; restore re-projects the vertical's roles from the dump's tuples). A one-shot
|
|
5837
5882
|
// DO storage blip heals on the in-request retry WITHOUT burning a CI attempt (which
|
|
5838
5883
|
// pushes a fresh version per try) — #559 (2).
|
|
5839
|
-
await restoreOrRecord(() => target.restoreScope(tenantId, previewId, tables));
|
|
5884
|
+
await restoreOrRecord(() => target.restoreScope(tenantId, previewId, tables, { sourceScopeId: source.id, exact: true }));
|
|
5840
5885
|
}
|
|
5841
5886
|
else {
|
|
5842
5887
|
// A clean-room preview (#509 (b)): an EMPTY scope, no source to export. No `forkedFrom`
|