@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/invocation-log.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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":"
|
|
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"}
|
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"}
|