@substrat-run/control-plane-api 0.139.0 → 0.140.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 +14 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +221 -63
- package/dist/api.js.map +1 -1
- package/dist/audited-change.d.ts +139 -0
- package/dist/audited-change.d.ts.map +1 -0
- package/dist/audited-change.js +214 -0
- package/dist/audited-change.js.map +1 -0
- package/dist/connect-links.d.ts +29 -0
- package/dist/connect-links.d.ts.map +1 -0
- package/dist/connect-links.js +139 -0
- package/dist/connect-links.js.map +1 -0
- package/dist/connect-url.d.ts +20 -1
- package/dist/connect-url.d.ts.map +1 -1
- package/dist/connect-url.js +1 -1
- package/dist/connect-url.js.map +1 -1
- package/dist/connection-relay.d.ts +24 -0
- package/dist/connection-relay.d.ts.map +1 -1
- package/dist/connection-relay.js +31 -12
- package/dist/connection-relay.js.map +1 -1
- package/dist/email-intent.d.ts +71 -0
- package/dist/email-intent.d.ts.map +1 -0
- package/dist/email-intent.js +186 -0
- package/dist/email-intent.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/platform-drain.d.ts +12 -2
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +15 -3
- package/dist/platform-drain.js.map +1 -1
- package/dist/reconcile.d.ts +17 -2
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +35 -1
- package/dist/reconcile.js.map +1 -1
- package/dist/staff-client.d.ts +3 -1
- package/dist/staff-client.d.ts.map +1 -1
- package/dist/staff-client.js +2 -0
- package/dist/staff-client.js.map +1 -1
- package/dist/vertical-client.d.ts +25 -1
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +89 -22
- package/dist/vertical-client.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { z } from '@substrat-run/contracts';
|
|
2
|
+
import { type ConnectionCertificate, z } from '@substrat-run/contracts';
|
|
3
3
|
import type { Connection, ConnectionActivity, ConnectionActivitySource, ConnectionCredential, ConnectionProbe, PlatformActorId } from '@substrat-run/contracts';
|
|
4
4
|
import type { CrossVerticalOptions, HostAdmin, ScopeHost } from '@substrat-run/kernel';
|
|
5
5
|
import type { PlatformActorAuth, BuilderAuth, Principal, TenantServiceAuth } from './auth.js';
|
|
6
6
|
import { type VerticalClient } from './vertical-client.js';
|
|
7
|
+
import { type ConnectionCandidatePrepare } from './connection-relay.js';
|
|
7
8
|
import type { DeployVerticalFn, FetchVerticalAssetFn, FetchVerticalModulesFn } from './deploy.js';
|
|
8
9
|
import type { PatchScriptBindingsFn } from './wfp.js';
|
|
9
10
|
import { type ObservabilityReader } from './observability.js';
|
|
@@ -44,6 +45,18 @@ export interface ConnectionInspector {
|
|
|
44
45
|
* provider is checked before it writes, and a REFUSED credential never lands.
|
|
45
46
|
*/
|
|
46
47
|
probeCandidate?: (secret: Record<string, string>) => Promise<ConnectionProbe>;
|
|
48
|
+
/**
|
|
49
|
+
* Finish a candidate credential before it is probed and stored (#2100) — for a provider
|
|
50
|
+
* where part of the credential is the platform's to make (a generated keypair). Gets the
|
|
51
|
+
* live connection's secret when the upsert rotates one, so what must survive an edit can.
|
|
52
|
+
* An `expiresAt` of `null` clears the connection's expiry (`ConnectionCandidatePrepare`).
|
|
53
|
+
*/
|
|
54
|
+
prepareCandidate?: ConnectionCandidatePrepare;
|
|
55
|
+
/**
|
|
56
|
+
* The PUBLIC certificate the connection authenticates with, for the tenant to register on
|
|
57
|
+
* the provider's side (#2100). `null` when this connection uses none (a client secret).
|
|
58
|
+
*/
|
|
59
|
+
certificate?: (host: ScopeHost, connection: Connection) => Promise<ConnectionCertificate | null>;
|
|
47
60
|
}
|
|
48
61
|
export interface ControlPlaneApiOptions {
|
|
49
62
|
/**
|
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,15 +1,16 @@
|
|
|
1
1
|
import { resolveObservabilityWindow } from './observability-window.js';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput,
|
|
3
|
+
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ownerTransferInput, memberInviteInput, memberRoleInput, ASSET_PART_PREFIX, assetHash, channelName, connectLinkId, connectionActivity, connectionActivitySource, connectionCredential, connectionCertificate, 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, verticalSlug, 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
|
-
import {
|
|
5
|
+
import { auditedChange, withAuditedOutcomes } from './audited-change.js';
|
|
6
|
+
import { BIND_EXPORT_BREAK_REFUSAL, CARRIED_AWAY_KEY, bindExportBreakRefusal, carriedAwayDump, dumpMetaValue, metaValueIn, crossVerticalHealth, exportBreakRefusal, isBindExportBreakRefusal, isExportBreakRefusal, isPrimaryScope, migrationProgress, ulid, auditWarningOf, } from '@substrat-run/kernel';
|
|
6
7
|
import { TENANT_HEADER, confinedTenant } from './auth.js';
|
|
7
8
|
import { mintTenantToken } from './tenant-token.js';
|
|
8
9
|
import { connectionGrantsForScope } from './vertical-client.js';
|
|
9
10
|
import { oidcCallbackUrl, retireClientsOfReapedScope, wirePreviewAuth } from './preview-auth.js';
|
|
10
11
|
import { versionReachedAt } from './scope-deployment.js';
|
|
11
12
|
import { reconcileConnectionGrants } from './connection-grants.js';
|
|
12
|
-
import { reconcileThenReassert, switchCarryFor } from './reconcile.js';
|
|
13
|
+
import { reconcileThenReassert, reviewedEntityGrants, switchCarryFor } from './reconcile.js';
|
|
13
14
|
import { ConnectionRelayError, relayConnectionUpsert } from './connection-relay.js';
|
|
14
15
|
import { ControlPlaneError } from '@substrat-run/control-plane-client';
|
|
15
16
|
import { provisionSiblingScope } from './platform-drain.js';
|
|
@@ -595,7 +596,16 @@ const TENANT_ROUTES = [
|
|
|
595
596
|
{ method: 'DELETE', re: /\/tenants\/[^/]+\/connections\/[^/]+$/, pin: 'path' },
|
|
596
597
|
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/activity$/, pin: 'path' },
|
|
597
598
|
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/credential$/, pin: 'path' },
|
|
599
|
+
// #2100: the PUBLIC certificate a connection signs in with — same pin as the credential view.
|
|
600
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connections\/[^/]+\/certificate$/, pin: 'path' },
|
|
598
601
|
{ method: 'POST', re: /\/tenants\/[^/]+\/connections\/[^/]+\/verify$/, pin: 'path' },
|
|
602
|
+
// A vertical's mailed connect links (connections.md §3.5.4): the consent landing's liveness
|
|
603
|
+
// read, the callback's spend and its undo, and the integrations card's list and revoke.
|
|
604
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connect-links$/, pin: 'path' },
|
|
605
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/connect-links\/[^/]+$/, pin: 'path' },
|
|
606
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/connect-links\/[^/]+\/consume$/, pin: 'path' },
|
|
607
|
+
{ method: 'POST', re: /\/tenants\/[^/]+\/connect-links\/[^/]+\/restore$/, pin: 'path' },
|
|
608
|
+
{ method: 'DELETE', re: /\/tenants\/[^/]+\/connect-links\/[^/]+$/, pin: 'path' },
|
|
599
609
|
// Members: identity links mirrored from the dashboard, orgs, role assignments.
|
|
600
610
|
{ method: 'GET', re: /\/tenants\/[^/]+\/identities$/, pin: 'path' },
|
|
601
611
|
{ method: 'PUT', re: /\/tenants\/[^/]+\/identities$/, pin: 'path' },
|
|
@@ -633,6 +643,8 @@ const TENANT_ROUTES = [
|
|
|
633
643
|
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/migrations$/, pin: 'path' },
|
|
634
644
|
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/owner-seat$/, pin: 'path' },
|
|
635
645
|
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
646
|
+
{ method: 'GET', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-bindings$/, pin: 'path' },
|
|
647
|
+
{ method: 'PUT', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-bindings$/, pin: 'path' },
|
|
636
648
|
// The peer switch (#1706): on is POST, off is DELETE, on the tenant's own scope.
|
|
637
649
|
{ method: 'POST', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
638
650
|
{ method: 'DELETE', re: /\/tenants\/[^/]+\/scopes\/[^/]+\/peer-grants$/, pin: 'path' },
|
|
@@ -1588,7 +1600,7 @@ export function createControlPlaneApi(options) {
|
|
|
1588
1600
|
return c.json({ error: 'body tenantId disagrees with the route tenant' }, 400);
|
|
1589
1601
|
}
|
|
1590
1602
|
try {
|
|
1591
|
-
return c.json(await relayConnectionUpsert(c.var.host, c.get('actor'), { ...body, tenantId }, { probeCandidate }));
|
|
1603
|
+
return c.json(await relayConnectionUpsert(c.var.host, c.get('actor'), { ...body, tenantId }, { probeCandidate, prepareCandidate }));
|
|
1592
1604
|
}
|
|
1593
1605
|
catch (err) {
|
|
1594
1606
|
if (err instanceof ConnectionRelayError && err.status < 500) {
|
|
@@ -1618,6 +1630,80 @@ export function createControlPlaneApi(options) {
|
|
|
1618
1630
|
await c.var.admin.revokeConnection(c.get('actor'), row.id);
|
|
1619
1631
|
return c.body(null, 204);
|
|
1620
1632
|
});
|
|
1633
|
+
// -- a vertical's mailed connect links, tenant-scoped (connections.md §3.5.4) ---------
|
|
1634
|
+
//
|
|
1635
|
+
// The dashboard's half of a link a VERTICAL minted (`/internal/connections/connect-links`):
|
|
1636
|
+
// its consent landing asks whether the row still stands, its callback spends the row
|
|
1637
|
+
// before storing and restores it if the store fails, and its integrations card lists and
|
|
1638
|
+
// revokes them. The tenant is the path's; the scope is named on every call and is half
|
|
1639
|
+
// the store's key, so another scope's link — or another tenant's — reads as absent (K-3).
|
|
1640
|
+
// Not in BUILDER_ROUTES: a builder has no business spending a tenant's consent round.
|
|
1641
|
+
const connectLinkScope = z.object({ scopeId: scopeIdSchema });
|
|
1642
|
+
app.get('/tenants/:tenantId/connect-links', async (c) => {
|
|
1643
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1644
|
+
const q = connectLinkScope
|
|
1645
|
+
.extend({ provider: connectionProvider.optional(), outstanding: z.literal('1').optional() })
|
|
1646
|
+
.parse({
|
|
1647
|
+
scopeId: c.req.query('scopeId'),
|
|
1648
|
+
provider: c.req.query('provider') || undefined,
|
|
1649
|
+
outstanding: c.req.query('outstanding') || undefined,
|
|
1650
|
+
});
|
|
1651
|
+
return c.json(await c.var.admin.listConnectLinks(c.get('actor'), {
|
|
1652
|
+
tenantId,
|
|
1653
|
+
scopeId: q.scopeId,
|
|
1654
|
+
...(q.provider ? { provider: q.provider } : {}),
|
|
1655
|
+
...(q.outstanding ? { outstandingOnly: true } : {}),
|
|
1656
|
+
}));
|
|
1657
|
+
});
|
|
1658
|
+
app.get('/tenants/:tenantId/connect-links/:linkId', async (c) => {
|
|
1659
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1660
|
+
const { scopeId } = connectLinkScope.parse({ scopeId: c.req.query('scopeId') });
|
|
1661
|
+
const link = await c.var.admin.getConnectLink(c.get('actor'), {
|
|
1662
|
+
tenantId,
|
|
1663
|
+
scopeId,
|
|
1664
|
+
id: connectLinkId.parse(c.req.param('linkId')),
|
|
1665
|
+
});
|
|
1666
|
+
return link ? c.json(link) : c.json({ error: 'unknown connect link' }, 404);
|
|
1667
|
+
});
|
|
1668
|
+
// The callback's spend. A refusal is a 200 carrying the typed reason, not an error status:
|
|
1669
|
+
// "already used" is the answer to the question asked, and the callback renders each one.
|
|
1670
|
+
app.post('/tenants/:tenantId/connect-links/:linkId/consume', async (c) => {
|
|
1671
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1672
|
+
const body = connectLinkScope
|
|
1673
|
+
.extend({
|
|
1674
|
+
provider: connectionProvider,
|
|
1675
|
+
accountRef: z.string().max(256).optional(),
|
|
1676
|
+
accountLabel: z.string().max(256).optional(),
|
|
1677
|
+
})
|
|
1678
|
+
.parse(await c.req.json());
|
|
1679
|
+
return c.json(await c.var.admin.consumeConnectLink(c.get('actor'), {
|
|
1680
|
+
...body,
|
|
1681
|
+
tenantId,
|
|
1682
|
+
id: connectLinkId.parse(c.req.param('linkId')),
|
|
1683
|
+
}));
|
|
1684
|
+
});
|
|
1685
|
+
app.post('/tenants/:tenantId/connect-links/:linkId/restore', async (c) => {
|
|
1686
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1687
|
+
const { scopeId } = connectLinkScope.parse(await c.req.json());
|
|
1688
|
+
const restored = await c.var.admin.restoreConnectLink(c.get('actor'), {
|
|
1689
|
+
tenantId,
|
|
1690
|
+
scopeId,
|
|
1691
|
+
id: connectLinkId.parse(c.req.param('linkId')),
|
|
1692
|
+
});
|
|
1693
|
+
return c.json({ restored });
|
|
1694
|
+
});
|
|
1695
|
+
// Idempotent, as the store is: a revoked or spent link answers 204 again. 404 only for a
|
|
1696
|
+
// link this scope never held.
|
|
1697
|
+
app.delete('/tenants/:tenantId/connect-links/:linkId', async (c) => {
|
|
1698
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1699
|
+
const { scopeId } = connectLinkScope.parse({ scopeId: c.req.query('scopeId') });
|
|
1700
|
+
const link = await c.var.admin.revokeConnectLink(c.get('actor'), {
|
|
1701
|
+
tenantId,
|
|
1702
|
+
scopeId,
|
|
1703
|
+
id: connectLinkId.parse(c.req.param('linkId')),
|
|
1704
|
+
});
|
|
1705
|
+
return link ? c.body(null, 204) : c.json({ error: 'unknown connect link' }, 404);
|
|
1706
|
+
});
|
|
1621
1707
|
/**
|
|
1622
1708
|
* The tenant-scoped connection lookup the two inspection routes share. Same K-3
|
|
1623
1709
|
* existence hiding as the revoke above — a foreign tenant's connection id reads
|
|
@@ -1630,6 +1716,8 @@ export function createControlPlaneApi(options) {
|
|
|
1630
1716
|
* in which case the upsert behaves exactly as it always did, storing unverified.
|
|
1631
1717
|
*/
|
|
1632
1718
|
const probeCandidate = async (provider, secret) => options.connectionInspectors?.[provider]?.probeCandidate?.(secret);
|
|
1719
|
+
/** #2100: the provider's own half of a credential, made before the probe sees it. */
|
|
1720
|
+
const prepareCandidate = (provider) => options.connectionInspectors?.[provider]?.prepareCandidate;
|
|
1633
1721
|
const inspectableConnection = async (c) => {
|
|
1634
1722
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1635
1723
|
const id = c.req.param('id');
|
|
@@ -1703,6 +1791,20 @@ export function createControlPlaneApi(options) {
|
|
|
1703
1791
|
}
|
|
1704
1792
|
return c.json(connectionCredential.parse(await credential(c.var.host, row)));
|
|
1705
1793
|
});
|
|
1794
|
+
/**
|
|
1795
|
+
* The public certificate a connection signs in with (#2100) — what the tenant downloads and
|
|
1796
|
+
* uploads to the provider. Public by construction: the private key never leaves the sealed
|
|
1797
|
+
* credential, and only a provider whose keypair the platform generates has one at all.
|
|
1798
|
+
*/
|
|
1799
|
+
app.get('/tenants/:tenantId/connections/:id/certificate', async (c) => {
|
|
1800
|
+
const row = await inspectableConnection(c);
|
|
1801
|
+
if (!row)
|
|
1802
|
+
return c.json({ error: 'unknown connection' }, 404);
|
|
1803
|
+
const certificate = await options.connectionInspectors?.[row.provider]?.certificate?.(c.var.host, row);
|
|
1804
|
+
if (!certificate)
|
|
1805
|
+
return c.json({ error: 'this connection signs in with no certificate' }, 404);
|
|
1806
|
+
return c.json(connectionCertificate.parse(certificate));
|
|
1807
|
+
});
|
|
1706
1808
|
// -- the scope directory (§3.2/§4.2) ---------------------------------------
|
|
1707
1809
|
app.get('/scopes', async (c) => {
|
|
1708
1810
|
const p = c.get('principal');
|
|
@@ -2825,6 +2927,9 @@ export function createControlPlaneApi(options) {
|
|
|
2825
2927
|
? await c.var.admin.verticalServing(actor, scope.vertical).catch(() => null)
|
|
2826
2928
|
: null;
|
|
2827
2929
|
const ranAs = versionReachedAt(reached.via, scope, serving);
|
|
2930
|
+
// #2071: the shapes this reconcile tops holders up to, from the reviewed registry of the
|
|
2931
|
+
// version it reaches — never anything the vertical's own code names.
|
|
2932
|
+
const entityGrants = scope.vertical && ranAs ? await reviewedEntityGrants(c.var.admin, actor, scope.vertical, ranAs) : undefined;
|
|
2828
2933
|
try {
|
|
2829
2934
|
// #1674: the recorded OFF positions go back after the reconcile, before the receipt.
|
|
2830
2935
|
const result = await reconcileThenReassert(c.var.admin, actor, { tenantId, scopeId }, (carry) => vertical.reconcileInstance({
|
|
@@ -2834,6 +2939,7 @@ export function createControlPlaneApi(options) {
|
|
|
2834
2939
|
identityLinks,
|
|
2835
2940
|
connectionGrants,
|
|
2836
2941
|
connectionKeys,
|
|
2942
|
+
...(entityGrants ? { entityGrants } : {}),
|
|
2837
2943
|
...carry,
|
|
2838
2944
|
// Handed over exactly as at provision: a store minted HERE has never been migrated
|
|
2839
2945
|
// by the vertical, so the reconcile must carry it into the same ready-gate — a bound
|
|
@@ -3256,28 +3362,46 @@ export function createControlPlaneApi(options) {
|
|
|
3256
3362
|
return { refusal: c.json({ error: await diagnoseUnboundScope(actor, scope) }, 501) };
|
|
3257
3363
|
return { tenantId, scopeId, actor, vertical, person: memberPerson(principal) };
|
|
3258
3364
|
};
|
|
3259
|
-
/**
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
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) };
|
|
3365
|
+
/**
|
|
3366
|
+
* Answer for one `auditedChange` (#2064). A refusal or failure answers with the vertical's own
|
|
3367
|
+
* status (a non-`ControlPlaneError` throw is rethrown). A change that went through answers
|
|
3368
|
+
* `ok`, with its `operationId`, whether or not its outcome row could be written. When it could
|
|
3369
|
+
* not, the answer also carries `auditWarning`, which says so. That is a success on purpose:
|
|
3370
|
+
* the change happened, so no client should read a retryable error and make it twice (a second
|
|
3371
|
+
* invite, a second hand-over). The missing row is logged, and the scheduled settle closes it.
|
|
3372
|
+
*/
|
|
3373
|
+
const auditedAnswer = (c, done, what, ok) => {
|
|
3374
|
+
const { operationId } = done;
|
|
3375
|
+
if ('error' in done) {
|
|
3376
|
+
if (done.error instanceof ControlPlaneError) {
|
|
3377
|
+
return c.json({ error: done.error.message, operationId }, done.error.status);
|
|
3276
3378
|
}
|
|
3277
|
-
throw
|
|
3379
|
+
throw done.error;
|
|
3278
3380
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3381
|
+
if ('unrecorded' in done) {
|
|
3382
|
+
return ok(done.result, {
|
|
3383
|
+
operationId,
|
|
3384
|
+
auditWarning: auditWarningOf(what, done.unrecorded),
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
return ok(done.result, { operationId });
|
|
3388
|
+
};
|
|
3389
|
+
/** Run one audited member change: intent, the vertical's call, then the outcome row. */
|
|
3390
|
+
const auditedMemberChange = (c, entry, run, applied) => {
|
|
3391
|
+
const actor = c.get('actor');
|
|
3392
|
+
const operationId = ulid();
|
|
3393
|
+
return auditedChange({
|
|
3394
|
+
flow: 'member-change',
|
|
3395
|
+
operationId,
|
|
3396
|
+
run,
|
|
3397
|
+
// A 4xx is the vertical's refusal: nothing was written there.
|
|
3398
|
+
refused: (e) => e instanceof ControlPlaneError && e.status < 500,
|
|
3399
|
+
record: (row) => c.var.admin.recordMemberChange(actor, {
|
|
3400
|
+
...entry,
|
|
3401
|
+
operationId,
|
|
3402
|
+
...(row.phase === 'applied' ? { phase: 'applied', ...applied(row.result) } : row),
|
|
3403
|
+
}),
|
|
3404
|
+
});
|
|
3281
3405
|
};
|
|
3282
3406
|
const NO_PERSON = 'a member change needs the signed-in person it is made for — this credential names nobody';
|
|
3283
3407
|
app.get('/tenants/:tenantId/scopes/:scopeId/members', async (c) => {
|
|
@@ -3306,7 +3430,7 @@ export function createControlPlaneApi(options) {
|
|
|
3306
3430
|
}
|
|
3307
3431
|
const { tenantId, scopeId, person } = target;
|
|
3308
3432
|
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
|
|
3433
|
+
return auditedAnswer(c, done, 'the invite', (link, audit) => c.json({ ...link, ...audit }, 201));
|
|
3310
3434
|
});
|
|
3311
3435
|
app.post('/tenants/:tenantId/scopes/:scopeId/members/:principal/role', async (c) => {
|
|
3312
3436
|
const target = await memberTarget(c);
|
|
@@ -3318,7 +3442,7 @@ export function createControlPlaneApi(options) {
|
|
|
3318
3442
|
const body = memberRoleInput.parse(await c.req.json());
|
|
3319
3443
|
const { tenantId, scopeId, person } = target;
|
|
3320
3444
|
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
|
|
3445
|
+
return auditedAnswer(c, done, 'the role change', (_, audit) => c.json({ principal: member, from: body.from, to: body.to, ...audit }));
|
|
3322
3446
|
});
|
|
3323
3447
|
app.post('/tenants/:tenantId/scopes/:scopeId/members/:principal/remove', async (c) => {
|
|
3324
3448
|
const target = await memberTarget(c);
|
|
@@ -3329,7 +3453,7 @@ export function createControlPlaneApi(options) {
|
|
|
3329
3453
|
const member = principalIdSchema.parse(c.req.param('principal'));
|
|
3330
3454
|
const { tenantId, scopeId, person } = target;
|
|
3331
3455
|
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
|
|
3456
|
+
return auditedAnswer(c, done, 'the removal', (removal, audit) => c.json({ ...removal, ...audit }));
|
|
3333
3457
|
});
|
|
3334
3458
|
// The owner HAND-OVER (#1665). The move runs in the VERTICAL, behind `/internal/owner-transfer`
|
|
3335
3459
|
// (vertical-host says in what order and why). This route decides who may ask, and audits every
|
|
@@ -3360,41 +3484,19 @@ export function createControlPlaneApi(options) {
|
|
|
3360
3484
|
// An abandon (`abandon: true`) is audited the same way, every row marked, so the log tells
|
|
3361
3485
|
// a hand-over closed without finishing from one that finished.
|
|
3362
3486
|
const base = { tenantId, scopeId, operationId, from: body.from, to: body.to, ...(body.abandon ? { abandon: true } : {}) };
|
|
3363
|
-
await
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
}
|
|
3377
|
-
throw e;
|
|
3378
|
-
}
|
|
3379
|
-
try {
|
|
3380
|
-
await c.var.admin.recordOwnerTransfer(actor, {
|
|
3381
|
-
...base,
|
|
3382
|
-
phase: 'applied',
|
|
3383
|
-
outcome: moved.outcome,
|
|
3384
|
-
fromRevoked: moved.fromRevoked,
|
|
3385
|
-
});
|
|
3386
|
-
}
|
|
3387
|
-
catch (e) {
|
|
3388
|
-
// The owner HAS moved; only the outcome row is missing. Say exactly that, so nobody retries
|
|
3389
|
-
// a hand-over believing it failed, and name the operation whose intent row stands alone.
|
|
3390
|
-
const why = e instanceof Error ? e.message : String(e);
|
|
3391
|
-
return c.json({
|
|
3392
|
-
error: `the hand-over completed, but its outcome could not be written to the admin log: ${why}`,
|
|
3393
|
-
operationId,
|
|
3394
|
-
...moved,
|
|
3395
|
-
}, 500);
|
|
3396
|
-
}
|
|
3397
|
-
return c.json({ operationId, ...moved });
|
|
3487
|
+
const done = await auditedChange({
|
|
3488
|
+
flow: 'owner-transfer',
|
|
3489
|
+
operationId,
|
|
3490
|
+
run: () => vertical.transferOwner({ tenantId, scopeId, ...body }),
|
|
3491
|
+
// Only the vertical's 409 is a refusal: anything else may have stopped part-way.
|
|
3492
|
+
refused: (e) => e instanceof ControlPlaneError && e.status === 409,
|
|
3493
|
+
record: (row) => c.var.admin.recordOwnerTransfer(actor, row.phase === 'applied'
|
|
3494
|
+
? { ...base, phase: 'applied', outcome: row.result.outcome, fromRevoked: row.result.fromRevoked }
|
|
3495
|
+
: { ...base, ...row }),
|
|
3496
|
+
});
|
|
3497
|
+
// A missing outcome row is said in `auditWarning` on a success, so nobody retries a hand-over
|
|
3498
|
+
// believing it failed, under the operation whose intent row stands alone.
|
|
3499
|
+
return auditedAnswer(c, done, 'the hand-over', (moved, audit) => c.json({ ...audit, ...moved }));
|
|
3398
3500
|
});
|
|
3399
3501
|
// Deliver per-instance CONFIG to the scope's own storage (vertical-auth-detach.md
|
|
3400
3502
|
// §2.2) — the missing "delivery" step behind the dashboard's Env tab. Same K-3
|
|
@@ -6224,6 +6326,9 @@ export function createControlPlaneApi(options) {
|
|
|
6224
6326
|
// `emailSender` grant itself is never touched by a push. Refreshes on every re-push
|
|
6225
6327
|
// like the rest of the install spec, exactly because it grants nothing.
|
|
6226
6328
|
...(manifest.sendsEmail ? { sendsEmail: true } : {}),
|
|
6329
|
+
// Where the scope lifecycle is held (#1713): `'router'` keeps the vertical's scopes out of
|
|
6330
|
+
// the lifecycle delivery. Refreshes on every re-push like the rest of the install spec.
|
|
6331
|
+
...(manifest.lifecycle ? { lifecycle: manifest.lifecycle } : {}),
|
|
6227
6332
|
// The declared surfaces (K-26) ride like envSpec: registry metadata for the
|
|
6228
6333
|
// hostname-binding picker, never behavior. Not part of any admission digest.
|
|
6229
6334
|
...(manifest.surfaces ? { surfaces: manifest.surfaces } : {}),
|
|
@@ -7667,6 +7772,56 @@ export function createControlPlaneApi(options) {
|
|
|
7667
7772
|
}
|
|
7668
7773
|
return c.json(await c.var.admin.peerGrantsStatus(actor, { tenantId, scopeId }));
|
|
7669
7774
|
});
|
|
7775
|
+
// #1720: a caller's explicit target is a tenant decision. The dashboard checks its own
|
|
7776
|
+
// app-management permission; this route confines the tenant and both directory endpoints.
|
|
7777
|
+
const peerBindingBody = z.object({ vertical: verticalSlug, targetScopeId: scopeIdSchema.nullable() }).strict();
|
|
7778
|
+
app.get('/tenants/:tenantId/scopes/:scopeId/peer-bindings', async (c) => {
|
|
7779
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
7780
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
7781
|
+
const vertical = c.req.query('vertical');
|
|
7782
|
+
const pin = confinedTenant(c.get('principal'));
|
|
7783
|
+
if (pin !== null && pin !== tenantId)
|
|
7784
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
7785
|
+
const actor = c.get('actor');
|
|
7786
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId)))
|
|
7787
|
+
return c.json({ error: 'app not found' }, 404);
|
|
7788
|
+
return c.json(vertical === undefined
|
|
7789
|
+
? await c.var.admin.peerBindings(actor, tenantId, scopeId)
|
|
7790
|
+
: await c.var.admin.peerBinding(actor, tenantId, scopeId, verticalSlug.parse(vertical)) ?? null);
|
|
7791
|
+
});
|
|
7792
|
+
app.put('/tenants/:tenantId/scopes/:scopeId/peer-bindings', async (c) => {
|
|
7793
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
7794
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
7795
|
+
const pin = confinedTenant(c.get('principal'));
|
|
7796
|
+
// The target choice is the tenant's alone. Staff may inspect it through GET, but cannot
|
|
7797
|
+
// choose a tenant's site on its behalf.
|
|
7798
|
+
if (pin !== tenantId)
|
|
7799
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
7800
|
+
const body = peerBindingBody.parse(await c.req.json());
|
|
7801
|
+
const actor = c.get('actor');
|
|
7802
|
+
if (!(await c.var.admin.getScopeRecord(actor, tenantId, scopeId)))
|
|
7803
|
+
return c.json({ error: 'app not found' }, 404);
|
|
7804
|
+
const operationId = ulid();
|
|
7805
|
+
const base = { tenantId, scopeId, operationId, vertical: body.vertical, targetScopeId: body.targetScopeId };
|
|
7806
|
+
const done = await auditedChange({
|
|
7807
|
+
flow: 'peer-binding', operationId,
|
|
7808
|
+
run: async () => {
|
|
7809
|
+
try {
|
|
7810
|
+
return await c.var.admin.setPeerBinding(actor, tenantId, scopeId, body.vertical, body.targetScopeId);
|
|
7811
|
+
}
|
|
7812
|
+
catch (e) {
|
|
7813
|
+
if (errorCodeOf(e) === 'not_found')
|
|
7814
|
+
throw new ControlPlaneError(404, e instanceof Error ? e.message : String(e));
|
|
7815
|
+
throw e;
|
|
7816
|
+
}
|
|
7817
|
+
},
|
|
7818
|
+
refused: (e) => e instanceof ControlPlaneError && e.status < 500,
|
|
7819
|
+
record: (row) => c.var.admin.recordPeerBindingChange(actor, row.phase === 'applied'
|
|
7820
|
+
? { ...base, phase: 'applied', changed: row.result.changed, previousScopeId: row.result.previous }
|
|
7821
|
+
: { ...base, ...row }),
|
|
7822
|
+
});
|
|
7823
|
+
return auditedAnswer(c, done, 'the peer binding', (result, audit) => c.json({ ...result, ...audit }));
|
|
7824
|
+
});
|
|
7670
7825
|
// Orgs: the portal-customer grouping (§4.1). Creating one mints no permission —
|
|
7671
7826
|
// members reach what the org was GRANTED, and granting stays off this surface.
|
|
7672
7827
|
app.post('/tenants/:tenantId/orgs', async (c) => {
|
|
@@ -7693,9 +7848,12 @@ export function createControlPlaneApi(options) {
|
|
|
7693
7848
|
order: c.req.query('order'),
|
|
7694
7849
|
});
|
|
7695
7850
|
const entries = await c.var.admin.auditLog(c.get('actor'), filter);
|
|
7851
|
+
// #2064: each audited change's row says the outcome its operation stands at — the latest
|
|
7852
|
+
// outcome row wins — beside the raw row, which is returned as written.
|
|
7853
|
+
const resolved = await withAuditedOutcomes(c.var.admin, c.get('actor'), entries);
|
|
7696
7854
|
// The cursor IS the last entry's id (ULID order is chronological), so the
|
|
7697
7855
|
// page carries its own continuation and the console never assembles one.
|
|
7698
|
-
return c.json(pageOf(
|
|
7856
|
+
return c.json(pageOf(resolved, filter.limit, (e) => e.id));
|
|
7699
7857
|
});
|
|
7700
7858
|
// The recorded operational failures (#559) — the console's failures view, and the
|
|
7701
7859
|
// "what does this `reference = <id>` belong to" lookup. Newest first by default,
|