@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.
@@ -1,11 +1,199 @@
1
+ import type { EmittedReport } from '@substrat-run/kernel';
2
+ import { type OutputFieldsReport } from '@substrat-run/kernel/invocation-line';
3
+ import type { HeaderReader } from './routed-node.js';
4
+ export type { InvocationLogLine, OutputFieldsReport } from '@substrat-run/kernel/invocation-line';
1
5
  /**
2
- * The per-request log line and the platform's entry wrapper (#1978).
6
+ * The per-request store key the middleware parks its {@link InvocationRecord} under (#1746).
7
+ * A published name, like `substratInvocationId`: `mountOperations` reads it in another
8
+ * package, and a vertical's own routes may fill it in too.
9
+ */
10
+ export declare const INVOCATION_RECORD_KEY = "substratInvocationRecord";
11
+ /**
12
+ * What the handler chain learns about a request that the middleware cannot see from
13
+ * outside it (#1746): which operation ran, who it ran as, how it failed, and what it
14
+ * touched. The middleware creates one per request, hands it down through the context, and
15
+ * writes whatever was filled in when the request ends.
16
+ *
17
+ * Mutable on purpose. The alternative was reading several context keys back in the
18
+ * `finally`, which would add a `get` to the structural context below and make every field
19
+ * a string-keyed lookup; one object handed down and written back is the smaller seam.
20
+ *
21
+ * Every field is optional, and an unfilled one is written as `null`: a custom route that
22
+ * never reaches an operation is still logged, it just has nothing to say about these.
23
+ */
24
+ export interface InvocationRecord {
25
+ /** The operation name `mountOperations` dispatched to. */
26
+ operation?: string;
27
+ /** The `errorCode` a failed call was classified with, when the kernel's vocabulary names it. */
28
+ problemCode?: string;
29
+ /** The kind of subject the stub acted as — `ScopeStub.subjectKind`. */
30
+ principalKind?: string;
31
+ /** What the operation itself emitted — `InvokeOptions.onEmitted`. */
32
+ emitted?: EmittedReport;
33
+ /**
34
+ * #1331: which of the operation's DECLARED output fields its response carried. Filled
35
+ * only on a request the router armed the walk for ({@link fieldCoverageArmed}), and absent
36
+ * otherwise — never an empty report, so "not walked" cannot read as "returned nothing".
37
+ */
38
+ outputFields?: OutputFieldsReport;
39
+ }
40
+ /**
41
+ * The middleware's context, taken STRUCTURALLY — the line is not tied to a web framework,
42
+ * not even for a type. The shape below is the subset of a Hono context this reads, so
43
+ * `app.use('*', invocationLog())` type-checks inside a vertical without this module
44
+ * knowing which framework that vertical chose. Same posture as `vertical-host` taking
45
+ * its scope host as `VerticalScopeHost` rather than importing a concrete adapter: the
46
+ * seam is the shape, not the vendor.
47
+ *
48
+ * This line belongs with the router-assertion family (`readRoutedNode`,
49
+ * `assertPlatformCall`) — the code that reads what the router asserted about a request —
50
+ * because it is the same family seen from the other end: it writes that assertion down so
51
+ * a reader can find it again. The family moved here from the kernel (#1978); this package
52
+ * keeps its AI SDK behind the `./model` subpath, so a vertical importing this from its root
53
+ * bundles none of it.
54
+ */
55
+ export interface InvocationLogContext<Env = unknown> {
56
+ req: {
57
+ method: string;
58
+ raw: {
59
+ url: string;
60
+ headers: HeaderReader;
61
+ };
62
+ };
63
+ res?: {
64
+ status: number;
65
+ };
66
+ /**
67
+ * The framework's per-request store (#1237). Hono's `Context` has one; the type is
68
+ * structural and narrow on purpose, so it is declared optional — a caller that does
69
+ * not provide it simply gets no invocation id on the scope's events, which reads as
70
+ * unrecorded exactly like every other absent stamp.
71
+ */
72
+ set?: {
73
+ (key: 'substratInvocationId', value: string): void;
74
+ /** #1746: the record the handler chain fills in. See {@link InvocationRecord}. */
75
+ (key: typeof INVOCATION_RECORD_KEY, value: InvocationRecord): void;
76
+ };
77
+ /** The worker's bindings — read ONLY through the options below, never otherwise. */
78
+ env: Env;
79
+ }
80
+ /**
81
+ * How this middleware verifies the router's assertion, in the vertical's own terms.
82
+ *
83
+ * Both are functions of the env rather than plain values because a Worker's bindings
84
+ * arrive per-request, while `app.use(...)` runs once at module scope. They are the same
85
+ * two knobs the vertical already passes to `readRoutedNode` in its `nodeFor`, and they
86
+ * must be given the same answers: a log that trusts more than the router does is the
87
+ * forged-tenant hole, and one that trusts less is silently empty.
88
+ */
89
+ export interface InvocationLogOptions<Env = unknown> {
90
+ /**
91
+ * The router's shared secret as this worker holds it — `(env) => env.ROUTER_SECRET`.
92
+ *
93
+ * Omitting it is not a shortcut: with no secret to check against, an assertion can
94
+ * only be accepted by `allowUnsigned`, so a bare `invocationLog()` in a deployed
95
+ * vertical writes NOTHING. `pnpm lint:invocation-log` refuses that arrangement rather
96
+ * than leaving a vertical to discover it from an empty log view.
97
+ */
98
+ routerSecret?: (env: Env) => string | undefined;
99
+ /**
100
+ * The vertical's own `ALLOW_DEV_NODE` — an un-routed local instance behind a dev
101
+ * router that holds no secret either. Anywhere else this must stay false, or the
102
+ * header is a claim again.
103
+ */
104
+ allowUnsigned?: (env: Env) => boolean;
105
+ }
106
+ /**
107
+ * #1893: one request's stamp — the id minted for it and the record the handler chain fills
108
+ * in — shared by whichever layer started it.
109
+ */
110
+ export interface InvocationStamp {
111
+ invocationId: string;
112
+ record: InvocationRecord;
113
+ /**
114
+ * #1923: the dispatch id the router armed this request's field walk with, when it did and
115
+ * the request's router assertion verified. Decided once, when the stamp begins — see
116
+ * {@link armedFieldCoverageId}. Written on the line beside the report, as its provenance.
117
+ */
118
+ fieldCoverageId?: string;
119
+ }
120
+ /**
121
+ * The stamp a request already carries, if a layer outside this one started it (#1893).
122
+ * `vertical-host` reads the record through this when the Hono context has none, which is
123
+ * the case when the platform's entry stamped the request and the vertical mounts nothing.
124
+ */
125
+ export declare function invocationStampOf(request: object): InvocationStamp | undefined;
126
+ /**
127
+ * #1923: whether the per-response field walk is armed for this request — the one question the
128
+ * walk's two callers in `vertical-host` (the operation routes and the MCP door) ask.
129
+ *
130
+ * `false` for a request no stamp covers, and for a stamp begun by a layer that predates the
131
+ * switch: every version skew between the platform's entry and a vertical's bundled copy
132
+ * reads as off, never as on.
133
+ */
134
+ export declare function fieldCoverageArmed(request: object): boolean;
135
+ /**
136
+ * Mount as the FIRST middleware on a vertical's app, with the same two answers the
137
+ * vertical gives `readRoutedNode` in its own `nodeFor`:
138
+ *
139
+ * ```ts
140
+ * const app = new Hono<{ Bindings: Env }>();
141
+ * app.use(
142
+ * '*',
143
+ * invocationLog<Env>({
144
+ * routerSecret: (env) => env.ROUTER_SECRET,
145
+ * allowUnsigned: (env) => env.ALLOW_DEV_NODE === 'true',
146
+ * }),
147
+ * );
148
+ * ```
149
+ *
150
+ * First, because Hono composes handlers in registration order and stops at the one that
151
+ * returns a response — middleware registered after a route does not wrap that route.
152
+ *
153
+ * **Since #1893 the platform stamps every deployed request itself**, by wrapping the
154
+ * uploaded entry with `withInvocationLog`. A vertical that still mounts this middleware
155
+ * then finds the request already stamped and steps aside — it hands the platform's stamp
156
+ * to the handler chain through the context, as it always did, and writes no second line.
157
+ * Mounted where no platform entry runs (a dev server behind a dev router), it stamps as
158
+ * before.
159
+ *
160
+ * Nothing here can fail a request: the line is written in a `finally`, and a throw from
161
+ * the handler is re-thrown untouched for `onError` to map as it always did.
162
+ */
163
+ export declare function invocationLog<Env = unknown>(options?: InvocationLogOptions<Env>): (c: InvocationLogContext<Env>, next: () => Promise<void>) => Promise<void>;
164
+ /** The request a module worker's `fetch` receives, as far as the stamp reads it. */
165
+ export interface IncomingRequest {
166
+ method: string;
167
+ url: string;
168
+ headers: HeaderReader;
169
+ }
170
+ /**
171
+ * The module-worker shape `withInvocationLog` wraps: a default export with handlers. Typed
172
+ * structurally, with no workers types, so a Hono app, a plain `{ fetch }` object and
173
+ * anything else with the method all fit.
174
+ */
175
+ export interface ModuleWorker<Env = unknown> {
176
+ fetch?: (request: IncomingRequest, env: Env, ctx: unknown) => {
177
+ status: number;
178
+ } | Promise<{
179
+ status: number;
180
+ }>;
181
+ [handler: string]: unknown;
182
+ }
183
+ /**
184
+ * The platform's half of the stamp (#1893): wrap a module worker's `fetch` so every request
185
+ * it serves is stamped, whatever framework the worker is written in and whether or not it
186
+ * mounts the middleware.
187
+ *
188
+ * The control plane builds this into the entry it uploads in front of every vertical's
189
+ * bundle (`platform-entry.generated.ts`), which is what makes the stamp the platform's
190
+ * rather than a line each vertical has to remember. A request the vertical's own
191
+ * middleware already stamped — possible only if something wrapped this one — is passed
192
+ * straight through. The other handlers (`scheduled`, `queue`, …) are passed through
193
+ * untouched, bound to the worker, since a stamp is about a routed request.
3
194
  *
4
- * Moving here from the kernel: this is host-side HTTP code that every deployed vertical
5
- * mounts, and nothing in it needs a kernel guarantee. For one release the definition still
6
- * lives in `@substrat-run/kernel` and this module re-exports that same binding, so an
7
- * import from either package is the one function. Import it from here.
195
+ * Nothing here can fail a request: the line is written in a `finally`, and a throw is
196
+ * re-thrown untouched.
8
197
  */
