@descryy/runtime-browser 0.2.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.
Files changed (41) hide show
  1. package/dist/browser-action-collector.d.ts +181 -0
  2. package/dist/browser-action-collector.d.ts.map +1 -0
  3. package/dist/browser-action-collector.js +427 -0
  4. package/dist/browser-action-collector.js.map +1 -0
  5. package/dist/browser-console-collector.d.ts +33 -0
  6. package/dist/browser-console-collector.d.ts.map +1 -0
  7. package/dist/browser-console-collector.js +185 -0
  8. package/dist/browser-console-collector.js.map +1 -0
  9. package/dist/browser-network-collector.d.ts +112 -0
  10. package/dist/browser-network-collector.d.ts.map +1 -0
  11. package/dist/browser-network-collector.js +682 -0
  12. package/dist/browser-network-collector.js.map +1 -0
  13. package/dist/browser-session.d.ts +144 -0
  14. package/dist/browser-session.d.ts.map +1 -0
  15. package/dist/browser-session.js +136 -0
  16. package/dist/browser-session.js.map +1 -0
  17. package/dist/collector-version.d.ts +10 -0
  18. package/dist/collector-version.d.ts.map +1 -0
  19. package/dist/collector-version.js +12 -0
  20. package/dist/collector-version.js.map +1 -0
  21. package/dist/dom-evidence.d.ts +20 -0
  22. package/dist/dom-evidence.d.ts.map +1 -0
  23. package/dist/dom-evidence.js +32 -0
  24. package/dist/dom-evidence.js.map +1 -0
  25. package/dist/frontend-initiator-capture.d.ts +87 -0
  26. package/dist/frontend-initiator-capture.d.ts.map +1 -0
  27. package/dist/frontend-initiator-capture.js +142 -0
  28. package/dist/frontend-initiator-capture.js.map +1 -0
  29. package/dist/index.d.ts +16 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +9 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/network-correlation.d.ts +15 -0
  34. package/dist/network-correlation.d.ts.map +1 -0
  35. package/dist/network-correlation.js +29 -0
  36. package/dist/network-correlation.js.map +1 -0
  37. package/dist/script-url-mapping.d.ts +40 -0
  38. package/dist/script-url-mapping.d.ts.map +1 -0
  39. package/dist/script-url-mapping.js +96 -0
  40. package/dist/script-url-mapping.js.map +1 -0
  41. package/package.json +36 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Fetch-initiator capture — the page-side half of resolving *which*
