@alwaysmeticulous/backend-recorder-workerd 2.343.0 → 2.350.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/README.md CHANGED
@@ -145,7 +145,7 @@ or set the `METICULOUS_BACKEND_PROVISIONAL_SESSION_IDS` var to `"false"` (a work
145
145
 
146
146
  `getMeticulousSessionId()` is also how the id reaches another process. A document navigation has no inbound header for your app to forward, so a worker that fans out to its own services has to pass the id on as `x-meticulous-session-id` itself; the shim does not add it to outbound requests.
147
147
 
148
- Request bodies have secret-looking JSON fields (`clientSecret`, `apiKey`, `token`, `password`, …) replaced with `REDACTED` before they leave the worker, since plenty of APIs carry a credential in the body rather than a header. Response bodies are stored verbatim. A KV `put` value is redacted the same way, for the same reason; a value read back from KV is stored verbatim, like a response body.
148
+ Request bodies have secret-looking JSON fields (`clientSecret`, `apiKey`, `token`, `password`, …) replaced with `REDACTED` before they leave the worker, since plenty of APIs carry a credential in the body rather than a header. Response bodies are stored verbatim: as text when they are UTF-8, and otherwise as base64 (marked with `http.response.body.encoding: "base64"`), so a binary response such as an archive or an image replays byte-for-byte. A KV `put` value is redacted the same way, for the same reason; a value read back from KV is stored verbatim, like a response body.
149
149
 
150
150
  ## Replay
151
151
 
@@ -165,6 +165,12 @@ untagged.
165
165
 
166
166
  Calls through bindings and KV namespaces are not served from the recording (see Limitations) and are never failed — they always reach the real binding.
167
167
 
168
+ ## Agent-swarm testing
169
+
170
+ A third mode sits beside replay for Meticulous-driven runs (agentic session generation). It activates on two deployment env vars, `METICULOUS_AGENT_SWARM_TESTING_SIDECAR_URL` and `METICULOUS_AGENT_SWARM_TESTING_RUN_ID`, set by whoever deploys the worker for a swarm run; the URL is validated like the replay sidecar URL. Every outgoing `fetch` call is offered to the interceptor service the URL names: the interceptor either supplies the response to serve, or the call passes through to the real service.
171
+
172
+ The mode is best-effort by design — the opposite of replay's hermeticity. A call the interceptor cannot answer (a miss, an unreachable or misbehaving interceptor, a response the shim cannot represent) is never failed; it simply reaches the real service. There is no frozen clock, no seeded randomness and no recording. The `meticulous-passthrough` header skips the interceptor for a call entirely.
173
+
168
174
  ## Code coverage
169
175
 
170
176
  Meticulous can report which of your server lines each replayed session executed. Workerd exposes no usable V8 coverage — its inspector never dispatches `Profiler.enable`, so `Profiler.startPreciseCoverage` is unreachable, and the one coverage call it does forward reports binary counts that never reset, which cannot be split per request. So the lines are marked at build time instead.
@@ -0,0 +1,40 @@
1
+ import { readRequestBodyWithCap } from "./body-capture.js";
2
+ import { getOriginalFetch } from "./original-fetch.js";
3
+ import { postAgentSwarmTestingOutboundFetch } from "./sidecar-client.js";
4
+ import { buildMockResponse, hasPassthroughHeader } from "./replay-fetch.js";
5
+ //#region src/agent-swarm-testing-fetch.ts
6
+ /**
7
+ * Offers one outbound call to the interceptor service, serving its response or passing the
8
+ * call through to the real service.
9
+ *
10
+ * The deliberate opposite of replay's hermeticity: replay fails a call it cannot serve
11
+ * because silently reaching the real service would turn a replay into live traffic, whereas
12
+ * agent-swarm testing exists to be driven live — the interceptor is advisory, and any failure
13
+ * to consult it (a miss, a timeout, an unparseable answer) means the call simply happens.
14
+ */
15
+ const agentSwarmTestingOutboundCall = async (ctx, request, invoke) => {
16
+ if (hasPassthroughHeader(request)) return invoke(request);
17
+ let result;
18
+ try {
19
+ const requestBody = request.body ? await readRequestBodyWithCap(request.clone().body).catch(() => void 0) : void 0;
20
+ const payload = {
21
+ runId: ctx.runId,
22
+ ...ctx.frontendSessionId !== void 0 ? { frontendSessionId: ctx.frontendSessionId } : {},
23
+ method: request.method,
24
+ url: request.url,
25
+ ...requestBody !== void 0 ? { requestBody } : {}
26
+ };
27
+ result = await postAgentSwarmTestingOutboundFetch(getOriginalFetch(), ctx.sidecarUrl, payload);
28
+ } catch {
29
+ result = void 0;
30
+ }
31
+ if (result !== void 0 && result.outcome === "mock") {
32
+ const mocked = buildMockResponse(result);
33
+ if (mocked !== void 0) return mocked;
34
+ }
35
+ return invoke(request);
36
+ };
37
+ //#endregion
38
+ export { agentSwarmTestingOutboundCall };
39
+
40
+ //# sourceMappingURL=agent-swarm-testing-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-swarm-testing-fetch.js","names":[],"sources":["../src/agent-swarm-testing-fetch.ts"],"sourcesContent":["import { readRequestBodyWithCap } from \"./body-capture\";\nimport type { RequestAgentSwarmTestingContext } from \"./context\";\nimport { getOriginalFetch } from \"./original-fetch\";\nimport {\n type OutboundFetchAgentSwarmTestingRequest,\n type OutboundFetchAgentSwarmTestingResponse,\n} from \"./protocol\";\nimport { postAgentSwarmTestingOutboundFetch } from \"./sidecar-client\";\nimport { buildMockResponse, hasPassthroughHeader } from \"./replay-fetch\";\n\n/**\n * Offers one outbound call to the interceptor service, serving its response or passing the\n * call through to the real service.\n *\n * The deliberate opposite of replay's hermeticity: replay fails a call it cannot serve\n * because silently reaching the real service would turn a replay into live traffic, whereas\n * agent-swarm testing exists to be driven live — the interceptor is advisory, and any failure\n * to consult it (a miss, a timeout, an unparseable answer) means the call simply happens.\n */\nexport const agentSwarmTestingOutboundCall = async (\n ctx: RequestAgentSwarmTestingContext,\n request: Request,\n invoke: (request: Request) => Promise<Response>,\n): Promise<Response> => {\n if (hasPassthroughHeader(request)) {\n return invoke(request);\n }\n\n let result: OutboundFetchAgentSwarmTestingResponse | undefined;\n try {\n // Read the clone and pass the original through untouched, mirroring the replay path.\n const requestBody = request.body\n ? await readRequestBodyWithCap(request.clone().body).catch(\n () => undefined,\n )\n : undefined;\n const payload: OutboundFetchAgentSwarmTestingRequest = {\n runId: ctx.runId,\n ...(ctx.frontendSessionId !== undefined\n ? { frontendSessionId: ctx.frontendSessionId }\n : {}),\n method: request.method,\n url: request.url,\n ...(requestBody !== undefined ? { requestBody } : {}),\n };\n result = await postAgentSwarmTestingOutboundFetch(\n getOriginalFetch(),\n ctx.sidecarUrl,\n payload,\n );\n } catch {\n result = undefined;\n }\n\n if (result !== undefined && result.outcome === \"mock\") {\n const mocked = buildMockResponse(result);\n if (mocked !== undefined) {\n return mocked;\n }\n }\n\n return invoke(request);\n};\n"],"mappings":";;;;;;;;;;;;;;AAmBA,MAAa,gCAAgC,OAC3C,KACA,SACA,WACsB;AACtB,KAAI,qBAAqB,QAAQ,CAC/B,QAAO,OAAO,QAAQ;CAGxB,IAAI;AACJ,KAAI;EAEF,MAAM,cAAc,QAAQ,OACxB,MAAM,uBAAuB,QAAQ,OAAO,CAAC,KAAK,CAAC,YAC3C,KAAA,EACP,GACD,KAAA;EACJ,MAAM,UAAiD;GACrD,OAAO,IAAI;GACX,GAAI,IAAI,sBAAsB,KAAA,IAC1B,EAAE,mBAAmB,IAAI,mBAAmB,GAC5C,EAAE;GACN,QAAQ,QAAQ;GAChB,KAAK,QAAQ;GACb,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,GAAG,EAAE;GACrD;AACD,WAAS,MAAM,mCACb,kBAAkB,EAClB,IAAI,YACJ,QACD;SACK;AACN,WAAS,KAAA;;AAGX,KAAI,WAAW,KAAA,KAAa,OAAO,YAAY,QAAQ;EACrD,MAAM,SAAS,kBAAkB,OAAO;AACxC,MAAI,WAAW,KAAA,EACb,QAAO;;AAIX,QAAO,OAAO,QAAQ"}
package/dist/base64.js ADDED
@@ -0,0 +1,25 @@
1
+ //#region src/base64.ts
2
+ const BASE64_CHUNK_SIZE = 32768;
3
+ /** Base64 of the bytes a view covers, through `Buffer` on Node and `btoa` in a Worker. */
4
+ const toBase64 = (view) => {
5
+ const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
6
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
7
+ let binary = "";
8
+ for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));
9
+ return btoa(binary);
10
+ };
11
+ /**
12
+ * The bytes a base64 string encodes. On Node this is a `Buffer`, which is also a `Uint8Array` to
13
+ * anything consuming it — so a Prisma `Bytes` field replays as the type pg returned at record time.
14
+ */
15
+ const fromBase64 = (base64) => {
16
+ if (typeof Buffer !== "undefined") return Buffer.from(base64, "base64");
17
+ const binary = atob(base64);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
20
+ return bytes;
21
+ };
22
+ //#endregion
23
+ export { fromBase64, toBase64 };
24
+
25
+ //# sourceMappingURL=base64.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.js","names":[],"sources":["../src/base64.ts"],"sourcesContent":["const BASE64_CHUNK_SIZE = 0x8000;\n\n/** Base64 of the bytes a view covers, through `Buffer` on Node and `btoa` in a Worker. */\nexport const toBase64 = (view: ArrayBufferView): string => {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) {\n binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));\n }\n return btoa(binary);\n};\n\n/**\n * The bytes a base64 string encodes. On Node this is a `Buffer`, which is also a `Uint8Array` to\n * anything consuming it — so a Prisma `Bytes` field replays as the type pg returned at record time.\n */\nexport const fromBase64 = (base64: string): Uint8Array => {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(base64, \"base64\");\n }\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\n"],"mappings":";AAAA,MAAM,oBAAoB;;AAG1B,MAAa,YAAY,SAAkC;CACzD,MAAM,QAAQ,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,WAAW;AAC3E,KAAI,OAAO,WAAW,YACpB,QAAO,OAAO,KAAK,MAAM,CAAC,SAAS,SAAS;CAE9C,IAAI,SAAS;AACb,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,kBACrC,WAAU,OAAO,aAAa,GAAG,MAAM,SAAS,GAAG,IAAI,kBAAkB,CAAC;AAE5E,QAAO,KAAK,OAAO;;;;;;AAOrB,MAAa,cAAc,WAA+B;AACxD,KAAI,OAAO,WAAW,YACpB,QAAO,OAAO,KAAK,QAAQ,SAAS;CAEtC,MAAM,SAAS,KAAK,OAAO;CAC3B,MAAM,QAAQ,IAAI,WAAW,OAAO,OAAO;AAC3C,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,IACjC,OAAM,KAAK,OAAO,WAAW,EAAE;AAEjC,QAAO"}
@@ -14,6 +14,10 @@ declare const MAX_BODY_CAPTURE_SIZE: number;
14
14
  * UTF-8. Stops (and marks the capture truncated) on the size cap or the time