9
- export { invocationLog, INVOCATION_RECORD_KEY, invocationStampOf, withInvocationLog, fieldCoverageArmed, } from '@substrat-run/kernel';
10
- export type { InvocationLogLine, InvocationLogContext, InvocationRecord, OutputFieldsReport, InvocationStamp, ModuleWorker, IncomingRequest, } from '@substrat-run/kernel';
198
+ export declare function withInvocationLog<Env = unknown>(worker: ModuleWorker<Env>, options?: InvocationLogOptions<Env>): ModuleWorker<Env>;
11
199
  //# sourceMappingURL=invocation-log.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"invocation-log.d.ts","sourceRoot":"","sources":["../src/invocation-log.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,eAAe,GAChB,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"invocation-log.d.ts","sourceRoot":"","sources":["../src/invocation-log.ts"],"names":[],"mappings":"AAiEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAG/F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAElG;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,6BAA6B,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,oBAAoB,CAAC,GAAG,GAAG,OAAO;IACjD,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,YAAY,CAAA;SAAE,CAAA;KAAE,CAAC;IACrE,GAAG,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzB;;;;;OAKG;IACH,GAAG,CAAC,EAAE;QACJ,CAAC,GAAG,EAAE,sBAAsB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACnD,kFAAkF;QAClF,CAAC,GAAG,EAAE,OAAO,qBAAqB,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;KACpE,CAAC;IACF,oFAAoF;IACpF,GAAG,EAAE,GAAG,CAAC;CACV;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB,CAAC,GAAG,GAAG,OAAO;IACjD;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;IAChD;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CACvC;AAmCD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAiBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAG,OAAO,EACzC,OAAO,GAAE,oBAAoB,CAAC,GAAG,CAAM,GACtC,CAAC,CAAC,EAAE,oBAAoB,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAkC5E;AAED,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,YAAY,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,GAAG,GAAG,OAAO;IACzC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,KAAK;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/G,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5B;AAKD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAG,OAAO,EAC7C,MAAM,EAAE,YAAY,CAAC,GAAG,CAAC,EACzB,OAAO,GAAE,oBAAoB,CAAC,GAAG,CAAM,GACtC,YAAY,CAAC,GAAG,CAAC,CA8BnB"}
@@ -1,10 +1,378 @@
1
1
  /**
2
- * The per-request log line and the platform's entry wrapper (#1978).
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
- * Moving here from the kernel: this is host-side HTTP code that every deployed vertical
5
- * mounts, and nothing in it needs a kernel guarantee. For one release the definition still
6
- * lives in `@substrat-run/kernel` and this module re-exports that same binding, so an
7
- * import from either package is the one function. Import it from here.
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
- export { invocationLog, INVOCATION_RECORD_KEY, invocationStampOf, withInvocationLog, fieldCoverageArmed, } from '@substrat-run/kernel';
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,EACL,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC"}
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"}