@descryy/runtime-external-service-observation 0.1.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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `Evidence.collectorVersion`'s source for every collector in this package:
3
+ * this package's own `package.json` "version", read from the artifact
4
+ * itself rather than hand-duplicated into a second string that can drift
5
+ * from what actually shipped. Same idea as `nodeVersion` elsewhere in this
6
+ * repo (`process.version`, always known, never probed) applied to a
7
+ * package instead of the Node runtime.
8
+ */
9
+ export declare const COLLECTOR_VERSION: string;
10
+ //# sourceMappingURL=collector-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector-version.d.ts","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,eAAO,MAAM,iBAAiB,EAAE,MAA6E,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `Evidence.collectorVersion`'s source for every collector in this package:
3
+ * this package's own `package.json` "version", read from the artifact
4
+ * itself rather than hand-duplicated into a second string that can drift
5
+ * from what actually shipped. Same idea as `nodeVersion` elsewhere in this
6
+ * repo (`process.version`, always known, never probed) applied to a
7
+ * package instead of the Node runtime.
8
+ */
9
+ import { createRequire } from "node:module";
10
+ const require = createRequire(import.meta.url);
11
+ export const COLLECTOR_VERSION = require("../package.json").version;
12
+ //# sourceMappingURL=collector-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector-version.js","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,iBAAiB,GAAY,OAAO,CAAC,iBAAiB,CAAkC,CAAC,OAAO,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `--import`-loaded preload: patches `globalThis.fetch` before the target
3
+ * application's own code runs, so every real outbound call the application
4
+ * issues through the bare global `fetch` is observed — proven directly (a
5
+ * probe script, not assumed): Node's global `fetch` is a single, mutable
6
+ * binding on `globalThis`, so a `--import` preload reassigning it here and
7
+ * an application's completely independent, later call to bare `fetch(...)`
8
+ * resolve to the same function. Same singleton-timing argument
9
+ * `sqlite-instrumentation.ts` (`@descryy/runtime-database-observation`,
10
+ * RT-141) already established for `node:sqlite`'s prototype, applied to a
11
+ * different kind of global.
12
+ *
13
+ * **Read, not write.** The wrapper calls the original `fetch` with the
14
+ * exact, untouched arguments it received — nothing about what leaves the
15
+ * process changes. Request/response bodies are never read here: `status`
16
+ * comes off the resolved `Response` before any body access, and describing
17
+ * the outbound request (`method`/`url`/headers) is done via the same
18
+ * `Request`/`Headers` normalisation `fetch` itself would do, on a
19
+ * side channel, not by touching the body stream passed to the real call.
20
+ *
21
+ * Deliberately minimal and import-free, like `sqlite-instrumentation.ts` and
22
+ * `test-runner`'s own `reporter.ts` — this runs inside the OBSERVED process,
23
+ * loaded by absolute path via `node --import`, never through this package's
24
+ * normal dependency graph. Interpretation (service identity, correlation
25
+ * ids) happens on the collector side, in the observing process, not here —
26
+ * this only captures.
27
+ *
28
+ * **The call-site stack is captured here and nowhere else.** It is the one
29
+ * fact only this process can supply: by the time the marker line reaches a
30
+ * collector it is text on a pipe, which is exactly why
31
+ * `ExternalRequestCollector` reported `stackTrace: null` for as long as this
32
+ * preload captured none. `observe_runtime`'s own header records the
33
+ * consequence — a backend-only run "resolves both kinds of node and writes no
34
+ * edge", because an edge needs a caller and an endpoint named by ONE
35
+ * observation and this one named only the endpoint. Capturing the stack at
36
+ * the call site is what makes an outbound call able to name its own caller.
37
+ *
38
+ * Raw text, deliberately unparsed. Parsing V8's format is
39
+ * `@descryy/runtime-adapter-typescript`'s job and belongs in the observing
40
+ * process, the same read-here/interpret-there split every marker in this
41
+ * repository already uses.
42
+ *
43
+ * The cost, stated rather than discovered: `new Error()` on every outbound
44
+ * call. That is a stack capture per request in a process that has opted into
45
+ * being observed by loading this preload; `Error.stackTraceLimit` is left
46
+ * exactly as the application set it, because raising it would be this file
47
+ * writing to the observed process's own configuration, which the paragraph
48
+ * above forbids.
49
+ *
50
+ * The marker prefix and the captured shape live in `request-marker.ts`, not
51
+ * here: this module has a module-scope effect and nothing that merely needs
52
+ * the vocabulary may be made to import it. See that file for the measurement
53
+ * behind that rule.
54
+ *
55
+ * One line of structured JSON per request, prefixed exactly like
56
+ * `sqlite-instrumentation.ts`'s own `DESCRY_DB_QUERY {json}` convention,
57
+ * itself following the benchmark harness's `BENCH_RESULT {json}` precedent
58
+ * (RT-072): scraping prose measures the prose, so nothing here is prose.
59
+ */
60
+ export {};
61
+ //# sourceMappingURL=fetch-instrumentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-instrumentation.d.ts","sourceRoot":"","sources":["../src/fetch-instrumentation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * `--import`-loaded preload: patches `globalThis.fetch` before the target
3
+ * application's own code runs, so every real outbound call the application
4
+ * issues through the bare global `fetch` is observed — proven directly (a
5
+ * probe script, not assumed): Node's global `fetch` is a single, mutable
6
+ * binding on `globalThis`, so a `--import` preload reassigning it here and
7
+ * an application's completely independent, later call to bare `fetch(...)`
8
+ * resolve to the same function. Same singleton-timing argument
9
+ * `sqlite-instrumentation.ts` (`@descryy/runtime-database-observation`,
10
+ * RT-141) already established for `node:sqlite`'s prototype, applied to a
11
+ * different kind of global.
12
+ *
13
+ * **Read, not write.** The wrapper calls the original `fetch` with the
14
+ * exact, untouched arguments it received — nothing about what leaves the
15
+ * process changes. Request/response bodies are never read here: `status`
16
+ * comes off the resolved `Response` before any body access, and describing
17
+ * the outbound request (`method`/`url`/headers) is done via the same
18
+ * `Request`/`Headers` normalisation `fetch` itself would do, on a
19
+ * side channel, not by touching the body stream passed to the real call.
20
+ *
21
+ * Deliberately minimal and import-free, like `sqlite-instrumentation.ts` and
22
+ * `test-runner`'s own `reporter.ts` — this runs inside the OBSERVED process,
23
+ * loaded by absolute path via `node --import`, never through this package's
24
+ * normal dependency graph. Interpretation (service identity, correlation
25
+ * ids) happens on the collector side, in the observing process, not here —
26
+ * this only captures.
27
+ *
28
+ * **The call-site stack is captured here and nowhere else.** It is the one
29
+ * fact only this process can supply: by the time the marker line reaches a
30
+ * collector it is text on a pipe, which is exactly why
31
+ * `ExternalRequestCollector` reported `stackTrace: null` for as long as this
32
+ * preload captured none. `observe_runtime`'s own header records the
33
+ * consequence — a backend-only run "resolves both kinds of node and writes no
34
+ * edge", because an edge needs a caller and an endpoint named by ONE
35
+ * observation and this one named only the endpoint. Capturing the stack at
36
+ * the call site is what makes an outbound call able to name its own caller.
37
+ *
38
+ * Raw text, deliberately unparsed. Parsing V8's format is
39
+ * `@descryy/runtime-adapter-typescript`'s job and belongs in the observing
40
+ * process, the same read-here/interpret-there split every marker in this
41
+ * repository already uses.
42
+ *
43
+ * The cost, stated rather than discovered: `new Error()` on every outbound
44
+ * call. That is a stack capture per request in a process that has opted into
45
+ * being observed by loading this preload; `Error.stackTraceLimit` is left
46
+ * exactly as the application set it, because raising it would be this file
47
+ * writing to the observed process's own configuration, which the paragraph
48
+ * above forbids.
49
+ *
50
+ * The marker prefix and the captured shape live in `request-marker.ts`, not
51
+ * here: this module has a module-scope effect and nothing that merely needs
52
+ * the vocabulary may be made to import it. See that file for the measurement
53
+ * behind that rule.
54
+ *
55
+ * One line of structured JSON per request, prefixed exactly like
56
+ * `sqlite-instrumentation.ts`'s own `DESCRY_DB_QUERY {json}` convention,
57
+ * itself following the benchmark harness's `BENCH_RESULT {json}` precedent
58
+ * (RT-072): scraping prose measures the prose, so nothing here is prose.
59
+ */
60
+ import { REQUEST_MARKER } from "./request-marker.js";
61
+ /** This module's own location, in both shapes a V8 frame can print it: an ESM frame stringifies a `file://` URL, a CJS one a plain path. */
62
+ const SELF_URL = import.meta.url;
63
+ const SELF_PATH = SELF_URL.startsWith("file://") ? SELF_URL.slice("file://".length) : SELF_URL;
64
+ /**
65
+ * Frames belonging to this file are dropped **by file, not by count**.
66
+ * `primaryFrameIndex` is 0 for every V8 stack this project parses, so a
67
+ * single surviving wrapper frame would make every downstream resolution
68
+ * point at the instrumentation instead of at the code under observation --
69
+ * a failure that produces confident, well-formed, entirely wrong answers
70
+ * rather than an error. Matching on `import.meta.url` also survives this
71
+ * module being loaded from `.js` (the compiled preload, how it actually
72
+ * runs) or `.ts` (type stripping, how a test may run it).
73
+ */
74
+ function callSiteStack() {
75
+ const raw = new Error().stack;
76
+ if (raw === undefined)
77
+ return null;
78
+ const lines = raw.split("\n");
79
+ const header = lines[0] ?? "Error";
80
+ const frames = lines.slice(1).filter((line) => !line.includes(SELF_URL) && !line.includes(SELF_PATH));
81
+ // Nothing but our own frames: no application call site was captured, which
82
+ // is `null` rather than a header line pretending to be a stack.
83
+ if (frames.length === 0)
84
+ return null;
85
+ return [header, ...frames].join("\n");
86
+ }
87
+ function emit(request) {
88
+ process.stdout.write(`${REQUEST_MARKER} ${JSON.stringify(request)}\n`);
89
+ }
90
+ function headersToObject(headers) {
91
+ const out = {};
92
+ for (const [name, value] of headers.entries())
93
+ out[name] = value;
94
+ return out;
95
+ }
96
+ /**
97
+ * `url`/`method`/`requestHeaders` off the call's own arguments, without
98
+ * touching the body -- a `Request` object already carries these structured;
99
+ * a `(input, init)` pair is normalised the same way `fetch` itself would,
100
+ * via the real `Headers` constructor rather than hand-parsed.
101
+ */
102
+ function describeRequest(input, init) {
103
+ if (input instanceof Request) {
104
+ return { url: input.url, method: input.method, requestHeaders: headersToObject(input.headers) };
105
+ }
106
+ const url = typeof input === "string" ? input : input.toString();
107
+ const method = (init?.method ?? "GET").toUpperCase();
108
+ return { url, method, requestHeaders: headersToObject(new Headers(init?.headers)) };
109
+ }
110
+ // Guards against double-patching if this module is somehow loaded twice
111
+ // (e.g. both `--import` and a transitive re-import) -- the second pass
112
+ // would otherwise wrap an already-wrapped fetch and double-emit. Same
113
+ // guard shape as sqlite-instrumentation.ts's own PATCHED symbol.
114
+ const PATCHED = Symbol.for("descry.external-service-observation.fetch-patched");
115
+ const target = globalThis;
116
+ if (target[PATCHED] !== true) {
117
+ target[PATCHED] = true;
118
+ const original = globalThis.fetch;
119
+ globalThis.fetch = async function patchedFetch(input, init) {
120
+ const described = describeRequest(input, init);
121
+ // Captured BEFORE the call, not in the handlers below: after `await` the
122
+ // application's own frames are gone from the stack.
123
+ const stack = callSiteStack();
124
+ const start = performance.now();
125
+ try {
126
+ const response = await original(input, init);
127
+ emit({ ...described, status: response.status, durationMs: performance.now() - start, success: true, error: null, stack });
128
+ return response;
129
+ }
130
+ catch (error) {
131
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
132
+ emit({ ...described, status: null, durationMs: performance.now() - start, success: false, error: detail, stack });
133
+ throw error;
134
+ }
135
+ };
136
+ }
137
+ //# sourceMappingURL=fetch-instrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-instrumentation.js","sourceRoot":"","sources":["../src/fetch-instrumentation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Correlation-id extraction from real HTTP headers — the same mechanism and
3
+ * the same closed set of header names as `extractHeaderCorrelationIds`
4
+ * (`@descryhq-wq/runtime-browser`'s `network-correlation.ts`).
5
+ *
6
+ * **Duplicated on purpose, not imported.** `browser` depends on Playwright
7
+ * to launch and drive a real browser — a real, heavy dependency this
8
+ * package has no other reason to carry, for a page-automation concern
9
+ * entirely unrelated to instrumenting a backend process's own outbound
10
+ * `fetch`. The function itself is small, pure, and reads a closed,
11
+ * unlikely-to-change set of conventional header names — the case this
12
+ * project's own `symbol.ts` distinguishes from (`toRepoRelative` is
13
+ * *shared* because its two callers must translate identically); these two
14
+ * copies have no such constraint, and importing across a Playwright
15
+ * dependency to avoid ten lines would be the worse coupling.
16
+ */
17
+ export interface HeaderCorrelationIds {
18
+ readonly traceId: string | null;
19
+ readonly requestId: string | null;
20
+ }
21
+ export declare function extractHeaderCorrelationIds(headers: Readonly<Record<string, string>>): HeaderCorrelationIds;
22
+ //# sourceMappingURL=header-correlation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header-correlation.d.ts","sourceRoot":"","sources":["../src/header-correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,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,35 @@
1
+ /**
2
+ * Correlation-id extraction from real HTTP headers — the same mechanism and
3
+ * the same closed set of header names as `extractHeaderCorrelationIds`
4
+ * (`@descryhq-wq/runtime-browser`'s `network-correlation.ts`).
5
+ *
6
+ * **Duplicated on purpose, not imported.** `browser` depends on Playwright
7
+ * to launch and drive a real browser — a real, heavy dependency this
8
+ * package has no other reason to carry, for a page-automation concern
9
+ * entirely unrelated to instrumenting a backend process's own outbound
10
+ * `fetch`. The function itself is small, pure, and reads a closed,
11
+ * unlikely-to-change set of conventional header names — the case this
12
+ * project's own `symbol.ts` distinguishes from (`toRepoRelative` is
13
+ * *shared* because its two callers must translate identically); these two
14
+ * copies have no such constraint, and importing across a Playwright
15
+ * dependency to avoid ten lines would be the worse coupling.
16
+ */
17
+ const TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
18
+ function firstHeader(headers, names) {
19
+ for (const name of names) {
20
+ const value = headers[name];
21
+ if (value !== undefined && value.length > 0)
22
+ return value;
23
+ }
24
+ return null;
25
+ }
26
+ export function extractHeaderCorrelationIds(headers) {
27
+ const explicitTraceId = firstHeader(headers, ["x-trace-id", "trace-id"]);
28
+ const traceparent = headers["traceparent"];
29
+ const w3cTraceId = traceparent !== undefined ? (TRACEPARENT_PATTERN.exec(traceparent)?.[2] ?? null) : null;
30
+ return {
31
+ traceId: explicitTraceId ?? w3cTraceId,
32
+ requestId: firstHeader(headers, ["x-request-id", "request-id"]),
33
+ };
34
+ }
35
+ //# sourceMappingURL=header-correlation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header-correlation.js","sourceRoot":"","sources":["../src/header-correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,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,7 @@
1
+ export { REQUEST_MARKER, type CapturedRequest } from "./request-marker.ts";
2
+ export { parseRequestLine } from "./request-line-parser.ts";
3
+ export { extractServiceIdentity } from "./service-identity.ts";
4
+ export { extractHeaderCorrelationIds, type HeaderCorrelationIds } from "./header-correlation.ts";
5
+ export type { ExternalRequestCollectorOptions } from "./request-collector.ts";
6
+ export { createExternalRequestCollector, FETCH_PRELOAD_MODULE_PATH } from "./request-collector.ts";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEjG,YAAY,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAE,8BAA8B,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // Deliberately NOT from `fetch-instrumentation.ts`: that module replaces
2
+ // `globalThis.fetch` at module scope, and re-exporting a constant out of it
3
+ // put that patch into every consumer of this package -- `query-marker.ts`'s
4
+ // measured defect, which this package still had. The preload is loaded by
5
+ // absolute path via `node --import`, inside the observed process, never
6
+ // through this dependency graph.
7
+ export { REQUEST_MARKER } from "./request-marker.js";
8
+ export { parseRequestLine } from "./request-line-parser.js";
9
+ export { extractServiceIdentity } from "./service-identity.js";
10
+ export { extractHeaderCorrelationIds } from "./header-correlation.js";
11
+ export { createExternalRequestCollector, FETCH_PRELOAD_MODULE_PATH } from "./request-collector.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,wEAAwE;AACxE,iCAAiC;AACjC,OAAO,EAAE,cAAc,EAAwB,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAA6B,MAAM,yBAAyB,CAAC;AAGjG,OAAO,EAAE,8BAA8B,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `ExternalRequestCollector` (plan §18 / checklist §18): translates
3
+ * `fetch-instrumentation.ts`'s captured requests into `EXTERNAL_REQUEST`
4
+ * Evidence. Same shape as `DatabaseQueryCollector`
5
+ * (`@descryy/runtime-database-observation`, RT-141) deliberately — a
6
+ * `ProcessOutputSource` in, `Evidence` out, streaming-first, a collector
7
+ * failure reported as evidence rather than swallowed — but for a different
8
+ * marker on the same stream.
9
+ *
10
+ * **What this collector does not do, named rather than silently absent:**
11
+ * no request/response body capture (the preload never reads either stream —
12
+ * see `fetch-instrumentation.ts`'s own "read, not write" note), no
13
+ * provider classification beyond a request's own hostname
14
+ * (`service-identity.ts`), and `graphNodeId` is always null — no resolver
15
+ * from a captured hostname to any graph node exists (nothing in the 15
16
+ * canonical node types names an external service). All real §18 rows, left
17
+ * open rather than guessed at, matching `DatabaseQueryCollector`'s own
18
+ * disclosure of the same shape of gap.
19
+ *
20
+ * ## The call-site stack, and why the parser is injected
21
+ *
22
+ * `stackTrace` used to be hard-null here, and `observe_runtime`'s own header
23
+ * gave the reason: *"it parses a log line the application printed and a
24
+ * printed line carries no stack."* The line is this package's own marker and
25
+ * the preload that writes it runs inside the calling process, so the stack
26
+ * was always available — nobody had captured it. It is captured now
27
+ * (`fetch-instrumentation.ts`), and this collector attaches it.
28
+ *
29
+ * Parsing it is **not** this package's job. V8's `Error.stack` format is one
30
+ * language's, and this collector observes the marker protocol, which is not.
31
+ * So a `StackTraceParser` is an option, supplied by whoever knows which
32
+ * runtime is being observed — `RuntimeAdapter.stackTraceParser` is exactly
33
+ * that value, already on the seam. Omitted, this collector behaves exactly as
34
+ * it did before the capture existed and **says so** in `capabilities()`:
35
+ * `stackCapture: unavailable`, with the reason. A half-wired capability that
36
+ * quietly reports nothing is the failure mode this project's honest-
37
+ * degradation rule exists to prevent.
38
+ */
39
+ import type { Collector } from "@descryy/runtime-contracts";
40
+ import type { ProcessOutputSource, StackTraceParser } from "@descryy/runtime-backend-observation";
41
+ /** Absolute path to the compiled preload, for a caller building `node --import <this> <app>`. Never imported directly -- loaded by Node itself, in the observed process, before that process's own code runs. */
42
+ export declare const FETCH_PRELOAD_MODULE_PATH: string;
43
+ export interface ExternalRequestCollectorOptions {
44
+ readonly source: ProcessOutputSource;
45
+ /** Which owned system this collector's evidence should be attributed to (plan §16.7). Null when not applicable. */
46
+ readonly service?: string;
47
+ /**
48
+ * The observed runtime's own stack parser — `RuntimeAdapter.stackTraceParser`.
49
+ * Supplied by a caller that knows which language it launched; this package
50
+ * knows no language and must not (the IR boundary rule, one layer down).
51
+ *
52
+ * Omitted means captured stacks are not parsed and `stackTrace` stays
53
+ * `null`, which `capabilities()` reports as `stackCapture: unavailable`
54
+ * rather than leaving a reader to infer it from an absence.
55
+ */
56
+ readonly stackTraceParser?: StackTraceParser;
57
+ }
58
+ export declare function createExternalRequestCollector(options: ExternalRequestCollectorOptions): Collector;
59
+ //# sourceMappingURL=request-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-collector.d.ts","sourceRoot":"","sources":["../src/request-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAGH,OAAO,KAAK,EAAE,SAAS,EAA+F,MAAM,4BAA4B,CAAC;AACzJ,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAOlG,iNAAiN;AACjN,eAAO,MAAM,yBAAyB,QAAwE,CAAC;AAE/G,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,mHAAmH;IACnH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CAC9C;AAmCD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,SAAS,CA+GlG"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `ExternalRequestCollector` (plan §18 / checklist §18): translates
3
+ * `fetch-instrumentation.ts`'s captured requests into `EXTERNAL_REQUEST`
4
+ * Evidence. Same shape as `DatabaseQueryCollector`
5
+ * (`@descryy/runtime-database-observation`, RT-141) deliberately — a
6
+ * `ProcessOutputSource` in, `Evidence` out, streaming-first, a collector
7
+ * failure reported as evidence rather than swallowed — but for a different
8
+ * marker on the same stream.
9
+ *
10
+ * **What this collector does not do, named rather than silently absent:**
11
+ * no request/response body capture (the preload never reads either stream —
12
+ * see `fetch-instrumentation.ts`'s own "read, not write" note), no
13
+ * provider classification beyond a request's own hostname
14
+ * (`service-identity.ts`), and `graphNodeId` is always null — no resolver
15
+ * from a captured hostname to any graph node exists (nothing in the 15
16
+ * canonical node types names an external service). All real §18 rows, left
17
+ * open rather than guessed at, matching `DatabaseQueryCollector`'s own
18
+ * disclosure of the same shape of gap.
19
+ *
20
+ * ## The call-site stack, and why the parser is injected
21
+ *
22
+ * `stackTrace` used to be hard-null here, and `observe_runtime`'s own header
23
+ * gave the reason: *"it parses a log line the application printed and a
24
+ * printed line carries no stack."* The line is this package's own marker and
25
+ * the preload that writes it runs inside the calling process, so the stack
26
+ * was always available — nobody had captured it. It is captured now
27
+ * (`fetch-instrumentation.ts`), and this collector attaches it.
28
+ *
29
+ * Parsing it is **not** this package's job. V8's `Error.stack` format is one
30
+ * language's, and this collector observes the marker protocol, which is not.
31
+ * So a `StackTraceParser` is an option, supplied by whoever knows which
32
+ * runtime is being observed — `RuntimeAdapter.stackTraceParser` is exactly
33
+ * that value, already on the seam. Omitted, this collector behaves exactly as
34
+ * it did before the capture existed and **says so** in `capabilities()`:
35
+ * `stackCapture: unavailable`, with the reason. A half-wired capability that
36
+ * quietly reports nothing is the failure mode this project's honest-
37
+ * degradation rule exists to prevent.
38
+ */
39
+ import { fileURLToPath } from "node:url";
40
+ import { parseRequestLine } from "./request-line-parser.js";
41
+ import { extractServiceIdentity } from "./service-identity.js";
42
+ import { extractHeaderCorrelationIds } from "./header-correlation.js";
43
+ import { COLLECTOR_VERSION } from "./collector-version.js";
44
+ /** Absolute path to the compiled preload, for a caller building `node --import <this> <app>`. Never imported directly -- loaded by Node itself, in the observed process, before that process's own code runs. */
45
+ export const FETCH_PRELOAD_MODULE_PATH = fileURLToPath(new URL("./fetch-instrumentation.js", import.meta.url));
46
+ const EVENT_TYPE = "EXTERNAL_REQUEST";
47
+ function computeCapabilities(hasStackParser) {
48
+ const reason = "ExternalRequestCollector observes EXTERNAL_REQUEST events only.";
49
+ const status = { availability: "unavailable", reason };
50
+ return {
51
+ domObservation: status,
52
+ consoleObservation: status,
53
+ networkObservation: status,
54
+ backendLogAccess: status,
55
+ // Real HTTP headers, structurally read (Headers/Request, not text
56
+ // parsed) -- the same mechanism and the same claim strength
57
+ // BrowserNetworkCollector already makes for its own captured requests.
58
+ distributedTrace: { availability: "available", reason: null },
59
+ sourceMapping: status,
60
+ // Real: the preload captures the application's own stack at the fetch
61
+ // call site, in the observed process, before the call leaves it. Claimed
62
+ // only when a parser was actually supplied to turn it into frames --
63
+ // capturing text nothing can read is not a capability.
64
+ stackCapture: hasStackParser
65
+ ? { availability: "available", reason: null }
66
+ : {
67
+ availability: "unavailable",
68
+ reason: "the fetch preload captures a call-site stack, but no StackTraceParser was supplied to this collector, " +
69
+ "so it is not parsed and `stackTrace` stays null. Pass `RuntimeAdapter.stackTraceParser` for the observed runtime.",
70
+ },
71
+ processLifecycle: status,
72
+ databaseObservation: { availability: "unavailable", reason: "ExternalRequestCollector instruments the global fetch only; queries are DatabaseQueryCollector's to observe (RT-141)." },
73
+ externalServiceObservation: { availability: "available", reason: null },
74
+ };
75
+ }
76
+ export function createExternalRequestCollector(options) {
77
+ const capabilities = computeCapabilities(options.stackTraceParser !== undefined);
78
+ let consumeLoopDone;
79
+ let consumptionFailure = null;
80
+ /**
81
+ * `null` for every honest reason it can be: nothing captured, no parser
82
+ * supplied, or a parser that refused the text outright. A parser's `null`
83
+ * is its own contract ("not recognizable as this parser's format, never a
84
+ * best-effort guess") and is passed through rather than softened.
85
+ */
86
+ async function parseStack(request) {
87
+ if (request.stack === null || options.stackTraceParser === undefined)
88
+ return null;
89
+ return await options.stackTraceParser.parse(request.stack);
90
+ }
91
+ async function emitCapturedRequest(request, context) {
92
+ const { traceId, requestId } = extractHeaderCorrelationIds(request.requestHeaders);
93
+ const stackTrace = await parseStack(request);
94
+ context.emit({
95
+ timestamp: new Date().toISOString(),
96
+ source: "backend-process",
97
+ service: options.service ?? null,
98
+ process: options.source.processId,
99
+ eventType: EVENT_TYPE,
100
+ payload: {
101
+ raw: `${request.method} ${request.url}`,
102
+ url: request.url,
103
+ method: request.method,
104
+ service: extractServiceIdentity(request.url),
105
+ status: request.status,
106
+ durationMs: request.durationMs,
107
+ success: request.success,
108
+ error: request.error,
109
+ },
110
+ traceId,
111
+ requestId,
112
+ correlationId: null,
113
+ // No resolver from a captured hostname to a graph node exists --
114
+ // none of the 15 canonical node types names an external service.
115
+ graphNodeId: null,
116
+ sourceLocation: null,
117
+ stackTrace,
118
+ // The observation itself (a real captured call, real timing, real
119
+ // status/failure) is not probabilistic.
120
+ confidence: 1,
121
+ redactionStatus: "pending-redaction",
122
+ collectorVersion: COLLECTOR_VERSION,
123
+ });
124
+ }
125
+ async function consume(context) {
126
+ for await (const line of options.source.lines) {
127
+ const request = parseRequestLine(line.text);
128
+ if (request !== null)
129
+ await emitCapturedRequest(request, context);
130
+ }
131
+ }
132
+ return {
133
+ collectorId: `external-request-collector:${options.source.processId}`,
134
+ start(context) {
135
+ consumeLoopDone = consume(context).catch((error) => {
136
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
137
+ consumptionFailure = detail;
138
+ try {
139
+ context.emit({
140
+ timestamp: new Date().toISOString(),
141
+ source: "backend-process",
142
+ service: options.service ?? null,
143
+ process: options.source.processId,
144
+ eventType: "COLLECTOR_ERROR",
145
+ payload: {
146
+ raw: `external request collector for process "${options.source.processId}" stopped consuming after an error: ${detail}`,
147
+ collectorId: `external-request-collector:${options.source.processId}`,
148
+ error: detail,
149
+ },
150
+ traceId: null,
151
+ requestId: null,
152
+ correlationId: null,
153
+ graphNodeId: null,
154
+ sourceLocation: null,
155
+ stackTrace: null,
156
+ confidence: 1,
157
+ redactionStatus: "pending-redaction",
158
+ collectorVersion: COLLECTOR_VERSION,
159
+ });
160
+ }
161
+ catch (emitError) {
162
+ console.error(`[external-request-collector:${options.source.processId}] consumption failed (${detail}) AND the failure could not be emitted: ${String(emitError)}`);
163
+ }
164
+ });
165
+ return Promise.resolve({ available: true });
166
+ },
167
+ async stop() {
168
+ await consumeLoopDone;
169
+ },
170
+ capabilities() {
171
+ if (consumptionFailure === null)
172
+ return capabilities;
173
+ return {
174
+ ...capabilities,
175
+ distributedTrace: {
176
+ availability: "unavailable",
177
+ reason: `consumption stopped early after an error (${consumptionFailure}); output after that point was never observed.`,
178
+ },
179
+ };
180
+ },
181
+ };
182
+ }
183
+ //# sourceMappingURL=request-collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-collector.js","sourceRoot":"","sources":["../src/request-collector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Reads one process-output line for the `REQUEST_MARKER` prefix.
3
+ *
4
+ * The marker comes from `request-marker.ts`, never from
5
+ * `fetch-instrumentation.ts` — importing the emitter to get a string would
6
+ * patch the parsing process's own `globalThis.fetch`. See `request-marker.ts`
7
+ * for the measurement. Everything else — the application's own real
8
+ * stdout/stderr — is not this parser's concern and is left alone; an
9
+ * external-request collector does not also try to be a log collector.
10
+ */
11
+ import { REQUEST_MARKER, type CapturedRequest } from "./request-marker.ts";
12
+ /**
13
+ * `null` for a non-matching line (ordinary application output) AND for a
14
+ * line that starts with the marker but fails to parse as one of this
15
+ * package's own captured requests — a malformed marker line is refused,
16
+ * never guessed at, the same distinction `parseQueryLine`
17
+ * (`@descryy/runtime-database-observation`) makes.
18
+ */
19
+ export declare function parseRequestLine(line: string): CapturedRequest | null;
20
+ export type { CapturedRequest };
21
+ export { REQUEST_MARKER };
22
+ //# sourceMappingURL=request-line-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-line-parser.d.ts","sourceRoot":"","sources":["../src/request-line-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAsB3E;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAcrE;AAED,YAAY,EAAE,eAAe,EAAE,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Reads one process-output line for the `REQUEST_MARKER` prefix.
3
+ *
4
+ * The marker comes from `request-marker.ts`, never from
5
+ * `fetch-instrumentation.ts` — importing the emitter to get a string would
6
+ * patch the parsing process's own `globalThis.fetch`. See `request-marker.ts`
7
+ * for the measurement. Everything else — the application's own real
8
+ * stdout/stderr — is not this parser's concern and is left alone; an
9
+ * external-request collector does not also try to be a log collector.
10
+ */
11
+ import { REQUEST_MARKER } from "./request-marker.js";
12
+ function isCapturedRequest(value) {
13
+ if (typeof value !== "object" || value === null)
14
+ return false;
15
+ const v = value;
16
+ if (typeof v.url !== "string" || typeof v.method !== "string")
17
+ return false;
18
+ if (typeof v.durationMs !== "number" || typeof v.success !== "boolean")
19
+ return false;
20
+ if (v.status !== null && typeof v.status !== "number")
21
+ return false;
22
+ if (v.error !== null && typeof v.error !== "string")
23
+ return false;
24
+ if (typeof v.requestHeaders !== "object" || v.requestHeaders === null)
25
+ return false;
26
+ // `stack` is the one field allowed to be absent. A marker line written by a
27
+ // preload from before call-site capture existed is a well-formed line that
28
+ // genuinely carries no stack -- normalised to `null` below, which is what
29
+ // "no stack was captured" already means everywhere downstream. Anything
30
+ // present but not a string is malformed and refused, same as every other
31
+ // field here.
32
+ if (v.stack !== undefined && v.stack !== null && typeof v.stack !== "string")
33
+ return false;
34
+ return Object.values(v.requestHeaders).every((value) => typeof value === "string");
35
+ }
36
+ /**
37
+ * `null` for a non-matching line (ordinary application output) AND for a
38
+ * line that starts with the marker but fails to parse as one of this
39
+ * package's own captured requests — a malformed marker line is refused,
40
+ * never guessed at, the same distinction `parseQueryLine`
41
+ * (`@descryy/runtime-database-observation`) makes.
42
+ */
43
+ export function parseRequestLine(line) {
44
+ if (!line.startsWith(`${REQUEST_MARKER} `))
45
+ return null;
46
+ const json = line.slice(REQUEST_MARKER.length + 1);
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(json);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ if (!isCapturedRequest(parsed))
55
+ return null;
56
+ // Absent `stack` becomes an explicit `null` so no consumer has to
57
+ // distinguish "the field was missing" from "nothing was captured" -- they
58
+ // are the same fact and only one of them is representable downstream.
59
+ return { ...parsed, stack: parsed.stack ?? null };
60
+ }
61
+ export { REQUEST_MARKER };
62
+ //# sourceMappingURL=request-line-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-line-parser.js","sourceRoot":"","sources":["../src/request-line-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAwB,MAAM,qBAAqB,CAAC;AAI3E,SAAS,iBAAiB,CAAC,KAAc;IACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5E,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACrF,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACpE,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClE,IAAI,OAAO,CAAC,CAAC,cAAc,KAAK,QAAQ,IAAI,CAAC,CAAC,cAAc,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACpF,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,wEAAwE;IACxE,yEAAyE;IACzE,cAAc;IACd,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3F,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,cAAyC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC;AAChH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,cAAc,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,kEAAkE;IAClE,0EAA0E;IAC1E,sEAAsE;IACtE,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;AACpD,CAAC;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The wire vocabulary shared by the two halves of external-service
3
+ * observation: the marker prefix and the shape that follows it.
4
+ *
5
+ * This module exists to be **free of side effects**, and that is its entire
6
+ * job. `fetch-instrumentation.ts` — the other holder of these names until now
7
+ * — replaces `globalThis.fetch` at module scope and writes to stdout, which
8
+ * is right for a `--import` preload inside an observed process and wrong
9
+ * everywhere else. `request-line-parser.ts` needed nothing from it but the
10
+ * string `"DESCRY_EXTERNAL_REQUEST"`, and importing a string is still
11
+ * importing a module: the patch and the stdout writer came along, through the
12
+ * package index, into every consumer.
13
+ *
14
+ * **This is `query-marker.ts`'s defect, one package over, and it was still
15
+ * open.** `@descryy/runtime-database-observation` found the identical shape,
16
+ * measured it into `@descryy/mcp`'s JSON-RPC transport — 14,906 marker lines
17
+ * in one `analyze` call — and carved the vocabulary out. Nothing did the same
18
+ * here, and nothing noticed, because nothing outside this repository imports
19
+ * this package yet. `observe_runtime` consuming this collector is precisely
20
+ * the change that would have noticed.
21
+ *
22
+ * So: the emitter imports the vocabulary, the parser imports the vocabulary,
23
+ * and neither imports the other. Anything added here must stay a constant or
24
+ * a type. A function with a module-scope effect would rebuild the same trap
25
+ * one file over.
26
+ */
27
+ export declare const REQUEST_MARKER = "DESCRY_EXTERNAL_REQUEST";
28
+ export interface CapturedRequest {
29
+ readonly url: string;
30
+ readonly method: string;
31
+ readonly requestHeaders: Readonly<Record<string, string>>;
32
+ /** `null` only when `success` is `false` -- the call never resolved to a response at all. */
33
+ readonly status: number | null;
34
+ readonly durationMs: number;
35
+ /** `true` iff `fetch` resolved rather than threw. An HTTP error status (4xx/5xx) is still `success: true` -- fetch itself does not reject on one. */
36
+ readonly success: boolean;
37
+ readonly error: string | null;
38
+ /**
39
+ * The **application's** own stack at the moment it called `fetch`, raw and
40
+ * unparsed, with the preload's own wrapper frames removed. `null` when the
41
+ * runtime produced no stack at all, or when nothing but the preload's own
42
+ * frames was in it.
43
+ *
44
+ * Raw text on purpose: parsing V8's format is
45
+ * `@descryy/runtime-adapter-typescript`'s job and belongs in the observing
46
+ * process, the same read-here/interpret-there split every marker in this
47
+ * repository already uses.
48
+ */
49
+ readonly stack: string | null;
50
+ }
51
+ //# sourceMappingURL=request-marker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-marker.d.ts","sourceRoot":"","sources":["../src/request-marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAOH,eAAO,MAAM,cAAc,4BAA8C,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,qJAAqJ;IACrJ,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The wire vocabulary shared by the two halves of external-service
3
+ * observation: the marker prefix and the shape that follows it.
4
+ *
5
+ * This module exists to be **free of side effects**, and that is its entire
6
+ * job. `fetch-instrumentation.ts` — the other holder of these names until now
7
+ * — replaces `globalThis.fetch` at module scope and writes to stdout, which
8
+ * is right for a `--import` preload inside an observed process and wrong
9
+ * everywhere else. `request-line-parser.ts` needed nothing from it but the
10
+ * string `"DESCRY_EXTERNAL_REQUEST"`, and importing a string is still
11
+ * importing a module: the patch and the stdout writer came along, through the
12
+ * package index, into every consumer.
13
+ *
14
+ * **This is `query-marker.ts`'s defect, one package over, and it was still
15
+ * open.** `@descryy/runtime-database-observation` found the identical shape,
16
+ * measured it into `@descryy/mcp`'s JSON-RPC transport — 14,906 marker lines
17
+ * in one `analyze` call — and carved the vocabulary out. Nothing did the same
18
+ * here, and nothing noticed, because nothing outside this repository imports
19
+ * this package yet. `observe_runtime` consuming this collector is precisely
20
+ * the change that would have noticed.
21
+ *
22
+ * So: the emitter imports the vocabulary, the parser imports the vocabulary,
23
+ * and neither imports the other. Anything added here must stay a constant or
24
+ * a type. A function with a module-scope effect would rebuild the same trap
25
+ * one file over.
26
+ */
27
+ import { RESERVED_MARKER_PREFIX } from "@descryy/runtime-contracts";
28
+ // Derived, not spelled again: `RESERVED_MARKER_PREFIX` is what the
29
+ // backend log collector skips on, so a marker that did not use it would
30
+ // come back as the application's own log output.
31
+ export const REQUEST_MARKER = `${RESERVED_MARKER_PREFIX}EXTERNAL_REQUEST`;
32
+ //# sourceMappingURL=request-marker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-marker.js","sourceRoot":"","sources":["../src/request-marker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,mEAAmE;AACnE,wEAAwE;AACxE,iDAAiD;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,sBAAsB,kBAAkB,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A captured request's "service identity" — the checklist's §18 own
3
+ * `service identity` row — is its URL's hostname, and nothing more.
4
+ *
5
+ * **Declared, never inferred.** This does not attempt to recognise "this
6
+ * hostname is Stripe" or classify a call into `authentication provider` /
7
+ * `payment provider` / etc. (§18's own "Potential integrations" list) —
8
+ * that would be a lookup table of well-known hosts this project has no
9
+ * source of truth for, guessed rather than read off anything the
10
+ * application declared. The hostname is a real, observed fact; a provider
11
+ * label would not be. If a consumer wants a friendlier name for a known
12
+ * host, that is a later, disclosed classification step layered on top of
13
+ * this, not something this function silently does.
14
+ *
15
+ * `null` for a URL that does not parse — refused rather than guessed, the
16
+ * same shape `extractTargetTable`
17
+ * (`@descryy/runtime-database-observation`) refuses a SQL statement it does
18
+ * not recognise.
19
+ */
20
+ export declare function extractServiceIdentity(url: string): string | null;
21
+ //# sourceMappingURL=service-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-identity.d.ts","sourceRoot":"","sources":["../src/service-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMjE"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A captured request's "service identity" — the checklist's §18 own
3
+ * `service identity` row — is its URL's hostname, and nothing more.
4
+ *
5
+ * **Declared, never inferred.** This does not attempt to recognise "this
6
+ * hostname is Stripe" or classify a call into `authentication provider` /
7
+ * `payment provider` / etc. (§18's own "Potential integrations" list) —
8
+ * that would be a lookup table of well-known hosts this project has no
9
+ * source of truth for, guessed rather than read off anything the
10
+ * application declared. The hostname is a real, observed fact; a provider
11
+ * label would not be. If a consumer wants a friendlier name for a known
12
+ * host, that is a later, disclosed classification step layered on top of
13
+ * this, not something this function silently does.
14
+ *
15
+ * `null` for a URL that does not parse — refused rather than guessed, the
16
+ * same shape `extractTargetTable`
17
+ * (`@descryy/runtime-database-observation`) refuses a SQL statement it does
18
+ * not recognise.
19
+ */
20
+ export function extractServiceIdentity(url) {
21
+ try {
22
+ return new URL(url).hostname;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ //# sourceMappingURL=service-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-identity.js","sourceRoot":"","sources":["../src/service-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@descryy/runtime-external-service-observation",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "External service observation (plan §18, checklist §18): real EXTERNAL_REQUEST capture via client instrumentation, never parsed application logs. Client #1 is the global fetch.",
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.1.0",
28
+ "@descryy/runtime-backend-observation": "0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@descryy/runtime-controller": "*",
32
+ "@descryy/runtime-orchestrator": "*",
33
+ "@descryy/runtime-adapter-typescript": "*"
34
+ }
35
+ }