@substrat-run/vertical-host 0.141.0 → 0.142.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/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -9
- package/dist/index.js.map +1 -1
- package/dist/invocation-log.d.ts +195 -7
- package/dist/invocation-log.d.ts.map +1 -1
- package/dist/invocation-log.js +374 -6
- package/dist/invocation-log.js.map +1 -1
- package/dist/kick-flags.d.ts +9 -0
- package/dist/kick-flags.d.ts.map +1 -0
- package/dist/kick-flags.js +20 -0
- package/dist/kick-flags.js.map +1 -0
- package/dist/live.d.ts +3 -3
- package/dist/live.js +3 -3
- package/dist/routed-node.d.ts +83 -7
- package/dist/routed-node.d.ts.map +1 -1
- package/dist/routed-node.js +64 -6
- package/dist/routed-node.js.map +1 -1
- package/package.json +7 -7
- package/dist/platform-call.d.ts +0 -10
- package/dist/platform-call.d.ts.map +0 -1
- package/dist/platform-call.js +0 -10
- package/dist/platform-call.js.map +0 -1
package/dist/live.d.ts
CHANGED
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
import type { Context, Env, Hono } from 'hono';
|
|
46
46
|
import { isUpgradeRequest, type LiveReadSurface } from '@substrat-run/kernel';
|
|
47
47
|
/**
|
|
48
|
-
* Is this request asking to be upgraded to a WebSocket?
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* Is this request asking to be upgraded to a WebSocket? Defined in the kernel beside the
|
|
49
|
+
* `LiveReadSurface` contract, because the hosted adapter's door asks it too (#1978); this is
|
|
50
|
+
* that same binding, beside the route that asks it first.
|
|
51
51
|
*/
|
|
52
52
|
export { isUpgradeRequest };
|
|
53
53
|
/** Where `mountLiveReads` mounts the route unless told otherwise. */
|
package/dist/live.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LIVE_MODE_HEADER } from '@substrat-run/contracts';
|
|
2
2
|
import { isUpgradeRequest } from '@substrat-run/kernel';
|
|
3
3
|
/**
|
|
4
|
-
* Is this request asking to be upgraded to a WebSocket?
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Is this request asking to be upgraded to a WebSocket? Defined in the kernel beside the
|
|
5
|
+
* `LiveReadSurface` contract, because the hosted adapter's door asks it too (#1978); this is
|
|
6
|
+
* that same binding, beside the route that asks it first.
|
|
7
7
|
*/
|
|
8
8
|
export { isUpgradeRequest };
|
|
9
9
|
/** Where `mountLiveReads` mounts the route unless told otherwise. */
|
package/dist/routed-node.d.ts
CHANGED
|
@@ -1,11 +1,87 @@
|
|
|
1
|
+
import type { ScopeId, TenantId } from '@substrat-run/contracts';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* A tenant or scope id: a ULID, exactly what `@substrat-run/contracts`' `tenantId` and
|
|
4
|
+
* `scopeId` schemas accept (`z.string().regex(ULID).brand()`).
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Checked with the pattern itself rather than through those schemas since #1893, because
|
|
7
|
+
* this file is bundled into the platform's entry in front of EVERY deployed vertical
|
|
8
|
+
* (`withInvocationLog`), and a runtime import of the contracts package pulls all of it —
|
|
9
|
+
* and zod — into that bundle: 750 KB added to every upload to run one regex. The brands
|
|
10
|
+
* are types, so they still come from contracts; `routed-node.test.ts` holds this pattern to
|
|
11
|
+
* those schemas so the two cannot drift.
|
|
8
12
|
*/
|
|
9
|
-
export
|
|
10
|
-
|
|
13
|
+
export declare const ROUTED_ID: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* Reading the node the router asserted (K-26).
|
|
16
|
+
*
|
|
17
|
+
* The router resolves `hostname → (tenant, scope, surface)` and forwards over a
|
|
18
|
+
* service binding with that resolution in headers. This is the vertical's side of
|
|
19
|
+
* that contract, shared because every vertical needs it and none of them should be
|
|
20
|
+
* re-deriving how to trust it. Here rather than in the kernel since #1978: nothing in it
|
|
21
|
+
* needs a kernel guarantee.
|
|
22
|
+
*
|
|
23
|
+
* Web-standard only and structurally typed: the base tsconfig is `ES2023` with no
|
|
24
|
+
* DOM lib, deliberately, so no package assumes a browser. Taking the one method we
|
|
25
|
+
* need rather than the whole `Headers` type keeps that true and costs nothing — a
|
|
26
|
+
* real `Headers` satisfies it, and so does a plain object in a test.
|
|
27
|
+
*/
|
|
28
|
+
/** The one method this needs from a `Headers`. A real `Headers` satisfies it. */
|
|
29
|
+
export interface HeaderReader {
|
|
30
|
+
get(name: string): string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface RoutedNode {
|
|
33
|
+
tenantId: TenantId;
|
|
34
|
+
scopeId: ScopeId;
|
|
35
|
+
/** Which app answers: `app`, `storefront`, `back-office`, … Vertical vocabulary. */
|
|
36
|
+
surface: string;
|
|
37
|
+
verticalSlug: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** Thrown when headers are present but not trustworthy. Never means "no router". */
|
|
40
|
+
export declare class RouterAssertionError extends Error {
|
|
41
|
+
}
|
|
42
|
+
export interface ReadRoutedNodeOptions {
|
|
43
|
+
/**
|
|
44
|
+
* The router's shared secret, as this worker holds it (`ROUTER_SECRET`). When set,
|
|
45
|
+
* an assertion is trusted only if `x-substrat-router` matches it — always, even
|
|
46
|
+
* with `allowUnsigned`.
|
|
47
|
+
*/
|
|
48
|
+
expectedSecret?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Accept an assertion nobody signed, when no secret is configured (#966).
|
|
51
|
+
*
|
|
52
|
+
* Off by default, and that default is the boundary: a worker deployed without
|
|
53
|
+
* its secret used to trust any `x-substrat-*` headers it was handed, which is a
|
|
54
|
+
* forged tenant from anyone who could reach the script directly. Now it refuses
|
|
55
|
+
* them — loudly, as a `RouterAssertionError` — until the secret is provisioned.
|
|
56
|
+
*
|
|
57
|
+
* The one legitimate exception is an un-routed local instance behind a dev
|
|
58
|
+
* router that has no secret either, which is what `ALLOW_DEV_NODE` already
|
|
59
|
+
* names; a host sets this from that flag and from nothing else.
|
|
60
|
+
*/
|
|
61
|
+
allowUnsigned?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The `(tenant, scope)` this request is for, or `null` when no router fronted it.
|
|
65
|
+
*
|
|
66
|
+
* Three outcomes, kept distinct on purpose:
|
|
67
|
+
*
|
|
68
|
+
* - **null** — no assertion at all. The caller decides: a single-tenant standalone
|
|
69
|
+
* deploy substitutes its own node, anything else refuses.
|
|
70
|
+
* - **throws** — headers are present but wrong: a bad or missing secret, a secret
|
|
71
|
+
* this worker cannot verify because it holds none, or ids that are not ULIDs.
|
|
72
|
+
* Present-but-wrong is a misconfiguration or an attack, and collapsing it into
|
|
73
|
+
* `null` would let either one fall through to whatever the caller does for
|
|
74
|
+
* "unrouted".
|
|
75
|
+
* - **a node** — trustworthy, and the tenant whose data this request may touch.
|
|
76
|
+
*
|
|
77
|
+
* `expectedSecret` is what makes the headers trustworthy in code. K-26's real
|
|
78
|
+
* boundary is that vertical workers have no public route, so the router is the only
|
|
79
|
+
* possible source — but that is a deployment fact, and `workers.dev` is on by
|
|
80
|
+
* default. When the secret is configured, a request that did not come from the
|
|
81
|
+
* router cannot assert a tenant no matter how the routes are configured. When it is
|
|
82
|
+
* NOT configured the assertion fails closed (#966): an unsigned assertion is refused
|
|
83
|
+
* unless the host opted in with `allowUnsigned`, so forgetting to provision the
|
|
84
|
+
* secret is a visible outage rather than an open door.
|
|
85
|
+
*/
|
|
86
|
+
export declare function readRoutedNode(headers: HeaderReader, options?: ReadRoutedNodeOptions): RoutedNode | null;
|
|
11
87
|
//# sourceMappingURL=routed-node.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routed-node.d.ts","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"routed-node.d.ts","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAIjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,QAA6B,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AAEH,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,oFAAoF;AACpF,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,qBAA0B,GAClC,UAAU,GAAG,IAAI,CA6BnB"}
|
package/dist/routed-node.js
CHANGED
|
@@ -1,10 +1,68 @@
|
|
|
1
|
+
// The subpath, not the root: this file is bundled into the platform's entry (#1893).
|
|
2
|
+
import { secretMatches } from '@substrat-run/contracts/wire-auth';
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* A tenant or scope id: a ULID, exactly what `@substrat-run/contracts`' `tenantId` and
|
|
5
|
+
* `scopeId` schemas accept (`z.string().regex(ULID).brand()`).
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Checked with the pattern itself rather than through those schemas since #1893, because
|
|
8
|
+
* this file is bundled into the platform's entry in front of EVERY deployed vertical
|
|
9
|
+
* (`withInvocationLog`), and a runtime import of the contracts package pulls all of it —
|
|
10
|
+
* and zod — into that bundle: 750 KB added to every upload to run one regex. The brands
|
|
11
|
+
* are types, so they still come from contracts; `routed-node.test.ts` holds this pattern to
|
|
12
|
+
* those schemas so the two cannot drift.
|
|
8
13
|
*/
|
|
9
|
-
export
|
|
14
|
+
export const ROUTED_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
15
|
+
/** Thrown when headers are present but not trustworthy. Never means "no router". */
|
|
16
|
+
export class RouterAssertionError extends Error {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The `(tenant, scope)` this request is for, or `null` when no router fronted it.
|
|
20
|
+
*
|
|
21
|
+
* Three outcomes, kept distinct on purpose:
|
|
22
|
+
*
|
|
23
|
+
* - **null** — no assertion at all. The caller decides: a single-tenant standalone
|
|
24
|
+
* deploy substitutes its own node, anything else refuses.
|
|
25
|
+
* - **throws** — headers are present but wrong: a bad or missing secret, a secret
|
|
26
|
+
* this worker cannot verify because it holds none, or ids that are not ULIDs.
|
|
27
|
+
* Present-but-wrong is a misconfiguration or an attack, and collapsing it into
|
|
28
|
+
* `null` would let either one fall through to whatever the caller does for
|
|
29
|
+
* "unrouted".
|
|
30
|
+
* - **a node** — trustworthy, and the tenant whose data this request may touch.
|
|
31
|
+
*
|
|
32
|
+
* `expectedSecret` is what makes the headers trustworthy in code. K-26's real
|
|
33
|
+
* boundary is that vertical workers have no public route, so the router is the only
|
|
34
|
+
* possible source — but that is a deployment fact, and `workers.dev` is on by
|
|
35
|
+
* default. When the secret is configured, a request that did not come from the
|
|
36
|
+
* router cannot assert a tenant no matter how the routes are configured. When it is
|
|
37
|
+
* NOT configured the assertion fails closed (#966): an unsigned assertion is refused
|
|
38
|
+
* unless the host opted in with `allowUnsigned`, so forgetting to provision the
|
|
39
|
+
* secret is a visible outage rather than an open door.
|
|
40
|
+
*/
|
|
41
|
+
export function readRoutedNode(headers, options = {}) {
|
|
42
|
+
const rawTenant = headers.get('x-substrat-tenant');
|
|
43
|
+
const rawScope = headers.get('x-substrat-scope');
|
|
44
|
+
if (!rawTenant && !rawScope)
|
|
45
|
+
return null;
|
|
46
|
+
const { expectedSecret, allowUnsigned = false } = options;
|
|
47
|
+
if (expectedSecret) {
|
|
48
|
+
if (!secretMatches(headers.get('x-substrat-router'), expectedSecret)) {
|
|
49
|
+
throw new RouterAssertionError('router assertion is not signed by a known router');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (!allowUnsigned) {
|
|
53
|
+
throw new RouterAssertionError('router assertion cannot be verified: this worker has no ROUTER_SECRET configured');
|
|
54
|
+
}
|
|
55
|
+
if (!rawTenant || !rawScope) {
|
|
56
|
+
throw new RouterAssertionError('router assertion is incomplete');
|
|
57
|
+
}
|
|
58
|
+
if (!ROUTED_ID.test(rawTenant) || !ROUTED_ID.test(rawScope)) {
|
|
59
|
+
throw new RouterAssertionError('router assertion carries a malformed id');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
tenantId: rawTenant,
|
|
63
|
+
scopeId: rawScope,
|
|
64
|
+
surface: headers.get('x-substrat-surface') ?? 'app',
|
|
65
|
+
verticalSlug: headers.get('x-substrat-vertical'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
10
68
|
//# sourceMappingURL=routed-node.js.map
|
package/dist/routed-node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routed-node.js","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"routed-node.js","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AACA,qFAAqF;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAElE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,0BAA0B,CAAC;AA8BpD,oFAAoF;AACpF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;CAAG;AAwBlD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAqB,EACrB,OAAO,GAA0B,EAAE;IAEnC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACjD,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,EAAE,cAAc,EAAE,aAAa,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1D,IAAI,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,cAAc,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,oBAAoB,CAAC,kDAAkD,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;SAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,IAAI,oBAAoB,CAC5B,kFAAkF,CACnF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,oBAAoB,CAAC,gCAAgC,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,oBAAoB,CAAC,yCAAyC,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,SAAqB;QAC/B,OAAO,EAAE,QAAmB;QAC5B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,KAAK;QACnD,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;KACjD,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@substrat-run/vertical-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.142.0",
|
|
4
4
|
"description": "The platform's /internal/* surface + error envelope, authored once — mount it in a vertical worker instead of re-copying the control-plane contract",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -29,18 +29,18 @@
|
|
|
29
29
|
"ai": "7.0.128",
|
|
30
30
|
"hono": "^4.13.13",
|
|
31
31
|
"workers-ai-provider": "^4.0.0",
|
|
32
|
-
"@substrat-run/contracts": "^0.
|
|
33
|
-
"@substrat-run/kernel": "^0.
|
|
34
|
-
"@substrat-run/model-providers": "^0.5.
|
|
32
|
+
"@substrat-run/contracts": "^0.142.0",
|
|
33
|
+
"@substrat-run/kernel": "^0.142.0",
|
|
34
|
+
"@substrat-run/model-providers": "^0.5.32"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/better-sqlite3": "^9.6.0",
|
|
38
38
|
"better-sqlite3": "^13.0.3",
|
|
39
39
|
"typescript": "^7.0.0",
|
|
40
40
|
"vitest": "^5.0.3",
|
|
41
|
-
"@substrat-run/
|
|
42
|
-
"@substrat-run/contract-tests": "^0.
|
|
43
|
-
"@substrat-run/
|
|
41
|
+
"@substrat-run/adapter-sqlite": "^0.142.0",
|
|
42
|
+
"@substrat-run/contract-tests": "^0.142.0",
|
|
43
|
+
"@substrat-run/vertical-auth": "^0.22.0"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/dist/platform-call.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authenticating a call from the platform to a vertical (K-31, #1978).
|
|
3
|
-
*
|
|
4
|
-
* Moving here from the kernel, beside `readRoutedNode`. For one release the definition
|
|
5
|
-
* still lives in `@substrat-run/kernel` and this module re-exports that same binding, so an
|
|
6
|
-
* import from either package is the one function. Import it from here. The header names
|
|
7
|
-
* it reads are wire vocabulary and live in `@substrat-run/contracts`.
|
|
8
|
-
*/
|
|
9
|
-
export { assertPlatformCall, PlatformCallError, kickFlags } from '@substrat-run/kernel';
|
|
10
|
-
//# sourceMappingURL=platform-call.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"platform-call.d.ts","sourceRoot":"","sources":["../src/platform-call.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/platform-call.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authenticating a call from the platform to a vertical (K-31, #1978).
|
|
3
|
-
*
|
|
4
|
-
* Moving here from the kernel, beside `readRoutedNode`. For one release the definition
|
|
5
|
-
* still lives in `@substrat-run/kernel` and this module re-exports that same binding, so an
|
|
6
|
-
* import from either package is the one function. Import it from here. The header names
|
|
7
|
-
* it reads are wire vocabulary and live in `@substrat-run/contracts`.
|
|
8
|
-
*/
|
|
9
|
-
export { assertPlatformCall, PlatformCallError, kickFlags } from '@substrat-run/kernel';
|
|
10
|
-
//# sourceMappingURL=platform-call.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"platform-call.js","sourceRoot":"","sources":["../src/platform-call.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
|