@substrat-run/control-plane-api 0.113.0 → 0.116.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":"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"}
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, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, REDRAIN_BATCH, } 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, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, REDRAIN_BATCH, } 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';
@@ -1660,6 +1660,23 @@ export function createControlPlaneApi(options) {
1660
1660
  colocated: () => admin.invocationEvents(c.get('actor'), tenantId, scopeId, input),
1661
1661
  }, (r) => r.events.length));
1662
1662
  });
1663
+ // #1525: every delivery in the scope that gave up — the list the walks can only reach
1664
+ // one event at a time. Delegated like them; the row names no subject, so it logs none.
1665
+ app.get('/tenants/:tenantId/scopes/:scopeId/dead-letters', async (c) => {
1666
+ const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
1667
+ const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
1668
+ const input = deadLettersInput.parse({
1669
+ limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
1670
+ cursor: c.req.query('cursor') ?? undefined,
1671
+ });
1672
+ const scope = await admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
1673
+ if (!scope)
1674
+ return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
1675
+ return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'deadLetters', input, {
1676
+ viaVertical: (v) => v.deadLetters(scopeId, input),
1677
+ colocated: () => admin.deadLetters(c.get('actor'), tenantId, scopeId, input),
1678
+ }, (r) => r.entries.length));
1679
+ });
1663
1680
  // #1235: one record's event history — the per-entity pivot of the audit spine.
1664
1681
  // Delegated exactly like the table reads: through the vertical that holds the
1665
1682
  // scope's data when one resolves, the co-located host otherwise.
@@ -2208,15 +2225,68 @@ export function createControlPlaneApi(options) {
2208
2225
  'already shipped to the rebuilt table. Pass the instant the old table stopped receiving.',
2209
2226
  }, 400);
2210
2227
  }
2228
+ // The input schema also carries `countOnly` (#1545), and this route does not honour it:
2229
+ // its answer (`redrained`, `more`) is a statement that rows moved. Ignoring the field
2230
+ // silently would be the exact failure the separate count route exists to prevent — a
2231
+ // caller asking for a rehearsal and getting a reopen — so it is REFUSED here and the
2232
+ // caller is told where counting lives. Refused whatever its value: on this route the
2233
+ // field has no meaning at all, and "accepted when false" is a rule nobody can rely on.
2234
+ if (parsed.data.countOnly !== undefined) {
2235
+ return c.json({ error: 'countOnly is not honoured here — POST the same body to …/redrain-count for a read-only count' }, 400);
2236
+ }
2211
2237
  const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
2212
2238
  if (!scope)
2213
2239
  return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
2214
- const redrained = await admin.redrainEvents(actor, tenantId, scopeId, parsed.data);
2240
+ // The instant alone, never `parsed.data`, so the field cannot reach the adapter from
2241
+ // this door even if the refusal above is ever relaxed.
2242
+ const redrained = await admin.redrainEvents(actor, tenantId, scopeId, {
2243
+ drainedBefore: parsed.data.drainedBefore,
2244
+ });
2215
2245
  // `more` rather than silence: the verb reopens a bounded batch, so a caller that posts
2216
2246
  // once can be holding a partial reopen. Saying so is what stops it reading as a finished
2217
2247
  // window — the operator (or `pnpm lake:redrain`) posts again until `redrained` is 0.
2218
2248
  return c.json({ redrained, more: redrained >= REDRAIN_BATCH, drainedBefore: parsed.data.drainedBefore });
2219
2249
  });
