@substrat-run/control-plane-api 0.141.0 → 0.143.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 +8 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +170 -82
- package/dist/api.js.map +1 -1
- package/dist/cf-observability.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/platform-drain.d.ts +17 -0
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +48 -27
- package/dist/platform-drain.js.map +1 -1
- package/dist/platform-entry.d.ts +7 -5
- package/dist/platform-entry.d.ts.map +1 -1
- package/dist/platform-entry.generated.d.ts +2 -2
- package/dist/platform-entry.generated.d.ts.map +1 -1
- package/dist/platform-entry.generated.js +5 -5
- package/dist/platform-entry.generated.js.map +1 -1
- package/dist/platform-entry.js +47 -13
- package/dist/platform-entry.js.map +1 -1
- package/dist/provider-error.d.ts +34 -5
- package/dist/provider-error.d.ts.map +1 -1
- package/dist/provider-error.js +60 -5
- package/dist/provider-error.js.map +1 -1
- package/dist/scope-copy-backfill.d.ts +106 -0
- package/dist/scope-copy-backfill.d.ts.map +1 -0
- package/dist/scope-copy-backfill.js +418 -0
- package/dist/scope-copy-backfill.js.map +1 -0
- package/dist/scope-copy-cleanup.d.ts +10 -0
- package/dist/scope-copy-cleanup.d.ts.map +1 -1
- package/dist/scope-copy-cleanup.js +12 -6
- package/dist/scope-copy-cleanup.js.map +1 -1
- package/dist/scope-deployment.d.ts +33 -0
- package/dist/scope-deployment.d.ts.map +1 -1
- package/dist/scope-deployment.js +38 -0
- package/dist/scope-deployment.js.map +1 -1
- package/dist/staff-client.d.ts +2 -1
- package/dist/staff-client.d.ts.map +1 -1
- package/dist/staff-client.js +3 -0
- package/dist/staff-client.js.map +1 -1
- package/dist/vertical-client.d.ts +16 -4
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +43 -6
- package/dist/vertical-client.js.map +1 -1
- package/package.json +7 -6
package/dist/api.d.ts
CHANGED
|
@@ -339,6 +339,14 @@ type Vars = {
|
|
|
339
339
|
host: ScopeHost;
|
|
340
340
|
/** `host.admin` of the same request — the spelling every route writes through. */
|
|
341
341
|
admin: HostAdmin;
|
|
342
|
+
/**
|
|
343
|
+
* The error this request answered with, when a route answered it itself instead of throwing
|
|
344
|
+
* (`failedWith`) or `onError` did. The 5xx backstop reads it to attribute the row, and to skip
|
|
345
|
+
* one a step already recorded.
|
|
346
|
+
*/
|
|
347
|
+
failure?: unknown;
|
|
348
|
+
/** A step recorded this request's failure with no error object to mark (a literal 5xx). */
|
|
349
|
+
failureRecorded?: true;
|
|
342
350
|
};
|
|
343
351
|
/**
|
|
344
352
|
* The K-35 denial-log filter (#867). Bounded by default like every other HTTP read
|
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,EAkBL,KAAK,qBAAqB,EA4E1B,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,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, 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';
|
|
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, capabilityId as capabilityIdSchema, PLATFORM_FEATURES_HEADER, PLATFORM_FEATURE_SCOPE_SWEEPER, } from '@substrat-run/contracts';
|
|
4
4
|
import { SCOPE_COPY_LEASE_MS, copyRestoreFence } from '@substrat-run/kernel';
|
|
5
5
|
import { attributeFailure } from './failure-attribution.js';
|
|
6
6
|
import { auditedChange, withAuditedOutcomes } from './audited-change.js';
|
|
@@ -9,8 +9,9 @@ import { TENANT_HEADER, confinedTenant } from './auth.js';
|
|
|
9
9
|
import { mintTenantToken } from './tenant-token.js';
|
|
10
10
|
import { connectionGrantsForScope } from './vertical-client.js';
|
|
11
11
|
import { assertNoUnreachableScopeCopies, listAllScopeScriptCopies, reapScopeScriptCopies, recordStrandedStorage } from './scope-copy-cleanup.js';
|
|
12
|
+
import { BACKFILL_PAGE_MAX, backfillScopeScriptCopies } from './scope-copy-backfill.js';
|
|
12
13
|
import { oidcCallbackUrl, retireClientsOfReapedScope, wirePreviewAuth } from './preview-auth.js';
|
|
13
|
-
import { versionReachedAt } from './scope-deployment.js';
|
|
14
|
+
import { scopeDeployment, versionReachedAt } from './scope-deployment.js';
|
|
14
15
|
import { reconcileConnectionGrants } from './connection-grants.js';
|
|
15
16
|
import { reconcileThenReassert, reviewedEntityGrants, switchCarryFor } from './reconcile.js';
|
|
16
17
|
import { ConnectionRelayError, relayConnectionUpsert } from './connection-relay.js';
|
|
@@ -772,6 +773,18 @@ const namedTenants = (c) => {
|
|
|
772
773
|
];
|
|
773
774
|
return named.filter((v) => typeof v === 'string' && v.length > 0);
|
|
774
775
|
};
|
|
776
|
+
function messageOfThrow(err) {
|
|
777
|
+
return err instanceof Error ? err.message : String(err);
|
|
778
|
+
}
|
|
779
|
+
/** What a 5xx answered with no error object said: the JSON body's `error` (and `detail`), else the status. */
|
|
780
|
+
async function answeredMessage(res) {
|
|
781
|
+
const body = (await res.clone().json().catch(() => null));
|
|
782
|
+
const error = typeof body?.['error'] === 'string' ? body['error'] : typeof body?.['title'] === 'string' ? body['title'] : null;
|
|
783
|
+
const detail = typeof body?.['detail'] === 'string' ? body['detail'] : null;
|
|
784
|
+
if (error && detail)
|
|
785
|
+
return `${error}: ${detail}`;
|
|
786
|
+
return error ?? detail ?? `HTTP ${res.status}`;
|
|
787
|
+
}
|
|
775
788
|
/**
|
|
776
789
|
* The audited HTTP surface over `HostAdmin` (control-plane.md §4.5).
|
|
777
790
|
*
|
|
@@ -803,6 +816,48 @@ export function createControlPlaneApi(options) {
|
|
|
803
816
|
const { host, authenticate, authenticateBuilder, authenticateTenantService } = options;
|
|
804
817
|
const copyLeaseMs = options.copyLeaseMs ?? SCOPE_COPY_LEASE_MS;
|
|
805
818
|
const app = new Hono();
|
|
819
|
+
// The 5xx backstop: every 5xx this API answers lands an ops-failure row, however it was
|
|
820
|
+
// answered. A 5xx is the PLATFORM failing (an unmapped throw, or a downstream vertical's own
|
|
821
|
+
// 5xx passing through, #559), so the console must be able to list it and a `reference = <id>`
|
|
822
|
+
// search must find it. It used to be recorded only in `onError`, which never sees the routes
|
|
823
|
+
// that catch a ControlPlaneError and answer it themselves: a preview re-push whose bind failed
|
|
824
|
+
// after a clean carry answered 502 with a reference and left no row behind.
|
|
825
|
+
//
|
|
826
|
+
// Registered first, so it wraps every other middleware and route. 501 stays out: an honest
|
|
827
|
+
// not-implemented is a capability statement, not a failure. 4xx stay out too: they are
|
|
828
|
+
// refusals the caller can already read. Actor is unset only when the request failed before
|
|
829
|
+
// authentication, where nothing worth recording refuses. A throw a step already recorded,
|
|
830
|
+
// with its stage and scope, is not recorded again.
|
|
831
|
+
app.use('*', async (c, next) => {
|
|
832
|
+
await next();
|
|
833
|
+
const status = c.res.status;
|
|
834
|
+
const actor = c.get('actor');
|
|
835
|
+
if (status >= 500 && status !== 501 && actor && !c.get('failureRecorded')) {
|
|
836
|
+
const cause = c.get('failure');
|
|
837
|
+
const marked = typeof cause === 'object' && cause !== null && recordedCauses.has(cause);
|
|
838
|
+
if (!marked) {
|
|
839
|
+
const { vertical, tenantId } = await failureSubject(c);
|
|
840
|
+
recordFailure({
|
|
841
|
+
actor,
|
|
842
|
+
operation: `${c.req.method} ${c.req.routePath}`,
|
|
843
|
+
vertical,
|
|
844
|
+
tenantId,
|
|
845
|
+
scopeId: c.req.param('scopeId') ?? null,
|
|
846
|
+
status,
|
|
847
|
+
message: cause !== undefined ? messageOfThrow(cause) : await answeredMessage(c.res),
|
|
848
|
+
}, cause === undefined ? NO_CAUSE : cause);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (inflightRecords.size) {
|
|
852
|
+
// `executionCtx` throws outside a Worker (tests, node); the write then simply runs on.
|
|
853
|
+
try {
|
|
854
|
+
c.executionCtx.waitUntil(Promise.allSettled([...inflightRecords]));
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
// no execution context
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
806
861
|
// The CLI version advisory (#971), stamped on EVERY response — including the 401 the
|
|
807
862
|
// auth middleware below answers with, since a CLI too old to authenticate is exactly
|
|
808
863
|
// the one that needs to hear it. Registered first so nothing can return around it.
|
|
@@ -1026,20 +1081,62 @@ export function createControlPlaneApi(options) {
|
|
|
1026
1081
|
// `undefined` (`throw undefined`, a bare rejection), and that throw deserves an
|
|
1027
1082
|
// `unknown` attribution, not a skipped one. Only true omission skips.
|
|
1028
1083
|
const NO_CAUSE = Symbol('no-cause');
|
|
1084
|
+
// The throws a step already recorded, so the 5xx backstop below does not record them twice.
|
|
1085
|
+
const recordedCauses = new WeakSet();
|
|
1086
|
+
// The writes still in flight. A Worker may end once its response is sent, and a write nobody
|
|
1087
|
+
// awaits goes with it, so the backstop hands these to `waitUntil`.
|
|
1088
|
+
const inflightRecords = new Set();
|
|
1029
1089
|
const recordFailure = (entry, cause = NO_CAUSE) => {
|
|
1090
|
+
if (typeof cause === 'object' && cause !== null)
|
|
1091
|
+
recordedCauses.add(cause);
|
|
1030
1092
|
// The error SHAPE rides beside the prose (#1233): attributed here — one place,
|
|
1031
1093
|
// from the throw itself — so a fingerprint groups on a column and never has to
|
|
1032
1094
|
// regex a message. A caller with no throw in hand leaves the columns null,
|
|
1033
1095
|
// which reads as "nobody classified this" rather than a guess nobody made.
|
|
1034
1096
|
const attributed = cause === NO_CAUSE ? undefined : attributeFailure(cause);
|
|
1035
|
-
|
|
1097
|
+
const write = host.admin
|
|
1036
1098
|
.recordOpsFailure({
|
|
1037
1099
|
...entry,
|
|
1038
1100
|
origin: entry.origin ?? attributed?.origin ?? null,
|
|
1039
1101
|
code: entry.code ?? attributed?.code ?? null,
|
|
1040
1102
|
reference: entry.reference ?? UPSTREAM_REFERENCE.exec(entry.message)?.[1] ?? null,
|
|
1041
1103
|
})
|
|
1042
|
-
.catch(() => undefined)
|
|
1104
|
+
.catch(() => undefined)
|
|
1105
|
+
.finally(() => inflightRecords.delete(write));
|
|
1106
|
+
inflightRecords.add(write);
|
|
1107
|
+
};
|
|
1108
|
+
/**
|
|
1109
|
+
* Whose failure the backstop is recording, so the dashboard's tenant-pinned failure view lists
|
|
1110
|
+
* it. The path's tenant when it names one; otherwise the credential's own (a CI push token, a
|
|
1111
|
+
* builder); otherwise, on a vertical route, the vertical's owner. The slug is resolved the way
|
|
1112
|
+
* the route resolves it, so a CI push's bare `todo` is recorded as the full slug the
|
|
1113
|
+
* dashboard filters on. Best effort: a lookup that fails still records the row, unattributed.
|
|
1114
|
+
*/
|
|
1115
|
+
const failureSubject = async (c) => {
|
|
1116
|
+
// Already decoded by Hono, and handed on as-is the way the routes hand it to
|
|
1117
|
+
// `resolveVerticalId`. Decoding again would throw on a slug carrying `%25`, and a throw here
|
|
1118
|
+
// replaces the 5xx being recorded with onError's answer.
|
|
1119
|
+
const raw = c.req.param('slug') ?? null;
|
|
1120
|
+
const pathTenant = c.req.param('tenantId') ?? null;
|
|
1121
|
+
const principal = c.get('principal');
|
|
1122
|
+
const confined = principal ? confinedTenant(principal) : null;
|
|
1123
|
+
if (!raw)
|
|
1124
|
+
return { vertical: null, tenantId: pathTenant ?? confined };
|
|
1125
|
+
try {
|
|
1126
|
+
const vertical = await resolveVerticalId(c, raw);
|
|
1127
|
+
const owner = (pathTenant ?? confined)
|
|
1128
|
+
? null
|
|
1129
|
+
: ((await verticalOf(c.get('actor'), vertical))?.ownerTenant ?? null);
|
|
1130
|
+
return { vertical, tenantId: pathTenant ?? confined ?? owner };
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return { vertical: raw, tenantId: pathTenant ?? confined };
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
/** The status of a ControlPlaneError a route answers itself, with the error marked for the backstop. */
|
|
1137
|
+
const failedWith = (c, e) => {
|
|
1138
|
+
c.set('failure', e);
|
|
1139
|
+
return e.status;
|
|
1043
1140
|
};
|
|
1044
1141
|
// Rides out a transient downstream window: the install path's binding-attach →
|
|
1045
1142
|
// script-settings propagation race (#424 case 2), and a one-shot DO storage blip
|
|
@@ -1083,25 +1180,8 @@ export function createControlPlaneApi(options) {
|
|
|
1083
1180
|
return c.json({ error: err.message, carrySourceChanged: { scopeId: err.scopeId, from: err.from, to: err.to, attempts: err.attempts } }, 409);
|
|
1084
1181
|
}
|
|
1085
1182
|
const { status, body } = mapError(err);
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
// founding case, #559) — so it lands a durable ops-failure row the console can
|
|
1089
|
-
// list and a `reference = <id>` search can find. 501 stays out: an honest
|
|
1090
|
-
// not-implemented is a capability statement, not a failure. 4xx stay out too:
|
|
1091
|
-
// they are refusals the caller can already read. Actor is unset only when the
|
|
1092
|
-
// throw happened before authentication — nothing worth recording refuses there.
|
|
1093
|
-
const actor = c.get('actor');
|
|
1094
|
-
if (status >= 500 && status !== 501 && actor) {
|
|
1095
|
-
recordFailure({
|
|
1096
|
-
actor,
|
|
1097
|
-
operation: `${c.req.method} ${c.req.routePath}`,
|
|
1098
|
-
vertical: c.req.param('slug') ? decodeURIComponent(c.req.param('slug')) : null,
|
|
1099
|
-
tenantId: c.req.param('tenantId') ?? null,
|
|
1100
|
-
scopeId: c.req.param('scopeId') ?? null,
|
|
1101
|
-
status,
|
|
1102
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1103
|
-
}, err);
|
|
1104
|
-
}
|
|
1183
|
+
// The 5xx backstop (registered first, above) records it, with this throw as the cause.
|
|
1184
|
+
c.set('failure', err);
|
|
1105
1185
|
// A 500 is, by definition, a throw whose message `mapError` did not recognise — so the
|
|
1106
1186
|
// client gets a GENERIC body that discloses nothing, and until now nothing recorded WHAT
|
|
1107
1187
|
// threw either. That left every unmapped failure (e.g. a raw SQLite constraint from a
|
|
@@ -1212,7 +1292,7 @@ export function createControlPlaneApi(options) {
|
|
|
1212
1292
|
}
|
|
1213
1293
|
catch (e) {
|
|
1214
1294
|
if (e instanceof ControlPlaneError) {
|
|
1215
|
-
return c.json({ error: e.message }, e
|
|
1295
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
1216
1296
|
}
|
|
1217
1297
|
throw e;
|
|
1218
1298
|
}
|
|
@@ -1884,7 +1964,7 @@ export function createControlPlaneApi(options) {
|
|
|
1884
1964
|
}
|
|
1885
1965
|
catch (e) {
|
|
1886
1966
|
if (e instanceof ControlPlaneError)
|
|
1887
|
-
return c.json({ error: e.message }, e
|
|
1967
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
1888
1968
|
throw e;
|
|
1889
1969
|
}
|
|
1890
1970
|
});
|
|
@@ -1940,42 +2020,14 @@ export function createControlPlaneApi(options) {
|
|
|
1940
2020
|
* served version's hook ran when the bound one's did. `versionReachedAt(via, …)` turns
|
|
1941
2021
|
* this answer into the version, so the choice and the receipt have one source.
|
|
1942
2022
|
*/
|
|
1943
|
-
const deploymentForScope =
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (serving)
|
|
1952
|
-
return { client: serving, via: 'serving-script' };
|
|
1953
|
-
}
|
|
1954
|
-
const bySlug = async (slug) => {
|
|
1955
|
-
if (scope.verticalVersionId && options.resolveVerticalVersion) {
|
|
1956
|
-
const bound = await options.resolveVerticalVersion(slug, scope.verticalVersionId, actor);
|
|
1957
|
-
if (bound)
|
|
1958
|
-
return { client: bound, via: 'bound-version' };
|
|
1959
|
-
}
|
|
1960
|
-
const bySlugClient = options.verticals?.[slug] ?? (await options.resolveVertical?.(slug, actor));
|
|
1961
|
-
return bySlugClient ? { client: bySlugClient, via: 'slug' } : undefined;
|
|
1962
|
-
};
|
|
1963
|
-
const direct = await bySlug(scope.vertical);
|
|
1964
|
-
if (direct)
|
|
1965
|
-
return direct;
|
|
1966
|
-
// Miss path only (#417) — a resolved scope never pays for these registry reads: a
|
|
1967
|
-
// scope bound to a BARE slug that is not registered, while the owning tenant's
|
|
1968
|
-
// prefixed registration of the same name exists, is addressing the prefixed lineage
|
|
1969
|
-
// under its bare spelling. Retry once under the registry id; anything else still
|
|
1970
|
-
// misses and surfaces through `diagnoseUnboundScope` as before.
|
|
1971
|
-
if (scope.tenantId && !scope.vertical.includes('/') && (await ownerOf(actor, scope.vertical)) === undefined) {
|
|
1972
|
-
const tenant = await c.var.admin.getTenant(actor, scope.tenantId).catch(() => null);
|
|
1973
|
-
const prefixed = tenant ? `${tenant.slug}/${scope.vertical}` : null;
|
|
1974
|
-
if (prefixed && (await ownerOf(actor, prefixed)) !== undefined)
|
|
1975
|
-
return bySlug(prefixed);
|
|
1976
|
-
}
|
|
1977
|
-
return undefined;
|
|
1978
|
-
};
|
|
2023
|
+
const deploymentForScope = (c, scope) => scopeDeployment({
|
|
2024
|
+
verticals: options.verticals,
|
|
2025
|
+
resolveVertical: options.resolveVertical,
|
|
2026
|
+
resolveVerticalVersion: options.resolveVerticalVersion,
|
|
2027
|
+
resolveVerticalRef: options.resolveVerticalRef,
|
|
2028
|
+
ownerOf,
|
|
2029
|
+
tenantSlugOf: async (actor, tenantId) => (await c.var.admin.getTenant(actor, tenantId).catch(() => null))?.slug ?? null,
|
|
2030
|
+
}, c.get('actor'), scope);
|
|
1979
2031
|
/**
|
|
1980
2032
|
* Why a scope's vertical did not resolve to a running deployment — an ACTIONABLE 501
|
|
1981
2033
|
* body, computed only on the miss path, in place of the bare "no deployment is bound"
|
|
@@ -3142,7 +3194,7 @@ export function createControlPlaneApi(options) {
|
|
|
3142
3194
|
// unbound store is exactly what makes a vertical refuse its own reconcile), so
|
|
3143
3195
|
// it must not be the half that gets dropped.
|
|
3144
3196
|
const detail = storeErrors.length ? `${e.message} (stores: ${storeErrors.join('; ')})` : e.message;
|
|
3145
|
-
return c.json({ error: detail }, e
|
|
3197
|
+
return c.json({ error: detail }, failedWith(c, e));
|
|
3146
3198
|
}
|
|
3147
3199
|
throw e;
|
|
3148
3200
|
}
|
|
@@ -3439,6 +3491,23 @@ export function createControlPlaneApi(options) {
|
|
|
3439
3491
|
colocated: () => c.var.admin.listCapabilities(c.get('actor'), tenantId, scopeId, filter),
|
|
3440
3492
|
}, (r) => r.entries.length));
|
|
3441
3493
|
});
|
|
3494
|
+
// The operator's revoke of one capability (#1686): the lever for a leaked link, beside the
|
|
3495
|
+
// read above and gated the same way, in the handler as well as by the builder default-deny.
|
|
3496
|
+
// `HostAdmin.revokeCapability` reaches the deployment serving a hosted scope (its
|
|
3497
|
+
// `capabilityDelegation`) and audits it on this side, intent then outcome; the record names the
|
|
3498
|
+
// actor as its revoker. Idempotent: revoking a revoked capability answers 204 again. A capability
|
|
3499
|
+
// the scope does not hold is 404; a deployment built before the far end answers 501 "redeploy".
|
|
3500
|
+
app.post('/tenants/:tenantId/scopes/:scopeId/capabilities/:capabilityId/revoke', async (c) => {
|
|
3501
|
+
if (confinedTenant(c.get('principal')) !== null) {
|
|
3502
|
+
return c.json({ error: 'forbidden: revoking a capability is staff-only' }, 403);
|
|
3503
|
+
}
|
|
3504
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
3505
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
3506
|
+
const capability = capabilityIdSchema.parse(c.req.param('capabilityId'));
|
|
3507
|
+
// An unknown scope, another tenant's included, is the verb's own `not_found`: a 404.
|
|
3508
|
+
await c.var.admin.revokeCapability(c.get('actor'), tenantId, scopeId, capability);
|
|
3509
|
+
return c.body(null, 204);
|
|
3510
|
+
});
|
|
3442
3511
|
// The owner seat (#925). Both reads go to the VERTICAL — the seat lives in its identity
|
|
3443
3512
|
// directory, in its deployment — after the same K-3 cross-check every scope-addressed read
|
|
3444
3513
|
// here makes. A co-located scope (contract-test host, self-host running no vertical code)
|
|
@@ -3464,7 +3533,7 @@ export function createControlPlaneApi(options) {
|
|
|
3464
3533
|
}
|
|
3465
3534
|
catch (e) {
|
|
3466
3535
|
if (e instanceof ControlPlaneError)
|
|
3467
|
-
return c.json({ error: e.message }, e
|
|
3536
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
3468
3537
|
throw e;
|
|
3469
3538
|
}
|
|
3470
3539
|
});
|
|
@@ -3504,7 +3573,7 @@ export function createControlPlaneApi(options) {
|
|
|
3504
3573
|
}
|
|
3505
3574
|
catch (e) {
|
|
3506
3575
|
if (e instanceof ControlPlaneError)
|
|
3507
|
-
return c.json({ error: e.message }, e
|
|
3576
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
3508
3577
|
throw e;
|
|
3509
3578
|
}
|
|
3510
3579
|
});
|
|
@@ -3550,7 +3619,7 @@ export function createControlPlaneApi(options) {
|
|
|
3550
3619
|
const { operationId } = done;
|
|
3551
3620
|
if ('error' in done) {
|
|
3552
3621
|
if (done.error instanceof ControlPlaneError) {
|
|
3553
|
-
return c.json({ error: done.error.message, operationId }, done.error
|
|
3622
|
+
return c.json({ error: done.error.message, operationId }, failedWith(c, done.error));
|
|
3554
3623
|
}
|
|
3555
3624
|
throw done.error;
|
|
3556
3625
|
}
|
|
@@ -3589,7 +3658,7 @@ export function createControlPlaneApi(options) {
|
|
|
3589
3658
|
}
|
|
3590
3659
|
catch (e) {
|
|
3591
3660
|
if (e instanceof ControlPlaneError)
|
|
3592
|
-
return c.json({ error: e.message }, e
|
|
3661
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
3593
3662
|
throw e;
|
|
3594
3663
|
}
|
|
3595
3664
|
});
|
|
@@ -3698,7 +3767,7 @@ export function createControlPlaneApi(options) {
|
|
|
3698
3767
|
}
|
|
3699
3768
|
catch (e) {
|
|
3700
3769
|
if (e instanceof ControlPlaneError) {
|
|
3701
|
-
return c.json({ error: e.message }, e
|
|
3770
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
3702
3771
|
}
|
|
3703
3772
|
throw e;
|
|
3704
3773
|
}
|
|
@@ -3754,6 +3823,7 @@ export function createControlPlaneApi(options) {
|
|
|
3754
3823
|
status: 503,
|
|
3755
3824
|
message: error,
|
|
3756
3825
|
}, failure.cause);
|
|
3826
|
+
c.set('failureRecorded', true);
|
|
3757
3827
|
return c.json({ error }, 503);
|
|
3758
3828
|
}
|
|
3759
3829
|
}
|
|
@@ -3883,7 +3953,7 @@ export function createControlPlaneApi(options) {
|
|
|
3883
3953
|
}
|
|
3884
3954
|
catch (e) {
|
|
3885
3955
|
if (e instanceof ControlPlaneError) {
|
|
3886
|
-
return c.json({ error: e.message }, e
|
|
3956
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
3887
3957
|
}
|
|
3888
3958
|
throw e;
|
|
3889
3959
|
}
|
|
@@ -3968,7 +4038,7 @@ export function createControlPlaneApi(options) {
|
|
|
3968
4038
|
}
|
|
3969
4039
|
catch (e) {
|
|
3970
4040
|
if (e instanceof ControlPlaneError) {
|
|
3971
|
-
return c.json({ error: e.message }, e
|
|
4041
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
3972
4042
|
}
|
|
3973
4043
|
throw e;
|
|
3974
4044
|
}
|
|
@@ -4059,7 +4129,7 @@ export function createControlPlaneApi(options) {
|
|
|
4059
4129
|
}
|
|
4060
4130
|
catch (e) {
|
|
4061
4131
|
if (e instanceof ControlPlaneError) {
|
|
4062
|
-
return c.json({ error: e.message }, e
|
|
4132
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4063
4133
|
}
|
|
4064
4134
|
throw e;
|
|
4065
4135
|
}
|
|
@@ -4560,7 +4630,7 @@ export function createControlPlaneApi(options) {
|
|
|
4560
4630
|
}
|
|
4561
4631
|
catch (e) {
|
|
4562
4632
|
if (e instanceof ControlPlaneError) {
|
|
4563
|
-
return c.json({ error: e.message }, e
|
|
4633
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4564
4634
|
}
|
|
4565
4635
|
return c.json({
|
|
4566
4636
|
error: 'backup failed — the scope was NOT reaped and its data is intact; ' +
|
|
@@ -4581,7 +4651,7 @@ export function createControlPlaneApi(options) {
|
|
|
4581
4651
|
}
|
|
4582
4652
|
catch (e) {
|
|
4583
4653
|
if (e instanceof ControlPlaneError) {
|
|
4584
|
-
return c.json({ error: e.message }, e
|
|
4654
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4585
4655
|
}
|
|
4586
4656
|
throw e;
|
|
4587
4657
|
}
|
|
@@ -4628,7 +4698,7 @@ export function createControlPlaneApi(options) {
|
|
|
4628
4698
|
}
|
|
4629
4699
|
catch (e) {
|
|
4630
4700
|
if (e instanceof ControlPlaneError) {
|
|
4631
|
-
return c.json({ error: e.message }, e
|
|
4701
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4632
4702
|
}
|
|
4633
4703
|
throw e;
|
|
4634
4704
|
}
|
|
@@ -4743,7 +4813,7 @@ export function createControlPlaneApi(options) {
|
|
|
4743
4813
|
}
|
|
4744
4814
|
catch (e) {
|
|
4745
4815
|
if (e instanceof ControlPlaneError) {
|
|
4746
|
-
return c.json({ error: e.message }, e
|
|
4816
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4747
4817
|
}
|
|
4748
4818
|
throw e;
|
|
4749
4819
|
}
|
|
@@ -4809,7 +4879,7 @@ export function createControlPlaneApi(options) {
|
|
|
4809
4879
|
}
|
|
4810
4880
|
catch (e) {
|
|
4811
4881
|
if (e instanceof ControlPlaneError) {
|
|
4812
|
-
return c.json({ error: e.message }, e
|
|
4882
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4813
4883
|
}
|
|
4814
4884
|
// A restore throw is driven by the caller-supplied dump (a shape the target cannot
|
|
4815
4885
|
// load, a DDL the engine rejects), so DISCLOSE it as an actionable 422 rather than
|
|
@@ -4889,7 +4959,7 @@ export function createControlPlaneApi(options) {
|
|
|
4889
4959
|
}
|
|
4890
4960
|
catch (e) {
|
|
4891
4961
|
if (e instanceof ControlPlaneError) {
|
|
4892
|
-
return c.json({ error: e.message }, e
|
|
4962
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
4893
4963
|
}
|
|
4894
4964
|
throw e;
|
|
4895
4965
|
}
|
|
@@ -4909,7 +4979,7 @@ export function createControlPlaneApi(options) {
|
|
|
4909
4979
|
}
|
|
4910
4980
|
catch (e) {
|
|
4911
4981
|
if (e instanceof ControlPlaneError) {
|
|
4912
|
-
return c.json(exportBreakBody(e), e
|
|
4982
|
+
return c.json(exportBreakBody(e), failedWith(c, e));
|
|
4913
4983
|
}
|
|
4914
4984
|
throw e;
|
|
4915
4985
|
}
|
|
@@ -4947,7 +5017,7 @@ export function createControlPlaneApi(options) {
|
|
|
4947
5017
|
}
|
|
4948
5018
|
catch (e) {
|
|
4949
5019
|
if (e instanceof ControlPlaneError) {
|
|
4950
|
-
return c.json({ ...exportBreakBody(e), adopted, alreadyAdopted }, e
|
|
5020
|
+
return c.json({ ...exportBreakBody(e), adopted, alreadyAdopted }, failedWith(c, e));
|
|
4951
5021
|
}
|
|
4952
5022
|
throw e;
|
|
4953
5023
|
}
|
|
@@ -4979,7 +5049,7 @@ export function createControlPlaneApi(options) {
|
|
|
4979
5049
|
}
|
|
4980
5050
|
catch (e) {
|
|
4981
5051
|
if (e instanceof ControlPlaneError) {
|
|
4982
|
-
return c.json(exportBreakBody(e), e
|
|
5052
|
+
return c.json(exportBreakBody(e), failedWith(c, e));
|
|
4983
5053
|
}
|
|
4984
5054
|
throw e;
|
|
4985
5055
|
}
|
|
@@ -5311,6 +5381,23 @@ export function createControlPlaneApi(options) {
|
|
|
5311
5381
|
await bind({ acknowledge });
|
|
5312
5382
|
return c.json(await c.var.admin.getScopeRecord(actor, tenantId, scopeId));
|
|
5313
5383
|
});
|
|
5384
|
+
// -- copy-ledger backfill (#1722) ------------------------------------------------------------
|
|
5385
|
+
// Records the scripts a scope was copied into before the ledger existed, read from the admin
|
|
5386
|
+
// log, as `retained`: reached by reap and erasure from then on, never wiped from here
|
|
5387
|
+
// (`scope-copy-backfill.ts`). A dry run unless the body says otherwise, one page per call,
|
|
5388
|
+
// resumed with the `nextCursor` it answers. Staff only, by default-deny: on neither
|
|
5389
|
+
// BUILDER_ROUTES nor TENANT_ROUTES.
|
|
5390
|
+
const copyBackfillBody = z.object({
|
|
5391
|
+
dryRun: z.boolean().default(true),
|
|
5392
|
+
cursor: z.string().min(1).optional(),
|
|
5393
|
+
limit: z.number().int().min(1).max(BACKFILL_PAGE_MAX).optional(),
|
|
5394
|
+
}).strict();
|
|
5395
|
+
app.post('/scope-copies/backfill', async (c) => {
|
|
5396
|
+
const body = await readJsonBody(c, copyBackfillBody);
|
|
5397
|
+
if (!options.resolveVerticalRef)
|
|
5398
|
+
throw new ControlPlaneError(501, 'the copy backfill needs dispatch resolution by script');
|
|
5399
|
+
return c.json(await backfillScopeScriptCopies({ admin: c.var.admin, actor: c.get('actor'), resolveRef: options.resolveVerticalRef }, body));
|
|
5400
|
+
});
|
|
5314
5401
|
// -- instances (K-31) -------------------------------------------------------
|
|
5315
5402
|
// The one place this surface calls OUT rather than sitting over `HostAdmin`, and
|
|
5316
5403
|
// it is unavoidable: only the vertical can create a usable scope DO, because the
|
|
@@ -5447,7 +5534,7 @@ export function createControlPlaneApi(options) {
|
|
|
5447
5534
|
message: e.message,
|
|
5448
5535
|
}, e);
|
|
5449
5536
|
}
|
|
5450
|
-
return c.json({ error: e.message }, e
|
|
5537
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
5451
5538
|
}
|
|
5452
5539
|
throw e;
|
|
5453
5540
|
}
|
|
@@ -6598,6 +6685,7 @@ export function createControlPlaneApi(options) {
|
|
|
6598
6685
|
status: rejected ? 422 : 502,
|
|
6599
6686
|
message: detail,
|
|
6600
6687
|
}, e);
|
|
6688
|
+
c.set('failureRecorded', true);
|
|
6601
6689
|
return rejected
|
|
6602
6690
|
? c.json({ error: 'deploy rejected', detail }, 422)
|
|
6603
6691
|
: c.json({ error: 'deploy upload failed', detail }, 502);
|
|
@@ -7908,7 +7996,7 @@ export function createControlPlaneApi(options) {
|
|
|
7908
7996
|
}
|
|
7909
7997
|
catch (e) {
|
|
7910
7998
|
if (e instanceof ControlPlaneError)
|
|
7911
|
-
return c.json({ error: e.message }, e
|
|
7999
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
7912
8000
|
throw e;
|
|
7913
8001
|
}
|
|
7914
8002
|
});
|
|
@@ -7959,7 +8047,7 @@ export function createControlPlaneApi(options) {
|
|
|
7959
8047
|
}
|
|
7960
8048
|
catch (e) {
|
|
7961
8049
|
if (e instanceof ControlPlaneError)
|
|
7962
|
-
return c.json({ error: e.message }, e
|
|
8050
|
+
return c.json({ error: e.message }, failedWith(c, e));
|
|
7963
8051
|
throw e;
|
|
7964
8052
|
}
|
|
7965
8053
|
});
|