15
15
  * guard, so a long-lived stream can never pin the capture work. Returns
16
16
  * undefined for absent bodies.
17
+ *
18
+ * The decode turns any byte that is not UTF-8 into U+FFFD, which is only acceptable for a body
19
+ * that is hashed rather than served back. Response bodies go through
20
+ * {@link readResponseBodyWithCap} instead.
17
21
  */
18
22
  declare const readBodyWithCap: (stream: ReadableStream<Uint8Array> | null) => Promise<CapturedBody | undefined>;
19
23
  /**
@@ -21,10 +25,15 @@ declare const readBodyWithCap: (stream: ReadableStream<Uint8Array> | null) => Pr
21
25
  *
22
26
  * Every request-body capture path goes through here, so record and replay redact
23
27
  * identically and any hash derived from the body still agrees on both sides. Response
24
- * bodies deliberately use {@link readBodyWithCap} directly — they are served back to the
28
+ * bodies deliberately use {@link readResponseBodyWithCap} — they are served back to the
25
29
  * app during replay and must stay byte-exact.
26
30
  */
27
31
  declare const readRequestBodyWithCap: (stream: ReadableStream<Uint8Array> | null) => Promise<CapturedBody | undefined>;
32
+ /**
33
+ * Reads a *response* body so that replay can serve back exactly the bytes the app received: as
34
+ * text when they are UTF-8, and as base64 otherwise (see `encodeBodyBytes`).
35
+ */
36
+ declare const readResponseBodyWithCap: (stream: ReadableStream<Uint8Array> | null) => Promise<CapturedBody | undefined>;
28
37
  //#endregion
29
- export { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap };
38
+ export { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap, readResponseBodyWithCap };
30
39
  //# sourceMappingURL=body-capture.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { encodeBodyBytes } from "./body-encoding.js";
1
2
  import { redactRequestBody } from "./redact-body.js";
2
3
  //#region src/body-capture.ts
3
4
  /**
@@ -16,12 +17,56 @@ const TIMED_OUT = Symbol("meticulous.bodyReadTimedOut");
16
17
  * UTF-8. Stops (and marks the capture truncated) on the size cap or the time
17
18
  * guard, so a long-lived stream can never pin the capture work. Returns
18
19
  * undefined for absent bodies.
20
+ *
21
+ * The decode turns any byte that is not UTF-8 into U+FFFD, which is only acceptable for a body
22
+ * that is hashed rather than served back. Response bodies go through
23
+ * {@link readResponseBodyWithCap} instead.
19
24
  */
20
25
  const readBodyWithCap = async (stream) => {
26
+ const read = await readBodyBytesWithCap(stream);
27
+ if (read === void 0) return;
28
+ return {
29
+ body: new TextDecoder("utf-8").decode(read.bytes),
30
+ truncated: read.truncated
31
+ };
32
+ };
33
+ /**
34
+ * Reads a *request* body, redacting secret-looking fields before it leaves the worker.
35
+ *
36
+ * Every request-body capture path goes through here, so record and replay redact
37
+ * identically and any hash derived from the body still agrees on both sides. Response
38
+ * bodies deliberately use {@link readResponseBodyWithCap} — they are served back to the
39
+ * app during replay and must stay byte-exact.
40
+ */
41
+ const readRequestBodyWithCap = async (stream) => {
42
+ const captured = await readBodyWithCap(stream);
43
+ if (captured === void 0) return;
44
+ return {
45
+ ...captured,
46
+ body: redactRequestBody(captured.body)
47
+ };
48
+ };
49
+ /**
50
+ * Reads a *response* body so that replay can serve back exactly the bytes the app received: as
51
+ * text when they are UTF-8, and as base64 otherwise (see `encodeBodyBytes`).
52
+ */
53
+ const readResponseBodyWithCap = async (stream) => {
54
+ const read = await readBodyBytesWithCap(stream);
55
+ if (read === void 0) return;
56
+ const { body, encoding, storedFraction } = encodeBodyBytes(read.bytes, {
57
+ maxLength: MAX_BODY_CAPTURE_SIZE,
58
+ incompleteTail: read.truncated
59
+ });
60
+ return {
61
+ body,
62
+ truncated: read.truncated || storedFraction < 1,
63
+ ...encoding !== void 0 ? { encoding } : {}
64
+ };
65
+ };
66
+ const readBodyBytesWithCap = async (stream) => {
21
67
  if (!stream) return;
22
68
  const reader = stream.getReader();
23
- const decoder = new TextDecoder("utf-8");
24
- let text = "";
69
+ const chunks = [];
25
70
  let bytes = 0;
26
71
  let truncated = false;
27
72
  let timer;
@@ -40,43 +85,36 @@ const readBodyWithCap = async (stream) => {
40
85
  if (!value || value.byteLength === 0) continue;
41
86
  const remaining = MAX_BODY_CAPTURE_SIZE - bytes;
42
87
  if (value.byteLength >= remaining) {
43
- text += decoder.decode(value.subarray(0, remaining), { stream: true });
88
+ chunks.push(value.subarray(0, remaining));
44
89
  bytes += remaining;
45
90
  truncated = truncated || value.byteLength > remaining;
46
91
  const next = await Promise.race([reader.read(), timedOut]);
47
92
  if (next === TIMED_OUT || !next.done) truncated = true;
48
93
  break;
49
94
  }
50
- text += decoder.decode(value, { stream: true });
95
+ chunks.push(value);
51
96
  bytes += value.byteLength;
52
97
  }
53
98
  } finally {
54
99
  clearTimeout(timer);
55
100
  reader.cancel().catch(() => {});
56
101
  }
57
- text += decoder.decode();
58
102
  return {
59
- body: text,
103
+ bytes: concatBytes(chunks, bytes),
60
104
  truncated
61
105
  };
62
106
  };