2250
+ // #1545 — how many rows the route above WOULD reopen, reopening none of them: what a dry
2251
+ // run is run to learn, and what nothing could answer before. Staff/service only for the
2252
+ // same reason as the reopen (absent from BUILDER_ROUTES, which is default-deny), even
2253
+ // though it changes nothing: the number is a fact about one tenant's history.
2254
+ //
2255
+ // Its own path rather than a `countOnly` field on the POST above, which is how the flag
2256
+ // travels everywhere BELOW this door. `scripts/lake-redrain.mjs` runs from a checkout
2257
+ // against a control plane deployed on its own clock, so the script is routinely newer
2258
+ // than the route. A field an older route does not know is stripped by its Zod parse —
2259
+ // and that route would reopen the window and answer `{ redrained: N }`, which a dry run
2260
+ // would print as its count. A path it does not serve 404s, and nothing moves.
2261
+ //
2262
+ // No `more`: the count is unbounded (an aggregate materialises no rows), so it answers
2263
+ // for the whole window in one call and there is no second page to hint at.
2264
+ app.post('/tenants/:tenantId/scopes/:scopeId/redrain-count', async (c) => {
2265
+ const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
2266
+ const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
2267
+ const actor = c.get('actor');
2268
+ const parsed = redrainEventsInput.safeParse(await c.req.json().catch(() => ({})));
2269
+ if (!parsed.success) {
2270
+ return c.json({ error: 'body must be { drainedBefore: <ISO 8601 instant> } — the window to count' }, 400);
2271
+ }
2272
+ // The same future refusal as the reopen. A count of a window that has not closed yet
2273
+ // is not dangerous, it is meaningless — and a dry run whose instant this door accepts
2274
+ // and the real run rejects would be the worst kind of rehearsal.
2275
+ if (Date.parse(parsed.data.drainedBefore) > Date.now()) {
2276
+ return c.json({
2277
+ error: `drainedBefore ${parsed.data.drainedBefore} is in the future — it would count rows ` +
2278
+ 'already shipped to the rebuilt table. Pass the instant the old table stopped receiving.',
2279
+ }, 400);
2280
+ }
2281
+ const scope = await admin.getScopeRecord(actor, tenantId, scopeId);
2282
+ if (!scope)
2283
+ return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
2284
+ const redrainable = await admin.redrainEvents(actor, tenantId, scopeId, {
2285
+ drainedBefore: parsed.data.drainedBefore,
2286
+ countOnly: true,
2287
+ });
2288
+ return c.json({ redrainable, drainedBefore: parsed.data.drainedBefore });
2289
+ });
2220
2290
  app.post('/tenants/:tenantId/scopes/:scopeId/reap', async (c) => {
2221
2291
  const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
2222
2292
  const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
@@ -2876,7 +2946,20 @@ export function createControlPlaneApi(options) {
2876
2946
  const p = c.get('principal');
2877
2947
  // A builder sees only what it owns; staff see the whole registry. The narrowing
2878
2948
  // runs above the adapter, so the page is sliced here — over the narrowed list.
2879
- const visible = p.kind === 'builder' ? all.filter((v) => v.ownerTenant === p.tenantId) : all;
2949
+ //
2950
+ // A staff/service caller may ask for one tenant's slice instead of the registry:
2951
+ // `ownerTenant` is what that tenant owns, `visibleTo` adds the published tier (what
2952
+ // its install catalog shows). Both only ever NARROW, and both are ignored for a
2953
+ // builder, whose view is fixed by its auth — `visibleTo` would otherwise widen it.
2954
+ // The dashboard's tenant-narrowed seam sends them so it stops paging the whole
2955
+ // registry to keep a handful of rows; it still filters what comes back, so a plane
2956
+ // that predates the params (the two ship separately) changes nothing but the size.
2957
+ const ownerTenant = c.req.query('ownerTenant');
2958
+ const visibleTo = c.req.query('visibleTo');
2959
+ const visible = p.kind === 'builder'
2960
+ ? all.filter((v) => v.ownerTenant === p.tenantId)
2961
+ : all.filter((v) => (ownerTenant === undefined || v.ownerTenant === ownerTenant) &&
2962
+ (visibleTo === undefined || v.listed || v.ownerTenant === visibleTo));
2880
2963
  return c.json(pageSlice(visible, page, (v) => v.slug));
2881
2964
  });
2882
2965
  // -- the service→dimensions resolver (#1231's last item) -----------------------