3
+ * frontend function issued an observed request, not merely which functions
4
+ * the graph says are capable of it (`resolveFrontendCallers`'s own
5
+ * structural limit, `@descryy/runtime-graph-correlator`).
6
+ *
7
+ * **This is a write, not a read**, in the sense
8
+ * `PROPOSAL-code-injection-into-observed-processes.md` asks every injection
9
+ * mechanism to state for itself: the wrapped `fetch()` adds a real header
10
+ * (`x-descry-initiator-id`) to a real outgoing request. That line does not
11
+ * run when Descry is not attached — a same-origin request can pick up a
12
+ * fresh CORS preflight it would not otherwise have had. That side effect is
13
+ * the one `PROPOSAL-observed-frontend-caller-needs-initiator-capture.md`
14
+ * asked a human to sign off on before this was built; nothing here reopens
15
+ * that call.
16
+ *
17
+ * The call-site stack itself never travels over the wire that carries the
18
+ * application's own traffic — only a short, opaque per-call id does, as the
19
+ * header value. The stack text instead travels over Playwright's own CDP
20
+ * binding channel (`page.exposeFunction`), straight to `take()` below,
21
+ * keyed by that same id.
22
+ *
23
+ * **Why the real `fetch()` is held until the exposed call returns.** Firing
24
+ * the buffering call and the real request concurrently would make
25
+ * correctness depend on which of two independent async channels — the CDP
26
+ * binding call, and Chromium's own network stack — happens to land at this
27
+ * process first. That is exactly the kind of timing assumption §27's
28
+ * "similarity must never be treated as causality" rules out. Awaiting the
29
+ * binding call before dispatching `fetch` makes the ordering true by
30
+ * construction instead: the real request cannot be sent until this module
31
+ * has already buffered the stack it will be looked up by.
32
+ *
33
+ * Chromium only — `page.addInitScript` + `page.exposeFunction` is the
34
+ * mechanism, and no non-CDP-based engine here exposes an equivalent pair.
35
+ * `fetch()` only, not `XMLHttpRequest`. Both boundaries are the scope
36
+ * `PROPOSAL-observed-frontend-caller-needs-initiator-capture.md` drew, not
37
+ * an oversight of this module.
38
+ */
39
+ export const INITIATOR_HEADER_NAME = "x-descry-initiator-id";
40
+ /**
41
+ * Bound on stacks buffered but never claimed. Without it, every request
42
+ * whose id is never looked up — the header stripped in transit, a page that
43
+ * never triggers a matching `onRequest` at all — would grow this map for
44
+ * the entire life of the page. FIFO eviction via `Map`'s own insertion
45
+ * order; a stack this old was never going to be claimed by the time
46
+ * something newer needs the room.
47
+ */
48
+ const MAX_BUFFERED_STACKS = 2000;
49
+ const CAPTURE_BINDING_NAME = "__descryCaptureInitiatorStack";
50
+ export function createFrontendInitiatorCapture() {
51
+ const buffered = new Map();
52
+ function record(id, stackText) {
53
+ if (buffered.size >= MAX_BUFFERED_STACKS) {
54
+ const oldest = buffered.keys().next().value;
55
+ if (oldest !== undefined)
56
+ buffered.delete(oldest);
57
+ }
58
+ buffered.set(id, stackText);
59
+ }
60
+ return {
61
+ async install(page) {
62
+ await page.exposeFunction(CAPTURE_BINDING_NAME, record);
63
+ await page.addInitScript({ content: initScriptSource() });
64
+ },
65
+ take(id) {
66
+ const stackText = buffered.get(id);
67
+ buffered.delete(id);
68
+ return stackText;
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * The injected wrapper, as a plain string — `page.addInitScript`'s
74
+ * string form, so this never needs its own transpilation step and runs
75
+ * exactly as written in the page's own JS engine, not this process's.
76
+ *
77
+ * Every call site outcome degrades to "no id header" or "no functionName"
78
+ * rather than throwing: `bind(...)` is wrapped in its own `.catch`, and a
79
+ * `fetch()` whose `input` is neither a string, `URL`, nor `Request` still
80
+ * reaches `originalFetch` unmodified so the native `TypeError` a caller
81
+ * would have gotten anyway is the one they still get — this wrapper adds a
82
+ * header, it does not change what counts as a valid `fetch()` call.
83
+ */
84
+ function initScriptSource() {
85
+ return `(() => {
86
+ const originalFetch = window.fetch;
87
+ if (typeof originalFetch !== "function") return;
88
+ const bind = window[${JSON.stringify(CAPTURE_BINDING_NAME)}];
89
+ if (typeof bind !== "function") return;
90
+ let counter = 0;
91
+ window.fetch = function descryFetch(input, init) {
92
+ const id = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2) + "-" + (counter++);
93
+ const stack = (new Error()).stack || "";
94
+ const self = this;
95
+ const dispatch = () => {
96
+ let headerSource = init && init.headers !== undefined ? init.headers : undefined;
97
+ if (headerSource === undefined && typeof Request !== "undefined" && input instanceof Request) {
98
+ headerSource = input.headers;
99
+ }
100
+ const headers = new Headers(headerSource);
101
+ headers.set(${JSON.stringify(INITIATOR_HEADER_NAME)}, id);
102
+ if (typeof Request !== "undefined" && input instanceof Request) {
103
+ return originalFetch.call(self, new Request(input, { headers: headers }));
104
+ }
105
+ return originalFetch.call(self, input, Object.assign({}, init, { headers: headers }));
106
+ };
107
+ return bind(id, stack).catch(function () {}).then(dispatch);
108
+ };
109
+ })();`;
110
+ }
111
+ const FRAME_LINE = /^\s*at\s/;
112
+ /**
113
+ * Strips `descryFetch`'s own frame from a raw V8 stack, so the remaining
114
+ * text's first frame is the real call site rather than this module's own
115
+ * wrapper.
116
+ *
117
+ * **Why this can be a fixed "drop the first frame line" rule rather than a
118
+ * search for a named function.** `new Error().stack` is captured
119
+ * synchronously, as the wrapper's first statement — before the `await`-
120
+ * shaped `.then(dispatch)` chain below it ever runs. V8's stack is built
121
+ * from the synchronous call chain live at capture time, so the innermost
122
+ * frame is deterministically `descryFetch` itself, every time, regardless
123
+ * of what called it. Stripping by *position* (first frame line) rather than
124
+ * by *name* also survives minification of this wrapper's own name, which a
125
+ * name-based strip would not.
126
+ *
127
+ * Applied to raw text, before parsing — same reasoning
128
+ * `rewriteScriptUrls`'s own doc gives for rewriting before rather than
129
+ * after: `createV8StackTraceParser` already treats frame 0 of whatever it
130
+ * receives as `primaryFrameIndex`, so removing the wrapper's line here
131
+ * means the parser's existing, already-tested convention does the right
132
+ * thing unmodified, instead of teaching every consumer a second index to
133
+ * skip.
134
+ */
135
+ export function stripInitiatorWrapperFrame(rawStackText) {
136
+ const lines = rawStackText.split(/\r\n|\r|\n/);
137
+ const frameIndex = lines.findIndex((line) => FRAME_LINE.test(line));
138
+ if (frameIndex === -1)
139
+ return rawStackText;
140
+ return [...lines.slice(0, frameIndex), ...lines.slice(frameIndex + 1)].join("\n");
141
+ }
142
+ //# sourceMappingURL=frontend-initiator-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontend-initiator-capture.js","sourceRoot":"","sources":["../src/frontend-initiator-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAEjC,MAAM,oBAAoB,GAAG,+BAA+B,CAAC;AAwB7D,MAAM,UAAU,8BAA8B;IAC5C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE3C,SAAS,MAAM,CAAC,EAAU,EAAE,SAAiB;QAC3C,IAAI,QAAQ,CAAC,IAAI,IAAI,mBAAmB,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,IAAU;YACtB,MAAM,IAAI,CAAC,cAAc,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;YACxD,MAAM,IAAI,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;QAC5D,CAAC;QACD,IAAI,CAAC,EAAU;YACb,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACpB,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,gBAAgB;IACvB,OAAO;;;0BAGiB,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;;;;;;;;;;;;;sBAaxC,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC;;;;;;;;QAQnD,CAAC;AACT,CAAC;AAED,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,0BAA0B,CAAC,YAAoB;IAC7D,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACpE,IAAI,UAAU,KAAK,CAAC,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpF,CAAC"}
@@ -0,0 +1,16 @@
1
+ export type { BrowserSessionOptions, BrowserSession, ChromiumUnavailable, ChromiumAvailability, BrowserSessionLaunch } from "./browser-session.ts";
2
+ export { launchBrowserSession, launchBrowserSessionOrRefuse, resolveChromiumExecutable, BrowserUnavailableError, CHROMIUM_INSTALL_REMEDY, } from "./browser-session.ts";
3
+ export type { DomElementSnapshot } from "./dom-evidence.ts";
4
+ export { captureElementSnapshot } from "./dom-evidence.ts";
5
+ export type { BrowserActionCollectorOptions, BrowserActionCollector, WaitForOptions } from "./browser-action-collector.ts";
6
+ export { createBrowserActionCollector } from "./browser-action-collector.ts";
7
+ export type { BrowserConsoleCollectorOptions } from "./browser-console-collector.ts";
8
+ export { createBrowserConsoleCollector } from "./browser-console-collector.ts";
9
+ export type { BrowserNetworkCollectorOptions } from "./browser-network-collector.ts";
10
+ export { createBrowserNetworkCollector } from "./browser-network-collector.ts";
11
+ export type { HeaderCorrelationIds } from "./network-correlation.ts";
12
+ export { extractHeaderCorrelationIds } from "./network-correlation.ts";
13
+ export type { ScriptUrlRewrite, SourceRootResolver } from "./script-url-mapping.ts";
14
+ export { rewriteScriptUrls } from "./script-url-mapping.ts";
15
+ export { INITIATOR_HEADER_NAME, stripInitiatorWrapperFrame } from "./frontend-initiator-capture.ts";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACnJ,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,YAAY,EAAE,6BAA6B,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC3H,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAE/E,YAAY,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAE/E,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAEvE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { launchBrowserSession, launchBrowserSessionOrRefuse, resolveChromiumExecutable, BrowserUnavailableError, CHROMIUM_INSTALL_REMEDY, } from "./browser-session.js";
2
+ export { captureElementSnapshot } from "./dom-evidence.js";
3
+ export { createBrowserActionCollector } from "./browser-action-collector.js";
4
+ export { createBrowserConsoleCollector } from "./browser-console-collector.js";
5
+ export { createBrowserNetworkCollector } from "./browser-network-collector.js";
6
+ export { extractHeaderCorrelationIds } from "./network-correlation.js";
7
+ export { rewriteScriptUrls } from "./script-url-mapping.js";
8
+ export { INITIATOR_HEADER_NAME, stripInitiatorWrapperFrame } from "./frontend-initiator-capture.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAG/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAG/E,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Correlation-id extraction from real HTTP headers (structured access, not
3
+ * text regex) -- covers the top three levels of plan §10's six-level
4
+ * preference order: trace ID, request ID, W3C trace context. Direct header
5
+ * access makes the W3C case cheap here in a way it wasn't for
6
+ * `backend-observation`'s `extractCorrelationIds`, which only ever sees
7
+ * unstructured log text and explicitly cuts W3C parsing as a documented
8
+ * V1 gap -- this collector doesn't need that same cut.
9
+ */
10
+ export interface HeaderCorrelationIds {
11
+ readonly traceId: string | null;
12
+ readonly requestId: string | null;
13
+ }
14
+ export declare function extractHeaderCorrelationIds(headers: Readonly<Record<string, string>>): HeaderCorrelationIds;
15
+ //# sourceMappingURL=network-correlation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-correlation.d.ts","sourceRoot":"","sources":["../src/network-correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAUD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,oBAAoB,CAS3G"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Correlation-id extraction from real HTTP headers (structured access, not
3
+ * text regex) -- covers the top three levels of plan §10's six-level
4
+ * preference order: trace ID, request ID, W3C trace context. Direct header
5
+ * access makes the W3C case cheap here in a way it wasn't for
6
+ * `backend-observation`'s `extractCorrelationIds`, which only ever sees
7
+ * unstructured log text and explicitly cuts W3C parsing as a documented
8
+ * V1 gap -- this collector doesn't need that same cut.
9
+ */
10
+ // W3C Trace Context: "00-<32 hex trace-id>-<16 hex parent-id>-<2 hex flags>".
11
+ const TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
12
+ function firstHeader(headers, names) {
13
+ for (const name of names) {
14
+ const value = headers[name];
15
+ if (value !== undefined && value.length > 0)
16
+ return value;
17
+ }
18
+ return null;
19
+ }
20
+ export function extractHeaderCorrelationIds(headers) {
21
+ const explicitTraceId = firstHeader(headers, ["x-trace-id", "trace-id"]);
22
+ const traceparent = headers["traceparent"];
23
+ const w3cTraceId = traceparent !== undefined ? (TRACEPARENT_PATTERN.exec(traceparent)?.[2] ?? null) : null;
24
+ return {
25
+ traceId: explicitTraceId ?? w3cTraceId,
26
+ requestId: firstHeader(headers, ["x-request-id", "request-id"]),
27
+ };
28
+ }
29
+ //# sourceMappingURL=network-correlation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-correlation.js","sourceRoot":"","sources":["../src/network-correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG,8DAA8D,CAAC;AAO3F,SAAS,WAAW,CAAC,OAAyC,EAAE,KAAwB;IACtF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,OAAyC;IACnF,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;IACzE,MAAM,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE3G,OAAO;QACL,OAAO,EAAE,eAAe,IAAI,UAAU;QACtC,SAAS,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;KAChE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * RT-023's collector half: rewriting a browser script URL to the on-disk
3
+ * file it was served from, so a page-error stack can be parsed into real
4
+ * `SourceLocation`s instead of preserved as text.
5
+ *
6
+ * **The rewrite happens on the stack text, before parsing, deliberately.**
7
+ * A `StackTraceParser` and its `SourceLocationResolver` already know how to
8
+ * turn `at f (/abs/path.js:12:5)` into a resolved location — including
9
+ * source-map handling and staleness detection, which took real work to get
10
+ * right. Rewriting the URL first lets all of that apply unchanged, where
11
+ * post-processing parsed frames would mean reimplementing it here and
12
+ * keeping two copies honest.
13
+ *
14
+ * **What this can and cannot claim.** `CollectorContext.resolveSourceRoot`
15
+ * answers "which service's declared root does this origin belong to", and
16
+ * `ConfiguredServiceRoot.declaredCwd` is named for what it is: configured,
17
+ * not verified. So a successful rewrite proves the origin maps to a
18
+ * directory — **not** that the file the browser executed came from the
19
+ * checkout in it. The downstream resolver's own existence check is the real
20
+ * refusal point, and an unmapped or non-existent path degrades to the URL
21
+ * itself rather than to a fabricated file. That is RT-031's class again:
22
+ * a match on an observable proxy is not proof of the underlying identity.
23
+ *
24
+ * Nothing is rewritten when the resolver refuses, when it is absent, or
25
+ * when the URL is not one this execution spawned — a CDN script, a browser
26
+ * extension, `about:blank`. Those are routine, not failures.
27
+ */
28
+ import type { ServiceRootLookup } from "@descryy/runtime-contracts";
29
+ export type SourceRootResolver = (origin: string) => ServiceRootLookup;
30
+ export interface ScriptUrlRewrite {
31
+ readonly text: string;
32
+ /** Every URL seen, and what happened to it — so a collector can report coverage rather than implying it. */
33
+ readonly outcomes: readonly {
34
+ readonly url: string;
35
+ readonly mapped: string | null;
36
+ readonly reason: string | null;
37
+ }[];
38
+ }
39
+ export declare function rewriteScriptUrls(stackText: string, resolveSourceRoot: SourceRootResolver | undefined): ScriptUrlRewrite;
40
+ //# sourceMappingURL=script-url-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"script-url-mapping.d.ts","sourceRoot":"","sources":["../src/script-url-mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAC;AAKvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4GAA4G;IAC5G,QAAQ,CAAC,QAAQ,EAAE,SAAS;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;CACxH;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,kBAAkB,GAAG,SAAS,GAAG,gBAAgB,CAoExH"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * RT-023's collector half: rewriting a browser script URL to the on-disk
3
+ * file it was served from, so a page-error stack can be parsed into real
4
+ * `SourceLocation`s instead of preserved as text.
5
+ *
6
+ * **The rewrite happens on the stack text, before parsing, deliberately.**
7
+ * A `StackTraceParser` and its `SourceLocationResolver` already know how to
8
+ * turn `at f (/abs/path.js:12:5)` into a resolved location — including
9
+ * source-map handling and staleness detection, which took real work to get
10
+ * right. Rewriting the URL first lets all of that apply unchanged, where
11
+ * post-processing parsed frames would mean reimplementing it here and
12
+ * keeping two copies honest.
13
+ *
14
+ * **What this can and cannot claim.** `CollectorContext.resolveSourceRoot`
15
+ * answers "which service's declared root does this origin belong to", and
16
+ * `ConfiguredServiceRoot.declaredCwd` is named for what it is: configured,
17
+ * not verified. So a successful rewrite proves the origin maps to a
18
+ * directory — **not** that the file the browser executed came from the
19
+ * checkout in it. The downstream resolver's own existence check is the real
20
+ * refusal point, and an unmapped or non-existent path degrades to the URL
21
+ * itself rather than to a fabricated file. That is RT-031's class again:
22
+ * a match on an observable proxy is not proof of the underlying identity.
23
+ *
24
+ * Nothing is rewritten when the resolver refuses, when it is absent, or
25
+ * when the URL is not one this execution spawned — a CDN script, a browser
26
+ * extension, `about:blank`. Those are routine, not failures.
27
+ */
28
+ import { statSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ /** `http://localhost:41237/public/app.js:12:5` — the trailing `:line:col` is left in place for the stack parser to read. */
31
+ const SCRIPT_URL = /\bhttps?:\/\/[^\s()]+/g;
32
+ export function rewriteScriptUrls(stackText, resolveSourceRoot) {
33
+ if (resolveSourceRoot === undefined) {
34
+ return { text: stackText, outcomes: [] };
35
+ }
36
+ const outcomes = [];
37
+ const text = stackText.replace(SCRIPT_URL, (match) => {
38
+ // Strip the stack's own `:line:column` suffix before treating the rest
39
+ // as a URL — `new URL()` would otherwise parse `:12` as part of the
40
+ // path and produce a file that never existed.
41
+ const suffix = /:(\d+):(\d+)$/.exec(match);
42
+ const urlText = suffix === null ? match : match.slice(0, match.length - suffix[0].length);
43
+ let url;
44
+ try {
45
+ url = new URL(urlText);
46
+ }
47
+ catch {
48
+ outcomes.push({ url: match, mapped: null, reason: "not a parseable URL" });
49
+ return match;
50
+ }
51
+ const lookup = resolveSourceRoot(url.origin);
52
+ if (!lookup.found) {
53
+ outcomes.push({ url: match, mapped: null, reason: lookup.message });
54
+ return match;
55
+ }
56
+ // `url.pathname` is always absolute; `join` would otherwise treat a
57
+ // leading slash as a fresh root and discard the service's own cwd.
58
+ const candidate = join(lookup.root.declaredCwd, url.pathname.replace(/^\/+/, ""));
59
+ // **`isFile()`, not `existsSync` — and the difference is not pedantry.**
60
+ // An inline `<script>` on a page served at `/` produces frames V8 names
61
+ // after the *document* URL, whose pathname is `/` and nothing else. That
62
+ // joins to `declaredCwd` itself: a real directory, so `existsSync` said
63
+ // yes and this function rewrote a stack frame to a directory no browser
64
+ // ever executed. The downstream `SourceLocationResolver` then reached
65
+ // `readFileSync(directory)`, threw `EISDIR`, and the whole
66
+ // `NETWORK_REQUEST` row was lost with it — an observation discarded to
67
+ // protect a nullable field. `statSync` answers the question that was
68
+ // always being asked (is there a FILE here?) rather than the weaker one
69
+ // that happened to be cheap.
70
+ //
71
+ // `statSync` throws on a missing path where `existsSync` returned false,
72
+ // so the absence case is caught rather than propagated: both branches end
73
+ // in the same refusal, with the reason naming which one it was.
74
+ let isFile;
75
+ try {
76
+ isFile = statSync(candidate).isFile();
77
+ }
78
+ catch {
79
+ outcomes.push({ url: match, mapped: null, reason: `resolved to "${candidate}", which does not exist on disk` });
80
+ return match;
81
+ }
82
+ if (!isFile) {
83
+ // The origin maps to a root and the pathname points at something that
84
+ // is not a file in it — a directory (the `/` case above), a socket, a
85
+ // device. Reported rather than rewritten: a path that is not a source
86
+ // file is worse than the URL, which at least says truthfully where the
87
+ // browser got it.
88
+ outcomes.push({ url: match, mapped: null, reason: `resolved to "${candidate}", which is not a file on disk` });
89
+ return match;
90
+ }
91
+ outcomes.push({ url: match, mapped: candidate, reason: null });
92
+ return suffix === null ? candidate : `${candidate}${suffix[0]}`;
93
+ });
94
+ return { text, outcomes };
95
+ }
96
+ //# sourceMappingURL=script-url-mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"script-url-mapping.js","sourceRoot":"","sources":["../src/script-url-mapping.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@descryy/runtime-browser",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Browser runtime: launch, isolated context, navigation, click/type/wait, DOM inspection, screenshots, console/page-error/network capture, all through the Collector contract (plan \u00a77/\u00a78). See documents/decisions-inbox/PROPOSAL-B-playwright-for-browser-runtime.md for the Playwright-vs-CDP call.",
6
+ "license": "UNLICENSED",
7
+ "engines": {
8
+ "node": ">=22.5"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "registry": "https://registry.npmjs.org",
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -b"
25
+ },
26
+ "dependencies": {
27
+ "@descryy/runtime-contracts": "0.2.0",
28
+ "@descryy/runtime-backend-observation": "0.2.0",
29
+ "playwright-core": "^1.62.1",
30
+ "@descryy/runtime-adapter-typescript": "0.2.0"
31
+ },
32
+ "devDependencies": {
33
+ "@descryy/runtime-controller": "*",
34
+ "@descryy/runtime-evidence-store": "*"
35
+ }
36
+ }