63
- /**
64
- * Reads a *request* body, redacting secret-looking fields before it leaves the worker.
65
- *
66
- * Every request-body capture path goes through here, so record and replay redact
67
- * identically and any hash derived from the body still agrees on both sides. Response
68
- * bodies deliberately use {@link readBodyWithCap} directly — they are served back to the
69
- * app during replay and must stay byte-exact.
70
- */
71
- const readRequestBodyWithCap = async (stream) => {
72
- const captured = await readBodyWithCap(stream);
73
- if (captured === void 0) return;
74
- return {
75
- ...captured,
76
- body: redactRequestBody(captured.body)
77
- };
107
+ const concatBytes = (chunks, byteLength) => {
108
+ if (chunks.length === 1) return chunks[0];
109
+ const out = new Uint8Array(byteLength);
110
+ let offset = 0;
111
+ for (const chunk of chunks) {
112
+ out.set(chunk, offset);
113
+ offset += chunk.byteLength;
114
+ }
115
+ return out;
78
116
  };
79
117
  //#endregion
80
- export { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap };
118
+ export { MAX_BODY_CAPTURE_SIZE, readBodyWithCap, readRequestBodyWithCap, readResponseBodyWithCap };
81
119
 
82
120
  //# sourceMappingURL=body-capture.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"body-capture.js","names":[],"sources":["../src/body-capture.ts"],"sourcesContent":["import type { CapturedBody } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * Cap on the bytes of any one captured body, and so on the size of the span attribute it\n * becomes. Defined here and re-exported by the Node recorder's `constants.ts` (same\n * arrangement as the shared attribute names) so a Worker recording and a Node recording\n * truncate at the same point, and so the replay side — which re-applies the cap to a live\n * body before hashing it — agrees with what was stored.\n */\nexport const MAX_BODY_CAPTURE_SIZE = 1024 * 1024;\n\n/** Cap on how long we keep reading a (possibly never-ending, e.g. SSE) body clone. */\nconst BODY_READ_TIMEOUT_MS = 10_000;\n\nconst TIMED_OUT = Symbol(\"meticulous.bodyReadTimedOut\");\n\n/**\n * Reads a body stream up to {@link MAX_BODY_CAPTURE_SIZE} bytes, decoding as\n * UTF-8. Stops (and marks the capture truncated) on the size cap or the time\n * guard, so a long-lived stream can never pin the capture work. Returns\n * undefined for absent bodies.\n */\nexport const readBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n if (!stream) {\n return undefined;\n }\n\n const reader = stream.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), BODY_READ_TIMEOUT_MS);\n });\n\n try {\n while (true) {\n const result = await Promise.race([reader.read(), timedOut]);\n if (result === TIMED_OUT) {\n truncated = true;\n break;\n }\n const { done, value } = result;\n if (done) {\n break;\n }\n if (!value || value.byteLength === 0) {\n continue;\n }\n const remaining = MAX_BODY_CAPTURE_SIZE - bytes;\n if (value.byteLength >= remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n bytes += remaining;\n truncated = truncated || value.byteLength > remaining;\n // Even an exact-cap chunk means we stop reading, so a longer stream\n // counts as truncated.\n const next = await Promise.race([reader.read(), timedOut]);\n if (next === TIMED_OUT || !next.done) {\n truncated = true;\n }\n break;\n }\n text += decoder.decode(value, { stream: true });\n bytes += value.byteLength;\n }\n } finally {\n clearTimeout(timer);\n reader.cancel().catch(() => {});\n }\n\n text += decoder.decode();\n return { body: text, truncated };\n};\n\n/**\n * Reads a *request* body, redacting secret-looking fields before it leaves the worker.\n *\n * Every request-body capture path goes through here, so record and replay redact\n * identically and any hash derived from the body still agrees on both sides. Response\n * bodies deliberately use {@link readBodyWithCap} directly — they are served back to the\n * app during replay and must stay byte-exact.\n */\nexport const readRequestBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n const captured = await readBodyWithCap(stream);\n if (captured === undefined) {\n return undefined;\n }\n return { ...captured, body: redactRequestBody(captured.body) };\n};\n"],"mappings":";;;;;;;;;AAUA,MAAa,wBAAwB,OAAO;;AAG5C,MAAM,uBAAuB;AAE7B,MAAM,YAAY,OAAO,8BAA8B;;;;;;;AAQvD,MAAa,kBAAkB,OAC7B,WACsC;AACtC,KAAI,CAAC,OACH;CAGF,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,UAAU,IAAI,YAAY,QAAQ;CACxC,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,YAAY;CAEhB,IAAI;CACJ,MAAM,WAAW,IAAI,SAA2B,YAAY;AAC1D,UAAQ,iBAAiB,QAAQ,UAAU,EAAE,qBAAqB;GAClE;AAEF,KAAI;AACF,SAAO,MAAM;GACX,MAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC5D,OAAI,WAAW,WAAW;AACxB,gBAAY;AACZ;;GAEF,MAAM,EAAE,MAAM,UAAU;AACxB,OAAI,KACF;AAEF,OAAI,CAAC,SAAS,MAAM,eAAe,EACjC;GAEF,MAAM,YAAY,wBAAwB;AAC1C,OAAI,MAAM,cAAc,WAAW;AACjC,YAAQ,QAAQ,OAAO,MAAM,SAAS,GAAG,UAAU,EAAE,EAAE,QAAQ,MAAM,CAAC;AACtE,aAAS;AACT,gBAAY,aAAa,MAAM,aAAa;IAG5C,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC1D,QAAI,SAAS,aAAa,CAAC,KAAK,KAC9B,aAAY;AAEd;;AAEF,WAAQ,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;AAC/C,YAAS,MAAM;;WAET;AACR,eAAa,MAAM;AACnB,SAAO,QAAQ,CAAC,YAAY,GAAG;;AAGjC,SAAQ,QAAQ,QAAQ;AACxB,QAAO;EAAE,MAAM;EAAM;EAAW;;;;;;;;;;AAWlC,MAAa,yBAAyB,OACpC,WACsC;CACtC,MAAM,WAAW,MAAM,gBAAgB,OAAO;AAC9C,KAAI,aAAa,KAAA,EACf;AAEF,QAAO;EAAE,GAAG;EAAU,MAAM,kBAAkB,SAAS,KAAK;EAAE"}
1
+ {"version":3,"file":"body-capture.js","names":[],"sources":["../src/body-capture.ts"],"sourcesContent":["import { encodeBodyBytes } from \"./body-encoding\";\nimport type { CapturedBody } from \"./protocol\";\nimport { redactRequestBody } from \"./redact-body\";\n\n/**\n * Cap on the bytes of any one captured body, and so on the size of the span attribute it\n * becomes. Defined here and re-exported by the Node recorder's `constants.ts` (same\n * arrangement as the shared attribute names) so a Worker recording and a Node recording\n * truncate at the same point, and so the replay side — which re-applies the cap to a live\n * body before hashing it — agrees with what was stored.\n */\nexport const MAX_BODY_CAPTURE_SIZE = 1024 * 1024;\n\n/** Cap on how long we keep reading a (possibly never-ending, e.g. SSE) body clone. */\nconst BODY_READ_TIMEOUT_MS = 10_000;\n\nconst TIMED_OUT = Symbol(\"meticulous.bodyReadTimedOut\");\n\n/**\n * Reads a body stream up to {@link MAX_BODY_CAPTURE_SIZE} bytes, decoding as\n * UTF-8. Stops (and marks the capture truncated) on the size cap or the time\n * guard, so a long-lived stream can never pin the capture work. Returns\n * undefined for absent bodies.\n *\n * The decode turns any byte that is not UTF-8 into U+FFFD, which is only acceptable for a body\n * that is hashed rather than served back. Response bodies go through\n * {@link readResponseBodyWithCap} instead.\n */\nexport const readBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n const read = await readBodyBytesWithCap(stream);\n if (read === undefined) {\n return undefined;\n }\n return {\n body: new TextDecoder(\"utf-8\").decode(read.bytes),\n truncated: read.truncated,\n };\n};\n\n/**\n * Reads a *request* body, redacting secret-looking fields before it leaves the worker.\n *\n * Every request-body capture path goes through here, so record and replay redact\n * identically and any hash derived from the body still agrees on both sides. Response\n * bodies deliberately use {@link readResponseBodyWithCap} — they are served back to the\n * app during replay and must stay byte-exact.\n */\nexport const readRequestBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n const captured = await readBodyWithCap(stream);\n if (captured === undefined) {\n return undefined;\n }\n return { ...captured, body: redactRequestBody(captured.body) };\n};\n\n/**\n * Reads a *response* body so that replay can serve back exactly the bytes the app received: as\n * text when they are UTF-8, and as base64 otherwise (see `encodeBodyBytes`).\n */\nexport const readResponseBodyWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<CapturedBody | undefined> => {\n const read = await readBodyBytesWithCap(stream);\n if (read === undefined) {\n return undefined;\n }\n const { body, encoding, storedFraction } = encodeBodyBytes(read.bytes, {\n maxLength: MAX_BODY_CAPTURE_SIZE,\n incompleteTail: read.truncated,\n });\n return {\n body,\n truncated: read.truncated || storedFraction < 1,\n ...(encoding !== undefined ? { encoding } : {}),\n };\n};\n\nconst readBodyBytesWithCap = async (\n stream: ReadableStream<Uint8Array> | null,\n): Promise<{ bytes: Uint8Array; truncated: boolean } | undefined> => {\n if (!stream) {\n return undefined;\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let bytes = 0;\n let truncated = false;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), BODY_READ_TIMEOUT_MS);\n });\n\n try {\n while (true) {\n const result = await Promise.race([reader.read(), timedOut]);\n if (result === TIMED_OUT) {\n truncated = true;\n break;\n }\n const { done, value } = result;\n if (done) {\n break;\n }\n if (!value || value.byteLength === 0) {\n continue;\n }\n const remaining = MAX_BODY_CAPTURE_SIZE - bytes;\n if (value.byteLength >= remaining) {\n chunks.push(value.subarray(0, remaining));\n bytes += remaining;\n truncated = truncated || value.byteLength > remaining;\n // Even an exact-cap chunk means we stop reading, so a longer stream\n // counts as truncated.\n const next = await Promise.race([reader.read(), timedOut]);\n if (next === TIMED_OUT || !next.done) {\n truncated = true;\n }\n break;\n }\n chunks.push(value);\n bytes += value.byteLength;\n }\n } finally {\n clearTimeout(timer);\n reader.cancel().catch(() => {});\n }\n\n return { bytes: concatBytes(chunks, bytes), truncated };\n};\n\nconst concatBytes = (chunks: Uint8Array[], byteLength: number): Uint8Array => {\n if (chunks.length === 1) {\n return chunks[0];\n }\n const out = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n};\n"],"mappings":";;;;;;;;;;AAWA,MAAa,wBAAwB,OAAO;;AAG5C,MAAM,uBAAuB;AAE7B,MAAM,YAAY,OAAO,8BAA8B;;;;;;;;;;;AAYvD,MAAa,kBAAkB,OAC7B,WACsC;CACtC,MAAM,OAAO,MAAM,qBAAqB,OAAO;AAC/C,KAAI,SAAS,KAAA,EACX;AAEF,QAAO;EACL,MAAM,IAAI,YAAY,QAAQ,CAAC,OAAO,KAAK,MAAM;EACjD,WAAW,KAAK;EACjB;;;;;;;;;;AAWH,MAAa,yBAAyB,OACpC,WACsC;CACtC,MAAM,WAAW,MAAM,gBAAgB,OAAO;AAC9C,KAAI,aAAa,KAAA,EACf;AAEF,QAAO;EAAE,GAAG;EAAU,MAAM,kBAAkB,SAAS,KAAK;EAAE;;;;;;AAOhE,MAAa,0BAA0B,OACrC,WACsC;CACtC,MAAM,OAAO,MAAM,qBAAqB,OAAO;AAC/C,KAAI,SAAS,KAAA,EACX;CAEF,MAAM,EAAE,MAAM,UAAU,mBAAmB,gBAAgB,KAAK,OAAO;EACrE,WAAW;EACX,gBAAgB,KAAK;EACtB,CAAC;AACF,QAAO;EACL;EACA,WAAW,KAAK,aAAa,iBAAiB;EAC9C,GAAI,aAAa,KAAA,IAAY,EAAE,UAAU,GAAG,EAAE;EAC/C;;AAGH,MAAM,uBAAuB,OAC3B,WACmE;AACnE,KAAI,CAAC,OACH;CAGF,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,SAAuB,EAAE;CAC/B,IAAI,QAAQ;CACZ,IAAI,YAAY;CAEhB,IAAI;CACJ,MAAM,WAAW,IAAI,SAA2B,YAAY;AAC1D,UAAQ,iBAAiB,QAAQ,UAAU,EAAE,qBAAqB;GAClE;AAEF,KAAI;AACF,SAAO,MAAM;GACX,MAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC5D,OAAI,WAAW,WAAW;AACxB,gBAAY;AACZ;;GAEF,MAAM,EAAE,MAAM,UAAU;AACxB,OAAI,KACF;AAEF,OAAI,CAAC,SAAS,MAAM,eAAe,EACjC;GAEF,MAAM,YAAY,wBAAwB;AAC1C,OAAI,MAAM,cAAc,WAAW;AACjC,WAAO,KAAK,MAAM,SAAS,GAAG,UAAU,CAAC;AACzC,aAAS;AACT,gBAAY,aAAa,MAAM,aAAa;IAG5C,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;AAC1D,QAAI,SAAS,aAAa,CAAC,KAAK,KAC9B,aAAY;AAEd;;AAEF,UAAO,KAAK,MAAM;AAClB,YAAS,MAAM;;WAET;AACR,eAAa,MAAM;AACnB,SAAO,QAAQ,CAAC,YAAY,GAAG;;AAGjC,QAAO;EAAE,OAAO,YAAY,QAAQ,MAAM;EAAE;EAAW;;AAGzD,MAAM,eAAe,QAAsB,eAAmC;AAC5E,KAAI,OAAO,WAAW,EACpB,QAAO,OAAO;CAEhB,MAAM,MAAM,IAAI,WAAW,WAAW;CACtC,IAAI,SAAS;AACb,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,IAAI,OAAO,OAAO;AACtB,YAAU,MAAM;;AAElB,QAAO"}
@@ -0,0 +1,49 @@
1
+ //#region src/body-encoding.d.ts
2
+ /**
3
+ * How a captured response body is stored on its span, so that replay can serve back exactly the
4
+ * bytes the app received.
5
+ *
6
+ * A body whose bytes are valid UTF-8 — JSON, HTML, text — is stored as the text they decode to, with
7
+ * no encoding attribute, exactly as every body was before this attribute existed. Any other body is
8
+ * stored as base64 under {@link HTTP_RESPONSE_BODY_ENCODING_ATTR}. That covers a body that is binary
9
+ * by type (a gzip or zip archive, an image, protobuf) or text in another charset. It arrives with no
10
+ * `content-encoding`, so decompressing first does not help, and decoding it as UTF-8 would turn every
11
+ * invalid byte into U+FFFD. That cannot be undone: replay would serve a body the app cannot parse.
12
+ *
13
+ * Only response bodies are stored this way. A request body is never served, only hashed into a
14
+ * match key, and replay hashes the live request through the same lossy decode the recording used,
15
+ * so the two still agree.
16
+ */
17
+ declare const HTTP_RESPONSE_BODY_ENCODING_ATTR = "http.response.body.encoding";
18
+ declare const BODY_ENCODING_BASE64 = "base64";
19
+ type BodyEncoding = typeof BODY_ENCODING_BASE64;
20
+ interface EncodedBody {
21
+ /** What the span stores: the body's text, or base64 of its bytes when `encoding` is set. */
22
+ body: string;
23
+ encoding?: BodyEncoding;
24
+ /** Share of the input that `body` represents: 1 unless the length cap cut it. */
25
+ storedFraction: number;
26
+ }
27
+ /**
28
+ * Encodes captured body bytes for storage, capped so the stored string is at most `maxLength`
29
+ * characters — the same bound whichever form the body takes.
30
+ *
31
+ * `incompleteTail` says the bytes are a prefix of a longer body, so a final UTF-8 sequence cut
32
+ * short is expected rather than a sign that the body is not text.
33
+ */
34
+ declare const encodeBodyBytes: (bytes: Uint8Array, {
35
+ maxLength,
36
+ incompleteTail
37
+ }: {
38
+ maxLength: number;
39
+ incompleteTail: boolean;
40
+ }) => EncodedBody;
41
+ /**
42
+ * The bytes a stored body stands for. A body with no encoding is text, so its bytes are its UTF-8
43
+ * encoding — which reproduces the recorded bytes exactly, since only valid UTF-8 is ever stored as
44
+ * text.
45
+ */
46
+ declare const decodeBodyBytes: (body: string, encoding: unknown) => Uint8Array;
47
+ //#endregion
48
+ export { BODY_ENCODING_BASE64, BodyEncoding, EncodedBody, HTTP_RESPONSE_BODY_ENCODING_ATTR, decodeBodyBytes, encodeBodyBytes };
49
+ //# sourceMappingURL=body-encoding.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { fromBase64, toBase64 } from "./base64.js";
2
+ //#region src/body-encoding.ts
3
+ /**
4
+ * How a captured response body is stored on its span, so that replay can serve back exactly the
5
+ * bytes the app received.
6
+ *
7
+ * A body whose bytes are valid UTF-8 — JSON, HTML, text — is stored as the text they decode to, with
8
+ * no encoding attribute, exactly as every body was before this attribute existed. Any other body is
9
+ * stored as base64 under {@link HTTP_RESPONSE_BODY_ENCODING_ATTR}. That covers a body that is binary
10
+ * by type (a gzip or zip archive, an image, protobuf) or text in another charset. It arrives with no
11
+ * `content-encoding`, so decompressing first does not help, and decoding it as UTF-8 would turn every
12
+ * invalid byte into U+FFFD. That cannot be undone: replay would serve a body the app cannot parse.
13
+ *
14
+ * Only response bodies are stored this way. A request body is never served, only hashed into a
15
+ * match key, and replay hashes the live request through the same lossy decode the recording used,
16
+ * so the two still agree.
17
+ */
18
+ const HTTP_RESPONSE_BODY_ENCODING_ATTR = "http.response.body.encoding";
19
+ const BODY_ENCODING_BASE64 = "base64";
20
+ /**
21
+ * Encodes captured body bytes for storage, capped so the stored string is at most `maxLength`
22
+ * characters — the same bound whichever form the body takes.
23
+ *
24
+ * `incompleteTail` says the bytes are a prefix of a longer body, so a final UTF-8 sequence cut
25
+ * short is expected rather than a sign that the body is not text.
26
+ */
27
+ const encodeBodyBytes = (bytes, { maxLength, incompleteTail }) => {
28
+ const text = decodeUtf8Exactly(bytes, incompleteTail);
29
+ if (text !== void 0) {
30
+ const body = text.length > maxLength ? text.substring(0, maxLength) : text;
31
+ return {
32
+ body,
33
+ storedFraction: text.length > 0 ? body.length / text.length : 1
34
+ };
35
+ }
36
+ const maxBytes = Math.floor(maxLength / 4) * 3;
37
+ const kept = bytes.byteLength > maxBytes ? bytes.subarray(0, maxBytes) : bytes;
38
+ return {
39
+ body: toBase64(kept),
40
+ encoding: BODY_ENCODING_BASE64,
41
+ storedFraction: bytes.byteLength > 0 ? kept.byteLength / bytes.byteLength : 1
42
+ };
43
+ };
44
+ /**
45
+ * The bytes a stored body stands for. A body with no encoding is text, so its bytes are its UTF-8
46
+ * encoding — which reproduces the recorded bytes exactly, since only valid UTF-8 is ever stored as
47
+ * text.
48
+ */
49
+ const decodeBodyBytes = (body, encoding) => encoding === "base64" ? fromBase64(body) : new TextEncoder().encode(body);
50
+ /**
51
+ * The text the bytes encode, or undefined when they are not UTF-8. `fatal` makes an invalid
52
+ * sequence throw instead of becoming U+FFFD, and `ignoreBOM` keeps a leading byte-order mark in
53
+ * the text, so the text re-encodes to exactly these bytes.
54
+ */
55
+ const decodeUtf8Exactly = (bytes, incompleteTail) => {
56
+ try {
57
+ return new TextDecoder("utf-8", {
58
+ fatal: true,
59
+ ignoreBOM: true
60
+ }).decode(bytes, { stream: incompleteTail });
61
+ } catch {
62
+ return;
63
+ }
64
+ };
65
+ //#endregion
66
+ export { BODY_ENCODING_BASE64, HTTP_RESPONSE_BODY_ENCODING_ATTR, decodeBodyBytes, encodeBodyBytes };
67
+
68
+ //# sourceMappingURL=body-encoding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-encoding.js","names":[],"sources":["../src/body-encoding.ts"],"sourcesContent":["import { fromBase64, toBase64 } from \"./base64\";\n\n/**\n * How a captured response body is stored on its span, so that replay can serve back exactly the\n * bytes the app received.\n *\n * A body whose bytes are valid UTF-8 — JSON, HTML, text — is stored as the text they decode to, with\n * no encoding attribute, exactly as every body was before this attribute existed. Any other body is\n * stored as base64 under {@link HTTP_RESPONSE_BODY_ENCODING_ATTR}. That covers a body that is binary\n * by type (a gzip or zip archive, an image, protobuf) or text in another charset. It arrives with no\n * `content-encoding`, so decompressing first does not help, and decoding it as UTF-8 would turn every\n * invalid byte into U+FFFD. That cannot be undone: replay would serve a body the app cannot parse.\n *\n * Only response bodies are stored this way. A request body is never served, only hashed into a\n * match key, and replay hashes the live request through the same lossy decode the recording used,\n * so the two still agree.\n */\nexport const HTTP_RESPONSE_BODY_ENCODING_ATTR = \"http.response.body.encoding\";\n\nexport const BODY_ENCODING_BASE64 = \"base64\";\n\nexport type BodyEncoding = typeof BODY_ENCODING_BASE64;\n\nexport interface EncodedBody {\n /** What the span stores: the body's text, or base64 of its bytes when `encoding` is set. */\n body: string;\n encoding?: BodyEncoding;\n /** Share of the input that `body` represents: 1 unless the length cap cut it. */\n storedFraction: number;\n}\n\n/**\n * Encodes captured body bytes for storage, capped so the stored string is at most `maxLength`\n * characters — the same bound whichever form the body takes.\n *\n * `incompleteTail` says the bytes are a prefix of a longer body, so a final UTF-8 sequence cut\n * short is expected rather than a sign that the body is not text.\n */\nexport const encodeBodyBytes = (\n bytes: Uint8Array,\n { maxLength, incompleteTail }: { maxLength: number; incompleteTail: boolean },\n): EncodedBody => {\n const text = decodeUtf8Exactly(bytes, incompleteTail);\n if (text !== undefined) {\n const body = text.length > maxLength ? text.substring(0, maxLength) : text;\n return {\n body,\n storedFraction: text.length > 0 ? body.length / text.length : 1,\n };\n }\n // Base64 spends four characters on every three bytes.\n const maxBytes = Math.floor(maxLength / 4) * 3;\n const kept =\n bytes.byteLength > maxBytes ? bytes.subarray(0, maxBytes) : bytes;\n return {\n body: toBase64(kept),\n encoding: BODY_ENCODING_BASE64,\n storedFraction:\n bytes.byteLength > 0 ? kept.byteLength / bytes.byteLength : 1,\n };\n};\n\n/**\n * The bytes a stored body stands for. A body with no encoding is text, so its bytes are its UTF-8\n * encoding — which reproduces the recorded bytes exactly, since only valid UTF-8 is ever stored as\n * text.\n */\nexport const decodeBodyBytes = (body: string, encoding: unknown): Uint8Array =>\n encoding === BODY_ENCODING_BASE64\n ? fromBase64(body)\n : new TextEncoder().encode(body);\n\n/**\n * The text the bytes encode, or undefined when they are not UTF-8. `fatal` makes an invalid\n * sequence throw instead of becoming U+FFFD, and `ignoreBOM` keeps a leading byte-order mark in\n * the text, so the text re-encodes to exactly these bytes.\n */\nconst decodeUtf8Exactly = (\n bytes: Uint8Array,\n incompleteTail: boolean,\n): string | undefined => {\n try {\n return new TextDecoder(\"utf-8\", { fatal: true, ignoreBOM: true }).decode(\n bytes,\n // A streaming decode holds back a final sequence cut short instead of rejecting it.\n { stream: incompleteTail },\n );\n } catch {\n return undefined;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAa,mCAAmC;AAEhD,MAAa,uBAAuB;;;;;;;;AAmBpC,MAAa,mBACX,OACA,EAAE,WAAW,qBACG;CAChB,MAAM,OAAO,kBAAkB,OAAO,eAAe;AACrD,KAAI,SAAS,KAAA,GAAW;EACtB,MAAM,OAAO,KAAK,SAAS,YAAY,KAAK,UAAU,GAAG,UAAU,GAAG;AACtE,SAAO;GACL;GACA,gBAAgB,KAAK,SAAS,IAAI,KAAK,SAAS,KAAK,SAAS;GAC/D;;CAGH,MAAM,WAAW,KAAK,MAAM,YAAY,EAAE,GAAG;CAC7C,MAAM,OACJ,MAAM,aAAa,WAAW,MAAM,SAAS,GAAG,SAAS,GAAG;AAC9D,QAAO;EACL,MAAM,SAAS,KAAK;EACpB,UAAU;EACV,gBACE,MAAM,aAAa,IAAI,KAAK,aAAa,MAAM,aAAa;EAC/D;;;;;;;AAQH,MAAa,mBAAmB,MAAc,aAC5C,aAAA,WACI,WAAW,KAAK,GAChB,IAAI,aAAa,CAAC,OAAO,KAAK;;;;;;AAOpC,MAAM,qBACJ,OACA,mBACuB;AACvB,KAAI;AACF,SAAO,IAAI,YAAY,SAAS;GAAE,OAAO;GAAM,WAAW;GAAM,CAAC,CAAC,OAChE,OAEA,EAAE,QAAQ,gBAAgB,CAC3B;SACK;AACN"}
@@ -1,3 +1,4 @@
1
+ import { fromBase64, toBase64 } from "./base64.js";
1
2
  //#region src/captured-result-codec.ts
2
3
  /**
3
4
  * JSON codec for the results a database client hands back, used by the Prisma, pg and
@@ -138,21 +139,6 @@ const reviveLegacyTimestamps = (value) => {
138
139
  return record;
139
140
  };
140
141
  const asLegacyTimestamp = (value) => ISO_TIMESTAMP.test(value) ? decodeDate(value) : void 0;
141
- const BASE64_CHUNK_SIZE = 32768;
142
- const toBase64 = (view) => {
143
- const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
144
- if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
145
- let binary = "";
146
- for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));
147
- return btoa(binary);
148
- };
149
- const fromBase64 = (base64) => {
150
- if (typeof Buffer !== "undefined") return Buffer.from(base64, "base64");
151
- const binary = atob(base64);
152
- const bytes = new Uint8Array(binary.length);
153
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
154
- return bytes;
155
- };
156
142
  //#endregion
157
143
  export { deserializeCapturedResult, serializeCapturedResult };
158
144
 
@@ -1 +1 @@
1
- {"version":3,"file":"captured-result-codec.js","names":[],"sources":["../src/captured-result-codec.ts"],"sourcesContent":["/**\n * JSON codec for the results a database client hands back, used by the Prisma, pg and\n * postgres.js capture paths on BOTH sides so a replayed result carries the same JS types\n * the driver produced at record time.\n *\n * Plain `JSON.stringify` loses those types silently. Every one of these clients returns a\n * `Date` for a timestamp column; it becomes an ISO string in the recording, and the app's\n * `row.createdAt.toISOString()` then throws during replay — while the mock is reported as a\n * clean hit, because the store did find the right recording. A `BigInt` is worse: stringify\n * throws, the record side catches, and the operation is never captured at all.\n *\n * So a value JSON cannot represent is replaced by a tagged wrapper\n * (`{ __meticulousType, value }`) and rebuilt on the way out, and the payload is wrapped once\n * more in an envelope carrying the encoding version. That envelope is also what marks a\n * recording as one this codec wrote: a recording made before it has none, and its timestamps\n * are recovered by shape instead — see {@link reviveLegacyTimestamps}.\n *\n * Lives here rather than in `backend-recorder-js` because a deployed Cloudflare Worker records\n * postgres.js and loses the same types, and a Worker cannot load that package — so the dependency\n * runs this way round, as it already does for `serializeCapturedError` and `serializeKvCaptureFields`.\n * Nothing loads this package at runtime on the Node path: it is bundled into the recorder the\n * launcher fetches.\n *\n * Still unhandled: a `Prisma.Decimal` (`decimal.js`) replays as the string its `toJSON` produced,\n * because rebuilding one needs the class and replay has no `@prisma/client` to reach it through.\n */\n\nconst ENCODING_VERSION = 1;\nconst ENVELOPE_VERSION_KEY = \"__meticulousEncoding\";\nconst TYPE_TAG_KEY = \"__meticulousType\";\n// Both the envelope and a tag carry their payload under this key.\nconst VALUE_KEY = \"value\";\n\ntype TypeTag = \"Date\" | \"BigInt\" | \"Binary\" | \"NonFiniteNumber\";\n\ninterface TaggedValue {\n [TYPE_TAG_KEY]: TypeTag;\n [VALUE_KEY]: string | null;\n}\n\n/**\n * Serializes a captured result to a JSON string, preserving the types JSON has no\n * representation for. Throws on a cyclic value, exactly as `JSON.stringify` does — the record\n * side catches that and captures nothing.\n */\nexport const serializeCapturedResult = (result: unknown): string =>\n JSON.stringify(\n { [ENVELOPE_VERSION_KEY]: ENCODING_VERSION, [VALUE_KEY]: result },\n tagUnrepresentableValues,\n );\n\n/**\n * Reconstructs a captured result, rebuilding the tagged values as the types they were\n * recorded from. A payload without the version envelope was written before this codec\n * existed, and falls back to recovering timestamps by shape.\n */\nexport const deserializeCapturedResult = (json: string): unknown => {\n const parsed: unknown = JSON.parse(json, rebuildTaggedValue);\n if (isEncodedEnvelope(parsed)) {\n return parsed[VALUE_KEY];\n }\n return reviveLegacyTimestamps(parsed);\n};\n\n/**\n * `JSON.stringify` replacer. It has to be a `function` rather than an arrow: `value` has\n * already been through the holder's `toJSON()` by the time a replacer sees it (a `Date`\n * arrives as a string, a `Buffer` as `{ type, data }`), so the only way to see what the\n * driver actually returned is `this[key]` on the bound holder.\n */\nfunction tagUnrepresentableValues(this: unknown, key: string, value: unknown) {\n const raw = (this as Record<string, unknown>)[key];\n if (typeof raw === \"bigint\") {\n return tagged(\"BigInt\", raw.toString());\n }\n if (typeof raw === \"number\" && !Number.isFinite(raw)) {\n return tagged(\"NonFiniteNumber\", String(raw));\n }\n if (raw instanceof Date) {\n return tagged(\n \"Date\",\n Number.isNaN(raw.getTime()) ? null : raw.toISOString(),\n );\n }\n // A DataView is left alone: no driver returns one, and it would come back a Uint8Array.\n if (ArrayBuffer.isView(raw) && !(raw instanceof DataView)) {\n return tagged(\"Binary\", toBase64(raw));\n }\n if (raw instanceof ArrayBuffer) {\n return tagged(\"Binary\", toBase64(new Uint8Array(raw)));\n }\n return value;\n}\n\nconst tagged = (type: TypeTag, value: string | null): TaggedValue => ({\n [TYPE_TAG_KEY]: type,\n [VALUE_KEY]: value,\n});\n\n/**\n * `JSON.parse` reviver. A tag is only honoured when its payload is one this codec could have\n * written, so an app value that happens to be shaped like a tag — and a tag type a later\n * recorder introduced — is passed through untouched rather than decoded into something else.\n * (The shim has no runtime dependencies, so `assertNever` is not available for the switch.)\n */\nconst rebuildTaggedValue = (_key: string, value: unknown): unknown => {\n if (!isTaggedValue(value)) {\n return value;\n }\n const encoded = value[VALUE_KEY];\n switch (value[TYPE_TAG_KEY]) {\n case \"Date\":\n return decodeDate(encoded) ?? value;\n case \"BigInt\":\n return typeof encoded === \"string\" && INTEGER.test(encoded)\n ? BigInt(encoded)\n : value;\n case \"NonFiniteNumber\":\n return typeof encoded === \"string\" && NON_FINITE_NUMBERS.has(encoded)\n ? Number(encoded)\n : value;\n case \"Binary\":\n return typeof encoded === \"string\" && BASE64.test(encoded)\n ? fromBase64(encoded)\n : value;\n }\n};\n\nconst INTEGER = /^-?\\d+$/;\nconst BASE64 =\n /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\nconst NON_FINITE_NUMBERS: ReadonlySet<string> = new Set([\n \"NaN\",\n \"Infinity\",\n \"-Infinity\",\n]);\n\n// `null` is how an invalid Date was recorded; anything else has to be the exact string\n// `toISOString` would have produced for it.\nconst decodeDate = (encoded: string | null): Date | undefined => {\n if (encoded == null) {\n return new Date(NaN);\n }\n const date = new Date(encoded);\n if (Number.isNaN(date.getTime()) || date.toISOString() !== encoded) {\n return undefined;\n }\n return date;\n};\n\nconst TYPE_TAGS: ReadonlySet<string> = new Set<TypeTag>([\n \"Date\",\n \"BigInt\",\n \"Binary\",\n \"NonFiniteNumber\",\n]);\n\nconst isTaggedValue = (value: unknown): value is TaggedValue => {\n if (value == null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const keys = Object.keys(value);\n if (keys.length !== 2 || !keys.includes(VALUE_KEY)) {\n return false;\n }\n const tag = (value as Record<string, unknown>)[TYPE_TAG_KEY];\n const encoded = (value as Record<string, unknown>)[VALUE_KEY];\n return (\n typeof tag === \"string\" &&\n TYPE_TAGS.has(tag) &&\n (typeof encoded === \"string\" || encoded === null)\n );\n};\n\nconst isEncodedEnvelope = (\n value: unknown,\n): value is Record<string, unknown> => {\n if (value == null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n return (\n (value as Record<string, unknown>)[ENVELOPE_VERSION_KEY] ===\n ENCODING_VERSION\n );\n};\n\n/**\n * The compatibility path for every recording made before this codec: the driver's `Date`s\n * are already flattened to strings there, and nothing in the payload says which strings they\n * were. Recovering them by shape is a guess, but a tightly bounded one — only the exact\n * 24-character form `Date.prototype.toJSON` emits, and only when it round-trips back to\n * itself, so a string column can collide with it solely by holding a real UTC millisecond\n * timestamp. Left as a guess rather than declined outright because the alternative is every\n * date-formatting call in the app throwing.\n *\n * Mutates in place: the value has just come out of `JSON.parse` and is owned by the caller.\n */\nconst ISO_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n\nconst reviveLegacyTimestamps = (value: unknown): unknown => {\n if (typeof value === \"string\") {\n return asLegacyTimestamp(value) ?? value;\n }\n if (value == null || typeof value !== \"object\") {\n return value;\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n value[i] = reviveLegacyTimestamps(value[i]);\n }\n return value;\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n record[key] = reviveLegacyTimestamps(record[key]);\n }\n return record;\n};\n\nconst asLegacyTimestamp = (value: string): Date | undefined =>\n ISO_TIMESTAMP.test(value) ? decodeDate(value) : undefined;\n\nconst BASE64_CHUNK_SIZE = 0x8000;\n\nconst toBase64 = (view: ArrayBufferView): string => {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(bytes).toString(\"base64\");\n }\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += BASE64_CHUNK_SIZE) {\n binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_SIZE));\n }\n return btoa(binary);\n};\n\n// Replay only ever runs on Node, so a `Buffer` is both what pg returned at record time and a\n// `Uint8Array` as far as a Prisma `Bytes` consumer is concerned.\nconst fromBase64 = (base64: string): Uint8Array => {\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(base64, \"base64\");\n }\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"captured-result-codec.js","names":[],"sources":["../src/captured-result-codec.ts"],"sourcesContent":["/**\n * JSON codec for the results a database client hands back, used by the Prisma, pg and\n * postgres.js capture paths on BOTH sides so a replayed result carries the same JS types\n * the driver produced at record time.\n *\n * Plain `JSON.stringify` loses those types silently. Every one of these clients returns a\n * `Date` for a timestamp column; it becomes an ISO string in the recording, and the app's\n * `row.createdAt.toISOString()` then throws during replay — while the mock is reported as a\n * clean hit, because the store did find the right recording. A `BigInt` is worse: stringify\n * throws, the record side catches, and the operation is never captured at all.\n *\n * So a value JSON cannot represent is replaced by a tagged wrapper\n * (`{ __meticulousType, value }`) and rebuilt on the way out, and the payload is wrapped once\n * more in an envelope carrying the encoding version. That envelope is also what marks a\n * recording as one this codec wrote: a recording made before it has none, and its timestamps\n * are recovered by shape instead — see {@link reviveLegacyTimestamps}.\n *\n * Lives here rather than in `backend-recorder-js` because a deployed Cloudflare Worker records\n * postgres.js and loses the same types, and a Worker cannot load that package — so the dependency\n * runs this way round, as it already does for `serializeCapturedError` and `serializeKvCaptureFields`.\n * Nothing loads this package at runtime on the Node path: it is bundled into the recorder the\n * launcher fetches.\n *\n * Still unhandled: a `Prisma.Decimal` (`decimal.js`) replays as the string its `toJSON` produced,\n * because rebuilding one needs the class and replay has no `@prisma/client` to reach it through.\n */\n\nimport { fromBase64, toBase64 } from \"./base64\";\n\nconst ENCODING_VERSION = 1;\nconst ENVELOPE_VERSION_KEY = \"__meticulousEncoding\";\nconst TYPE_TAG_KEY = \"__meticulousType\";\n// Both the envelope and a tag carry their payload under this key.\nconst VALUE_KEY = \"value\";\n\ntype TypeTag = \"Date\" | \"BigInt\" | \"Binary\" | \"NonFiniteNumber\";\n\ninterface TaggedValue {\n [TYPE_TAG_KEY]: TypeTag;\n [VALUE_KEY]: string | null;\n}\n\n/**\n * Serializes a captured result to a JSON string, preserving the types JSON has no\n * representation for. Throws on a cyclic value, exactly as `JSON.stringify` does — the record\n * side catches that and captures nothing.\n */\nexport const serializeCapturedResult = (result: unknown): string =>\n JSON.stringify(\n { [ENVELOPE_VERSION_KEY]: ENCODING_VERSION, [VALUE_KEY]: result },\n tagUnrepresentableValues,\n );\n\n/**\n * Reconstructs a captured result, rebuilding the tagged values as the types they were\n * recorded from. A payload without the version envelope was written before this codec\n * existed, and falls back to recovering timestamps by shape.\n */\nexport const deserializeCapturedResult = (json: string): unknown => {\n const parsed: unknown = JSON.parse(json, rebuildTaggedValue);\n if (isEncodedEnvelope(parsed)) {\n return parsed[VALUE_KEY];\n }\n return reviveLegacyTimestamps(parsed);\n};\n\n/**\n * `JSON.stringify` replacer. It has to be a `function` rather than an arrow: `value` has\n * already been through the holder's `toJSON()` by the time a replacer sees it (a `Date`\n * arrives as a string, a `Buffer` as `{ type, data }`), so the only way to see what the\n * driver actually returned is `this[key]` on the bound holder.\n */\nfunction tagUnrepresentableValues(this: unknown, key: string, value: unknown) {\n const raw = (this as Record<string, unknown>)[key];\n if (typeof raw === \"bigint\") {\n return tagged(\"BigInt\", raw.toString());\n }\n if (typeof raw === \"number\" && !Number.isFinite(raw)) {\n return tagged(\"NonFiniteNumber\", String(raw));\n }\n if (raw instanceof Date) {\n return tagged(\n \"Date\",\n Number.isNaN(raw.getTime()) ? null : raw.toISOString(),\n );\n }\n // A DataView is left alone: no driver returns one, and it would come back a Uint8Array.\n if (ArrayBuffer.isView(raw) && !(raw instanceof DataView)) {\n return tagged(\"Binary\", toBase64(raw));\n }\n if (raw instanceof ArrayBuffer) {\n return tagged(\"Binary\", toBase64(new Uint8Array(raw)));\n }\n return value;\n}\n\nconst tagged = (type: TypeTag, value: string | null): TaggedValue => ({\n [TYPE_TAG_KEY]: type,\n [VALUE_KEY]: value,\n});\n\n/**\n * `JSON.parse` reviver. A tag is only honoured when its payload is one this codec could have\n * written, so an app value that happens to be shaped like a tag — and a tag type a later\n * recorder introduced — is passed through untouched rather than decoded into something else.\n * (The shim has no runtime dependencies, so `assertNever` is not available for the switch.)\n */\nconst rebuildTaggedValue = (_key: string, value: unknown): unknown => {\n if (!isTaggedValue(value)) {\n return value;\n }\n const encoded = value[VALUE_KEY];\n switch (value[TYPE_TAG_KEY]) {\n case \"Date\":\n return decodeDate(encoded) ?? value;\n case \"BigInt\":\n return typeof encoded === \"string\" && INTEGER.test(encoded)\n ? BigInt(encoded)\n : value;\n case \"NonFiniteNumber\":\n return typeof encoded === \"string\" && NON_FINITE_NUMBERS.has(encoded)\n ? Number(encoded)\n : value;\n case \"Binary\":\n return typeof encoded === \"string\" && BASE64.test(encoded)\n ? fromBase64(encoded)\n : value;\n }\n};\n\nconst INTEGER = /^-?\\d+$/;\nconst BASE64 =\n /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\nconst NON_FINITE_NUMBERS: ReadonlySet<string> = new Set([\n \"NaN\",\n \"Infinity\",\n \"-Infinity\",\n]);\n\n// `null` is how an invalid Date was recorded; anything else has to be the exact string\n// `toISOString` would have produced for it.\nconst decodeDate = (encoded: string | null): Date | undefined => {\n if (encoded == null) {\n return new Date(NaN);\n }\n const date = new Date(encoded);\n if (Number.isNaN(date.getTime()) || date.toISOString() !== encoded) {\n return undefined;\n }\n return date;\n};\n\nconst TYPE_TAGS: ReadonlySet<string> = new Set<TypeTag>([\n \"Date\",\n \"BigInt\",\n \"Binary\",\n \"NonFiniteNumber\",\n]);\n\nconst isTaggedValue = (value: unknown): value is TaggedValue => {\n if (value == null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const keys = Object.keys(value);\n if (keys.length !== 2 || !keys.includes(VALUE_KEY)) {\n return false;\n }\n const tag = (value as Record<string, unknown>)[TYPE_TAG_KEY];\n const encoded = (value as Record<string, unknown>)[VALUE_KEY];\n return (\n typeof tag === \"string\" &&\n TYPE_TAGS.has(tag) &&\n (typeof encoded === \"string\" || encoded === null)\n );\n};\n\nconst isEncodedEnvelope = (\n value: unknown,\n): value is Record<string, unknown> => {\n if (value == null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n return (\n (value as Record<string, unknown>)[ENVELOPE_VERSION_KEY] ===\n ENCODING_VERSION\n );\n};\n\n/**\n * The compatibility path for every recording made before this codec: the driver's `Date`s\n * are already flattened to strings there, and nothing in the payload says which strings they\n * were. Recovering them by shape is a guess, but a tightly bounded one — only the exact\n * 24-character form `Date.prototype.toJSON` emits, and only when it round-trips back to\n * itself, so a string column can collide with it solely by holding a real UTC millisecond\n * timestamp. Left as a guess rather than declined outright because the alternative is every\n * date-formatting call in the app throwing.\n *\n * Mutates in place: the value has just come out of `JSON.parse` and is owned by the caller.\n */\nconst ISO_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n\nconst reviveLegacyTimestamps = (value: unknown): unknown => {\n if (typeof value === \"string\") {\n return asLegacyTimestamp(value) ?? value;\n }\n if (value == null || typeof value !== \"object\") {\n return value;\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n value[i] = reviveLegacyTimestamps(value[i]);\n }\n return value;\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n record[key] = reviveLegacyTimestamps(record[key]);\n }\n return record;\n};\n\nconst asLegacyTimestamp = (value: string): Date | undefined =>\n ISO_TIMESTAMP.test(value) ? decodeDate(value) : undefined;\n"],"mappings":"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"}
package/dist/context.js CHANGED
@@ -3,7 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  //#region src/context.ts
4
4
  const requestCaptureContext = new AsyncLocalStorage();
