@substrat-run/control-plane-api 0.108.0 → 0.110.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 +21 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +141 -2
- package/dist/api.js.map +1 -1
- package/dist/cf-observability.d.ts +18 -0
- package/dist/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +360 -23
- package/dist/cf-observability.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -25
- package/dist/client.js.map +1 -1
- package/dist/observability.d.ts +68 -0
- package/dist/observability.d.ts.map +1 -1
- package/dist/r2-backups.d.ts +20 -12
- package/dist/r2-backups.d.ts.map +1 -1
- package/dist/r2-backups.js +90 -0
- package/dist/r2-backups.js.map +1 -1
- package/dist/vertical-client.d.ts +3 -1
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +25 -16
- package/dist/vertical-client.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { z } from '@substrat-run/contracts';
|
|
2
3
|
import type { Connection, ConnectionActivity, ConnectionActivitySource, ConnectionCredential, ConnectionProbe, PlatformActorId } from '@substrat-run/contracts';
|
|
3
4
|
import type { ScopeHost } from '@substrat-run/kernel';
|
|
4
5
|
import type { PlatformActorAuth, BuilderAuth, Principal } from './auth.js';
|
|
@@ -288,6 +289,26 @@ type Vars = {
|
|
|
288
289
|
actor: PlatformActorId;
|
|
289
290
|
principal: Principal;
|
|
290
291
|
};
|
|
292
|
+
/**
|
|
293
|
+
* The K-35 denial-log filter (#867). Bounded by default like every other HTTP read
|
|
294
|
+
* here: the log's volume is attacker-influenceable by design (a probing client mints
|
|
295
|
+
* rows), so an unbounded `GET` is exactly the wrong default.
|
|
296
|
+
*
|
|
297
|
+
* The DECODER half of contracts' `denialFilterParams` (#971), and deliberately not the
|
|
298
|
+
* same schema: a query param arrives as a string, so `limit` is coerced here and given
|
|
299
|
+
* this route's own default, which the filter itself must not carry. What the two DO owe
|
|
300
|
+
* each other is the field set — Zod strips an unknown key, so a field added to the
|
|
301
|
+
* filter and sent by every client would be dropped here in silence. Exported (not from
|
|
302
|
+
* `index.ts` — this is not public surface) so `api.test.ts` can pin that.
|
|
303
|
+
*/
|
|
304
|
+
export declare const denialLogQuery: z.ZodObject<{
|
|
305
|
+
actor: z.ZodOptional<z.ZodString>;
|
|
306
|
+
permission: z.ZodOptional<z.ZodString>;
|
|
307
|
+
operation: z.ZodOptional<z.ZodString>;
|
|
308
|
+
since: z.ZodOptional<z.ZodString>;
|
|
309
|
+
until: z.ZodOptional<z.ZodString>;
|
|
310
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
311
|
+
}, z.core.$strip>;
|
|
291
312
|
export declare const DEFAULT_MODEL_MARGIN_PERCENT = 20;
|
|
292
313
|
/**
|
|
293
314
|
* The audited HTTP surface over `HostAdmin` (control-plane.md §4.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;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, createOrgInput, roleKey as roleKeySchema, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, } from '@substrat-run/contracts';
|
|
2
|
+
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, createOrgInput, roleKey as roleKeySchema, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, } from '@substrat-run/contracts';
|
|
3
3
|
import { attributeFailure } from './failure-attribution.js';
|
|
4
4
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
5
5
|
import { TENANT_HEADER } from './auth.js';
|
|
@@ -248,8 +248,15 @@ const auditLogQuery = z.object({
|
|
|
248
248
|
* The K-35 denial-log filter (#867). Bounded by default like every other HTTP read
|
|
249
249
|
* here: the log's volume is attacker-influenceable by design (a probing client mints
|
|
250
250
|
* rows), so an unbounded `GET` is exactly the wrong default.
|
|
251
|
+
*
|
|
252
|
+
* The DECODER half of contracts' `denialFilterParams` (#971), and deliberately not the
|
|
253
|
+
* same schema: a query param arrives as a string, so `limit` is coerced here and given
|
|
254
|
+
* this route's own default, which the filter itself must not carry. What the two DO owe
|
|
255
|
+
* each other is the field set — Zod strips an unknown key, so a field added to the
|
|
256
|
+
* filter and sent by every client would be dropped here in silence. Exported (not from
|
|
257
|
+
* `index.ts` — this is not public surface) so `api.test.ts` can pin that.
|
|
251
258
|
*/
|
|
252
|
-
const denialLogQuery = z.object({
|
|
259
|
+
export const denialLogQuery = z.object({
|
|
253
260
|
actor: z.string().optional(),
|
|
254
261
|
permission: z.string().optional(),
|
|
255
262
|
operation: z.string().optional(),
|
|
@@ -491,6 +498,13 @@ export function createControlPlaneApi(options) {
|
|
|
491
498
|
// go. Tenant-narrowed in the handler (the forced-filter pattern); the allowlist
|
|
492
499
|
// alone is not authz.
|
|
493
500
|
{ method: 'GET', re: /\/sweep-runs$/ },
|
|
501
|
+
// MY traffic and MY logs — the tenant grain (observability.md §3 view 4). The only
|
|
502
|
+
// observability routes a non-staff caller may reach, and safe to reach precisely
|
|
503
|
+
// because they are a different grain: their backends are keyed on the tenant, so
|
|
504
|
+
// there is no fleet-wide answer for a forgotten filter to fall back to. Narrowed in
|
|
505
|
+
// the handler by the forced-filter pattern; the allowlist alone is not authz.
|
|
506
|
+
{ method: 'GET', re: /\/observability\/tenant-metrics$/ },
|
|
507
|
+
{ method: 'GET', re: /\/observability\/tenant-logs$/ },
|
|
494
508
|
];
|
|
495
509
|
app.use('*', async (c, next) => {
|
|
496
510
|
if (c.get('principal').kind === 'builder') {
|
|
@@ -1519,6 +1533,29 @@ export function createControlPlaneApi(options) {
|
|
|
1519
1533
|
throw e;
|
|
1520
1534
|
}
|
|
1521
1535
|
});
|
|
1536
|
+
// #1239: facets over one scope's outbox — narrow, group, count. Delegated like
|
|
1537
|
+
// the history read below: through the vertical that holds the data when one
|
|
1538
|
+
// resolves, the co-located host otherwise.
|
|
1539
|
+
app.get('/tenants/:tenantId/scopes/:scopeId/facets', async (c) => {
|
|
1540
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1541
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
1542
|
+
const input = eventFacetInput.parse({
|
|
1543
|
+
groupBy: c.req.query('field') !== undefined
|
|
1544
|
+
? { kind: 'payload', field: c.req.query('field') }
|
|
1545
|
+
: { kind: c.req.query('groupBy') },
|
|
1546
|
+
type: c.req.query('type') ?? undefined,
|
|
1547
|
+
since: c.req.query('since') ?? undefined,
|
|
1548
|
+
until: c.req.query('until') ?? undefined,
|
|
1549
|
+
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
|
|
1550
|
+
});
|
|
1551
|
+
const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
1552
|
+
if (!scope)
|
|
1553
|
+
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
1554
|
+
const vertical = await verticalForScope(c, scope);
|
|
1555
|
+
return c.json(vertical
|
|
1556
|
+
? await vertical.facetEvents(scopeId, input)
|
|
1557
|
+
: await admin.facetEvents(c.get('actor'), tenantId, scopeId, input));
|
|
1558
|
+
});
|
|
1522
1559
|
// #1235: one record's event history — the per-entity pivot of the audit spine.
|
|
1523
1560
|
// Delegated exactly like the table reads: through the vertical that holds the
|
|
1524
1561
|
// scope's data when one resolves, the co-located host otherwise.
|
|
@@ -2849,6 +2886,39 @@ export function createControlPlaneApi(options) {
|
|
|
2849
2886
|
// sibling route would cost a second full parse for a field sitting beside this one.
|
|
2850
2887
|
return c.json({ schedules: parsed?.schedules ?? null, freshness: parsed?.freshness ?? null });
|
|
2851
2888
|
});
|
|
2889
|
+
/**
|
|
2890
|
+
* The declared FLOW of one version (#1234): every event type its modules say they
|
|
2891
|
+
* emit or consume, and the providers the vertical requires — the declared half of
|
|
2892
|
+
* a declared-vs-observed finding.
|
|
2893
|
+
*
|
|
2894
|
+
* Its own route rather than a ride on `/model`, unlike freshness on `/schedules`:
|
|
2895
|
+
* the model is the largest thing on a manifest, and the flow card needs none of it.
|
|
2896
|
+
* `declaredEvents` is null for a version pushed before the field existed, which the
|
|
2897
|
+
* caller must keep apart from a version that declares no events at all — the second
|
|
2898
|
+
* is a finding, the first is nothing to say.
|
|
2899
|
+
*/
|
|
2900
|
+
app.get('/verticals/:slug/versions/:id/flow', async (c) => {
|
|
2901
|
+
const p = c.get('principal');
|
|
2902
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
2903
|
+
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
2904
|
+
return c.json({ error: 'not found' }, 404);
|
|
2905
|
+
}
|
|
2906
|
+
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
2907
|
+
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
2908
|
+
return c.json({
|
|
2909
|
+
declaredEvents: parsed?.declaredEvents ?? null,
|
|
2910
|
+
// Whether that surface is the whole declaration or a sample cut at the cap. The
|
|
2911
|
+
// reader's "N of M checked" is a completeness claim, and it may not be made about
|
|
2912
|
+
// a manifest that says it was truncated.
|
|
2913
|
+
declaredEventsTruncated: parsed?.declaredEventsTruncated ?? false,
|
|
2914
|
+
requires: parsed?.requires ?? [],
|
|
2915
|
+
// The other two bands of the flow map (#1234), off the same parse. `schedules`
|
|
2916
|
+
// also has its own route, which answers a different question — did the run
|
|
2917
|
+
// happen — while this one wants the declaration as a node.
|
|
2918
|
+
schedules: parsed?.schedules ?? [],
|
|
2919
|
+
outbound: parsed?.outbound ?? [],
|
|
2920
|
+
});
|
|
2921
|
+
});
|
|
2852
2922
|
app.get('/verticals/:slug/versions/:id/model', async (c) => {
|
|
2853
2923
|
const p = c.get('principal');
|
|
2854
2924
|
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
@@ -3597,6 +3667,75 @@ export function createControlPlaneApi(options) {
|
|
|
3597
3667
|
});
|
|
3598
3668
|
return c.json(await options.observability.recentLogs(input));
|
|
3599
3669
|
});
|
|
3670
|
+
// -- the TENANT grain (observability.md §3 view 4, §4.2) -----------------------
|
|
3671
|
+
//
|
|
3672
|
+
// Everything above is script grain: one deployed unit, which serves every tenant that
|
|
3673
|
+
// installed the vertical. Safe for staff and for the vertical's builder, forbidden to
|
|
3674
|
+
// an installer — §3 is explicit that per-tenant numbers from script-grain data leak.
|
|
3675
|
+
// These two routes are the other grain, and the difference is not a filter over the
|
|
3676
|
+
// same data: they read the router's own tenant-stamped datapoints and the verticals'
|
|
3677
|
+
// stamped log lines, which is the only place the tenant dimension exists at all.
|
|
3678
|
+
//
|
|
3679
|
+
// Unlike their neighbours these ARE in BUILDER_ROUTES, because the whole point is that
|
|
3680
|
+
// an installer may read them — about themselves. The tenant comes from the principal
|
|
3681
|
+
// for a builder and must be named explicitly by a staff/service caller: the
|
|
3682
|
+
// `/service-refs` forced-filter pattern, for its reason, since answering fleet-wide on
|
|
3683
|
+
// a forgotten parameter is the exact leak being closed here.
|
|
3684
|
+
app.get('/observability/tenant-metrics', async (c) => {
|
|
3685
|
+
if (!options.observability?.tenantMetrics) {
|
|
3686
|
+
return c.json({ error: 'tenant-grain observability is not configured on this control plane' }, 501);
|
|
3687
|
+
}
|
|
3688
|
+
const p = c.get('principal');
|
|
3689
|
+
const tenantId = p.kind === 'builder' ? p.tenantId : c.req.query('tenantId');
|
|
3690
|
+
if (!tenantId)
|
|
3691
|
+
throw new ControlPlaneError(400, 'tenantId is required');
|
|
3692
|
+
const input = z
|
|
3693
|
+
.object({
|
|
3694
|
+
tenantId: tenantIdSchema,
|
|
3695
|
+
// Narrowing WITHIN the tenant. Neither can widen the answer — the reader always
|
|
3696
|
+
// applies the tenant predicate — so a foreign scope id yields zero rows rather
|
|
3697
|
+
// than somebody else's, and needs no ownership check of its own to be safe.
|
|
3698
|
+
scopeId: z.string().min(1).max(64).optional(),
|
|
3699
|
+
vertical: z.string().min(1).max(200).optional(),
|
|
3700
|
+
hours: z.coerce.number().int().min(1).max(72).default(24),
|
|
3701
|
+
})
|
|
3702
|
+
.parse({
|
|
3703
|
+
tenantId,
|
|
3704
|
+
scopeId: c.req.query('scopeId') || undefined,
|
|
3705
|
+
vertical: c.req.query('vertical') || undefined,
|
|
3706
|
+
hours: c.req.query('hours'),
|
|
3707
|
+
});
|
|
3708
|
+
return c.json(await options.observability.tenantMetrics(input));
|
|
3709
|
+
});
|
|
3710
|
+
app.get('/observability/tenant-logs', async (c) => {
|
|
3711
|
+
if (!options.observability?.tenantLogs) {
|
|
3712
|
+
return c.json({ error: 'tenant-grain observability is not configured on this control plane' }, 501);
|
|
3713
|
+
}
|
|
3714
|
+
const p = c.get('principal');
|
|
3715
|
+
const tenantId = p.kind === 'builder' ? p.tenantId : c.req.query('tenantId');
|
|
3716
|
+
if (!tenantId)
|
|
3717
|
+
throw new ControlPlaneError(400, 'tenantId is required');
|
|
3718
|
+
const input = z
|
|
3719
|
+
.object({
|
|
3720
|
+
tenantId: tenantIdSchema,
|
|
3721
|
+
scopeId: z.string().min(1).max(64).optional(),
|
|
3722
|
+
vertical: z.string().min(1).max(200).optional(),
|
|
3723
|
+
level: z.enum(['log', 'info', 'warn', 'error', 'debug']).optional(),
|
|
3724
|
+
search: z.string().min(1).max(200).optional(),
|
|
3725
|
+
hours: z.coerce.number().int().min(1).max(72).default(24),
|
|
3726
|
+
limit: z.coerce.number().int().min(1).max(200).default(100),
|
|
3727
|
+
})
|
|
3728
|
+
.parse({
|
|
3729
|
+
tenantId,
|
|
3730
|
+
scopeId: c.req.query('scopeId') || undefined,
|
|
3731
|
+
vertical: c.req.query('vertical') || undefined,
|
|
3732
|
+
level: c.req.query('level') || undefined,
|
|
3733
|
+
search: c.req.query('search') || undefined,
|
|
3734
|
+
hours: c.req.query('hours'),
|
|
3735
|
+
limit: c.req.query('limit'),
|
|
3736
|
+
});
|
|
3737
|
+
return c.json(await options.observability.tenantLogs(input));
|
|
3738
|
+
});
|
|
3600
3739
|
/**
|
|
3601
3740
|
* Observed-vs-declared outbound egress for a vertical's deployed versions (#859, D-46).
|
|
3602
3741
|
*
|