@substrat-run/vertical-host 0.141.0 → 0.143.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/errors.d.ts +6 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +73 -26
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +25 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -10
- 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/dist/scope-sweep-host.d.ts +7 -1
- package/dist/scope-sweep-host.d.ts.map +1 -1
- package/dist/scope-sweep-host.js +21 -4
- package/dist/scope-sweep-host.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/invocation-log.js
CHANGED
|
@@ -1,10 +1,378 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* One structured log line per invocation, carrying the two dimensions Cloudflare
|
|
3
|
+
* structurally cannot record: which TENANT and which SCOPE a request belonged to
|
|
4
|
+
* (`docs/architecture/observability.md` §4.2/§4.3).
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* ## Why this exists at all
|
|
7
|
+
*
|
|
8
|
+
* Observability is keyed on the deployed unit — a worker script — and one vertical's
|
|
9
|
+
* script serves every tenant that installed it. That makes script-grain data safe for
|
|
10
|
+
* staff and for the vertical's builder, and unsafe for an installer: their own numbers
|
|
11
|
+
* are inseparable from everyone else's (§3). The router already stamps `tenantId` on the
|
|
12
|
+
* datapoints and log lines IT emits, which is why tenant-keyed *metrics* need no code in
|
|
13
|
+
* this package. Logs are the other half, and the router cannot supply it: a trace does
|
|
14
|
+
* not cross the dispatch hop. Verified against production — a router line's `traceId`
|
|
15
|
+
* reaches `substrat-control-plane` (a service binding) but never the dispatched vertical,
|
|
16
|
+
* and every vertical event is a trace of exactly one event. There is nothing to join on,
|
|
17
|
+
* so the line has to be emitted on this side.
|
|
18
|
+
*
|
|
19
|
+
* ## What it makes possible that nothing else does
|
|
20
|
+
*
|
|
21
|
+
* A vertical's successful request emits NO log event whatsoever — the host logs only on a
|
|
22
|
+
* platform fault, and the scope host logs nothing at all. So before this middleware, a
|
|
23
|
+
* vertical's entire log presence was its crashes. The stamped line is therefore not only
|
|
24
|
+
* the correlation key; it is the thing that makes a tenant-facing log view have any rows.
|
|
25
|
+
*
|
|
26
|
+
* ## The correlation contract
|
|
27
|
+
*
|
|
28
|
+
* Every log line emitted during one invocation shares `$metadata.requestId`, so a reader
|
|
29
|
+
* resolves a tenant's lines in two phases: filter on `tenantId` to find the invocations,
|
|
30
|
+
* then fetch every line sharing their request ids. That is what attributes a vertical's
|
|
31
|
+
* OWN `console.log` output — which carries no tenant of its own — to the tenant whose
|
|
32
|
+
* request produced it.
|
|
33
|
+
*
|
|
34
|
+
* The inverse of that contract is a rule, not an optimisation: a line with no `tenantId`
|
|
35
|
+
* is never shown to a tenant (§4.3). An un-routed local invocation has no asserted
|
|
36
|
+
* headers and therefore emits no line — there is no tenant it could be attributed to, and
|
|
37
|
+
* inventing one is the only way this could ever leak.
|
|
38
|
+
*
|
|
39
|
+
* ## The tenant is a VERIFIED assertion, never a header
|
|
40
|
+
*
|
|
41
|
+
* Which is why the line is written from `readRoutedNode`'s answer and not from
|
|
42
|
+
* `headers.get('x-substrat-tenant')`. The two look interchangeable — behind the router
|
|
43
|
+
* they are — and they are not, for exactly the reason #966 made `readRoutedNode` itself
|
|
44
|
+
* fail closed: K-26's boundary is that a vertical's script has no public route, and that
|
|
45
|
+
* is a DEPLOYMENT fact, with `workers.dev` on by default. Reading the header directly
|
|
46
|
+
* meant anyone who could reach the script could name any tenant they liked and have the
|
|
47
|
+
* line filed under it — a request the vertical then refuses with a 400, since `nodeFor`
|
|
48
|
+
* does verify, while this middleware's `finally` had already written the forged line.
|
|
49
|
+
*
|
|
50
|
+
* That is not a cosmetic wrong row. The read path uses the stamped line as PROOF that an
|
|
51
|
+
* invocation was a given tenant's, and admits that invocation's other log lines — which
|
|
52
|
+
* carry no tenant of their own — into that tenant's view on the strength of it. A forged
|
|
53
|
+
* stamp is therefore a way to put chosen text on somebody else's dashboard. So the same
|
|
54
|
+
* verification the vertical does for its own routing happens here, with the same secret
|
|
55
|
+
* and the same dev opt-out, and a failed one writes nothing at all.
|
|
56
|
+
*/
|
|
57
|
+
// The subpath, not the root: this module is bundled into every vertical's upload (the
|
|
58
|
+
// platform entry, #1893), and the root carries every schema in the vocabulary.
|
|
59
|
+
// The same holds for the kernel, whose two zero-import subpaths this reaches it through.
|
|
60
|
+
import { encodeInvocationRecord, FIELD_COVERAGE_HEADER, INVOCATION_RECORD_HEADER, invocationLevelOf, } from '@substrat-run/contracts/invocation-record';
|
|
61
|
+
import { invocationLine } from '@substrat-run/kernel/invocation-line';
|
|
62
|
+
import { ulid } from '@substrat-run/kernel/ulid';
|
|
63
|
+
import { readRoutedNode, ROUTED_ID, RouterAssertionError } from './routed-node.js';
|
|
64
|
+
/**
|
|
65
|
+
* The per-request store key the middleware parks its {@link InvocationRecord} under (#1746).
|
|
66
|
+
* A published name, like `substratInvocationId`: `mountOperations` reads it in another
|
|
67
|
+
* package, and a vertical's own routes may fill it in too.
|
|
68
|
+
*/
|
|
69
|
+
export const INVOCATION_RECORD_KEY = 'substratInvocationRecord';
|
|
70
|
+
/**
|
|
71
|
+
* The platform's version binding, read straight off the env.
|
|
72
|
+
*
|
|
73
|
+
* The one exception to "bindings only through the options": `SUBSTRAT_VERSION_ID` is not
|
|
74
|
+
* the vertical's binding to name, the platform attaches it to every pushed script, and a
|
|
75
|
+
* vertical that had to pass it through would be one more line to forget with no way for
|
|
76
|
+
* it to be wrong. Read defensively — the env is typed `unknown` here.
|
|
77
|
+
*/
|
|
78
|
+
function versionIdOf(env) {
|
|
79
|
+
const v = env?.SUBSTRAT_VERSION_ID;
|
|
80
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The path, with the query string DISCARDED.
|
|
84
|
+
*
|
|
85
|
+
* Not a tidiness choice. A vertical that speaks OIDC carries `code`, `state` and
|
|
86
|
+
* `id_token_hint` in its query strings, an invite or magic-link flow carries a
|
|
87
|
+
* single-use token, and the platform's own internal calls carry `tenantId`/`scopeId`.
|
|
88
|
+
* An access log that swallowed those would put live credentials into a store that is
|
|
89
|
+
* read by a wider audience than the request ever had, and keep them there for the
|
|
90
|
+
* backend's whole retention window. Logging the path alone loses nothing a reader
|
|
91
|
+
* needs: which route ran is the question, and the identifiers are already dimensions.
|
|
92
|
+
*/
|
|
93
|
+
function pathOf(url) {
|
|
94
|
+
try {
|
|
95
|
+
return new URL(url).pathname;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A URL the runtime accepted but `URL` will not parse is not worth failing over.
|
|
99
|
+
return '/';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The registry of stamps, keyed by the incoming `Request`, on `globalThis` under a GLOBAL
|
|
104
|
+
* symbol. Global rather than module state, because two copies of this code meet in one
|
|
105
|
+
* request: the platform's entry (`withInvocationLog`, prebuilt into the upload) and the
|
|
106
|
+
* copy the vertical bundled (this package, or a kernel from before #1978) — different
|
|
107
|
+
* module instances, one `Symbol.for`. Weak, so a
|
|
108
|
+
* finished request's stamp goes with its `Request`.
|
|
109
|
+
*/
|
|
110
|
+
const STAMPS = Symbol.for('substrat.invocation-stamp');
|
|
111
|
+
function stamps() {
|
|
112
|
+
const g = globalThis;
|
|
113
|
+
return (g[STAMPS] ??= new WeakMap());
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The stamp a request already carries, if a layer outside this one started it (#1893).
|
|
117
|
+
* `vertical-host` reads the record through this when the Hono context has none, which is
|
|
118
|
+
* the case when the platform's entry stamped the request and the vertical mounts nothing.
|
|
119
|
+
*/
|
|
120
|
+
export function invocationStampOf(request) {
|
|
121
|
+
return stamps().get(request);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* #1923: whether the per-response field walk is armed for this request — the one question the
|
|
125
|
+
* walk's two callers in `vertical-host` (the operation routes and the MCP door) ask.
|
|
126
|
+
*
|
|
127
|
+
* `false` for a request no stamp covers, and for a stamp begun by a layer that predates the
|
|
128
|
+
* switch: every version skew between the platform's entry and a vertical's bundled copy
|
|
129
|
+
* reads as off, never as on.
|
|
130
|
+
*/
|
|
131
|
+
export function fieldCoverageArmed(request) {
|
|
132
|
+
return typeof stamps().get(request)?.fieldCoverageId === 'string';
|
|
133
|
+
}
|
|
134
|
+
function beginStamp(request, env, options) {
|
|
135
|
+
const stamp = { invocationId: ulid(), record: {} };
|
|
136
|
+
const fieldCoverageId = armedFieldCoverageId(request.headers, env, options);
|
|
137
|
+
if (fieldCoverageId)
|
|
138
|
+
stamp.fieldCoverageId = fieldCoverageId;
|
|
139
|
+
stamps().set(request, stamp);
|
|
140
|
+
return stamp;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* #1923: the dispatch id the ROUTER armed the field walk with for this request, or `undefined`.
|
|
144
|
+
*
|
|
145
|
+
* The header alone is a claim. It is honoured only when its value is a ULID and the request's
|
|
146
|
+
* router assertion verifies, with the same secret and the same dev opt-out as the tenant on
|
|
147
|
+
* the line, because it is trusted for the same reason: the router strips every inbound
|
|
148
|
+
* `x-substrat-*` header, and a caller that reaches the script some other way holds no secret.
|
|
149
|
+
* An unrouted request, or one whose assertion fails, is never armed.
|
|
150
|
+
*
|
|
151
|
+
* The header is read first, so a request the router did not sample — almost all of them —
|
|
152
|
+
* pays one header read and no verification.
|
|
153
|
+
*/
|
|
154
|
+
function armedFieldCoverageId(headers, env, options) {
|
|
155
|
+
let id;
|
|
156
|
+
try {
|
|
157
|
+
id = headers.get(FIELD_COVERAGE_HEADER);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
if (id === null || !ROUTED_ID.test(id))
|
|
163
|
+
return undefined;
|
|
164
|
+
return routedNodeOrNull(headers, env, options) !== null ? id : undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Write the line for a finished request — the one place it is written, whichever layer
|
|
168
|
+
* started the stamp.
|
|
169
|
+
*
|
|
170
|
+
* No VERIFIED tenant ⇒ no line, and the three ways that happens are all silence here: no
|
|
171
|
+
* assertion at all (an un-routed call), an assertion this worker cannot verify or that is
|
|
172
|
+
* not the router's (`RouterAssertionError`), and — defensively — anything else the read
|
|
173
|
+
* throws. Writing a line for any of them would be filing one caller's request under a
|
|
174
|
+
* tenant of their choosing.
|
|
175
|
+
*/
|
|
176
|
+
function writeInvocationLine(stamp, done, options) {
|
|
177
|
+
// Called from a `finally`: a throw here would replace the vertical's answer, or its own
|
|
178
|
+
// error, with a logging failure. The platform wraps every uploaded script in this (#1893),
|
|
179
|
+
// so nothing in it may escape — a line that cannot be written is one missing line.
|
|
180
|
+
try {
|
|
181
|
+
writeLineOrThrow(stamp, done, options);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
try {
|
|
185
|
+
console.log(JSON.stringify({ substrat: 'invocation-log-fault', detail: String(e) }));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* the console itself is gone; there is nowhere left to say so */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function writeLineOrThrow(stamp, done, options) {
|
|
193
|
+
const node = routedNodeOrNull(done.request.headers, done.env, options);
|
|
194
|
+
if (!node)
|
|
195
|
+
return;
|
|
196
|
+
const { record, invocationId } = stamp;
|
|
197
|
+
const line = invocationLine({
|
|
198
|
+
tenantId: node.tenantId,
|
|
199
|
+
scopeId: node.scopeId,
|
|
200
|
+
vertical: node.verticalSlug,
|
|
201
|
+
surface: node.surface,
|
|
202
|
+
method: done.request.method,
|
|
203
|
+
path: pathOf(done.request.url),
|
|
204
|
+
status: done.status,
|
|
205
|
+
threw: done.threw,
|
|
206
|
+
durationMs: Date.now() - done.started,
|
|
207
|
+
invocationId,
|
|
208
|
+
level: invocationLevelOf(done.status, done.threw, record.problemCode),
|
|
209
|
+
operation: record.operation,
|
|
210
|
+
problemCode: record.problemCode,
|
|
211
|
+
principalKind: record.principalKind,
|
|
212
|
+
...(record.emitted ? { emitted: record.emitted } : {}),
|
|
213
|
+
versionId: versionIdOf(done.env),
|
|
214
|
+
...(record.outputFields ? { outputFields: record.outputFields } : {}),
|
|
215
|
+
...(stamp.fieldCoverageId ? { fieldCoverageId: stamp.fieldCoverageId } : {}),
|
|
216
|
+
});
|
|
217
|
+
console.log(JSON.stringify(line));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Mount as the FIRST middleware on a vertical's app, with the same two answers the
|
|
221
|
+
* vertical gives `readRoutedNode` in its own `nodeFor`:
|
|
222
|
+
*
|
|
223
|
+
* ```ts
|
|
224
|
+
* const app = new Hono<{ Bindings: Env }>();
|
|
225
|
+
* app.use(
|
|
226
|
+
* '*',
|
|
227
|
+
* invocationLog<Env>({
|
|
228
|
+
* routerSecret: (env) => env.ROUTER_SECRET,
|
|
229
|
+
* allowUnsigned: (env) => env.ALLOW_DEV_NODE === 'true',
|
|
230
|
+
* }),
|
|
231
|
+
* );
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* First, because Hono composes handlers in registration order and stops at the one that
|
|
235
|
+
* returns a response — middleware registered after a route does not wrap that route.
|
|
236
|
+
*
|
|
237
|
+
* **Since #1893 the platform stamps every deployed request itself**, by wrapping the
|
|
238
|
+
* uploaded entry with `withInvocationLog`. A vertical that still mounts this middleware
|
|
239
|
+
* then finds the request already stamped and steps aside — it hands the platform's stamp
|
|
240
|
+
* to the handler chain through the context, as it always did, and writes no second line.
|
|
241
|
+
* Mounted where no platform entry runs (a dev server behind a dev router), it stamps as
|
|
242
|
+
* before.
|
|
243
|
+
*
|
|
244
|
+
* Nothing here can fail a request: the line is written in a `finally`, and a throw from
|
|
245
|
+
* the handler is re-thrown untouched for `onError` to map as it always did.
|
|
246
|
+
*/
|
|
247
|
+
export function invocationLog(options = {}) {
|
|
248
|
+
return async (c, next) => {
|
|
249
|
+
// Stamped already by the platform's entry: pass the stamp down, write nothing.
|
|
250
|
+
const existing = invocationStampOf(c.req.raw);
|
|
251
|
+
if (existing) {
|
|
252
|
+
c.set?.('substratInvocationId', existing.invocationId);
|
|
253
|
+
c.set?.(INVOCATION_RECORD_KEY, existing.record);
|
|
254
|
+
await next();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Host code, so a real clock is correct here — `ctx.now()` is the module-code rule,
|
|
258
|
+
// and this middleware runs outside any operation's transaction.
|
|
259
|
+
const started = Date.now();
|
|
260
|
+
// Minted per request, before anything can emit. A ULID so it sorts by time like
|
|
261
|
+
// every other id on the spine.
|
|
262
|
+
const stamp = beginStamp(c.req.raw, c.env, options);
|
|
263
|
+
c.set?.('substratInvocationId', stamp.invocationId);
|
|
264
|
+
c.set?.(INVOCATION_RECORD_KEY, stamp.record);
|
|
265
|
+
let threw = false;
|
|
266
|
+
try {
|
|
267
|
+
await next();
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
threw = true;
|
|
271
|
+
// Always re-thrown: the envelope `mountPlatformSurface` installs is what answers
|
|
272
|
+
// the caller, and swallowing here would turn a fault into a silent 200.
|
|
273
|
+
throw e;
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
writeInvocationLine(stamp, { request: { method: c.req.method, url: c.req.raw.url, headers: c.req.raw.headers }, env: c.env, status: threw ? null : (c.res?.status ?? null), threw, started }, options);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** The handlers a module worker may export besides `fetch`, passed through untouched. */
|
|
281
|
+
const OTHER_HANDLERS = ['scheduled', 'queue', 'email', 'tail', 'trace', 'alarm', 'test'];
|
|
282
|
+
/**
|
|
283
|
+
* The platform's half of the stamp (#1893): wrap a module worker's `fetch` so every request
|
|
284
|
+
* it serves is stamped, whatever framework the worker is written in and whether or not it
|
|
285
|
+
* mounts the middleware.
|
|
286
|
+
*
|
|
287
|
+
* The control plane builds this into the entry it uploads in front of every vertical's
|
|
288
|
+
* bundle (`platform-entry.generated.ts`), which is what makes the stamp the platform's
|
|
289
|
+
* rather than a line each vertical has to remember. A request the vertical's own
|
|
290
|
+
* middleware already stamped — possible only if something wrapped this one — is passed
|
|
291
|
+
* straight through. The other handlers (`scheduled`, `queue`, …) are passed through
|
|
292
|
+
* untouched, bound to the worker, since a stamp is about a routed request.
|
|
293
|
+
*
|
|
294
|
+
* Nothing here can fail a request: the line is written in a `finally`, and a throw is
|
|
295
|
+
* re-thrown untouched.
|
|
296
|
+
*/
|
|
297
|
+
export function withInvocationLog(worker, options = {}) {
|
|
298
|
+
// The platform wraps whatever a bundle's default export is (#1893): a class entrypoint or
|
|
299
|
+
// no default at all has no `fetch` to stamp, and is handed back untouched.
|
|
300
|
+
const inner = worker?.fetch;
|
|
301
|
+
if (typeof inner !== 'function')
|
|
302
|
+
return worker;
|
|
303
|
+
const wrapped = {
|
|
304
|
+
async fetch(request, env, ctx) {
|
|
305
|
+
if (invocationStampOf(request))
|
|
306
|
+
return inner.call(worker, request, env, ctx);
|
|
307
|
+
const started = Date.now();
|
|
308
|
+
const stamp = beginStamp(request, env, options);
|
|
309
|
+
let status = null;
|
|
310
|
+
let threw = false;
|
|
311
|
+
try {
|
|
312
|
+
const response = await inner.call(worker, request, env, ctx);
|
|
313
|
+
status = response.status;
|
|
314
|
+
handRecordToRouter(response, stamp.record, request, env, options);
|
|
315
|
+
return response;
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
threw = true;
|
|
319
|
+
throw e;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
writeInvocationLine(stamp, { request, env, status, threw, started }, options);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
for (const name of OTHER_HANDLERS) {
|
|
327
|
+
const h = worker[name];
|
|
328
|
+
if (typeof h === 'function')
|
|
329
|
+
wrapped[name] = h.bind(worker);
|
|
330
|
+
}
|
|
331
|
+
return wrapped;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* #1904: hand the record back to the router on the response, which meters the request with
|
|
335
|
+
* it and strips the header before the response leaves (`INVOCATION_RECORD_HEADER`).
|
|
336
|
+
*
|
|
337
|
+
* Only on a request the router vouched for: a direct caller gets no header, and the router
|
|
338
|
+
* is the only reader. Never a throw — a response whose headers are immutable (one passed
|
|
339
|
+
* straight through from a `fetch`) reached no operation and has nothing to hand back, and
|
|
340
|
+
* metering must not fail a request either way.
|
|
341
|
+
*/
|
|
342
|
+
function handRecordToRouter(response, record, request, env, options) {
|
|
343
|
+
try {
|
|
344
|
+
const value = encodeInvocationRecord(record);
|
|
345
|
+
if (value === null || !routedNodeOrNull(request.headers, env, options))
|
|
346
|
+
return;
|
|
347
|
+
response.headers?.set?.(INVOCATION_RECORD_HEADER, value);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
/* immutable headers: see above */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The verified node, or `null` — never a throw.
|
|
355
|
+
*
|
|
356
|
+
* `readRoutedNode` is deliberately loud: present-but-wrong headers are a
|
|
357
|
+
* misconfiguration or an attack and a caller that routes on them must hear about it.
|
|
358
|
+
* This caller does not route on them; it writes a log line in a `finally`, where a throw
|
|
359
|
+
* would replace the vertical's real answer with a logging failure. So the loudness is
|
|
360
|
+
* swallowed HERE and only here, and the request is unaffected either way.
|
|
8
361
|
*/
|
|
9
|
-
|
|
362
|
+
function routedNodeOrNull(headers, env, options) {
|
|
363
|
+
try {
|
|
364
|
+
return readRoutedNode(headers, {
|
|
365
|
+
expectedSecret: options.routerSecret?.(env),
|
|
366
|
+
allowUnsigned: options.allowUnsigned?.(env) ?? false,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
// `RouterAssertionError` is the expected shape — a bad, missing or unverifiable
|
|
371
|
+
// secret, or a malformed id. Anything else would be a bug in the read, and a bug in
|
|
372
|
+
// the LOGGER must not become a failed request either.
|
|
373
|
+
if (!(e instanceof RouterAssertionError))
|
|
374
|
+
console.log(JSON.stringify({ substrat: 'invocation-log-fault', detail: String(e) }));
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
10
378
|
//# sourceMappingURL=invocation-log.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invocation-log.js","sourceRoot":"","sources":["../src/invocation-log.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"invocation-log.js","sourceRoot":"","sources":["../src/invocation-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScopeStubOptions } from '@substrat-run/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* Both kick flags as the stub options that raise them (#1705 PR 2): spread into `getScope`'s
|
|
4
|
+
* options with the handler's header setter. One call per worker rather than one line per flag,
|
|
5
|
+
* so a flag added later reaches every vertical that uses this, and not only the ones that
|
|
6
|
+
* remembered the line. A vertical that skips it still works, and waits for the sweep.
|
|
7
|
+
*/
|
|
8
|
+
export declare function kickFlags(setHeader: (name: string, value: string) => void): Pick<ScopeStubOptions, 'onPlatformRequests' | 'onExportedEvents'>;
|
|
9
|
+
//# sourceMappingURL=kick-flags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kick-flags.d.ts","sourceRoot":"","sources":["../src/kick-flags.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAC/C,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,GAAG,kBAAkB,CAAC,CAKnE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two response flags that ask the router for a kick (#1705 PR 2, #1998).
|
|
3
|
+
*
|
|
4
|
+
* Here rather than in the kernel since #1998: only a vertical's worker raises them, from the
|
|
5
|
+
* scope stub's observers, and the header names are contracts' wire vocabulary.
|
|
6
|
+
*/
|
|
7
|
+
import { EXPORTED_EVENTS_HEADER, PLATFORM_REQUEST_HEADER } from '@substrat-run/contracts/wire-headers';
|
|
8
|
+
/**
|
|
9
|
+
* Both kick flags as the stub options that raise them (#1705 PR 2): spread into `getScope`'s
|
|
10
|
+
* options with the handler's header setter. One call per worker rather than one line per flag,
|
|
11
|
+
* so a flag added later reaches every vertical that uses this, and not only the ones that
|
|
12
|
+
* remembered the line. A vertical that skips it still works, and waits for the sweep.
|
|
13
|
+
*/
|
|
14
|
+
export function kickFlags(setHeader) {
|
|
15
|
+
return {
|
|
16
|
+
onPlatformRequests: () => setHeader(PLATFORM_REQUEST_HEADER, '1'),
|
|
17
|
+
onExportedEvents: () => setHeader(EXPORTED_EVENTS_HEADER, '1'),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=kick-flags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kick-flags.js","sourceRoot":"","sources":["../src/kick-flags.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAGvG;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,SAAgD;IAEhD,OAAO;QACL,kBAAkB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,uBAAuB,EAAE,GAAG,CAAC;QACjE,gBAAgB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,sBAAsB,EAAE,GAAG,CAAC;KAC/D,CAAC;AACJ,CAAC"}
|
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
|