@substrat-run/control-plane-api 0.14.0 → 0.17.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/README.md +58 -0
- package/dist/api.d.ts +25 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +381 -17
- package/dist/api.js.map +1 -1
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +11 -0
- package/dist/auth.js.map +1 -1
- package/dist/cf-observability.d.ts +22 -0
- package/dist/cf-observability.d.ts.map +1 -0
- package/dist/cf-observability.js +110 -0
- package/dist/cf-observability.js.map +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -1
- package/dist/deploy.d.ts +3 -46
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +22 -35
- package/dist/deploy.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mask.d.ts +4 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +71 -0
- package/dist/mask.js.map +1 -0
- package/dist/observability.d.ts +53 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +17 -0
- package/dist/observability.js.map +1 -0
- package/dist/push-token.d.ts +33 -0
- package/dist/push-token.d.ts.map +1 -0
- package/dist/push-token.js +114 -0
- package/dist/push-token.js.map +1 -0
- package/dist/vertical-client.d.ts +76 -5
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +65 -0
- package/dist/vertical-client.js.map +1 -1
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +4 -0
- package/dist/wfp.js.map +1 -1
- package/package.json +5 -5
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ControlPlaneError } from './client.js';
|
|
1
2
|
/**
|
|
2
3
|
* Map an adapter throw onto an HTTP status.
|
|
3
4
|
*
|
|
@@ -56,11 +57,23 @@ const STATUS_PATTERNS = [
|
|
|
56
57
|
[/unknown scope /, 404],
|
|
57
58
|
// A read-only introspection read (§5.4) for a table the scope's schema does not have.
|
|
58
59
|
[/unknown table /, 404],
|
|
60
|
+
// The SQL console's gate (#219) refused the statement — a malformed request, not a
|
|
61
|
+
// server fault. The prefix is pinned by the contract suite against both adapters.
|
|
62
|
+
[/read-only console/, 400],
|
|
59
63
|
[/unknown vertical /, 404],
|
|
60
64
|
[/unknown version /, 404],
|
|
61
65
|
[/scope has no tenant record/, 404],
|
|
62
66
|
];
|
|
63
67
|
export function mapError(err) {
|
|
68
|
+
// A ControlPlaneError is a DELIBERATE downstream answer, not an unreviewed throw —
|
|
69
|
+
// the VerticalClient wraps the vertical's own JSON status/message in it. Passing it
|
|
70
|
+
// through verbatim is what lets an honest refusal (e.g. auth-server's 501 for an
|
|
71
|
+
// unimplemented verb) reach the dashboard as itself, instead of collapsing into the
|
|
72
|
+
// generic 500 below (the shape of the 2026-07-25 incident, on this side of the seam).
|
|
73
|
+
// Several routes hand-catch it already; this makes the boundary consistent for the rest.
|
|
74
|
+
if (err instanceof ControlPlaneError) {
|
|
75
|
+
return { status: err.status, body: { error: err.message } };
|
|
76
|
+
}
|
|
64
77
|
const message = err instanceof Error ? err.message : String(err);
|
|
65
78
|
for (const [pattern, status] of STATUS_PATTERNS) {
|
|
66
79
|
if (pattern.test(message))
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;;;;;;GAOG;AACH,MAAM,eAAe,GAA8C;IACjE,gFAAgF;IAChF,wEAAwE;IACxE,CAAC,6CAA6C,EAAE,GAAG,CAAC;IACpD,CAAC,eAAe,EAAE,GAAG,CAAC;IACtB,CAAC,0BAA0B,EAAE,GAAG,CAAC;IACjC,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,sBAAsB,EAAE,GAAG,CAAC;IAC7B,iFAAiF;IACjF,mFAAmF;IACnF,CAAC,uBAAuB,EAAE,GAAG,CAAC;IAC9B,iFAAiF;IACjF,8EAA8E;IAC9E,kEAAkE;IAClE,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,kCAAkC,EAAE,GAAG,CAAC;IACzC,CAAC,qBAAqB,EAAE,GAAG,CAAC;IAC5B,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,sCAAsC,EAAE,GAAG,CAAC;IAC7C,+EAA+E;IAC/E,2EAA2E;IAC3E,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,6EAA6E;IAC7E,oEAAoE;IACpE,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,CAAC,0BAA0B,EAAE,GAAG,CAAC;IACjC,CAAC,gBAAgB,EAAE,GAAG,CAAC;IACvB,sFAAsF;IACtF,CAAC,gBAAgB,EAAE,GAAG,CAAC;IACvB,mFAAmF;IACnF,kFAAkF;IAClF,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,kBAAkB,EAAE,GAAG,CAAC;IACzB,CAAC,4BAA4B,EAAE,GAAG,CAAC;CACpC,CAAC;AAOF,MAAM,UAAU,QAAQ,CAAC,GAAY;IACnC,mFAAmF;IACnF,oFAAoF;IACpF,iFAAiF;IACjF,oFAAoF;IACpF,sFAAsF;IACtF,yFAAyF;IACzF,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;QACrC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAA8B,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;IACtF,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,KAAK,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,eAAe,EAAE,CAAC;QAChD,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC;AAC5D,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
export { createControlPlaneApi } from './api.js';
|
|
2
2
|
export type { ControlPlaneApiOptions } from './api.js';
|
|
3
|
-
export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, } from './auth.js';
|
|
3
|
+
export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, firstBuilderAuth, } from './auth.js';
|
|
4
|
+
export { mintPushToken, verifyPushToken, pushTokenBuilderAuth, pushActorFor } from './push-token.js';
|
|
4
5
|
export type { PlatformActorAuth, StaffIdentity, StaffSessionReader, StaffActorResolver, BuilderIdentity, BuilderAuth, Principal, } from './auth.js';
|
|
5
6
|
export { ControlPlaneClient, ControlPlaneError } from './client.js';
|
|
6
7
|
export type { ControlPlaneClientOptions, ClientProvisionScopeInput } from './client.js';
|
|
7
8
|
export { VerticalClient } from './vertical-client.js';
|
|
8
|
-
export type { VerticalClientOptions, ProvisionInstanceInput, ProvisionedInstance, } from './vertical-client.js';
|
|
9
|
+
export type { VerticalClientOptions, ProvisionInstanceInput, ProvisionedInstance, ConfigureInstanceInput, } from './vertical-client.js';
|
|
9
10
|
export { assertSandboxContract, deployManifest, deploymentRefFor } from './deploy.js';
|
|
10
11
|
export type { DeployVerticalFn, VerticalBundle, DeclaredBinding, DeployManifest } from './deploy.js';
|
|
11
12
|
export { createWfpUploader } from './wfp.js';
|
|
12
13
|
export type { WfpUploaderOptions } from './wfp.js';
|
|
14
|
+
export type { ObservabilityReader, ServiceMetricsRow, RecentLogEvent } from './observability.js';
|
|
15
|
+
export { createCfObservabilityReader } from './cf-observability.js';
|
|
16
|
+
export type { CfObservabilityOptions } from './cf-observability.js';
|
|
13
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACrG,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,SAAS,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACtF,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACjG,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { createControlPlaneApi } from './api.js';
|
|
2
|
-
export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, } from './auth.js';
|
|
2
|
+
export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, firstBuilderAuth, } from './auth.js';
|
|
3
|
+
export { mintPushToken, verifyPushToken, pushTokenBuilderAuth, pushActorFor } from './push-token.js';
|
|
3
4
|
export { ControlPlaneClient, ControlPlaneError } from './client.js';
|
|
4
5
|
export { VerticalClient } from './vertical-client.js';
|
|
5
6
|
export { assertSandboxContract, deployManifest, deploymentRefFor } from './deploy.js';
|
|
6
7
|
export { createWfpUploader } from './wfp.js';
|
|
8
|
+
export { createCfObservabilityReader } from './cf-observability.js';
|
|
7
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAUrG,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAOtD,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtF,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/mask.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAoD9D,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAqBnE"}
|
package/dist/mask.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default masking pass over a scope dump (preview-and-snapshots.md §6/§8).
|
|
3
|
+
*
|
|
4
|
+
* A `scope pull` moves real customer data out of the governed environment, so the
|
|
5
|
+
* dump is masked BY DEFAULT and full fidelity is the explicit break-glass flag.
|
|
6
|
+
* This is the GENERIC sweep — §10's open question 2 answered for v1: a name-based
|
|
7
|
+
* column heuristic, with declarative per-vertical redaction rules as the later
|
|
8
|
+
* refinement. Two rules:
|
|
9
|
+
*
|
|
10
|
+
* 1. A string cell in a column whose name matches the PII heuristic is replaced
|
|
11
|
+
* with `[masked]`. Non-strings (ids, counts, flags) pass through — the sweep
|
|
12
|
+
* targets free text, and ids are what make the copy debuggable at all.
|
|
13
|
+
* 2. A string cell in a JSON-carrying column (`payload`, `detail`, `before`,
|
|
14
|
+
* `after`, `data`) that parses as JSON is swept RECURSIVELY by key with the
|
|
15
|
+
* same heuristic, then re-serialized — fat event payloads keep their shape
|
|
16
|
+
* (consumers and timelines stay debuggable) while the PII fields inside them
|
|
17
|
+
* are masked.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately lossy and deliberately dumb: a heuristic sweep can miss a column
|
|
20
|
+
* named `x7`, which is why the pull is ALSO staff-gated, audited, and
|
|
21
|
+
* jurisdiction-checked — masking is one layer of §6's defense, not the gate.
|
|
22
|
+
*/
|
|
23
|
+
// Free-text/PII column names. `name` is included on purpose: entity names
|
|
24
|
+
// (customers, properties, contacts) are customer data even when they look benign.
|
|
25
|
+
const PII_COLUMN = /(^|_)(email|e?mail_address|phone|mobile|tel|address|street|city|postal|zip|ssn|personnummer|name|first_name|last_name|full_name|contact|note|notes|comment|comments|message|subject|body|description)($|_)/i;
|
|
26
|
+
// Columns that carry JSON documents worth sweeping by key rather than blanking.
|
|
27
|
+
const JSON_COLUMN = /(^|_)(payload|detail|details|data|before|after)($|_)/i;
|
|
28
|
+
const MASKED = '[masked]';
|
|
29
|
+
// Column names are snake_case but JSON payload keys are camelCase — normalize to
|
|
30
|
+
// snake before testing so `customerEmail` matches the same heuristic as `email`.
|
|
31
|
+
const matchesPii = (name) => PII_COLUMN.test(name.replace(/([a-z0-9])([A-Z])/g, '$1_$2'));
|
|
32
|
+
function maskJsonValue(value, keyMatched) {
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
return keyMatched ? MASKED : value;
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.map((v) => maskJsonValue(v, keyMatched));
|
|
37
|
+
if (value !== null && typeof value === 'object') {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [k, v] of Object.entries(value)) {
|
|
40
|
+
out[k] = maskJsonValue(v, keyMatched || matchesPii(k));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/** Mask one dump in place-shape (returns new arrays; never mutates the input). */
|
|
47
|
+
export function maskDump(tables) {
|
|
48
|
+
return tables.map((t) => {
|
|
49
|
+
const piiCols = t.columns.map((c) => matchesPii(c));
|
|
50
|
+
const jsonCols = t.columns.map((c) => JSON_COLUMN.test(c));
|
|
51
|
+
if (!piiCols.some(Boolean) && !jsonCols.some(Boolean))
|
|
52
|
+
return t;
|
|
53
|
+
const rows = t.rows.map((row) => row.map((cell, i) => {
|
|
54
|
+
if (typeof cell !== 'string')
|
|
55
|
+
return cell;
|
|
56
|
+
if (piiCols[i])
|
|
57
|
+
return MASKED;
|
|
58
|
+
if (jsonCols[i]) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(maskJsonValue(JSON.parse(cell), false));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return cell; // not JSON — leave it; the column heuristic did not claim it
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return cell;
|
|
67
|
+
}));
|
|
68
|
+
return { ...t, rows };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=mask.js.map
|
package/dist/mask.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,0EAA0E;AAC1E,kFAAkF;AAClF,MAAM,UAAU,GAAG,6MAA6M,CAAC;AAEjO,gFAAgF;AAChF,MAAM,WAAW,GAAG,uDAAuD,CAAC;AAE5E,MAAM,MAAM,GAAG,UAAU,CAAC;AAE1B,iFAAiF;AACjF,iFAAiF;AACjF,MAAM,UAAU,GAAG,CAAC,IAAY,EAAW,EAAE,CAC3C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,CAAC;AAE/D,SAAS,aAAa,CAAC,KAAc,EAAE,UAAmB;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;IAChF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YACtE,GAAG,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,EAAE,UAAU,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,MAAwB;IAC/C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAC9B,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YAClB,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1C,IAAI,OAAO,CAAC,CAAC,CAAC;gBAAE,OAAO,MAAM,CAAC;YAC9B,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,OAAO,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBAChE,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,IAAI,CAAC,CAAC,6DAA6D;gBAC5E,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CACH,CAAC;QACF,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The observability read seam (design/observability.md §4.1) — what the console's
|
|
3
|
+
* fleet view and (later, owner-narrowed) the dashboard's builder view consume.
|
|
4
|
+
*
|
|
5
|
+
* This file is the CONTRACT, deliberately free of provider vocabulary: a "service"
|
|
6
|
+
* is a deployed unit of code (a Cloudflare worker script today, a container
|
|
7
|
+
* tomorrow), a "namespace" is whatever pool the platform runs pushed verticals in
|
|
8
|
+
* (a WfP dispatch namespace today). The same posture as `DeployVerticalFn` in
|
|
9
|
+
* `deploy.ts`: the seam lives here, each provider's implementation lives in its own
|
|
10
|
+
* module (`cf-observability.ts` for Cloudflare), and the host injects one — so an
|
|
11
|
+
* APM/OTel backend can slot in behind the identical routes later
|
|
12
|
+
* (master-plan §5.7, §6 "Convention + adapter") without touching this package's
|
|
13
|
+
* consumers. Absent ⇒ the observability routes 501, the platform's standard shape
|
|
14
|
+
* for an unconfigured capability.
|
|
15
|
+
*/
|
|
16
|
+
/** One service's invocation aggregates over the queried window. */
|
|
17
|
+
export interface ServiceMetricsRow {
|
|
18
|
+
service: string;
|
|
19
|
+
/** The pushed-vertical pool the service runs in, or null for platform services. */
|
|
20
|
+
namespace: string | null;
|
|
21
|
+
requests: number;
|
|
22
|
+
errors: number;
|
|
23
|
+
subrequests: number;
|
|
24
|
+
/** Per-request CPU time quantiles, microseconds. */
|
|
25
|
+
cpuTimeP50: number;
|
|
26
|
+
cpuTimeP99: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RecentLogEvent {
|
|
29
|
+
/** Unix ms, when the event was recorded. */
|
|
30
|
+
timestamp: number | null;
|
|
31
|
+
level: string | null;
|
|
32
|
+
message: string | null;
|
|
33
|
+
service: string | null;
|
|
34
|
+
/** How the invocation ended (e.g. `ok`, `exception`), provider-worded. */
|
|
35
|
+
outcome: string | null;
|
|
36
|
+
/** The event as the backend returned it — the fields above are a best-effort
|
|
37
|
+
* projection, and the raw event is what makes a projection miss debuggable. */
|
|
38
|
+
raw: unknown;
|
|
39
|
+
}
|
|
40
|
+
export interface ObservabilityReader {
|
|
41
|
+
/** Per-service invocation metrics for the trailing window (fleet + builder views). */
|
|
42
|
+
serviceMetrics(input: {
|
|
43
|
+
hours: number;
|
|
44
|
+
}): Promise<ServiceMetricsRow[]>;
|
|
45
|
+
/** Recent log events, optionally narrowed to one service and/or level. */
|
|
46
|
+
recentLogs(input: {
|
|
47
|
+
service?: string;
|
|
48
|
+
level?: string;
|
|
49
|
+
hours: number;
|
|
50
|
+
limit: number;
|
|
51
|
+
}): Promise<RecentLogEvent[]>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=observability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;oFACgF;IAChF,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,sFAAsF;IACtF,cAAc,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IACvE,0EAA0E;IAC1E,UAAU,CAAC,KAAK,EAAE;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CAC/B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The observability read seam (design/observability.md §4.1) — what the console's
|
|
3
|
+
* fleet view and (later, owner-narrowed) the dashboard's builder view consume.
|
|
4
|
+
*
|
|
5
|
+
* This file is the CONTRACT, deliberately free of provider vocabulary: a "service"
|
|
6
|
+
* is a deployed unit of code (a Cloudflare worker script today, a container
|
|
7
|
+
* tomorrow), a "namespace" is whatever pool the platform runs pushed verticals in
|
|
8
|
+
* (a WfP dispatch namespace today). The same posture as `DeployVerticalFn` in
|
|
9
|
+
* `deploy.ts`: the seam lives here, each provider's implementation lives in its own
|
|
10
|
+
* module (`cf-observability.ts` for Cloudflare), and the host injects one — so an
|
|
11
|
+
* APM/OTel backend can slot in behind the identical routes later
|
|
12
|
+
* (master-plan §5.7, §6 "Convention + adapter") without touching this package's
|
|
13
|
+
* consumers. Absent ⇒ the observability routes 501, the platform's standard shape
|
|
14
|
+
* for an unconfigured capability.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type TenantId } from '@substrat-run/contracts';
|
|
2
|
+
import { type BuilderAuth } from './auth.js';
|
|
3
|
+
interface PushTokenClaim {
|
|
4
|
+
v: 1;
|
|
5
|
+
tenantId: string;
|
|
6
|
+
tenantSlug: string;
|
|
7
|
+
actor: string;
|
|
8
|
+
iat: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The audited subject for a tenant's CI pushes — deterministic from the tenant, so
|
|
12
|
+
* every push from that tenant's pipelines lands in the admin log under ONE nameable
|
|
13
|
+
* actor (the mirror of builder-auth's `builderActorFor`, which is per-human; CI is
|
|
14
|
+
* per-tenant because the pipeline, not a person, is what authenticated).
|
|
15
|
+
*/
|
|
16
|
+
export declare function pushActorFor(tenantId: TenantId): Promise<string>;
|
|
17
|
+
/** Mint a push token for one tenant. `iat` is informational (no expiry in v1). */
|
|
18
|
+
export declare function mintPushToken(secret: string, identity: {
|
|
19
|
+
actor: string;
|
|
20
|
+
tenantId: TenantId;
|
|
21
|
+
tenantSlug: string;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
/** Verify a push token string → its claim, or null (bad prefix/shape/signature). */
|
|
24
|
+
export declare function verifyPushToken(secret: string, token: string): Promise<PushTokenClaim | null>;
|
|
25
|
+
/**
|
|
26
|
+
* A `BuilderAuth` over push tokens: reads `x-service-token`, handles only `spt1.…`
|
|
27
|
+
* values (anything else → null, falling through to the other readers), verifies, and
|
|
28
|
+
* returns the builder principal the token carries. Compose with the OIDC reader via
|
|
29
|
+
* `firstBuilderAuth`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pushTokenBuilderAuth(secret: string): BuilderAuth;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=push-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-token.d.ts","sourceRoot":"","sources":["../src/push-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+C,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACrG,OAAO,EAAwB,KAAK,WAAW,EAAwB,MAAM,WAAW,CAAC;AA6BzF,UAAU,cAAc;IACtB,CAAC,EAAE,CAAC,CAAC;IACL,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAgBD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAOtE;AAED,kFAAkF;AAClF,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAClE,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BnG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAYhE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { platformActorId, tenantId as tenantIdSchema } from '@substrat-run/contracts';
|
|
2
|
+
import { SERVICE_TOKEN_HEADER } from './auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tenant-scoped push tokens — the CI credential the builder plane was missing.
|
|
5
|
+
*
|
|
6
|
+
* A builder's OIDC session (builder-auth in the worker) is per-human and short-lived;
|
|
7
|
+
* the platform `SERVICE_TOKEN` is staff-equivalent and must never land in a customer
|
|
8
|
+
* repo. A push token sits between: a long-lived MACHINE credential that authenticates
|
|
9
|
+
* as a **builder principal** for ONE tenant — so it reaches only the builder-allowlisted
|
|
10
|
+
* routes, only that tenant's `<tenantSlug>/…` namespace, and never prod promotion or
|
|
11
|
+
* admission (those checks live in api.ts and apply to every builder alike).
|
|
12
|
+
*
|
|
13
|
+
* Format: `spt1.<b64url payload>.<b64url HMAC-SHA256 sig>` — stateless, verified
|
|
14
|
+
* against a dedicated `pushTokenSecret`. Deliberately NOT the platform secrets:
|
|
15
|
+
* PLATFORM_SECRET is injected into every pushed vertical (a vertical holding the
|
|
16
|
+
* push-token signing key could forge tokens for any tenant), and deriving from
|
|
17
|
+
* SERVICE_TOKEN would tie CI-credential rotation to service-token rotation. Rotating
|
|
18
|
+
* the dedicated secret invalidates every issued push token (customers reconnect) —
|
|
19
|
+
* that is the whole revocation story in v1, so treat it as set-once like the
|
|
20
|
+
* dashboard's SECRET_BOX_KEY and keep it out of routine rotation.
|
|
21
|
+
*
|
|
22
|
+
* The token is presented in the SAME header the CLI already sends its credential in
|
|
23
|
+
* (`x-service-token`, via `SUBSTRAT_SERVICE_TOKEN`), discriminated by the `spt1.`
|
|
24
|
+
* prefix — a random-hex service token can never collide with it, and the CLI and the
|
|
25
|
+
* generated CI workflow need no changes at all.
|
|
26
|
+
*/
|
|
27
|
+
const PREFIX = 'spt1';
|
|
28
|
+
const enc = new TextEncoder();
|
|
29
|
+
const b64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
30
|
+
const b64urlToBytes = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
|
|
31
|
+
async function hmacKey(secret, usage) {
|
|
32
|
+
return crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, [usage]);
|
|
33
|
+
}
|
|
34
|
+
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
35
|
+
/**
|
|
36
|
+
* The audited subject for a tenant's CI pushes — deterministic from the tenant, so
|
|
37
|
+
* every push from that tenant's pipelines lands in the admin log under ONE nameable
|
|
38
|
+
* actor (the mirror of builder-auth's `builderActorFor`, which is per-human; CI is
|
|
39
|
+
* per-tenant because the pipeline, not a person, is what authenticated).
|
|
40
|
+
*/
|
|
41
|
+
export async function pushActorFor(tenantId) {
|
|
42
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', enc.encode(`push-token:${tenantId}`)));
|
|
43
|
+
let s = '';
|
|
44
|
+
for (let i = 0; i < 26; i++)
|
|
45
|
+
s += CROCKFORD[digest[i] % 32];
|
|
46
|
+
return platformActorId.parse(s);
|
|
47
|
+
}
|
|
48
|
+
/** Mint a push token for one tenant. `iat` is informational (no expiry in v1). */
|
|
49
|
+
export async function mintPushToken(secret, identity) {
|
|
50
|
+
const claim = {
|
|
51
|
+
v: 1,
|
|
52
|
+
tenantId: identity.tenantId,
|
|
53
|
+
tenantSlug: identity.tenantSlug,
|
|
54
|
+
actor: identity.actor,
|
|
55
|
+
iat: Date.now(),
|
|
56
|
+
};
|
|
57
|
+
const payload = b64url(enc.encode(JSON.stringify(claim)));
|
|
58
|
+
const signingInput = `${PREFIX}.${payload}`;
|
|
59
|
+
const sig = new Uint8Array(await crypto.subtle.sign('HMAC', await hmacKey(secret, 'sign'), enc.encode(signingInput)));
|
|
60
|
+
return `${signingInput}.${b64url(sig)}`;
|
|
61
|
+
}
|
|
62
|
+
/** Verify a push token string → its claim, or null (bad prefix/shape/signature). */
|
|
63
|
+
export async function verifyPushToken(secret, token) {
|
|
64
|
+
const parts = token.split('.');
|
|
65
|
+
if (parts.length !== 3 || parts[0] !== PREFIX)
|
|
66
|
+
return null;
|
|
67
|
+
const [, payload, sig] = parts;
|
|
68
|
+
let ok = false;
|
|
69
|
+
try {
|
|
70
|
+
ok = await crypto.subtle.verify('HMAC', await hmacKey(secret, 'verify'), b64urlToBytes(sig), enc.encode(`${PREFIX}.${payload}`));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (!ok)
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
const claim = JSON.parse(new TextDecoder().decode(b64urlToBytes(payload)));
|
|
79
|
+
if (claim.v !== 1)
|
|
80
|
+
return null;
|
|
81
|
+
// Parse, don't trust — the signature proves WE minted it, the parse proves the
|
|
82
|
+
// fields still are what a BuilderIdentity needs (a future format bump fails closed).
|
|
83
|
+
tenantIdSchema.parse(claim.tenantId);
|
|
84
|
+
platformActorId.parse(claim.actor);
|
|
85
|
+
if (typeof claim.tenantSlug !== 'string' || !claim.tenantSlug)
|
|
86
|
+
return null;
|
|
87
|
+
return claim;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A `BuilderAuth` over push tokens: reads `x-service-token`, handles only `spt1.…`
|
|
95
|
+
* values (anything else → null, falling through to the other readers), verifies, and
|
|
96
|
+
* returns the builder principal the token carries. Compose with the OIDC reader via
|
|
97
|
+
* `firstBuilderAuth`.
|
|
98
|
+
*/
|
|
99
|
+
export function pushTokenBuilderAuth(secret) {
|
|
100
|
+
return async (request) => {
|
|
101
|
+
const presented = request.headers.get(SERVICE_TOKEN_HEADER);
|
|
102
|
+
if (!presented || !presented.startsWith(`${PREFIX}.`))
|
|
103
|
+
return null;
|
|
104
|
+
const claim = await verifyPushToken(secret, presented);
|
|
105
|
+
if (!claim)
|
|
106
|
+
return null;
|
|
107
|
+
return {
|
|
108
|
+
actor: platformActorId.parse(claim.actor),
|
|
109
|
+
tenantId: tenantIdSchema.parse(claim.tenantId),
|
|
110
|
+
tenantSlug: claim.tenantSlug,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=push-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-token.js","sourceRoot":"","sources":["../src/push-token.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PrincipalId, ReadScopeTableInput, ScopeId, ScopeTable, ScopeTablePage, TenantId } from '@substrat-run/contracts';
|
|
1
|
+
import type { PrincipalId, QueryScopeInput, ReadScopeTableInput, ScopeDumpTable, ScopeId, ScopeQueryResult, ScopeTable, ScopeTablePage, TenantId } from '@substrat-run/contracts';
|
|
2
2
|
/**
|
|
3
3
|
* The platform's client for calling a VERTICAL (K-31).
|
|
4
4
|
*
|
|
@@ -8,10 +8,16 @@ import type { PrincipalId, ReadScopeTableInput, ScopeId, ScopeTable, ScopeTableP
|
|
|
8
8
|
* because only the vertical can create a usable scope DO — the DO class bundles the
|
|
9
9
|
* modules and lives in the vertical's own deployment.
|
|
10
10
|
*
|
|
11
|
-
* Deliberately tiny. The platform asks a vertical to do
|
|
12
|
-
* instance (K-31)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Deliberately tiny. The platform asks a vertical to do three kinds of thing: create an
|
|
12
|
+
* instance (K-31); — read-only — introspect a scope's own database (§5.4), because the
|
|
13
|
+
* scope's data DO lives in the vertical's deployment, not the platform's; and manage
|
|
14
|
+
* scope-STORAGE lifecycle — snapshot a scope into a sibling, wipe a reaped fork
|
|
15
|
+
* (preview-and-snapshots.md §9, the ratified trust line: infrastructure verbs over the
|
|
16
|
+
* DO's storage, extending the authority provisionInstance already asserts). Every other
|
|
17
|
+
* verb — anything that reads or writes DOMAIN data — would be authority the platform
|
|
18
|
+
* holds over someone else's code. Note the lifecycle verbs move no data across the
|
|
19
|
+
* boundary: a snapshot copies between two DOs inside the vertical's own deployment and
|
|
20
|
+
* returns only a table count.
|
|
15
21
|
*/
|
|
16
22
|
export interface VerticalClientOptions {
|
|
17
23
|
/**
|
|
@@ -32,6 +38,24 @@ export interface ProvisionInstanceInput {
|
|
|
32
38
|
owner: PrincipalId;
|
|
33
39
|
slug: string;
|
|
34
40
|
name: string;
|
|
41
|
+
/**
|
|
42
|
+
* Per-instance config delivered WITH provisioning, so a new app arrives configured
|
|
43
|
+
* atomically — no window where the instance is live but unconfigured (an issuer with
|
|
44
|
+
* no admin, an app with no auth). Same entries `configureInstance` upserts later; a
|
|
45
|
+
* vertical that predates the field ignores it (its body parse strips unknown keys).
|
|
46
|
+
*/
|
|
47
|
+
config?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
export interface ConfigureInstanceInput {
|
|
50
|
+
/** The scope's tenant — CP-less verticals shard identity/config storage per tenant
|
|
51
|
+
* (e.g. Meridian's IdentityDO is addressed by tenant id), so the address rides along. */
|
|
52
|
+
tenantId: TenantId;
|
|
53
|
+
scopeId: ScopeId;
|
|
54
|
+
/** Upserts, key by key — never a full replace, so partial writes compose. */
|
|
55
|
+
entries: Array<{
|
|
56
|
+
key: string;
|
|
57
|
+
value: string;
|
|
58
|
+
}>;
|
|
35
59
|
}
|
|
36
60
|
export interface ProvisionedInstance {
|
|
37
61
|
tenantId: TenantId;
|
|
@@ -49,6 +73,15 @@ export declare class VerticalClient {
|
|
|
49
73
|
* phase of a two-phase creation and the reconciliation sweep re-runs exactly it.
|
|
50
74
|
*/
|
|
51
75
|
provisionInstance(input: ProvisionInstanceInput): Promise<ProvisionedInstance>;
|
|
76
|
+
/**
|
|
77
|
+
* Deliver per-instance CONFIG to the scope's own storage (vertical-auth-detach.md
|
|
78
|
+
* §2.2) — the write half of the dashboard's Env tab, and the same trust line as
|
|
79
|
+
* provisionInstance: the platform names a scope inside the vertical's deployment and
|
|
80
|
+
* hands it settings; the vertical owns what they mean. Idempotent upserts, so the
|
|
81
|
+
* reconciliation sweep can re-run it. A vertical that has no live-config support
|
|
82
|
+
* answers 501, which the caller may treat as "authored but not delivered".
|
|
83
|
+
*/
|
|
84
|
+
configureInstance(input: ConfigureInstanceInput): Promise<void>;
|
|
52
85
|
/**
|
|
53
86
|
* Read the scope's OWN database tables (kernel-design §5.4 admin-query RPC).
|
|
54
87
|
*
|
|
@@ -60,6 +93,44 @@ export declare class VerticalClient {
|
|
|
60
93
|
listScopeTables(scopeId: ScopeId): Promise<ScopeTable[]>;
|
|
61
94
|
/** A bounded page of one of the scope's tables. */
|
|
62
95
|
readScopeTable(scopeId: ScopeId, input: ReadScopeTableInput): Promise<ScopeTablePage>;
|
|
96
|
+
/**
|
|
97
|
+
* One read-only SQL statement against the scope's DB — the console (#219). The
|
|
98
|
+
* vertical enforces read-only-ness in its own deployment (the kernel gate + the
|
|
99
|
+
* DO's rolled-back transaction); a vertical that cannot answer safely (e.g. one
|
|
100
|
+
* that redacts secret columns on table reads) refuses with its own status, which
|
|
101
|
+
* the ControlPlaneError relays verbatim.
|
|
102
|
+
*/
|
|
103
|
+
queryScope(scopeId: ScopeId, input: QueryScopeInput): Promise<ScopeQueryResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Copy one scope's data into a fresh sibling scope DO, inside the vertical's own
|
|
106
|
+
* deployment (§9's data half). The platform names source and destination; the bytes
|
|
107
|
+
* never cross the boundary — the response is a table count, not a dump. The
|
|
108
|
+
* directory half (provenance row, activation, version bind) is the caller's job.
|
|
109
|
+
*/
|
|
110
|
+
snapshotScope(input: {
|
|
111
|
+
sourceScopeId: ScopeId;
|
|
112
|
+
newScopeId: ScopeId;
|
|
113
|
+
}): Promise<{
|
|
114
|
+
tables: number;
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Wipe a reaped fork's storage (§9's reap half). The platform calls this before
|
|
118
|
+
* deleting the directory row — same storage-before-row ordering as the in-process
|
|
119
|
+
* deleteSnapshot, so a crash between the two converges on retry. The fork-only
|
|
120
|
+
* refusal lives with the directory record, on the platform's side.
|
|
121
|
+
*/
|
|
122
|
+
deleteScope(input: {
|
|
123
|
+
scopeId: ScopeId;
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* The scope's full dump — the ONE verb here that deliberately moves scope bytes
|
|
127
|
+
* across the boundary, for the governed `scope pull` (§8). The control-plane route
|
|
128
|
+
* in front of it is the gate: staff-only, audited, masked by default, jurisdiction-
|
|
129
|
+
* checked. Everything else on this surface stays byte-free by design.
|
|
130
|
+
*/
|
|
131
|
+
exportScope(scopeId: ScopeId): Promise<ScopeDumpTable[]>;
|
|
132
|
+
/** A platform-authenticated POST to the vertical's `/internal/*` surface. */
|
|
133
|
+
private postInternal;
|
|
63
134
|
/** A platform-authenticated GET to the vertical's `/internal/*` surface. */
|
|
64
135
|
private getInternal;
|
|
65
136
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vertical-client.d.ts","sourceRoot":"","sources":["../src/vertical-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,OAAO,EACP,UAAU,EACV,cAAc,EACd,QAAQ,EACT,MAAM,yBAAyB,CAAC;AAIjC
|
|
1
|
+
{"version":3,"file":"vertical-client.d.ts","sourceRoot":"","sources":["../src/vertical-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,QAAQ,EACT,MAAM,yBAAyB,CAAC;AAIjC;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC;8FAC0F;IAC1F,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,qBAAqB,EAAI;IAE/D;;;;;;OAMG;IACG,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqBnF;IAED;;;;;;;OAOG;IACG,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpE;IAED;;;;;;;OAOG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE7D;IAED,mDAAmD;IAC7C,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAS1F;IAED;;;;;;OAMG;IACG,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEpF;IAED;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE;QACzB,aAAa,EAAE,OAAO,CAAC;QACvB,UAAU,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAE9B;IAED;;;;;OAKG;IACG,WAAW,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;IAED;;;;;OAKG;IACG,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAI7D;IAED,6EAA6E;YAC/D,YAAY;IAoB1B,4EAA4E;YAC9D,WAAW;CAc1B"}
|
package/dist/vertical-client.js
CHANGED
|
@@ -30,6 +30,17 @@ export class VerticalClient {
|
|
|
30
30
|
}
|
|
31
31
|
return (await res.json());
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Deliver per-instance CONFIG to the scope's own storage (vertical-auth-detach.md
|
|
35
|
+
* §2.2) — the write half of the dashboard's Env tab, and the same trust line as
|
|
36
|
+
* provisionInstance: the platform names a scope inside the vertical's deployment and
|
|
37
|
+
* hands it settings; the vertical owns what they mean. Idempotent upserts, so the
|
|
38
|
+
* reconciliation sweep can re-run it. A vertical that has no live-config support
|
|
39
|
+
* answers 501, which the caller may treat as "authored but not delivered".
|
|
40
|
+
*/
|
|
41
|
+
async configureInstance(input) {
|
|
42
|
+
await this.postInternal('/internal/configure', input, 'configure');
|
|
43
|
+
}
|
|
33
44
|
/**
|
|
34
45
|
* Read the scope's OWN database tables (kernel-design §5.4 admin-query RPC).
|
|
35
46
|
*
|
|
@@ -50,6 +61,60 @@ export class VerticalClient {
|
|
|
50
61
|
});
|
|
51
62
|
return this.getInternal(`/internal/tables/${encodeURIComponent(input.table)}?${q}`);
|
|
52
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* One read-only SQL statement against the scope's DB — the console (#219). The
|
|
66
|
+
* vertical enforces read-only-ness in its own deployment (the kernel gate + the
|
|
67
|
+
* DO's rolled-back transaction); a vertical that cannot answer safely (e.g. one
|
|
68
|
+
* that redacts secret columns on table reads) refuses with its own status, which
|
|
69
|
+
* the ControlPlaneError relays verbatim.
|
|
70
|
+
*/
|
|
71
|
+
async queryScope(scopeId, input) {
|
|
72
|
+
return this.postInternal('/internal/query', { scopeId, sql: input.sql }, 'query');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Copy one scope's data into a fresh sibling scope DO, inside the vertical's own
|
|
76
|
+
* deployment (§9's data half). The platform names source and destination; the bytes
|
|
77
|
+
* never cross the boundary — the response is a table count, not a dump. The
|
|
78
|
+
* directory half (provenance row, activation, version bind) is the caller's job.
|
|
79
|
+
*/
|
|
80
|
+
async snapshotScope(input) {
|
|
81
|
+
return this.postInternal('/internal/snapshot', input, 'snapshot');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Wipe a reaped fork's storage (§9's reap half). The platform calls this before
|
|
85
|
+
* deleting the directory row — same storage-before-row ordering as the in-process
|
|
86
|
+
* deleteSnapshot, so a crash between the two converges on retry. The fork-only
|
|
87
|
+
* refusal lives with the directory record, on the platform's side.
|
|
88
|
+
*/
|
|
89
|
+
async deleteScope(input) {
|
|
90
|
+
await this.postInternal('/internal/delete-scope', input, 'delete-scope');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The scope's full dump — the ONE verb here that deliberately moves scope bytes
|
|
94
|
+
* across the boundary, for the governed `scope pull` (§8). The control-plane route
|
|
95
|
+
* in front of it is the gate: staff-only, audited, masked by default, jurisdiction-
|
|
96
|
+
* checked. Everything else on this surface stays byte-free by design.
|
|
97
|
+
*/
|
|
98
|
+
async exportScope(scopeId) {
|
|
99
|
+
return this.getInternal(`/internal/export?scopeId=${encodeURIComponent(scopeId)}`);
|
|
100
|
+
}
|
|
101
|
+
/** A platform-authenticated POST to the vertical's `/internal/*` surface. */
|
|
102
|
+
async postInternal(path, body, verb) {
|
|
103
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
104
|
+
const res = await this.options.fetch(`${base}${path}`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'content-type': 'application/json',
|
|
108
|
+
[PLATFORM_SECRET_HEADER]: this.options.platformSecret,
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const parsed = (await res.json().catch(() => null));
|
|
114
|
+
throw new ControlPlaneError(res.status, parsed?.error ?? `vertical refused ${verb}: ${res.status} ${res.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
return (await res.json());
|
|
117
|
+
}
|
|
53
118
|
/** A platform-authenticated GET to the vertical's `/internal/*` surface. */
|
|
54
119
|
async getInternal(path) {
|
|
55
120
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|