5
5
  /** The origin the shim's own sidecar traffic goes to, for the self-capture guards. */
6
- const sidecarOriginOf = (ctx) => ctx.mode === "replay" ? ctx.sidecarUrl : transportOrigin(ctx.transport);
6
+ const sidecarOriginOf = (ctx) => ctx.mode === "record" ? transportOrigin(ctx.transport) : ctx.sidecarUrl;
7
7
  //#endregion
8
8
  export { requestCaptureContext, sidecarOriginOf };
9
9
 
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { CaptureBuffer } from \"./capture-buffer\";\nimport { type SidecarTransport, transportOrigin } from \"./sidecar-transport\";\n\n/**\n * Per-inbound-request context, entered by the withMeticulous wrapper and read by the fetch\n * patch. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag in the worker's\n * wrangler configuration.\n */\ninterface BaseRequestContext {\n requestId: string;\n /** The inbound request's ctx.waitUntil, for post-response capture work. */\n waitUntil: (promise: Promise<unknown>) => void;\n /**\n * Per-request line-coverage sink, indexed by the global line id the build\n * plugin assigned. Present only when the bundle is instrumented and the\n * request is being replayed.\n *\n * Lives here rather than in a module-scope global because workerd interleaves\n * concurrent requests in one isolate — a shared sink would merge sessions'\n * coverage together.\n */\n coverage?: Uint8Array;\n}\n\n/** Recording: capture events are reported to the sidecar, which stays out of the request path. */\nexport interface RequestCaptureContext extends BaseRequestContext {\n mode: \"record\";\n frontendSessionId: string | undefined;\n /**\n * Set only when we minted {@link frontendSessionId} ourselves for a document navigation the\n * browser could not tag, rather than reading it off the request. Recorded on the inbound\n * event so ingestion can tell an id the page may never have adopted from one it sent us.\n * See provisional-session-id.ts.\n */\n sessionIdOrigin?: \"backend\";\n transport: SidecarTransport;\n /** Batches this request's events into one report. */\n buffer: CaptureBuffer;\n /**\n * Trace this request's spans belong to, and the id of its SERVER span. Minted here rather than\n * by the sidecar so span assembly needs no cross-batch memory — see `CorrelatedEvent`.\n */\n traceId: string;\n serverSpanId: string;\n}\n\n/**\n * Replaying: outbound calls are looked up against the sidecar's mock store before they leave the\n * worker.\n *\n * Both ids are required here, unlike recording. Mocks are indexed by session and their\n * consume-once state is isolated by replay, so omitting either would make deterministic\n * lookup impossible. Making both non-optional keeps that state unrepresentable.\n *\n * Replay is URL-only, deliberately: the sidecar URL arrives on a request header the replay runner\n * injects, validated down to a loopback / docker-gateway / private-network origin. There is no\n * binding transport here because a replay sidecar is a Node process holding the mock store, never\n * a Worker.\n */\nexport interface RequestReplayContext extends BaseRequestContext {\n mode: \"replay\";\n frontendSessionId: string;\n replayId: string;\n /** Normalized origin of the sidecar, e.g. \"http://127.0.0.1:9670\". No trailing slash. */\n sidecarUrl: string;\n /**\n * Instant to freeze the worker's clock at while serving this session, or undefined to leave the\n * real clock alone.\n */\n clockAnchorMs: number | undefined;\n /**\n * Elapsed virtual time (ms from replay start) of this inbound request, when\n * the replay runner stamped {@link import(\"./protocol\").VIRTUAL_TIME_HEADER}.\n * Forwarded on every outbound lookup so concurrent calls from this request\n * share one timestamp and hash across the matching recorded burst.\n */\n virtualTimeMs?: number;\n}\n\nexport type RequestContext = RequestCaptureContext | RequestReplayContext;\n\nexport const requestCaptureContext = new AsyncLocalStorage<RequestContext>();\n\n/** The origin the shim's own sidecar traffic goes to, for the self-capture guards. */\nexport const sidecarOriginOf = (ctx: RequestContext): string =>\n ctx.mode === \"replay\" ? ctx.sidecarUrl : transportOrigin(ctx.transport);\n"],"mappings":";;;AAkFA,MAAa,wBAAwB,IAAI,mBAAmC;;AAG5E,MAAa,mBAAmB,QAC9B,IAAI,SAAS,WAAW,IAAI,aAAa,gBAAgB,IAAI,UAAU"}
1
+ {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { CaptureBuffer } from \"./capture-buffer\";\nimport { type SidecarTransport, transportOrigin } from \"./sidecar-transport\";\n\n/**\n * Per-inbound-request context, entered by the withMeticulous wrapper and read by the fetch\n * patch. Requires the `nodejs_als` (or `nodejs_compat`) compatibility flag in the worker's\n * wrangler configuration.\n */\ninterface BaseRequestContext {\n requestId: string;\n /** The inbound request's ctx.waitUntil, for post-response capture work. */\n waitUntil: (promise: Promise<unknown>) => void;\n /**\n * Per-request line-coverage sink, indexed by the global line id the build\n * plugin assigned. Present only when the bundle is instrumented and the\n * request is being replayed.\n *\n * Lives here rather than in a module-scope global because workerd interleaves\n * concurrent requests in one isolate — a shared sink would merge sessions'\n * coverage together.\n */\n coverage?: Uint8Array;\n}\n\n/** Recording: capture events are reported to the sidecar, which stays out of the request path. */\nexport interface RequestCaptureContext extends BaseRequestContext {\n mode: \"record\";\n frontendSessionId: string | undefined;\n /**\n * Set only when we minted {@link frontendSessionId} ourselves for a document navigation the\n * browser could not tag, rather than reading it off the request. Recorded on the inbound\n * event so ingestion can tell an id the page may never have adopted from one it sent us.\n * See provisional-session-id.ts.\n */\n sessionIdOrigin?: \"backend\";\n transport: SidecarTransport;\n /** Batches this request's events into one report. */\n buffer: CaptureBuffer;\n /**\n * Trace this request's spans belong to, and the id of its SERVER span. Minted here rather than\n * by the sidecar so span assembly needs no cross-batch memory — see `CorrelatedEvent`.\n */\n traceId: string;\n serverSpanId: string;\n}\n\n/**\n * Replaying: outbound calls are looked up against the sidecar's mock store before they leave the\n * worker.\n *\n * Both ids are required here, unlike recording. Mocks are indexed by session and their\n * consume-once state is isolated by replay, so omitting either would make deterministic\n * lookup impossible. Making both non-optional keeps that state unrepresentable.\n *\n * Replay is URL-only, deliberately: the sidecar URL arrives on a request header the replay runner\n * injects, validated down to a loopback / docker-gateway / private-network origin. There is no\n * binding transport here because a replay sidecar is a Node process holding the mock store, never\n * a Worker.\n */\nexport interface RequestReplayContext extends BaseRequestContext {\n mode: \"replay\";\n frontendSessionId: string;\n replayId: string;\n /** Normalized origin of the sidecar, e.g. \"http://127.0.0.1:9670\". No trailing slash. */\n sidecarUrl: string;\n /**\n * Instant to freeze the worker's clock at while serving this session, or undefined to leave the\n * real clock alone.\n */\n clockAnchorMs: number | undefined;\n /**\n * Elapsed virtual time (ms from replay start) of this inbound request, when\n * the replay runner stamped {@link import(\"./protocol\").VIRTUAL_TIME_HEADER}.\n * Forwarded on every outbound lookup so concurrent calls from this request\n * share one timestamp and hash across the matching recorded burst.\n */\n virtualTimeMs?: number;\n}\n\n/**\n * Agent-swarm testing: outbound calls are offered to an interceptor service before they leave\n * the worker. The interceptor either serves a response or the call passes through to the real\n * service — unlike replay there is no hermeticity, no frozen clock and no coverage, so this\n * context is deliberately a strict subset of the replay one.\n */\nexport interface RequestAgentSwarmTestingContext extends BaseRequestContext {\n mode: \"agent-swarm-testing\";\n /** Identifies the agent-swarm run to the interceptor, from the deployment env. */\n runId: string;\n frontendSessionId: string | undefined;\n /** Normalized origin of the interceptor service, e.g. \"http://127.0.0.1:9670\". No trailing slash. */\n sidecarUrl: string;\n}\n\nexport type RequestContext =\n | RequestCaptureContext\n | RequestReplayContext\n | RequestAgentSwarmTestingContext;\n\nexport const requestCaptureContext = new AsyncLocalStorage<RequestContext>();\n\n/** The origin the shim's own sidecar traffic goes to, for the self-capture guards. */\nexport const sidecarOriginOf = (ctx: RequestContext): string =>\n ctx.mode === \"record\" ? transportOrigin(ctx.transport) : ctx.sidecarUrl;\n"],"mappings":";;;AAoGA,MAAa,wBAAwB,IAAI,mBAAmC;;AAG5E,MAAa,mBAAmB,QAC9B,IAAI,SAAS,WAAW,gBAAgB,IAAI,UAAU,GAAG,IAAI"}
@@ -3,6 +3,7 @@ import { captureOutboundCall } from "./outbound-capture.js";
3
3
  import { getOriginalFetch, setOriginalFetch } from "./original-fetch.js";
