@substrat-run/vertical-host 0.140.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/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? Moving here from the kernel (#1978),
49
- * which keeps the `LiveReadSurface` contract. For one release the definition still lives in
50
- * `@substrat-run/kernel` and this is that same binding. Import it from here.
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? Moving here from the kernel (#1978),
5
- * which keeps the `LiveReadSurface` contract. For one release the definition still lives in
6
- * `@substrat-run/kernel` and this is that same binding. Import it from here.
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. */
@@ -1,11 +1,87 @@
1
+ import type { ScopeId, TenantId } from '@substrat-run/contracts';
1
2
  /**
2
- * Reading the node the router asserted (K-26, #1978).
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
- * Moving here from the kernel: every vertical reads it, and nothing in it needs a kernel
5
- * guarantee. For one release the definition still lives in `@substrat-run/kernel` and this
6
- * module re-exports that same binding, so an import from either package is the one
7
- * function. Import it from here.
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 { readRoutedNode, RouterAssertionError } from '@substrat-run/kernel';
10
- export type { RoutedNode, HeaderReader, ReadRoutedNodeOptions } from '@substrat-run/kernel';
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;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
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"}
@@ -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
- * Reading the node the router asserted (K-26, #1978).
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
- * Moving here from the kernel: every vertical reads it, and nothing in it needs a kernel
5
- * guarantee. For one release the definition still lives in `@substrat-run/kernel` and this
6
- * module re-exports that same binding, so an import from either package is the one
7
- * function. Import it from here.
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 { readRoutedNode, RouterAssertionError } from '@substrat-run/kernel';
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
@@ -1 +1 @@
1
- {"version":3,"file":"routed-node.js","sourceRoot":"","sources":["../src/routed-node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
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.140.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": {
@@ -26,21 +26,21 @@
26
26
  "dist"
27
27
  ],
28
28
  "dependencies": {
29
- "ai": "7.0.127",
30
- "hono": "^4.13.12",
29
+ "ai": "7.0.128",
30
+ "hono": "^4.13.13",
31
31
  "workers-ai-provider": "^4.0.0",
32
- "@substrat-run/contracts": "^0.140.0",
33
- "@substrat-run/kernel": "^0.140.0",
34
- "@substrat-run/model-providers": "^0.5.30"
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
- "vitest": "^3.2.7",
41
- "@substrat-run/adapter-sqlite": "^0.140.0",
42
- "@substrat-run/contract-tests": "^0.140.0",
43
- "@substrat-run/vertical-auth": "^0.21.0"
40
+ "vitest": "^5.0.3",
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"
@@ -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"}
@@ -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"}