@substrat-run/control-plane-api 0.137.0 → 0.139.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 +217 -10
- package/dist/api.js.map +1 -1
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -109
- package/dist/errors.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/reconcile.d.ts +23 -7
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +19 -11
- package/dist/reconcile.js.map +1 -1
- package/dist/vertical-client.d.ts +71 -2
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +58 -1
- package/dist/vertical-client.js.map +1 -1
- package/package.json +8 -8
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveObservabilityWindow } from './observability-window.js';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, OWNER_TRANSFER_AUDIT_ERROR_MAX, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionHealthState, connectionProvider, connectorDispatchKind, PROVISION_SIBLING_KIND, ARCHIVE_SCOPE_KIND, PROVISION_TENANT_KIND, SET_ENTITLEMENTS_KIND, MODEL_USAGE_KIND, SWEEP_RUNS_KIND, toConnectionHealthEntry, CONNECTION_EXPIRY_WARNING_DAYS, CONNECTION_STALE_DAYS, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, impersonationStamp, promotionAcknowledgement, bindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, platformScriptCollision, platformScriptRefusal, PLATFORM_SCRIPT_NAMES, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, errorCodeOf, capabilityFilterQuery, PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER, } from '@substrat-run/contracts';
|
|
3
|
+
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, OWNER_TRANSFER_AUDIT_ERROR_MAX, memberInviteInput, memberRoleInput, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionHealthState, connectionProvider, connectorDispatchKind, PROVISION_SIBLING_KIND, ARCHIVE_SCOPE_KIND, PROVISION_TENANT_KIND, SET_ENTITLEMENTS_KIND, MODEL_USAGE_KIND, SWEEP_RUNS_KIND, toConnectionHealthEntry, CONNECTION_EXPIRY_WARNING_DAYS, CONNECTION_STALE_DAYS, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, errorCode, issueStatus, issueStatusInput, findingKind, findingRuleInput, findingStatus, findingStatusInput, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, impersonationStamp, promotionAcknowledgement, bindAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, entityHistoryInput, eventFacetInput, eventCauseInput, eventEffectsInput, invocationEventsInput, deadLettersInput, lifecycleFlowInput, operationSeriesCount, operationSeriesInput, delegatedReadParams, createOrgInput, roleKey as roleKeySchema, peerSwitch, systemSwitch, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, denialGroupBy, registerVerticalInput, platformScriptCollision, platformScriptRefusal, PLATFORM_SCRIPT_NAMES, serviceDimensions, sweepRunKind, sweepRunOutcome, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, redrainEventsInput, importCursorMove, importCursorAcknowledgementMissing, REDRAIN_BATCH, migrationsOnTop, errorCodeOf, capabilityFilterQuery, PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER, } from '@substrat-run/contracts';
|
|
4
4
|
import { attributeFailure } from './failure-attribution.js';
|
|
5
5
|
import { BIND_EXPORT_BREAK_REFUSAL, CARRIED_AWAY_KEY, bindExportBreakRefusal, carriedAwayDump, dumpMetaValue, metaValueIn, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, isPrimaryScope, migrationProgress, ulid, } from '@substrat-run/kernel';
|
|
6
6
|
import { TENANT_HEADER, confinedTenant } from './auth.js';
|
|
@@ -478,6 +478,22 @@ const issueStatusUpdate = z.object({
|
|
|
478
478
|
fingerprint: z.string().min(1),
|
|
479
479
|
status: issueStatusInput,
|
|
480
480
|
});
|
|
481
|
+
// The findings read (#1748). No cursor, for the issues read's reason: the cardinality is the
|
|
482
|
+
// number of distinct shapes a tenant has. `tenantId` is forced for a confined principal.
|
|
483
|
+
const findingsQuery = z.object({
|
|
484
|
+
tenantId: tenantIdSchema.optional(),
|
|
485
|
+
status: findingStatus.optional(),
|
|
486
|
+
kind: findingKind.optional(),
|
|
487
|
+
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
488
|
+
});
|
|
489
|
+
const findingStatusUpdate = z.object({ status: findingStatusInput }).strict();
|
|
490
|
+
const findingRulesQuery = z.object({
|
|
491
|
+
active: z
|
|
492
|
+
.enum(['true', 'false'])
|
|
493
|
+
.optional()
|
|
494
|
+
.transform((v) => v === 'true'),
|
|
495
|
+
limit: z.coerce.number().int().min(1).max(500).optional(),
|
|
496
|
+
});
|
|
481
497
|
/**
|
|
482
498
|
* A TENANT-LEVEL role assignment under an addressed tenant (#1343) — which is
|
|
483
499
|
* what a team membership is, and all this surface does.
|
|
@@ -592,6 +608,12 @@ const TENANT_ROUTES = [
|
|
|
592
608
|
// grant (`tenantGrantableEntitlement`): the pin alone would let it grant itself any.
|
|
593
609
|
{ method: 'PUT', re: /\/tenants\/[^/]+\/entitlements\/[^/]+$/, pin: 'path' },
|
|
594
610
|
{ method: 'GET', re: /\/tenants\/[^/]+\/cross-vertical\/edges$/, pin: 'path' },
|
|
611
|
+
// Findings (#1748): the tenant's inbox, its verdicts and its suppress rules. The dashboard
|
|
612
|
+
// checks WHO inside the tenant may read or act (its findings permissions) before calling.
|
|
613
|
+
{ method: 'PUT', re: /\/tenants\/[^/]+\/findings\/[^/]+\/status$/, pin: 'path' },
|
|
614
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/finding-rules$/, pin: 'path' },
|
|
615
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/finding-rules$/, pin: 'path' },
|
|
616
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/finding-rules\/[^/]+$/, pin: 'path' },
|
|
595
617
|
// Scopes: provision, read, and the lifecycle acts the Data and Deployments tabs offer.
|
|
596
618
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes$/, pin: 'path' },
|
|
597
619
|
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+$/, pin: 'path' },
|
|
@@ -622,7 +644,13 @@ const TENANT_ROUTES = [
|
|
|
622
644
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/configure$/, pin: 'path' },
|
|
623
645
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/import-cursor$/, pin: 'path' },
|
|
624
646
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/lifecycle-flow$/, pin: 'path' },
|
|
647
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/operation-series$/, pin: 'path' },
|
|
625
648
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/owner-claim$/, pin: 'path' },
|
|
649
|
+
// An installed vertical's members (#1150): the roster, an invite, a role move, a removal.
|
|
650
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/members$/, pin: 'path' },
|
|
651
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/members$/, pin: 'path' },
|
|
652
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/members\/[^/]+\/role$/, pin: 'path' },
|
|
653
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/members\/[^/]+\/remove$/, pin: 'path' },
|
|
626
654
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/query$/, pin: 'path' },
|
|
627
655
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/reap$/, pin: 'path' },
|
|
628
656
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/rebind-vertical$/, pin: 'path' },
|
|
@@ -651,6 +679,7 @@ const TENANT_ROUTES = [
|
|
|
651
679
|
{ method: 'GET', re: /\/admin-log$/, pin: 'query' },
|
|
652
680
|
{ method: 'GET', re: /\/ops-failures$/, pin: 'query' },
|
|
653
681
|
{ method: 'GET', re: /\/sweep-runs$/, pin: 'query' },
|
|
682
|
+
{ method: 'GET', re: /\/findings$/, pin: 'query' },
|
|
654
683
|
{ method: 'GET', re: /\/service-refs$/, pin: 'query' },
|
|
655
684
|
{ method: 'GET', re: /\/observability\/tenant-metrics$/, pin: 'query' },
|
|
656
685
|
{ method: 'GET', re: /\/observability\/tenant-metrics-series$/, pin: 'query' },
|
|
@@ -2993,6 +3022,21 @@ export function createControlPlaneApi(options) {
|
|
|
2993
3022
|
colocated: () => c.var.admin.lifecycleFlow(c.get('actor'), tenantId, scopeId, input),
|
|
2994
3023
|
}, (r) => r.observation.events));
|
|
2995
3024
|
});
|
|
3025
|
+
// #1750: calls per (entity, operation) per bucket over the scope's outbox — Pulse's
|
|
3026
|
+
// business volumes. POST, because the pairs travel in the body; delegated like every
|
|
3027
|
+
// outbox read, and logged with the window and the entity types only.
|
|
3028
|
+
app.post('/tenants/:tenantId/scopes/:scopeId/operation-series', async (c) => {
|
|
3029
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3030
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3031
|
+
const input = operationSeriesInput.parse(await c.req.json());
|
|
3032
|
+
const scope = await c.var.admin.getScopeRecord(c.get('actor'), tenantId, scopeId);
|
|
3033
|
+
if (!scope)
|
|
3034
|
+
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
3035
|
+
return c.json(await delegatedRead(c, tenantId, scopeId, scope, 'operationSeries', input, {
|
|
3036
|
+
viaVertical: (v) => v.operationSeries(scopeId, input),
|
|
3037
|
+
colocated: () => c.var.admin.operationSeries(c.get('actor'), tenantId, scopeId, input),
|
|
3038
|
+
}, operationSeriesCount));
|
|
3039
|
+
});
|
|
2996
3040
|
// #1235: one record's event history — the per-entity pivot of the audit spine.
|
|
2997
3041
|
// Delegated exactly like the table reads: through the vertical that holds the
|
|
2998
3042
|
// scope's data when one resolves, the co-located host otherwise.
|
|
@@ -3142,6 +3186,19 @@ export function createControlPlaneApi(options) {
|
|
|
3142
3186
|
throw e;
|
|
3143
3187
|
}
|
|
3144
3188
|
});
|
|
3189
|
+
/**
|
|
3190
|
+
* The address a link into an instance opens on: the scope's canonical `app` hostname,
|
|
3191
|
+
* preferring one the platform has activated but not requiring it — a platform hostname is
|
|
3192
|
+
* bound `pending` and activated a step later, and that step can fail while the app is fully
|
|
3193
|
+
* reachable (#294). `failed` is the one state that never routes, so only it is skipped. The
|
|
3194
|
+
* platform owns the hostname directory, so the origin is never taken from a body.
|
|
3195
|
+
*/
|
|
3196
|
+
const instanceOrigin = async (admin, actor, scopeId) => {
|
|
3197
|
+
const bound = (await admin.listHostnames(actor, { scopeId })).filter((h) => h.status !== 'failed');
|
|
3198
|
+
const pick = (hs) => hs.find((h) => h.surface === 'app' && h.canonical) ?? hs.find((h) => h.surface === 'app') ?? hs[0];
|
|
3199
|
+
const host = pick(bound.filter((h) => h.status === 'active')) ?? pick(bound);
|
|
3200
|
+
return host ? `https://${host.hostname}` : null;
|
|
3201
|
+
};
|
|
3145
3202
|
app.post('/tenants/:tenantId/scopes/:scopeId/owner-claim', async (c) => {
|
|
3146
3203
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3147
3204
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
@@ -3156,18 +3213,12 @@ export function createControlPlaneApi(options) {
|
|
|
3156
3213
|
const vertical = await verticalForScope(c, scope);
|
|
3157
3214
|
if (!vertical)
|
|
3158
3215
|
return c.json({ error: await diagnoseUnboundScope(actor, scope) }, 501);
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
// `pending` and activated a step later, and that step can fail while the app is fully
|
|
3162
|
-
// reachable (#294). `failed` is the one state that never routes, so only it is skipped.
|
|
3163
|
-
const bound = (await c.var.admin.listHostnames(actor, { scopeId })).filter((h) => h.status !== 'failed');
|
|
3164
|
-
const pick = (hs) => hs.find((h) => h.surface === 'app' && h.canonical) ?? hs.find((h) => h.surface === 'app') ?? hs[0];
|
|
3165
|
-
const host = pick(bound.filter((h) => h.status === 'active')) ?? pick(bound);
|
|
3166
|
-
if (!host) {
|
|
3216
|
+
const origin = await instanceOrigin(c.var.admin, actor, scopeId);
|
|
3217
|
+
if (!origin) {
|
|
3167
3218
|
return c.json({ error: `scope ${scopeId} has no hostname bound — a claim link needs an address to open on` }, 409);
|
|
3168
3219
|
}
|
|
3169
3220
|
try {
|
|
3170
|
-
return c.json(await vertical.mintOwnerClaim({ tenantId, scopeId, origin
|
|
3221
|
+
return c.json(await vertical.mintOwnerClaim({ tenantId, scopeId, origin, actor }), 201);
|
|
3171
3222
|
}
|
|
3172
3223
|
catch (e) {
|
|
3173
3224
|
if (e instanceof ControlPlaneError)
|
|
@@ -3175,6 +3226,111 @@ export function createControlPlaneApi(options) {
|
|
|
3175
3226
|
throw e;
|
|
3176
3227
|
}
|
|
3177
3228
|
});
|
|
3229
|
+
// An installed vertical's MEMBERS (#1150) — the dashboard's Members section. The roster and
|
|
3230
|
+
// every change live in the VERTICAL (its scope's role tuples, its identity directory), so each
|
|
3231
|
+
// route makes the same K-3 cross-check as the owner seat and then asks the deployment that
|
|
3232
|
+
// holds the scope. One scope per call: no walk across scopes (#1581).
|
|
3233
|
+
//
|
|
3234
|
+
// **Who is acting is the credential's person, and only that.** Every change is bounded in the
|
|
3235
|
+
// vertical by the K-21 set comparison at the vertical's scope, asked about a principal — and
|
|
3236
|
+
// the one principal this plane can vouch for is the `onBehalfOf` the dashboard's tenant token
|
|
3237
|
+
// carries (#977), which only the staff mint writes. A builder session, a staff token or a
|
|
3238
|
+
// tenant token minted for nobody names no person, so the writes refuse it rather than
|
|
3239
|
+
// borrowing an authority it does not have. Being a tenant (dashboard) admin confers nothing in
|
|
3240
|
+
// the vertical's scope: what the person holds THERE is what bounds them.
|
|
3241
|
+
//
|
|
3242
|
+
// Every change is audited intent-first then outcome (`manageScopeMember`), like the hand-over.
|
|
3243
|
+
const memberPerson = (p) => p.kind === 'tenant' && p.onBehalfOf ? p.onBehalfOf.principal : null;
|
|
3244
|
+
/** The scope, its vertical and the acting person, or the answer that refuses the request. */
|
|
3245
|
+
const memberTarget = async (c) => {
|
|
3246
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3247
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3248
|
+
const principal = c.get('principal');
|
|
3249
|
+
const actor = c.get('actor');
|
|
3250
|
+
// K-3: another tenant's pair, or no such scope, reads the same — absent.
|
|
3251
|
+
const scope = outsideTenant(principal, tenantId) ? null : await c.var.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
3252
|
+
if (!scope)
|
|
3253
|
+
return { refusal: c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404) };
|
|
3254
|
+
const vertical = await verticalForScope(c, scope);
|
|
3255
|
+
if (!vertical)
|
|
3256
|
+
return { refusal: c.json({ error: await diagnoseUnboundScope(actor, scope) }, 501) };
|
|
3257
|
+
return { tenantId, scopeId, actor, vertical, person: memberPerson(principal) };
|
|
3258
|
+
};
|
|
3259
|
+
/** Run one audited member change: intent, the vertical's call, then the outcome row. */
|
|
3260
|
+
const auditedMemberChange = async (c, entry, run, applied) => {
|
|
3261
|
+
const actor = c.get('actor');
|
|
3262
|
+
const base = { ...entry, operationId: ulid() };
|
|
3263
|
+
await c.var.admin.recordMemberChange(actor, { ...base, phase: 'intent' });
|
|
3264
|
+
let result;
|
|
3265
|
+
try {
|
|
3266
|
+
result = await run();
|
|
3267
|
+
}
|
|
3268
|
+
catch (e) {
|
|
3269
|
+
const status = e instanceof ControlPlaneError ? e.status : 500;
|
|
3270
|
+
const error = (e instanceof Error ? e.message : String(e)).slice(0, OWNER_TRANSFER_AUDIT_ERROR_MAX);
|
|
3271
|
+
await c.var.admin
|
|
3272
|
+
.recordMemberChange(actor, { ...base, phase: status < 500 ? 'refused' : 'failed', error })
|
|
3273
|
+
.catch(() => undefined);
|
|
3274
|
+
if (e instanceof ControlPlaneError) {
|
|
3275
|
+
return { ok: false, response: c.json({ error: e.message }, e.status) };
|
|
3276
|
+
}
|
|
3277
|
+
throw e;
|
|
3278
|
+
}
|
|
3279
|
+
await c.var.admin.recordMemberChange(actor, { ...base, phase: 'applied', ...applied(result) });
|
|
3280
|
+
return { ok: true, result };
|
|
3281
|
+
};
|
|
3282
|
+
const NO_PERSON = 'a member change needs the signed-in person it is made for — this credential names nobody';
|
|
3283
|
+
app.get('/tenants/:tenantId/scopes/:scopeId/members', async (c) => {
|
|
3284
|
+
const target = await memberTarget(c);
|
|
3285
|
+
if ('refusal' in target)
|
|
3286
|
+
return target.refusal;
|
|
3287
|
+
try {
|
|
3288
|
+
return c.json(await target.vertical.listMembers(target.tenantId, target.scopeId));
|
|
3289
|
+
}
|
|
3290
|
+
catch (e) {
|
|
3291
|
+
if (e instanceof ControlPlaneError)
|
|
3292
|
+
return c.json({ error: e.message }, e.status);
|
|
3293
|
+
throw e;
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
app.post('/tenants/:tenantId/scopes/:scopeId/members', async (c) => {
|
|
3297
|
+
const target = await memberTarget(c);
|
|
3298
|
+
if ('refusal' in target)
|
|
3299
|
+
return target.refusal;
|
|
3300
|
+
if (!target.person)
|
|
3301
|
+
return c.json({ error: NO_PERSON }, 403);
|
|
3302
|
+
const body = memberInviteInput.parse(await c.req.json());
|
|
3303
|
+
const origin = await instanceOrigin(c.var.admin, target.actor, target.scopeId);
|
|
3304
|
+
if (!origin) {
|
|
3305
|
+
return c.json({ error: `scope ${target.scopeId} has no hostname bound — an invite link needs an address to open on` }, 409);
|
|
3306
|
+
}
|
|
3307
|
+
const { tenantId, scopeId, person } = target;
|
|
3308
|
+
const done = await auditedMemberChange(c, { tenantId, scopeId, change: 'invite', caller: person, roleKey: body.roleKey }, () => target.vertical.inviteMember({ tenantId, scopeId, caller: person, origin, roleKey: body.roleKey, email: body.email ?? null }), (link) => ({ principal: link.principal }));
|
|
3309
|
+
return done.ok ? c.json(done.result, 201) : done.response;
|
|
3310
|
+
});
|
|
3311
|
+
app.post('/tenants/:tenantId/scopes/:scopeId/members/:principal/role', async (c) => {
|
|
3312
|
+
const target = await memberTarget(c);
|
|
3313
|
+
if ('refusal' in target)
|
|
3314
|
+
return target.refusal;
|
|
3315
|
+
if (!target.person)
|
|
3316
|
+
return c.json({ error: NO_PERSON }, 403);
|
|
3317
|
+
const member = principalIdSchema.parse(c.req.param('principal'));
|
|
3318
|
+
const body = memberRoleInput.parse(await c.req.json());
|
|
3319
|
+
const { tenantId, scopeId, person } = target;
|
|
3320
|
+
const done = await auditedMemberChange(c, { tenantId, scopeId, change: 'role', caller: person, principal: member, from: body.from, to: body.to }, () => target.vertical.changeMemberRole({ tenantId, scopeId, caller: person, principal: member, from: body.from, to: body.to }), () => ({}));
|
|
3321
|
+
return done.ok ? c.json({ principal: member, from: body.from, to: body.to }) : done.response;
|
|
3322
|
+
});
|
|
3323
|
+
app.post('/tenants/:tenantId/scopes/:scopeId/members/:principal/remove', async (c) => {
|
|
3324
|
+
const target = await memberTarget(c);
|
|
3325
|
+
if ('refusal' in target)
|
|
3326
|
+
return target.refusal;
|
|
3327
|
+
if (!target.person)
|
|
3328
|
+
return c.json({ error: NO_PERSON }, 403);
|
|
3329
|
+
const member = principalIdSchema.parse(c.req.param('principal'));
|
|
3330
|
+
const { tenantId, scopeId, person } = target;
|
|
3331
|
+
const done = await auditedMemberChange(c, { tenantId, scopeId, change: 'remove', caller: person, principal: member }, () => target.vertical.removeMember({ tenantId, scopeId, caller: person, principal: member }), (removal) => ({ revoked: removal.revoked }));
|
|
3332
|
+
return done.ok ? c.json(done.result) : done.response;
|
|
3333
|
+
});
|
|
3178
3334
|
// The owner HAND-OVER (#1665). The move runs in the VERTICAL, behind `/internal/owner-transfer`
|
|
3179
3335
|
// (vertical-host says in what order and why). This route decides who may ask, and audits every
|
|
3180
3336
|
// attempt: an `intent` row before the vertical is reached, so a row the log cannot take means
|
|
@@ -7658,6 +7814,57 @@ export function createControlPlaneApi(options) {
|
|
|
7658
7814
|
return c.json({ error: 'unknown issue fingerprint' }, 404);
|
|
7659
7815
|
return c.json(updated);
|
|
7660
7816
|
});
|
|
7817
|
+
// -- findings (#1748): the tenant inbox, with a lifecycle and suppress rules ----
|
|
7818
|
+
// Staff read the fleet; a tenant credential's tenant is forced from the principal, the
|
|
7819
|
+
// /ops-failures posture. Builders never reach any of these (absent from BUILDER_ROUTES): a
|
|
7820
|
+
// finding is the tenant's, not the vertical author's.
|
|
7821
|
+
app.get('/findings', async (c) => {
|
|
7822
|
+
const filter = findingsQuery.parse({
|
|
7823
|
+
tenantId: confinedTenant(c.get('principal')) ?? c.req.query('tenantId'),
|
|
7824
|
+
status: c.req.query('status'),
|
|
7825
|
+
kind: c.req.query('kind'),
|
|
7826
|
+
limit: c.req.query('limit'),
|
|
7827
|
+
});
|
|
7828
|
+
return c.json({ entries: await c.var.admin.listFindings(c.get('actor'), filter) });
|
|
7829
|
+
});
|
|
7830
|
+
// A verdict: acknowledge, resolve or reopen. Keyed on the tenant in the path as well as the id,
|
|
7831
|
+
// so another tenant's finding is a 404, never a write.
|
|
7832
|
+
app.put('/tenants/:tenantId/findings/:id/status', async (c) => {
|
|
7833
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
7834
|
+
if (outsideTenant(c.get('principal'), tenantId))
|
|
7835
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
7836
|
+
const { status } = findingStatusUpdate.parse(await c.req.json());
|
|
7837
|
+
const updated = await c.var.admin.setFindingStatus(c.get('actor'), tenantId, c.req.param('id'), status);
|
|
7838
|
+
if (!updated)
|
|
7839
|
+
return c.json({ error: 'unknown finding' }, 404);
|
|
7840
|
+
return c.json(updated);
|
|
7841
|
+
});
|
|
7842
|
+
app.get('/tenants/:tenantId/finding-rules', async (c) => {
|
|
7843
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
7844
|
+
if (outsideTenant(c.get('principal'), tenantId))
|
|
7845
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
7846
|
+
const filter = findingRulesQuery.parse({ active: c.req.query('active'), limit: c.req.query('limit') });
|
|
7847
|
+
const entries = await c.var.admin.listFindingRules(c.get('actor'), tenantId, filter);
|
|
7848
|
+
return c.json({ entries });
|
|
7849
|
+
});
|
|
7850
|
+
// Suppress with a rule: a scope plus an expiry, audited. 201 with the rule and how many
|
|
7851
|
+
// findings it suppressed at once.
|
|
7852
|
+
app.post('/tenants/:tenantId/finding-rules', async (c) => {
|
|
7853
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
7854
|
+
if (outsideTenant(c.get('principal'), tenantId))
|
|
7855
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
7856
|
+
const input = findingRuleInput.parse(await c.req.json());
|
|
7857
|
+
return c.json(await c.var.admin.createFindingRule(c.get('actor'), tenantId, input), 201);
|
|
7858
|
+
});
|
|
7859
|
+
app.delete('/tenants/:tenantId/finding-rules/:ruleId', async (c) => {
|
|
7860
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
7861
|
+
if (outsideTenant(c.get('principal'), tenantId))
|
|
7862
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
7863
|
+
const revoked = await c.var.admin.revokeFindingRule(c.get('actor'), tenantId, c.req.param('ruleId'));
|
|
7864
|
+
if (!revoked)
|
|
7865
|
+
return c.json({ error: 'unknown finding rule' }, 404);
|
|
7866
|
+
return c.json(revoked);
|
|
7867
|
+
});
|
|
7661
7868
|
// -- model usage (#1054): meter 3, the one D-30 could not compute -------------
|
|
7662
7869
|
// The ledger the `model-usage` intents drain into. Staff-only: the tenant-facing
|
|
7663
7870
|
// read is the vertical's own usage screen over its metering engine; this is the
|