@substrat-run/control-plane-api 0.58.0 → 0.60.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 +41 -7
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +126 -15
- package/dist/api.js.map +1 -1
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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 +5 -5
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import type { PlatformActorId } from '@substrat-run/contracts';
|
|
2
|
+
import type { Connection, ConnectionActivity, ConnectionProbe, 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
5
|
import { type VerticalClient } from './vertical-client.js';
|
|
@@ -10,6 +10,23 @@ import type { PlatformRuntime } from './platform-runtime.js';
|
|
|
10
10
|
import { type DoNamespaceReader } from './do-namespaces.js';
|
|
11
11
|
import type { DirectoryBackupStore, ScopeBackupStore } from './backups.js';
|
|
12
12
|
import { type CustomHostnameProvisioner } from './custom-hostnames.js';
|
|
13
|
+
/**
|
|
14
|
+
* What one provider can answer about a live connection (#605) — the seam behind
|
|
15
|
+
* `POST /tenants/:t/connections/:id/verify` and `GET …/activity`.
|
|
16
|
+
*
|
|
17
|
+
* Both halves take the connection ROW, never a credential: opening the secret is the
|
|
18
|
+
* connector's own act through `HostAdmin`, so the plaintext still never crosses this
|
|
19
|
+
* package. `probe` says whether the provider accepts the credential and whose account
|
|
20
|
+
* it is; `activity` projects the connector's dispatch ledger into the declared shape —
|
|
21
|
+
* a projection precisely because a raw ledger row may carry connector secrets (Scrive's
|
|
22
|
+
* callback capability token), and redaction has to be structural.
|
|
23
|
+
*/
|
|
24
|
+
export interface ConnectionInspector {
|
|
25
|
+
probe?: (host: ScopeHost, connection: Connection) => Promise<ConnectionProbe>;
|
|
26
|
+
activity?: (host: ScopeHost, connection: Connection, opts: {
|
|
27
|
+
live: boolean;
|
|
28
|
+
}) => Promise<ConnectionActivity>;
|
|
29
|
+
}
|
|
13
30
|
export interface ControlPlaneApiOptions {
|
|
14
31
|
host: ScopeHost;
|
|
15
32
|
/**
|
|
@@ -64,12 +81,15 @@ export interface ControlPlaneApiOptions {
|
|
|
64
81
|
*/
|
|
65
82
|
fetchVerticalModules?: FetchVerticalModulesFn;
|
|
66
83
|
/**
|
|
67
|
-
* Reads one static file back from a script
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* script
|
|
71
|
-
*
|
|
72
|
-
*
|
|
84
|
+
* Reads one static file's bytes back from a script in the namespace (#578) — the
|
|
85
|
+
* asset twin of `fetchVerticalModules`. The runtime's asset store dedupes per
|
|
86
|
+
* SCRIPT, not namespace-wide, so the first serve of an asset-carrying version onto
|
|
87
|
+
* the stable serving script always finds its hashes missing there; this seam is how
|
|
88
|
+
* the serve recovers the bytes the push uploaded to the version's archive script.
|
|
89
|
+
* Host-injected like `deployVertical` (on Cloudflare, a dispatch fetch — the archive
|
|
90
|
+
* script's edge serves its own assets without invoking the worker). Absent ⇒ a
|
|
91
|
+
* re-serve can only ride what the stable script already holds and refuses honestly
|
|
92
|
+
* otherwise.
|
|
73
93
|
*/
|
|
74
94
|
fetchVerticalAsset?: FetchVerticalAssetFn;
|
|
75
95
|
/**
|
|
@@ -121,6 +141,20 @@ export interface ControlPlaneApiOptions {
|
|
|
121
141
|
* means forgetting that costs a feature, never a leak.
|
|
122
142
|
*/
|
|
123
143
|
observability?: ObservabilityReader;
|
|
144
|
+
/**
|
|
145
|
+
* Per-provider connection **inspectors** (#605), keyed by provider slug — what makes
|
|
146
|
+
* an integration something an operator can interrogate rather than trust.
|
|
147
|
+
*
|
|
148
|
+
* Host-injected for the same reason `deployVertical` and `observability` are: this
|
|
149
|
+
* package holds no connector and must learn no provider's vocabulary. The host wires
|
|
150
|
+
* the same connector closure it already registers for dispatch and sweep (the
|
|
151
|
+
* `sweepers` idiom, `apps/control-plane/src/worker.ts`), and the routes below stay
|
|
152
|
+
* pure transport over a declared, provider-agnostic shape.
|
|
153
|
+
*
|
|
154
|
+
* An unregistered provider 501s rather than answering emptily — "this platform cannot
|
|
155
|
+
* verify a Fortnox key yet" is a true statement; "your Fortnox key is fine" is not.
|
|
156
|
+
*/
|
|
157
|
+
connectionInspectors?: Record<string, ConnectionInspector>;
|
|
124
158
|
/**
|
|
125
159
|
* Where this platform's compute actually runs — the coordinates a staff surface needs
|
|
126
160
|
* to hand an operator a link INTO the provider's own console (the right script, the
|
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,8 +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, connectionActivity, connectionFilter, connectionProbe, 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
5
|
import { connectionGrantsForScope } from './vertical-client.js';
|
|
6
|
+
import { ConnectionRelayError, relayConnectionUpsert } from './connection-relay.js';
|
|
6
7
|
import { ControlPlaneError } from './client.js';
|
|
7
8
|
import { provisionSiblingScope } from './platform-drain.js';
|
|
8
9
|
import { mapError } from './errors.js';
|
|
@@ -617,6 +618,116 @@ export function createControlPlaneApi(options) {
|
|
|
617
618
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
618
619
|
return c.json(await admin.listConnectionGrants(c.get('actor'), tenantId));
|
|
619
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
|
+
});
|
|
676
|
+
/**
|
|
677
|
+
* The tenant-scoped connection lookup the two inspection routes share. Same K-3
|
|
678
|
+
* existence hiding as the revoke above — a foreign tenant's connection id reads
|
|
679
|
+
* exactly like an absent one — and revoked rows are excluded: a withdrawn credential
|
|
680
|
+
* has no secret left to probe with, so "revoked" is a 404 for these reads too.
|
|
681
|
+
*/
|
|
682
|
+
const inspectableConnection = async (c) => {
|
|
683
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
684
|
+
const id = c.req.param('id');
|
|
685
|
+
const rows = await admin.listConnections(c.get('actor'), { tenantId });
|
|
686
|
+
return rows.find((r) => r.id === id);
|
|
687
|
+
};
|
|
688
|
+
/**
|
|
689
|
+
* **Verify a credential against the provider** (#605) — the read that turns "stored"
|
|
690
|
+
* into "works, and for this account".
|
|
691
|
+
*
|
|
692
|
+
* A POST because it reaches out: it spends an authenticated call at the provider and
|
|
693
|
+
* writes health (`recordConnectionUse` rides the connector's sanctioned `fetch`), so
|
|
694
|
+
* it is not the safe, cacheable read a GET promises. Not audited — like every other
|
|
695
|
+
* connection USE, it is machine telemetry, and §3.4's rule is that the audit log
|
|
696
|
+
* records control-plane mutations, not outbound calls.
|
|
697
|
+
*
|
|
698
|
+
* A rejected credential is `200 { ok: false, error }`, not an HTTP error: the
|
|
699
|
+
* provider answered, and its answer is the payload. Only a platform fault (no
|
|
700
|
+
* inspector wired, an unopenable secret) is a non-200.
|
|
701
|
+
*/
|
|
702
|
+
app.post('/tenants/:tenantId/connections/:id/verify', async (c) => {
|
|
703
|
+
const row = await inspectableConnection(c);
|
|
704
|
+
if (!row)
|
|
705
|
+
return c.json({ error: 'unknown connection' }, 404);
|
|
706
|
+
const probe = options.connectionInspectors?.[row.provider]?.probe;
|
|
707
|
+
if (!probe) {
|
|
708
|
+
return c.json({ error: `no probe registered for provider '${row.provider}'` }, 501);
|
|
709
|
+
}
|
|
710
|
+
return c.json(connectionProbe.parse(await probe(host, row)));
|
|
711
|
+
});
|
|
712
|
+
/**
|
|
713
|
+
* **What the connection has done** (#605) — the connector's dispatch ledger, projected.
|
|
714
|
+
*
|
|
715
|
+
* `?live=1` asks the provider for current state as well; the result says which it got
|
|
716
|
+
* (`live`), because presenting the ledger's view as the provider's would be inventing
|
|
717
|
+
* facts. The projection is the connector's, never this package's: a raw ledger row can
|
|
718
|
+
* carry connector secrets, so it is never serialized here.
|
|
719
|
+
*/
|
|
720
|
+
app.get('/tenants/:tenantId/connections/:id/activity', async (c) => {
|
|
721
|
+
const row = await inspectableConnection(c);
|
|
722
|
+
if (!row)
|
|
723
|
+
return c.json({ error: 'unknown connection' }, 404);
|
|
724
|
+
const activity = options.connectionInspectors?.[row.provider]?.activity;
|
|
725
|
+
if (!activity) {
|
|
726
|
+
return c.json({ error: `no activity view registered for provider '${row.provider}'` }, 501);
|
|
727
|
+
}
|
|
728
|
+
const live = c.req.query('live') === '1';
|
|
729
|
+
return c.json(connectionActivity.parse(await activity(host, row, { live })));
|
|
730
|
+
});
|
|
620
731
|
// -- the scope directory (§3.2/§4.2) ---------------------------------------
|
|
621
732
|
app.get('/scopes', async (c) => {
|
|
622
733
|
const p = c.get('principal');
|
|
@@ -2252,6 +2363,7 @@ export function createControlPlaneApi(options) {
|
|
|
2252
2363
|
if (!version?.deploymentRef) {
|
|
2253
2364
|
throw new ControlPlaneError(502, `version ${versionId} has no archive script to serve from`);
|
|
2254
2365
|
}
|
|
2366
|
+
const archiveRef = version.deploymentRef;
|
|
2255
2367
|
const manifestJson = await admin.versionManifest(actor, slug, versionId);
|
|
2256
2368
|
if (!manifestJson) {
|
|
2257
2369
|
throw new ControlPlaneError(502, `version ${versionId} retained no manifest — pushed pre-#286; push it again to serve in place`);
|
|
@@ -2282,24 +2394,23 @@ export function createControlPlaneApi(options) {
|
|
|
2282
2394
|
modules,
|
|
2283
2395
|
doClasses: manifest.doClasses,
|
|
2284
2396
|
bindings: [...manifest.bindings, ...storeBindings],
|
|
2285
|
-
// #340: the version's static files travel with it onto the serving script —
|
|
2286
|
-
// the RETAINED manifest, with no bytes
|
|
2287
|
-
// SCRIPT (
|
|
2288
|
-
//
|
|
2289
|
-
//
|
|
2290
|
-
//
|
|
2291
|
-
//
|
|
2292
|
-
//
|
|
2397
|
+
// #340/#578: the version's static files travel with it onto the serving script —
|
|
2398
|
+
// from the RETAINED manifest, with no bytes. The runtime's asset store dedupes
|
|
2399
|
+
// per SCRIPT (not namespace-wide — the #578 finding), so the serving script only
|
|
2400
|
+
// skips hashes it has itself held before; everything else — every first serve of
|
|
2401
|
+
// new content — is recovered on demand from the version's archive script, the
|
|
2402
|
+
// same store the modules come back from (#286), and verified against its
|
|
2403
|
+
// content-address before upload. This is why the manifest is retained rather
|
|
2404
|
+
// than the bytes: the archive script gives back modules AND assets.
|
|
2293
2405
|
...(manifest.assets
|
|
2294
2406
|
? {
|
|
2295
2407
|
assets: {
|
|
2296
2408
|
...manifest.assets,
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
})),
|
|
2409
|
+
...(options.fetchVerticalAsset
|
|
2410
|
+
? {
|
|
2411
|
+
recoverContent: (asset) => options.fetchVerticalAsset(archiveRef, asset),
|
|
2412
|
+
}
|
|
2413
|
+
: {}),
|
|
2303
2414
|
},
|
|
2304
2415
|
}
|
|
2305
2416
|
: {}),
|