4
4
  import { isOrphanedDatadogAgentUrl } from "./datadog-agent.js";
5
5
  import { replayOutboundCall } from "./replay-fetch.js";
6
+ import { agentSwarmTestingOutboundCall } from "./agent-swarm-testing-fetch.js";
6
7
  //#region src/fetch-patch.ts
7
8
  const FETCH_PATCHED = Symbol.for("meticulous.workerd.fetchPatched");
8
9
  /**
@@ -36,7 +37,9 @@ const patchedFetch = async (input, init) => {
36
37
  }
37
38
  if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) return original(request);
38
39
  if (ctx.mode === "record" && isOrphanedDatadogAgentUrl(request.url, ctx.frontendSessionId)) return original(request);
39
- return ctx.mode === "replay" ? replayOutboundCall(ctx, request, (req) => original(req)) : captureOutboundCall(ctx, { kind: "outbound" }, request, (req) => original(req));
40
+ if (ctx.mode === "replay") return replayOutboundCall(ctx, request, (req) => original(req));
41
+ if (ctx.mode === "agent-swarm-testing") return agentSwarmTestingOutboundCall(ctx, request, (req) => original(req));
42
+ return captureOutboundCall(ctx, { kind: "outbound" }, request, (req) => original(req));
40
43
  };
41
44
  //#endregion
42
45
  export { installFetchPatch };
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-patch.js","names":[],"sources":["../src/fetch-patch.ts"],"sourcesContent":["import { requestCaptureContext, sidecarOriginOf } from \"./context\";\nimport { isOrphanedDatadogAgentUrl } from \"./datadog-agent\";\nimport { getOriginalFetch, setOriginalFetch } from \"./original-fetch\";\nimport { captureOutboundCall } from \"./outbound-capture\";\nimport { replayOutboundCall } from \"./replay-fetch\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst FETCH_PATCHED = Symbol.for(\"meticulous.workerd.fetchPatched\");\n\nexport { getOriginalFetch } from \"./original-fetch\";\nexport { headersToRecord } from \"./outbound-capture\";\n\n/**\n * Replaces globalThis.fetch with the mode-appropriate interception:\n *\n * - recording: a passive capture tee. The real fetch is called directly (the sidecar is\n * never in the request path); request and response are cloned and reported in the\n * background.\n * - replaying: decide-then-serve. The call is looked up against the sidecar's mock store\n * and a recorded response is synthesised, or the call fails — replay is hermetic unless\n * the request opts out with the `meticulous-passthrough` header.\n *\n * Outside a withMeticulous request context the patch is a pure pass-through. Idempotent.\n */\nexport const installFetchPatch = (): void => {\n const holder = globalThis as { [FETCH_PATCHED]?: boolean };\n if (holder[FETCH_PATCHED]) {\n return;\n }\n holder[FETCH_PATCHED] = true;\n setOriginalFetch(globalThis.fetch.bind(globalThis));\n globalThis.fetch = patchedFetch;\n};\n\nconst patchedFetch: FetchFn = async (input, init) => {\n const original = getOriginalFetch();\n const ctx = requestCaptureContext.getStore();\n if (!ctx) {\n return original(input, init);\n }\n\n // Normalize to a single Request. This may consume `input`'s body (when\n // `input` is a Request), so from here on the real fetch must be given\n // `request`, never the original arguments.\n let request: Request;\n try {\n request = new Request(input, init);\n } catch {\n // Unconstructable arguments — let the real fetch produce the error.\n return original(input, init);\n }\n\n // Never intercept the shim's own traffic to the sidecar. Defence in depth: every\n // shim → sidecar call already goes through the unpatched fetch, or a service binding.\n if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) {\n return original(request);\n }\n\n // The app's own telemetry leaving for a Datadog agent or intake, with nothing to tie it to\n // a session, is recorded as nothing at all. Record mode only, by construction: a replay\n // context always carries a session id, so the rule could not fire there either way.\n if (\n ctx.mode === \"record\" &&\n isOrphanedDatadogAgentUrl(request.url, ctx.frontendSessionId)\n ) {\n return original(request);\n }\n\n return ctx.mode === \"replay\"\n ? replayOutboundCall(ctx, request, (req) => original(req))\n : captureOutboundCall(ctx, { kind: \"outbound\" }, request, (req) =>\n original(req),\n );\n};\n"],"mappings":";;;;;;AASA,MAAM,gBAAgB,OAAO,IAAI,kCAAkC;;;;;;;;;;;;;AAiBnE,MAAa,0BAAgC;CAC3C,MAAM,SAAS;AACf,KAAI,OAAO,eACT;AAEF,QAAO,iBAAiB;AACxB,kBAAiB,WAAW,MAAM,KAAK,WAAW,CAAC;AACnD,YAAW,QAAQ;;AAGrB,MAAM,eAAwB,OAAO,OAAO,SAAS;CACnD,MAAM,WAAW,kBAAkB;CACnC,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,CAAC,IACH,QAAO,SAAS,OAAO,KAAK;CAM9B,IAAI;AACJ,KAAI;AACF,YAAU,IAAI,QAAQ,OAAO,KAAK;SAC5B;AAEN,SAAO,SAAS,OAAO,KAAK;;AAK9B,KAAI,QAAQ,IAAI,WAAW,GAAG,gBAAgB,IAAI,CAAC,GAAG,CACpD,QAAO,SAAS,QAAQ;AAM1B,KACE,IAAI,SAAS,YACb,0BAA0B,QAAQ,KAAK,IAAI,kBAAkB,CAE7D,QAAO,SAAS,QAAQ;AAG1B,QAAO,IAAI,SAAS,WAChB,mBAAmB,KAAK,UAAU,QAAQ,SAAS,IAAI,CAAC,GACxD,oBAAoB,KAAK,EAAE,MAAM,YAAY,EAAE,UAAU,QACvD,SAAS,IAAI,CACd"}
1
+ {"version":3,"file":"fetch-patch.js","names":[],"sources":["../src/fetch-patch.ts"],"sourcesContent":["import { requestCaptureContext, sidecarOriginOf } from \"./context\";\nimport { isOrphanedDatadogAgentUrl } from \"./datadog-agent\";\nimport { getOriginalFetch, setOriginalFetch } from \"./original-fetch\";\nimport { captureOutboundCall } from \"./outbound-capture\";\nimport { agentSwarmTestingOutboundCall } from \"./agent-swarm-testing-fetch\";\nimport { replayOutboundCall } from \"./replay-fetch\";\n\ntype FetchFn = typeof globalThis.fetch;\n\n// Symbol.for so a second copy of the shim (e.g. bundled twice) still detects the patch.\nconst FETCH_PATCHED = Symbol.for(\"meticulous.workerd.fetchPatched\");\n\nexport { getOriginalFetch } from \"./original-fetch\";\nexport { headersToRecord } from \"./outbound-capture\";\n\n/**\n * Replaces globalThis.fetch with the mode-appropriate interception:\n *\n * - recording: a passive capture tee. The real fetch is called directly (the sidecar is\n * never in the request path); request and response are cloned and reported in the\n * background.\n * - replaying: decide-then-serve. The call is looked up against the sidecar's mock store\n * and a recorded response is synthesised, or the call fails — replay is hermetic unless\n * the request opts out with the `meticulous-passthrough` header.\n *\n * Outside a withMeticulous request context the patch is a pure pass-through. Idempotent.\n */\nexport const installFetchPatch = (): void => {\n const holder = globalThis as { [FETCH_PATCHED]?: boolean };\n if (holder[FETCH_PATCHED]) {\n return;\n }\n holder[FETCH_PATCHED] = true;\n setOriginalFetch(globalThis.fetch.bind(globalThis));\n globalThis.fetch = patchedFetch;\n};\n\nconst patchedFetch: FetchFn = async (input, init) => {\n const original = getOriginalFetch();\n const ctx = requestCaptureContext.getStore();\n if (!ctx) {\n return original(input, init);\n }\n\n // Normalize to a single Request. This may consume `input`'s body (when\n // `input` is a Request), so from here on the real fetch must be given\n // `request`, never the original arguments.\n let request: Request;\n try {\n request = new Request(input, init);\n } catch {\n // Unconstructable arguments — let the real fetch produce the error.\n return original(input, init);\n }\n\n // Never intercept the shim's own traffic to the sidecar. Defence in depth: every\n // shim → sidecar call already goes through the unpatched fetch, or a service binding.\n if (request.url.startsWith(`${sidecarOriginOf(ctx)}/`)) {\n return original(request);\n }\n\n // The app's own telemetry leaving for a Datadog agent or intake, with nothing to tie it to\n // a session, is recorded as nothing at all. Record mode only, by construction: a replay\n // context always carries a session id, so the rule could not fire there either way.\n if (\n ctx.mode === \"record\" &&\n isOrphanedDatadogAgentUrl(request.url, ctx.frontendSessionId)\n ) {\n return original(request);\n }\n\n if (ctx.mode === \"replay\") {\n return replayOutboundCall(ctx, request, (req) => original(req));\n }\n if (ctx.mode === \"agent-swarm-testing\") {\n return agentSwarmTestingOutboundCall(ctx, request, (req) => original(req));\n }\n return captureOutboundCall(ctx, { kind: \"outbound\" }, request, (req) =>\n original(req),\n );\n};\n"],"mappings":";;;;;;;AAUA,MAAM,gBAAgB,OAAO,IAAI,kCAAkC;;;;;;;;;;;;;AAiBnE,MAAa,0BAAgC;CAC3C,MAAM,SAAS;AACf,KAAI,OAAO,eACT;AAEF,QAAO,iBAAiB;AACxB,kBAAiB,WAAW,MAAM,KAAK,WAAW,CAAC;AACnD,YAAW,QAAQ;;AAGrB,MAAM,eAAwB,OAAO,OAAO,SAAS;CACnD,MAAM,WAAW,kBAAkB;CACnC,MAAM,MAAM,sBAAsB,UAAU;AAC5C,KAAI,CAAC,IACH,QAAO,SAAS,OAAO,KAAK;CAM9B,IAAI;AACJ,KAAI;AACF,YAAU,IAAI,QAAQ,OAAO,KAAK;SAC5B;AAEN,SAAO,SAAS,OAAO,KAAK;;AAK9B,KAAI,QAAQ,IAAI,WAAW,GAAG,gBAAgB,IAAI,CAAC,GAAG,CACpD,QAAO,SAAS,QAAQ;AAM1B,KACE,IAAI,SAAS,YACb,0BAA0B,QAAQ,KAAK,IAAI,kBAAkB,CAE7D,QAAO,SAAS,QAAQ;AAG1B,KAAI,IAAI,SAAS,SACf,QAAO,mBAAmB,KAAK,UAAU,QAAQ,SAAS,IAAI,CAAC;AAEjE,KAAI,IAAI,SAAS,sBACf,QAAO,8BAA8B,KAAK,UAAU,QAAQ,SAAS,IAAI,CAAC;AAE5E,QAAO,oBAAoB,KAAK,EAAE,MAAM,YAAY,EAAE,UAAU,QAC9D,SAAS,IAAI,CACd"}