@substrat-run/control-plane-api 0.98.0 → 0.102.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 +215 -8
- package/dist/api.js.map +1 -1
- package/dist/cf-observability.js +1 -1
- package/dist/cf-observability.js.map +1 -1
- package/dist/custom-hostnames.d.ts +6 -1
- package/dist/custom-hostnames.d.ts.map +1 -1
- package/dist/custom-hostnames.js +1 -1
- package/dist/custom-hostnames.js.map +1 -1
- package/dist/deploy.d.ts +5 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +9 -0
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- 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 +4 -2
- package/dist/observability.d.ts.map +1 -1
- package/dist/platform-drain.d.ts +18 -0
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +54 -1
- package/dist/platform-drain.js.map +1 -1
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +12 -1
- package/dist/wfp.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;
|
|
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,6 @@
|
|
|
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, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, registerVerticalInput, 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, 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
|
+
import { attributeFailure } from './failure-attribution.js';
|
|
3
4
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
4
5
|
import { TENANT_HEADER } from './auth.js';
|
|
5
6
|
import { connectionGrantsForScope } from './vertical-client.js';
|
|
@@ -253,14 +254,46 @@ const opsFailuresQuery = z.object({
|
|
|
253
254
|
tenantId: tenantIdSchema.optional(),
|
|
254
255
|
scopeId: scopeIdSchema.optional(),
|
|
255
256
|
vertical: z.string().optional(),
|
|
257
|
+
// The version-registry id — the release-health narrowing (#1231).
|
|
258
|
+
version: z.string().optional(),
|
|
256
259
|
operation: z.string().optional(),
|
|
257
260
|
// Exact match — the lookup a CI log's `reference = <id>` line lands on.
|
|
258
261
|
reference: z.string().optional(),
|
|
262
|
+
// The taxonomy code — the error-shape narrowing an issues view groups by (#1233).
|
|
263
|
+
code: errorCode.optional(),
|
|
259
264
|
since: z.string().optional(),
|
|
260
265
|
until: z.string().optional(),
|
|
261
266
|
// Bounded by default exactly as /admin-log, and for the same reason.
|
|
262
267
|
...listPageQuery.shape,
|
|
263
268
|
});
|
|
269
|
+
// The issues read (#1233). No cursor by design: grouping IS the compression —
|
|
270
|
+
// cardinality is the number of distinct failure shapes — and `limit` bounds it.
|
|
271
|
+
const issuesQuery = z.object({
|
|
272
|
+
status: issueStatus.optional(),
|
|
273
|
+
operation: z.string().optional(),
|
|
274
|
+
code: errorCode.optional(),
|
|
275
|
+
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
276
|
+
});
|
|
277
|
+
// The lifecycle verdict (#1233). The fingerprint rides in the BODY, not the path:
|
|
278
|
+
// it embeds U+001F by construction, and a path segment would force every caller
|
|
279
|
+
// through percent-encoding it can get subtly wrong.
|
|
280
|
+
const issueStatusUpdate = z.object({
|
|
281
|
+
fingerprint: z.string().min(1),
|
|
282
|
+
status: issueStatusInput,
|
|
283
|
+
});
|
|
284
|
+
const sweepRunsQuery = z.object({
|
|
285
|
+
kind: sweepRunKind.optional(),
|
|
286
|
+
unit: z.string().optional(),
|
|
287
|
+
outcome: sweepRunOutcome.optional(),
|
|
288
|
+
tenantId: tenantIdSchema.optional(),
|
|
289
|
+
scopeId: scopeIdSchema.optional(),
|
|
290
|
+
vertical: z.string().optional(),
|
|
291
|
+
connectionId: z.string().optional(),
|
|
292
|
+
since: z.string().optional(),
|
|
293
|
+
until: z.string().optional(),
|
|
294
|
+
// Bounded by default exactly as /ops-failures, and for the same reason.
|
|
295
|
+
...listPageQuery.shape,
|
|
296
|
+
});
|
|
264
297
|
const modelUsageQuery = z.object({
|
|
265
298
|
tenantId: tenantIdSchema.optional(),
|
|
266
299
|
scopeId: scopeIdSchema.optional(),
|
|
@@ -355,6 +388,10 @@ export function createControlPlaneApi(options) {
|
|
|
355
388
|
// The declared permission registry of one version (D-39, #336) — owner-narrowed in the
|
|
356
389
|
// handler like the versions list; the builder-facing Permissions tab reads it.
|
|
357
390
|
{ method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/registry$/ },
|
|
391
|
+
// The emitted entity model of one version (#1214) — owner-narrowed the same way;
|
|
392
|
+
// the dashboard's Model tab reads it.
|
|
393
|
+
{ method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/model$/ },
|
|
394
|
+
{ method: 'GET', re: /\/verticals\/[^/]+\/versions\/[^/]+\/schedules$/ },
|
|
358
395
|
{ method: 'GET', re: /\/verticals\/[^/]+\/channels$/ },
|
|
359
396
|
{ method: 'GET', re: /\/verticals\/[^/]+\/channels\/[^/]+\/history$/ },
|
|
360
397
|
{ method: 'POST', re: /\/verticals\/[^/]+\/channels\/[^/]+\/promote$/ },
|
|
@@ -399,6 +436,14 @@ export function createControlPlaneApi(options) {
|
|
|
399
436
|
// preview / provision fail. Tenant-narrowed in the handler (the forced-filter
|
|
400
437
|
// pattern, like GET /scopes); the allowlist alone is not authz.
|
|
401
438
|
{ method: 'GET', re: /\/ops-failures$/ },
|
|
439
|
+
// The service→(vertical, version) join (#1231): what MY deployed scripts mean in
|
|
440
|
+
// signal dimensions. Tenant-narrowed in the handler (the same forced-filter
|
|
441
|
+
// pattern); the allowlist alone is not authz.
|
|
442
|
+
{ method: 'GET', re: /\/service-refs$/ },
|
|
443
|
+
// The sweep record (#1232): when were MY connections last swept, and how did it
|
|
444
|
+
// go. Tenant-narrowed in the handler (the forced-filter pattern); the allowlist
|
|
445
|
+
// alone is not authz.
|
|
446
|
+
{ method: 'GET', re: /\/sweep-runs$/ },
|
|
402
447
|
];
|
|
403
448
|
app.use('*', async (c, next) => {
|
|
404
449
|
if (c.get('principal').kind === 'builder') {
|
|
@@ -412,10 +457,21 @@ export function createControlPlaneApi(options) {
|
|
|
412
457
|
// mask the failure it is recording, so every path through this swallows its own
|
|
413
458
|
// errors. The upstream reference is extracted here — one place — so a caller
|
|
414
459
|
// that only has the message still lands a searchable row.
|
|
415
|
-
|
|
460
|
+
// Not `cause?: unknown` with an undefined check: a caught value CAN be literally
|
|
461
|
+
// `undefined` (`throw undefined`, a bare rejection), and that throw deserves an
|
|
462
|
+
// `unknown` attribution, not a skipped one. Only true omission skips.
|
|
463
|
+
const NO_CAUSE = Symbol('no-cause');
|
|
464
|
+
const recordFailure = (entry, cause = NO_CAUSE) => {
|
|
465
|
+
// The error SHAPE rides beside the prose (#1233): attributed here — one place,
|
|
466
|
+
// from the throw itself — so a fingerprint groups on a column and never has to
|
|
467
|
+
// regex a message. A caller with no throw in hand leaves the columns null,
|
|
468
|
+
// which reads as "nobody classified this" rather than a guess nobody made.
|
|
469
|
+
const attributed = cause === NO_CAUSE ? undefined : attributeFailure(cause);
|
|
416
470
|
void admin
|
|
417
471
|
.recordOpsFailure({
|
|
418
472
|
...entry,
|
|
473
|
+
origin: entry.origin ?? attributed?.origin ?? null,
|
|
474
|
+
code: entry.code ?? attributed?.code ?? null,
|
|
419
475
|
reference: entry.reference ?? UPSTREAM_REFERENCE.exec(entry.message)?.[1] ?? null,
|
|
420
476
|
})
|
|
421
477
|
.catch(() => undefined);
|
|
@@ -474,7 +530,7 @@ export function createControlPlaneApi(options) {
|
|
|
474
530
|
scopeId: c.req.param('scopeId') ?? null,
|
|
475
531
|
status,
|
|
476
532
|
message: err instanceof Error ? err.message : String(err),
|
|
477
|
-
});
|
|
533
|
+
}, err);
|
|
478
534
|
}
|
|
479
535
|
// A 500 is, by definition, a throw whose message `mapError` did not recognise — so the
|
|
480
536
|
// client gets a GENERIC body that discloses nothing, and until now nothing recorded WHAT
|
|
@@ -1350,9 +1406,10 @@ export function createControlPlaneApi(options) {
|
|
|
1350
1406
|
tenantId,
|
|
1351
1407
|
scopeId,
|
|
1352
1408
|
vertical: scope.vertical,
|
|
1409
|
+
version: scope.verticalVersionId ?? null,
|
|
1353
1410
|
status: e instanceof ControlPlaneError ? e.status : null,
|
|
1354
1411
|
message,
|
|
1355
|
-
});
|
|
1412
|
+
}, e);
|
|
1356
1413
|
storeErrors.push(message);
|
|
1357
1414
|
return undefined;
|
|
1358
1415
|
}
|
|
@@ -2490,7 +2547,7 @@ export function createControlPlaneApi(options) {
|
|
|
2490
2547
|
tenantId: input.tenantId,
|
|
2491
2548
|
status: e.status,
|
|
2492
2549
|
message: e.message,
|
|
2493
|
-
});
|
|
2550
|
+
}, e);
|
|
2494
2551
|
}
|
|
2495
2552
|
return c.json({ error: e.message }, e.status);
|
|
2496
2553
|
}
|
|
@@ -2562,6 +2619,68 @@ export function createControlPlaneApi(options) {
|
|
|
2562
2619
|
const visible = p.kind === 'builder' ? all.filter((v) => v.ownerTenant === p.tenantId) : all;
|
|
2563
2620
|
return c.json(pageSlice(visible, page, (v) => v.slug));
|
|
2564
2621
|
});
|
|
2622
|
+
// -- the service→dimensions resolver (#1231's last item) -----------------------
|
|
2623
|
+
// What a Cloudflare service ref MEANS: each owned vertical's serving script and
|
|
2624
|
+
// per-version archive scripts, mapped to the signals stamp {vertical, version}
|
|
2625
|
+
// plus the human label beside it. A DIRECTORY read — deliberately not behind the
|
|
2626
|
+
// `options.observability` 501 guard, because the join must answer (for the
|
|
2627
|
+
// release-health and issues views) whether or not a telemetry backend is
|
|
2628
|
+
// configured. The dashboard's ownedServiceRefs() fetches this instead of
|
|
2629
|
+
// re-deriving it, so every consumer means the same thing by `version`.
|
|
2630
|
+
app.get('/service-refs', async (c) => {
|
|
2631
|
+
const p = c.get('principal');
|
|
2632
|
+
// The /ops-failures forced-filter pattern: a builder's tenant comes from the
|
|
2633
|
+
// principal, never the query. A staff/service caller must SAY whose view it
|
|
2634
|
+
// wants — answering fleet-wide on a forgotten param would leak, so refuse.
|
|
2635
|
+
const tenantId = p.kind === 'builder' ? p.tenantId : c.req.query('tenantId');
|
|
2636
|
+
if (!tenantId)
|
|
2637
|
+
throw new ControlPlaneError(400, 'tenantId is required');
|
|
2638
|
+
const query = z
|
|
2639
|
+
.object({ tenantId: tenantIdSchema, vertical: z.string().optional() })
|
|
2640
|
+
.parse({ tenantId, vertical: c.req.query('vertical') });
|
|
2641
|
+
const actor = c.get('actor');
|
|
2642
|
+
const all = (await admin.listVerticals(actor)).filter((v) => v.ownerTenant === query.tenantId);
|
|
2643
|
+
let considered = all;
|
|
2644
|
+
if (query.vertical !== undefined) {
|
|
2645
|
+
const slug = await resolveVerticalId(c, query.vertical);
|
|
2646
|
+
considered = all.filter((v) => v.slug === slug);
|
|
2647
|
+
// Non-ownership reads as absence (K-3), exactly like the per-slug registry reads.
|
|
2648
|
+
if (considered.length === 0)
|
|
2649
|
+
throw new ControlPlaneError(404, 'not found');
|
|
2650
|
+
}
|
|
2651
|
+
// Cap the per-vertical fan-out and REPORT the cut — truncating silently is the
|
|
2652
|
+
// defect the egress route names (#859), and the same posture holds here.
|
|
2653
|
+
const MAX_VERTICALS = 50;
|
|
2654
|
+
const verticalsTruncated = considered.length > MAX_VERTICALS;
|
|
2655
|
+
const entries = [];
|
|
2656
|
+
for (const v of considered.slice(0, MAX_VERTICALS)) {
|
|
2657
|
+
const versions = await admin.listVersions(actor, v.slug);
|
|
2658
|
+
const labelOf = new Map(versions.map((ver) => [ver.id, ver.version]));
|
|
2659
|
+
// The stable serving script — where real traffic lands. Its version is the
|
|
2660
|
+
// registry row's own servingVersionId: authoritative, not the scope-derived
|
|
2661
|
+
// approximation the dashboard used to make.
|
|
2662
|
+
if (v.servingRef && v.servingVersionId) {
|
|
2663
|
+
entries.push(serviceDimensions.parse({
|
|
2664
|
+
service: v.servingRef,
|
|
2665
|
+
role: 'serving',
|
|
2666
|
+
stamp: { vertical: v.slug, version: v.servingVersionId },
|
|
2667
|
+
versionLabel: labelOf.get(v.servingVersionId) ?? null,
|
|
2668
|
+
}));
|
|
2669
|
+
}
|
|
2670
|
+
// Each per-version archive script IS its version (previews serve these).
|
|
2671
|
+
for (const ver of versions) {
|
|
2672
|
+
if (!ver.deploymentRef)
|
|
2673
|
+
continue;
|
|
2674
|
+
entries.push(serviceDimensions.parse({
|
|
2675
|
+
service: ver.deploymentRef,
|
|
2676
|
+
role: 'archive',
|
|
2677
|
+
stamp: { vertical: v.slug, version: ver.id },
|
|
2678
|
+
versionLabel: ver.version,
|
|
2679
|
+
}));
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return c.json({ entries, verticalsTruncated });
|
|
2683
|
+
});
|
|
2565
2684
|
app.post('/verticals', async (c) => {
|
|
2566
2685
|
const input = registerVerticalInput.parse(await c.req.json());
|
|
2567
2686
|
const p = c.get('principal');
|
|
@@ -2628,6 +2747,35 @@ export function createControlPlaneApi(options) {
|
|
|
2628
2747
|
const registry = json ? (storedDeployManifest.parse(JSON.parse(json)).registry ?? null) : null;
|
|
2629
2748
|
return c.json({ registry });
|
|
2630
2749
|
});
|
|
2750
|
+
// The emitted entity model (#1214) one version ships: entities, field schemas, parent
|
|
2751
|
+
// edges, declared lifecycles — read out of the same retained manifest as the registry
|
|
2752
|
+
// above, and owner-narrowed the same way. `model` is null for a version pushed by a
|
|
2753
|
+
// pre-#1214 CLI or by a vertical with no model.json — the dashboard's Model tab renders
|
|
2754
|
+
// an empty state for both, and distinguishing them buys the tenant nothing.
|
|
2755
|
+
// #1232: the declared schedules of ONE version — what the dashboard's schedule-health
|
|
2756
|
+
// view joins the sweep record against. Null for a version pushed before the field.
|
|
2757
|
+
app.get('/verticals/:slug/versions/:id/schedules', async (c) => {
|
|
2758
|
+
const p = c.get('principal');
|
|
2759
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
2760
|
+
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
2761
|
+
return c.json({ error: 'not found' }, 404);
|
|
2762
|
+
}
|
|
2763
|
+
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
2764
|
+
const parsed = json ? storedDeployManifest.parse(JSON.parse(json)) : null;
|
|
2765
|
+
// #1232: freshness rides the same read — the manifest is already in hand, and a
|
|
2766
|
+
// sibling route would cost a second full parse for a field sitting beside this one.
|
|
2767
|
+
return c.json({ schedules: parsed?.schedules ?? null, freshness: parsed?.freshness ?? null });
|
|
2768
|
+
});
|
|
2769
|
+
app.get('/verticals/:slug/versions/:id/model', async (c) => {
|
|
2770
|
+
const p = c.get('principal');
|
|
2771
|
+
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
2772
|
+
if (p.kind === 'builder' && (await ownerOf(p.actor, slug)) !== p.tenantId) {
|
|
2773
|
+
return c.json({ error: 'not found' }, 404);
|
|
2774
|
+
}
|
|
2775
|
+
const json = await admin.versionManifest(c.get('actor'), slug, c.req.param('id'));
|
|
2776
|
+
const model = json ? (storedDeployManifest.parse(JSON.parse(json)).model ?? null) : null;
|
|
2777
|
+
return c.json({ model });
|
|
2778
|
+
});
|
|
2631
2779
|
// The static files (#340) one version ships: path, size, content type, content address —
|
|
2632
2780
|
// read straight out of the retained manifest, which is where they were persisted for the
|
|
2633
2781
|
// promote path anyway. Owner-narrowed exactly like the registry route above. `assets` is
|
|
@@ -2792,6 +2940,11 @@ export function createControlPlaneApi(options) {
|
|
|
2792
2940
|
modules,
|
|
2793
2941
|
doClasses: manifest.doClasses,
|
|
2794
2942
|
bindings: [...manifest.bindings, ...storeBindings],
|
|
2943
|
+
// #1242: the version now being served, injected as `SUBSTRAT_VERSION_ID` so the
|
|
2944
|
+
// scope host stamps the signals `version` dimension. The serving script is one
|
|
2945
|
+
// stable script updated in place, so this refresh is what keeps the binding
|
|
2946
|
+
// naming the code that actually runs.
|
|
2947
|
+
versionId,
|
|
2795
2948
|
// #1054: the model runtime is bound only for a version that ASKED for it, so the
|
|
2796
2949
|
// capability is visible in the manifest diff at admit rather than fleet-wide.
|
|
2797
2950
|
...(manifest.usesModels ? { usesModels: true } : {}),
|
|
@@ -2883,7 +3036,7 @@ export function createControlPlaneApi(options) {
|
|
|
2883
3036
|
catch (e) {
|
|
2884
3037
|
const message = e instanceof Error ? e.message : String(e);
|
|
2885
3038
|
console.error('stores.backfill.failed', { slug, detail: message });
|
|
2886
|
-
recordFailure({ actor, operation: 'promote.store-backfill', vertical: slug, message });
|
|
3039
|
+
recordFailure({ actor, operation: 'promote.store-backfill', vertical: slug, message }, e);
|
|
2887
3040
|
return { minted: [], error: message };
|
|
2888
3041
|
}
|
|
2889
3042
|
};
|
|
@@ -3178,6 +3331,9 @@ export function createControlPlaneApi(options) {
|
|
|
3178
3331
|
modules,
|
|
3179
3332
|
doClasses: manifest.doClasses,
|
|
3180
3333
|
bindings: manifest.bindings,
|
|
3334
|
+
// #1242: the freshly minted version id — this archive script IS that version,
|
|
3335
|
+
// and a preview routed at it stamps the signals `version` dimension honestly.
|
|
3336
|
+
versionId: id,
|
|
3181
3337
|
// #340: the verified bytes go up with the bundle. The manifest's routing config
|
|
3182
3338
|
// rides along untouched — it decides what the RUNTIME does with paths, and carries
|
|
3183
3339
|
// no reach, so there is nothing in it for the sandbox contract to refuse.
|
|
@@ -3210,7 +3366,7 @@ export function createControlPlaneApi(options) {
|
|
|
3210
3366
|
tenantId: ownerTenant ?? null,
|
|
3211
3367
|
status: rejected ? 422 : 502,
|
|
3212
3368
|
message: detail,
|
|
3213
|
-
});
|
|
3369
|
+
}, e);
|
|
3214
3370
|
return rejected
|
|
3215
3371
|
? c.json({ error: 'deploy rejected', detail }, 422)
|
|
3216
3372
|
: c.json({ error: 'deploy upload failed', detail }, 502);
|
|
@@ -3801,9 +3957,10 @@ export function createControlPlaneApi(options) {
|
|
|
3801
3957
|
tenantId,
|
|
3802
3958
|
scopeId: previewId,
|
|
3803
3959
|
vertical: slug,
|
|
3960
|
+
version: opts.versionId,
|
|
3804
3961
|
status: e.status,
|
|
3805
3962
|
message: e.message,
|
|
3806
|
-
});
|
|
3963
|
+
}, e);
|
|
3807
3964
|
}
|
|
3808
3965
|
throw e;
|
|
3809
3966
|
}
|
|
@@ -4021,7 +4178,9 @@ export function createControlPlaneApi(options) {
|
|
|
4021
4178
|
tenantId: p.kind === 'builder' ? p.tenantId : c.req.query('tenantId'),
|
|
4022
4179
|
scopeId: c.req.query('scopeId'),
|
|
4023
4180
|
vertical: c.req.query('vertical'),
|
|
4181
|
+
version: c.req.query('version'),
|
|
4024
4182
|
operation: c.req.query('operation'),
|
|
4183
|
+
code: c.req.query('code'),
|
|
4025
4184
|
reference: c.req.query('reference'),
|
|
4026
4185
|
since: c.req.query('since'),
|
|
4027
4186
|
until: c.req.query('until'),
|
|
@@ -4032,6 +4191,54 @@ export function createControlPlaneApi(options) {
|
|
|
4032
4191
|
const entries = await admin.listOpsFailures(c.get('actor'), filter);
|
|
4033
4192
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
4034
4193
|
});
|
|
4194
|
+
// -- sweep runs (#1232): the durable record of what a sweep pass touched -------
|
|
4195
|
+
// Staff read fleet-wide (the console's later view); a builder's tenant is forced
|
|
4196
|
+
// from the principal, the /ops-failures posture exactly.
|
|
4197
|
+
app.get('/sweep-runs', async (c) => {
|
|
4198
|
+
const p = c.get('principal');
|
|
4199
|
+
const filter = sweepRunsQuery.parse({
|
|
4200
|
+
kind: c.req.query('kind'),
|
|
4201
|
+
unit: c.req.query('unit'),
|
|
4202
|
+
outcome: c.req.query('outcome'),
|
|
4203
|
+
tenantId: p.kind === 'builder' ? p.tenantId : c.req.query('tenantId'),
|
|
4204
|
+
scopeId: c.req.query('scopeId'),
|
|
4205
|
+
vertical: c.req.query('vertical'),
|
|
4206
|
+
connectionId: c.req.query('connectionId'),
|
|
4207
|
+
since: c.req.query('since'),
|
|
4208
|
+
until: c.req.query('until'),
|
|
4209
|
+
limit: c.req.query('limit'),
|
|
4210
|
+
cursor: c.req.query('cursor'),
|
|
4211
|
+
order: c.req.query('order'),
|
|
4212
|
+
});
|
|
4213
|
+
const entries = await admin.listSweepRuns(c.get('actor'), filter);
|
|
4214
|
+
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
4215
|
+
});
|
|
4216
|
+
// -- issues (#1233): failures grouped by fingerprint, with a lifecycle --------
|
|
4217
|
+
// Staff-only (absent from BUILDER_ROUTES): an issue is a fleet-scoped aggregate
|
|
4218
|
+
// with no tenant column, so the forced-filter posture cannot narrow it — a
|
|
4219
|
+
// builder's per-app view derives from their tenant-forced /ops-failures instead.
|
|
4220
|
+
app.get('/issues', async (c) => {
|
|
4221
|
+
const filter = issuesQuery.parse({
|
|
4222
|
+
status: c.req.query('status'),
|
|
4223
|
+
operation: c.req.query('operation'),
|
|
4224
|
+
code: c.req.query('code'),
|
|
4225
|
+
limit: c.req.query('limit'),
|
|
4226
|
+
});
|
|
4227
|
+
const entries = await admin.listIssues(c.get('actor'), filter);
|
|
4228
|
+
// No cursor, deliberately — see issuesQuery. `entries` alone, so a client
|
|
4229
|
+
// never walks a continuation that cannot exist.
|
|
4230
|
+
return c.json({ entries });
|
|
4231
|
+
});
|
|
4232
|
+
// The staff verdict on one issue: resolve, ignore, or reopen. `regressed` is
|
|
4233
|
+
// ingest's word and the input schema refuses it. 404 for an unknown fingerprint
|
|
4234
|
+
// rather than an invented row.
|
|
4235
|
+
app.put('/issues/status', async (c) => {
|
|
4236
|
+
const input = issueStatusUpdate.parse(await c.req.json());
|
|
4237
|
+
const updated = await admin.setIssueStatus(c.get('actor'), input.fingerprint, input.status);
|
|
4238
|
+
if (!updated)
|
|
4239
|
+
return c.json({ error: 'unknown issue fingerprint' }, 404);
|
|
4240
|
+
return c.json(updated);
|
|
4241
|
+
});
|
|
4035
4242
|
// -- model usage (#1054): meter 3, the one D-30 could not compute -------------
|
|
4036
4243
|
// The ledger the `model-usage` intents drain into. Staff-only: the tenant-facing
|
|
4037
4244
|
// read is the vertical's own usage screen over its metering engine; this is the
|