@substrat-run/control-plane-api 0.57.1 → 0.59.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 +10 -7
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +95 -16
- package/dist/api.js.map +1 -1
- package/dist/connection-relay.d.ts +32 -0
- package/dist/connection-relay.d.ts.map +1 -0
- package/dist/connection-relay.js +100 -0
- package/dist/connection-relay.js.map +1 -0
- package/dist/deploy.d.ts +20 -14
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/platform-drain.d.ts +1 -1
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +5 -1
- package/dist/platform-drain.js.map +1 -1
- package/dist/vertical-client.d.ts +27 -1
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +20 -0
- package/dist/vertical-client.js.map +1 -1
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +28 -25
- package/dist/wfp.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import type { PlatformActorId } from '@substrat-run/contracts';
|
|
3
3
|
import type { ScopeHost } from '@substrat-run/kernel';
|
|
4
4
|
import type { PlatformActorAuth, BuilderAuth, Principal } from './auth.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type VerticalClient } from './vertical-client.js';
|
|
6
6
|
import type { DeployVerticalFn, FetchVerticalAssetFn, FetchVerticalModulesFn } from './deploy.js';
|
|
7
7
|
import type { PatchScriptBindingsFn } from './wfp.js';
|
|
8
8
|
import type { ObservabilityReader } from './observability.js';
|
|
@@ -64,12 +64,15 @@ export interface ControlPlaneApiOptions {
|
|
|
64
64
|
*/
|
|
65
65
|
fetchVerticalModules?: FetchVerticalModulesFn;
|
|
66
66
|
/**
|
|
67
|
-
* Reads one static file back from a script
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* script
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
* Reads one static file's bytes back from a script in the namespace (#578) — the
|
|
68
|
+
* asset twin of `fetchVerticalModules`. The runtime's asset store dedupes per
|
|
69
|
+
* SCRIPT, not namespace-wide, so the first serve of an asset-carrying version onto
|
|
70
|
+
* the stable serving script always finds its hashes missing there; this seam is how
|
|
71
|
+
* the serve recovers the bytes the push uploaded to the version's archive script.
|
|
72
|
+
* Host-injected like `deployVertical` (on Cloudflare, a dispatch fetch — the archive
|
|
73
|
+
* script's edge serves its own assets without invoking the worker). Absent ⇒ a
|
|
74
|
+
* re-serve can only ride what the stable script already holds and refuses honestly
|
|
75
|
+
* otherwise.
|
|
73
76
|
*/
|
|
74
77
|
fetchVerticalAsset?: FetchVerticalAssetFn;
|
|
75
78
|
/**
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { adminAction, ASSET_PART_PREFIX, assetHash, channelName, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, pageOf, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, registerVerticalInput, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, } from '@substrat-run/contracts';
|
|
2
|
+
import { adminAction, ASSET_PART_PREFIX, assetHash, channelName, connectionFilter, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, pageOf, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, registerVerticalInput, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, } from '@substrat-run/contracts';
|
|
3
3
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
4
4
|
import { TENANT_HEADER } from './auth.js';
|
|
5
|
+
import { connectionGrantsForScope } from './vertical-client.js';
|
|
6
|
+
import { ConnectionRelayError, relayConnectionUpsert } from './connection-relay.js';
|
|
5
7
|
import { ControlPlaneError } from './client.js';
|
|
6
8
|
import { provisionSiblingScope } from './platform-drain.js';
|
|
7
9
|
import { mapError } from './errors.js';
|
|
@@ -610,6 +612,67 @@ export function createControlPlaneApi(options) {
|
|
|
610
612
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
611
613
|
return c.json(await admin.listIdentityLinks(c.get('actor'), tenantId));
|
|
612
614
|
});
|
|
615
|
+
// The tenant's live connection grants (#592) — the readable "what may this connection
|
|
616
|
+
// invoke" (connections.md §6.2.4 Q2), and the rows provision/reconcile deliver from.
|
|
617
|
+
app.get('/tenants/:tenantId/connection-grants', async (c) => {
|
|
618
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
619
|
+
return c.json(await admin.listConnectionGrants(c.get('actor'), tenantId));
|
|
620
|
+
});
|
|
621
|
+
// -- the connection store, tenant-scoped (connections.md §3.5) -------------
|
|
622
|
+
//
|
|
623
|
+
// The dashboard's door to the PLATFORM's connection store. The dashboard keeps its own
|
|
624
|
+
// directory (its GitHub App connections live there, consumed by the dashboard itself),
|
|
625
|
+
// but a provider credential a PLATFORM-run connector consumes (Scrive, Fortnox) must
|
|
626
|
+
// land in THIS directory — `connector:<provider>` dispatch opens connections here, and
|
|
627
|
+
// a row written anywhere else is invisible to it. Metadata only on the read: the
|
|
628
|
+
// `Connection` type cannot carry a secret (contract-tested), so listing is safe as-is.
|
|
629
|
+
// Not in BUILDER_ROUTES — service/staff only, fail-closed, same law as the identity
|
|
630
|
+
// mirror above.
|
|
631
|
+
app.get('/tenants/:tenantId/connections', async (c) => {
|
|
632
|
+
const filter = connectionFilter.parse({
|
|
633
|
+
tenantId: tenantIdSchema.parse(c.req.param('tenantId')),
|
|
634
|
+
vertical: c.req.query('vertical'),
|
|
635
|
+
provider: c.req.query('provider'),
|
|
636
|
+
includeRevoked: c.req.query('includeRevoked') === '1' ? true : undefined,
|
|
637
|
+
});
|
|
638
|
+
return c.json(await admin.listConnections(c.get('actor'), filter));
|
|
639
|
+
});
|
|
640
|
+
// The same upsert semantics as `/internal/connections/upsert` (§3.5.2) — create under a
|
|
641
|
+
// fresh id, or rotate the one live row in place so its grant tuples survive — behind
|
|
642
|
+
// platform-actor auth instead of the vertical-harness PLATFORM_SECRET. The tenant is
|
|
643
|
+
// the path's, never the body's: a body naming a different tenant is refused, not
|
|
644
|
+
// silently rewritten.
|
|
645
|
+
app.post('/tenants/:tenantId/connections', async (c) => {
|
|
646
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
647
|
+
const raw = await c.req.json();
|
|
648
|
+
const body = typeof raw === 'object' && raw !== null ? raw : {};
|
|
649
|
+
if (body.tenantId !== undefined && body.tenantId !== tenantId) {
|
|
650
|
+
return c.json({ error: 'body tenantId disagrees with the route tenant' }, 400);
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
return c.json(await relayConnectionUpsert(host, c.get('actor'), { ...body, tenantId }));
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
if (err instanceof ConnectionRelayError)
|
|
657
|
+
return c.json({ error: err.message }, err.status);
|
|
658
|
+
throw err;
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
// Revoke — terminal (the sealed secret is deleted, grants tombstone; a replacement is a
|
|
662
|
+
// new connection). Tenant-scoped with K-3 existence hiding: a foreign tenant's
|
|
663
|
+
// connection id is indistinguishable from an absent one. Idempotent: revoking an
|
|
664
|
+
// already-revoked row is a 204 no-op, not an error.
|
|
665
|
+
app.delete('/tenants/:tenantId/connections/:id', async (c) => {
|
|
666
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
667
|
+
const id = c.req.param('id');
|
|
668
|
+
const rows = await admin.listConnections(c.get('actor'), { tenantId, includeRevoked: true });
|
|
669
|
+
const row = rows.find((r) => r.id === id);
|
|
670
|
+
if (!row)
|
|
671
|
+
return c.json({ error: 'unknown connection' }, 404);
|
|
672
|
+
if (row.status !== 'revoked')
|
|
673
|
+
await admin.revokeConnection(c.get('actor'), row.id);
|
|
674
|
+
return c.body(null, 204);
|
|
675
|
+
});
|
|
613
676
|
// -- the scope directory (§3.2/§4.2) ---------------------------------------
|
|
614
677
|
app.get('/scopes', async (c) => {
|
|
615
678
|
const p = c.get('principal');
|
|
@@ -1021,8 +1084,18 @@ export function createControlPlaneApi(options) {
|
|
|
1021
1084
|
// #406: re-gathered and re-delivered like entitlements, so a reconcile also repairs a
|
|
1022
1085
|
// dropped identity-link delivery — and is the channel a link/unlink after provision rides.
|
|
1023
1086
|
const identityLinks = (await admin.listIdentityLinks(actor, tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
1087
|
+
// #592: connection grants ride the same authoritative gather — the back-fill for a
|
|
1088
|
+
// scope provisioned before `grantToConnection` ran, and how a revoked connection's
|
|
1089
|
+
// grants stop being delivered (they are absent from the directory's live rows).
|
|
1090
|
+
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(actor, tenantId), scope.vertical, scopeId);
|
|
1024
1091
|
try {
|
|
1025
|
-
const result = await vertical.reconcileInstance({
|
|
1092
|
+
const result = await vertical.reconcileInstance({
|
|
1093
|
+
tenantId,
|
|
1094
|
+
scopeId,
|
|
1095
|
+
entitlements,
|
|
1096
|
+
identityLinks,
|
|
1097
|
+
connectionGrants,
|
|
1098
|
+
});
|
|
1026
1099
|
return c.json(result);
|
|
1027
1100
|
}
|
|
1028
1101
|
catch (e) {
|
|
@@ -1906,6 +1979,11 @@ export function createControlPlaneApi(options) {
|
|
|
1906
1979
|
// so its auth adapter resolves logins from local storage, and offboarding becomes an
|
|
1907
1980
|
// unlink + re-deliver instead of a source edit + deploy.
|
|
1908
1981
|
const identityLinks = (await admin.listIdentityLinks(c.get('actor'), input.tenantId)).map(({ tenantId: _tenantId, ...link }) => link);
|
|
1982
|
+
// #592: connection grants too — tenant-wide rows materialize for the NEW scope, so an
|
|
1983
|
+
// install provisioned after `grantToConnection` holds the same `connection:<id>` tuple
|
|
1984
|
+
// as one provisioned before it, and the connector return path works without a human
|
|
1985
|
+
// replaying grants per install.
|
|
1986
|
+
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(c.get('actor'), input.tenantId), slug, input.scopeId);
|
|
1909
1987
|
// #301 PR-2: per-tenant relational stores the vertical DECLARED, minted here (before
|
|
1910
1988
|
// the callback — the vertical migrates the store inside the K-31 ready-gate, so it
|
|
1911
1989
|
// must exist and be bound first). Idempotent like the endpoint: a retried provision
|
|
@@ -1937,6 +2015,7 @@ export function createControlPlaneApi(options) {
|
|
|
1937
2015
|
...input,
|
|
1938
2016
|
entitlements,
|
|
1939
2017
|
identityLinks,
|
|
2018
|
+
connectionGrants,
|
|
1940
2019
|
...(tenantStores.length ? { tenantStores } : {}),
|
|
1941
2020
|
}));
|
|
1942
2021
|
return c.json(instance, 201);
|
|
@@ -2229,6 +2308,7 @@ export function createControlPlaneApi(options) {
|
|
|
2229
2308
|
if (!version?.deploymentRef) {
|
|
2230
2309
|
throw new ControlPlaneError(502, `version ${versionId} has no archive script to serve from`);
|
|
2231
2310
|
}
|
|
2311
|
+
const archiveRef = version.deploymentRef;
|
|
2232
2312
|
const manifestJson = await admin.versionManifest(actor, slug, versionId);
|
|
2233
2313
|
if (!manifestJson) {
|
|
2234
2314
|
throw new ControlPlaneError(502, `version ${versionId} retained no manifest — pushed pre-#286; push it again to serve in place`);
|
|
@@ -2259,24 +2339,23 @@ export function createControlPlaneApi(options) {
|
|
|
2259
2339
|
modules,
|
|
2260
2340
|
doClasses: manifest.doClasses,
|
|
2261
2341
|
bindings: [...manifest.bindings, ...storeBindings],
|
|
2262
|
-
// #340: the version's static files travel with it onto the serving script —
|
|
2263
|
-
// the RETAINED manifest, with no bytes
|
|
2264
|
-
// SCRIPT (
|
|
2265
|
-
//
|
|
2266
|
-
//
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2269
|
-
//
|
|
2342
|
+
// #340/#578: the version's static files travel with it onto the serving script —
|
|
2343
|
+
// from the RETAINED manifest, with no bytes. The runtime's asset store dedupes
|
|
2344
|
+
// per SCRIPT (not namespace-wide — the #578 finding), so the serving script only
|
|
2345
|
+
// skips hashes it has itself held before; everything else — every first serve of
|
|
2346
|
+
// new content — is recovered on demand from the version's archive script, the
|
|
2347
|
+
// same store the modules come back from (#286), and verified against its
|
|
2348
|
+
// content-address before upload. This is why the manifest is retained rather
|
|
2349
|
+
// than the bytes: the archive script gives back modules AND assets.
|
|
2270
2350
|
...(manifest.assets
|
|
2271
2351
|
? {
|
|
2272
2352
|
assets: {
|
|
2273
2353
|
...manifest.assets,
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
})),
|
|
2354
|
+
...(options.fetchVerticalAsset
|
|
2355
|
+
? {
|
|
2356
|
+
recoverContent: (asset) => options.fetchVerticalAsset(archiveRef, asset),
|
|
2357
|
+
}
|
|
2358
|
+
: {}),
|
|
2280
2359
|
},
|
|
2281
2360
|
}
|
|
2282
2361
|
: {}),
|