@tallyrow/safesignal 1.0.1-rc.1

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 @@
1
+ {"version":3,"sources":["../src/testing/secret-fixtures.ts","../src/testing/assert-transport-contract.ts"],"names":[],"mappings":"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","file":"testing.cjs","sourcesContent":["/**\n * Stable bag of secret-looking values for testing. Every value here is\n * fake but shaped like the real thing — long enough that finding it in\n * a URL, query string, log payload, etc. is meaningful evidence of a\n * leak (no false positives from short substrings).\n *\n * Consumers (and the package's own tests T028, T041, T058) place these\n * values in attributes/context/etc. then assert downstream sinks never\n * see any of them.\n *\n * The keys mirror the documented default redaction denylist in\n * `contracts/redaction.md` so any new key here should track a denylist\n * entry — making the fixture the canonical \"things the package promises\n * to mask\" reference for consumers.\n */\n\n/**\n * Concrete shape of {@link makeSecretFixture}'s return value. Named\n * `string` fields (rather than `Record<string, string>`) so callers get\n * `string` — not `string | undefined` — per-field under the package's\n * `noUncheckedIndexedAccess` setting. Not part of the public `/testing`\n * surface; it only types the helper's return.\n *\n * Declared as a `type` (not an `interface`) on purpose: a type alias\n * whose properties are all `string` gets an *implicit* index signature,\n * so the fixture stays assignable to `Record<string, string>` / OTel\n * `Attributes` — while `keyof SecretFixture` remains the precise named-key\n * union, keeping dynamic `fixture[someKey]` access typed `string`.\n */\ntype SecretFixture = {\n readonly password: string;\n readonly passwd: string;\n readonly token: string;\n readonly accessToken: string;\n readonly refreshToken: string;\n readonly bearerToken: string;\n readonly authorization: string;\n readonly auth: string;\n readonly cookie: string;\n readonly setCookie: string;\n readonly secret: string;\n readonly apiKey: string;\n readonly sessionId: string;\n readonly sid: string;\n readonly ssn: string;\n readonly creditCard: string;\n readonly cardNumber: string;\n readonly cvv: string;\n readonly jwt: string;\n};\n\n/**\n * Return a stable record of secret-looking values keyed by category.\n * Values are deterministic across calls so tests can assert against\n * exact strings. Never mutate the returned object across tests — call\n * `makeSecretFixture()` again to get a fresh copy.\n */\nexport function makeSecretFixture(): SecretFixture {\n return {\n password: 'p4ssw0rd-correct-horse-battery-staple',\n passwd: 'p4ssw0rd-shadow-file-style',\n token: 'tok_AAAABBBBCCCCDDDD1234EEEEFFFFGGGG',\n accessToken: 'access_AAAA1111BBBB2222CCCC3333DDDD4444EEEE5555',\n refreshToken: 'refresh_FFFF6666GGGG7777HHHH8888IIII9999JJJJ0000',\n bearerToken:\n 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.bearerFixtureSignature',\n authorization: 'Basic dXNlcjpwYXNzOmZpeHR1cmUtbm90LXJlYWw',\n auth: 'auth_KKKK1111LLLL2222MMMM3333NNNN4444',\n cookie:\n 'sessionId=fixture-session-id-not-real-abc123; Secure; HttpOnly; SameSite=Strict',\n setCookie:\n 'auth=fixture-auth-cookie-not-real-xyz789; Path=/; Secure; HttpOnly',\n secret: 'sk_test_FIXTURE_4eC39HqLyjWDarjtT1zdp7dc_NOT_REAL',\n apiKey: 'pk_live_FIXTURE_ABCD1234EFGH5678IJKL9012MNOP_NOT_REAL',\n sessionId: 'sess_01HXYZ123ABCDEFGHIJKLMNOPQR',\n sid: 'sid_FIXTURE_QQQQ1111RRRR2222SSSS3333',\n ssn: '123-45-6789',\n creditCard: '4242 4242 4242 4242',\n cardNumber: '5555555555554444',\n cvv: '123',\n jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.fixtureJwtSignatureNotReal',\n };\n}\n\n/**\n * The complete list of fixture VALUES. Useful when scanning an\n * arbitrary string (e.g., a captured URL or POST body) for any leaked\n * fixture: `if (FIXTURE_VALUES.some((v) => url.includes(v))) { leak! }`.\n */\nexport const FIXTURE_VALUES: ReadonlyArray<string> = Object.values(\n makeSecretFixture(),\n);\n","/**\n * `assertTransportContract(transport)` — runs the documented Transport\n * contract battery against a consumer-provided `Transport`. Throws on the\n * first violation with a clear diagnostic message.\n *\n * Covers `contracts/transport.md`:\n * - Structural: `name: string`, `send(event)` exists\n * - T-S1: no `LogEvent` data appears in any `fetch` URL or\n * `navigator.sendBeacon` URL the transport produces\n * - T-S2: cross-origin delivery uses request body (POST/PUT/PATCH JSON\n * or a `sendBeacon` `Blob`) — never URL params\n * - T-S3: any URL with a scheme uses `https:`\n * - T-S4: the transport does NOT mutate the event it receives\n * - T-S5: `flush()` and `shutdown()` are idempotent (safe to call > 1x)\n *\n * The helper temporarily monkey-patches `globalThis.fetch` and\n * `globalThis.navigator.sendBeacon` to capture invocations, then restores\n * the originals when each assertion completes — even on failure. Tests\n * can run multiple consumer transports in series safely.\n *\n * This module is reached only via the package's `./testing` subpath; the\n * runtime entry does NOT re-export it.\n */\n\nimport type { LogEvent, Transport } from '../api/types.js';\nimport { FIXTURE_VALUES, makeSecretFixture } from './secret-fixtures.js';\n\n// ──────────────────────────────────────────────────────────────────────\n// Public entry point\n// ──────────────────────────────────────────────────────────────────────\n\n/**\n * Run the full Transport contract battery against `transport`. Resolves\n * when all assertions pass; rejects with an `Error` carrying a\n * diagnostic message on the first failure.\n */\nexport async function assertTransportContract(\n transport: Transport,\n): Promise<void> {\n assertStructural(transport);\n await assertSendDoesNotThrow(transport);\n await assertNoEventDataInURLs(transport);\n await assertBodyOnlyDelivery(transport);\n await assertHttpsForAbsoluteUrls(transport);\n await assertEventImmutability(transport);\n await assertFlushIdempotent(transport);\n await assertShutdownIdempotent(transport);\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Individual assertions\n// ──────────────────────────────────────────────────────────────────────\n\nfunction assertStructural(transport: Transport): void {\n if (typeof transport !== 'object' || transport === null) {\n throw fail('transport must be an object');\n }\n if (typeof transport.name !== 'string' || transport.name.length === 0) {\n throw fail('transport.name must be a non-empty string');\n }\n if (typeof transport.send !== 'function') {\n throw fail('transport.send must be a function');\n }\n}\n\nasync function assertSendDoesNotThrow(transport: Transport): Promise<void> {\n const event = makeProbeEvent();\n await withInterceptors(async () => {\n try {\n const result = transport.send(event);\n if (result instanceof Promise) {\n await result;\n }\n } catch (err) {\n throw fail(\n transport,\n 'send() threw to the caller — should fail silently or be wrapped',\n err,\n );\n }\n });\n}\n\nasync function assertNoEventDataInURLs(transport: Transport): Promise<void> {\n const event = makeProbeEvent({ attributes: makeSecretFixture() });\n await withInterceptors(async (captured) => {\n await invokeSendSafely(transport, event);\n\n for (const url of allUrls(captured)) {\n // 1. Any literal fixture value in the URL is a clear leak.\n const leakedValue = FIXTURE_VALUES.find((v) => url.includes(v));\n if (leakedValue !== undefined) {\n throw fail(\n transport,\n `URL contains a secret fixture value (T-S1 violation): ` +\n `url='${url}', leaked='${leakedValue}'`,\n );\n }\n // 2. The probe event's marker message in the URL also indicates\n // a leak — the consumer encoded event content there.\n if (url.includes(PROBE_MESSAGE)) {\n throw fail(\n transport,\n `URL contains the probe event message (T-S1 violation): ` +\n `url='${url}'`,\n );\n }\n }\n });\n}\n\nasync function assertBodyOnlyDelivery(transport: Transport): Promise<void> {\n const event = makeProbeEvent();\n await withInterceptors(async (captured) => {\n await invokeSendSafely(transport, event);\n\n for (const call of captured.fetchCalls) {\n const method = (call.init?.method ?? 'GET').toUpperCase();\n const allowedMethods = ['POST', 'PUT', 'PATCH'];\n if (!allowedMethods.includes(method)) {\n throw fail(\n transport,\n `fetch used HTTP method '${method}' for delivery — ` +\n `must use POST/PUT/PATCH with body (T-S2): url='${call.url}'`,\n );\n }\n if (call.init?.body === undefined || call.init.body === null) {\n throw fail(\n transport,\n `fetch was called with method='${method}' but no body — ` +\n `events must travel in the request body (T-S2): url='${call.url}'`,\n );\n }\n }\n\n for (const call of captured.beaconCalls) {\n if (call.data === null || call.data === undefined) {\n throw fail(\n transport,\n `navigator.sendBeacon was called without data — ` +\n `events must travel in the body (T-S2): url='${call.url}'`,\n );\n }\n }\n });\n}\n\nasync function assertHttpsForAbsoluteUrls(\n transport: Transport,\n): Promise<void> {\n const event = makeProbeEvent();\n await withInterceptors(async (captured) => {\n await invokeSendSafely(transport, event);\n\n for (const url of allUrls(captured)) {\n // Relative URLs are same-origin by definition and inherit the\n // page's scheme — skip them. Absolute URLs MUST be HTTPS.\n if (!isAbsoluteUrl(url)) continue;\n if (!url.toLowerCase().startsWith('https://')) {\n throw fail(\n transport,\n `cross-origin URL is not HTTPS (T-S3): url='${url}'`,\n );\n }\n }\n });\n}\n\nasync function assertEventImmutability(transport: Transport): Promise<void> {\n const event = makeProbeEvent({ attributes: { mutateMe: 'before' } });\n const before = JSON.stringify(event);\n await withInterceptors(async () => {\n await invokeSendSafely(transport, event);\n });\n const after = JSON.stringify(event);\n if (before !== after) {\n throw fail(\n transport,\n `transport mutated the received event (T-S4 violation):\\n` +\n ` before: ${before}\\n after: ${after}`,\n );\n }\n}\n\nasync function assertFlushIdempotent(transport: Transport): Promise<void> {\n if (typeof transport.flush !== 'function') return; // optional hook\n try {\n await transport.flush();\n await transport.flush();\n await transport.flush();\n } catch (err) {\n throw fail(\n transport,\n 'flush() is not idempotent — repeated calls must each resolve (T-S5)',\n err,\n );\n }\n}\n\nasync function assertShutdownIdempotent(transport: Transport): Promise<void> {\n if (typeof transport.shutdown !== 'function') return; // optional hook\n try {\n await transport.shutdown();\n await transport.shutdown();\n await transport.shutdown();\n } catch (err) {\n throw fail(\n transport,\n 'shutdown() is not idempotent — repeated calls must each resolve (T-S5)',\n err,\n );\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// fetch / sendBeacon interceptor\n// ──────────────────────────────────────────────────────────────────────\n\ninterface FetchCall {\n url: string;\n init: RequestInit | undefined;\n}\n\ninterface BeaconCall {\n url: string;\n data: BodyInit | null | undefined;\n}\n\ninterface CapturedCalls {\n fetchCalls: FetchCall[];\n beaconCalls: BeaconCall[];\n}\n\n/**\n * Run `body` with `globalThis.fetch` and `navigator.sendBeacon` replaced\n * by capturing stubs. Restores originals on success and failure.\n */\nasync function withInterceptors(\n body: (captured: CapturedCalls) => Promise<void>,\n): Promise<void> {\n const captured: CapturedCalls = { fetchCalls: [], beaconCalls: [] };\n\n const g = globalThis as unknown as {\n fetch?: typeof fetch;\n navigator?: { sendBeacon?: Navigator['sendBeacon'] };\n };\n\n const originalFetch = g.fetch;\n const originalBeacon = g.navigator?.sendBeacon?.bind(g.navigator);\n\n g.fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n const url = extractFetchUrl(input);\n captured.fetchCalls.push({ url, init });\n return new Response('', { status: 204 });\n };\n\n if (g.navigator !== undefined) {\n g.navigator.sendBeacon = (\n url: string | URL,\n data?: BodyInit | null,\n ): boolean => {\n captured.beaconCalls.push({\n url: typeof url === 'string' ? url : url.toString(),\n data,\n });\n return true;\n };\n }\n\n try {\n await body(captured);\n } finally {\n if (originalFetch === undefined) {\n delete g.fetch;\n } else {\n g.fetch = originalFetch;\n }\n if (g.navigator !== undefined) {\n if (originalBeacon === undefined) {\n delete g.navigator.sendBeacon;\n } else {\n g.navigator.sendBeacon = originalBeacon;\n }\n }\n }\n}\n\nfunction extractFetchUrl(input: RequestInfo | URL): string {\n if (typeof input === 'string') return input;\n if (input instanceof URL) return input.toString();\n // Request\n return input.url;\n}\n\nfunction allUrls(captured: CapturedCalls): string[] {\n return [\n ...captured.fetchCalls.map((c) => c.url),\n ...captured.beaconCalls.map((c) => c.url),\n ];\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Probe event + helpers\n// ──────────────────────────────────────────────────────────────────────\n\nconst PROBE_MESSAGE = 'FLSDK-transport-contract-probe-message';\n\nfunction makeProbeEvent(overrides: Partial<LogEvent> = {}): LogEvent {\n return {\n timestamp: new Date().toISOString(),\n level: 'info',\n message: PROBE_MESSAGE,\n attributes: {},\n context: {\n application: { name: 'transport-contract-probe' },\n environment: 'test',\n },\n ...overrides,\n };\n}\n\nasync function invokeSendSafely(\n transport: Transport,\n event: LogEvent,\n): Promise<void> {\n const result = transport.send(event);\n if (result instanceof Promise) {\n // Some intentionally-bad transports return rejected Promises; allow\n // the rejection to surface only as a contract-failure diagnostic,\n // not as an unhandled rejection in the test runner.\n await result.catch(() => undefined);\n }\n}\n\nfunction isAbsoluteUrl(url: string): boolean {\n // Match an URL scheme followed by `://` (http://, https://, ftp://, etc.)\n return /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url);\n}\n\nfunction fail(...args: unknown[]): Error;\nfunction fail(message: string): Error;\nfunction fail(transport: Transport, message: string, cause?: unknown): Error;\nfunction fail(...args: unknown[]): Error {\n let message: string;\n let cause: unknown;\n if (typeof args[0] === 'string') {\n message = `[assertTransportContract] ${args[0]}`;\n } else {\n const transport = args[0] as Transport;\n const msg = args[1] as string;\n cause = args[2];\n message = `[assertTransportContract] transport '${transport.name}': ${msg}`;\n }\n const err = new Error(message);\n if (cause !== undefined) {\n (err as Error & { cause?: unknown }).cause = cause;\n }\n return err;\n}\n"]}
@@ -0,0 +1,97 @@
1
+ import { T as Transport } from './types-D-xVvmvX.cjs';
2
+
3
+ /**
4
+ * `assertTransportContract(transport)` — runs the documented Transport
5
+ * contract battery against a consumer-provided `Transport`. Throws on the
6
+ * first violation with a clear diagnostic message.
7
+ *
8
+ * Covers `contracts/transport.md`:
9
+ * - Structural: `name: string`, `send(event)` exists
10
+ * - T-S1: no `LogEvent` data appears in any `fetch` URL or
11
+ * `navigator.sendBeacon` URL the transport produces
12
+ * - T-S2: cross-origin delivery uses request body (POST/PUT/PATCH JSON
13
+ * or a `sendBeacon` `Blob`) — never URL params
14
+ * - T-S3: any URL with a scheme uses `https:`
15
+ * - T-S4: the transport does NOT mutate the event it receives
16
+ * - T-S5: `flush()` and `shutdown()` are idempotent (safe to call > 1x)
17
+ *
18
+ * The helper temporarily monkey-patches `globalThis.fetch` and
19
+ * `globalThis.navigator.sendBeacon` to capture invocations, then restores
20
+ * the originals when each assertion completes — even on failure. Tests
21
+ * can run multiple consumer transports in series safely.
22
+ *
23
+ * This module is reached only via the package's `./testing` subpath; the
24
+ * runtime entry does NOT re-export it.
25
+ */
26
+
27
+ /**
28
+ * Run the full Transport contract battery against `transport`. Resolves
29
+ * when all assertions pass; rejects with an `Error` carrying a
30
+ * diagnostic message on the first failure.
31
+ */
32
+ declare function assertTransportContract(transport: Transport): Promise<void>;
33
+
34
+ /**
35
+ * Stable bag of secret-looking values for testing. Every value here is
36
+ * fake but shaped like the real thing — long enough that finding it in
37
+ * a URL, query string, log payload, etc. is meaningful evidence of a
38
+ * leak (no false positives from short substrings).
39
+ *
40
+ * Consumers (and the package's own tests T028, T041, T058) place these
41
+ * values in attributes/context/etc. then assert downstream sinks never
42
+ * see any of them.
43
+ *
44
+ * The keys mirror the documented default redaction denylist in
45
+ * `contracts/redaction.md` so any new key here should track a denylist
46
+ * entry — making the fixture the canonical "things the package promises
47
+ * to mask" reference for consumers.
48
+ */
49
+ /**
50
+ * Concrete shape of {@link makeSecretFixture}'s return value. Named
51
+ * `string` fields (rather than `Record<string, string>`) so callers get
52
+ * `string` — not `string | undefined` — per-field under the package's
53
+ * `noUncheckedIndexedAccess` setting. Not part of the public `/testing`
54
+ * surface; it only types the helper's return.
55
+ *
56
+ * Declared as a `type` (not an `interface`) on purpose: a type alias
57
+ * whose properties are all `string` gets an *implicit* index signature,
58
+ * so the fixture stays assignable to `Record<string, string>` / OTel
59
+ * `Attributes` — while `keyof SecretFixture` remains the precise named-key
60
+ * union, keeping dynamic `fixture[someKey]` access typed `string`.
61
+ */
62
+ type SecretFixture = {
63
+ readonly password: string;
64
+ readonly passwd: string;
65
+ readonly token: string;
66
+ readonly accessToken: string;
67
+ readonly refreshToken: string;
68
+ readonly bearerToken: string;
69
+ readonly authorization: string;
70
+ readonly auth: string;
71
+ readonly cookie: string;
72
+ readonly setCookie: string;
73
+ readonly secret: string;
74
+ readonly apiKey: string;
75
+ readonly sessionId: string;
76
+ readonly sid: string;
77
+ readonly ssn: string;
78
+ readonly creditCard: string;
79
+ readonly cardNumber: string;
80
+ readonly cvv: string;
81
+ readonly jwt: string;
82
+ };
83
+ /**
84
+ * Return a stable record of secret-looking values keyed by category.
85
+ * Values are deterministic across calls so tests can assert against
86
+ * exact strings. Never mutate the returned object across tests — call
87
+ * `makeSecretFixture()` again to get a fresh copy.
88
+ */
89
+ declare function makeSecretFixture(): SecretFixture;
90
+ /**
91
+ * The complete list of fixture VALUES. Useful when scanning an
92
+ * arbitrary string (e.g., a captured URL or POST body) for any leaked
93
+ * fixture: `if (FIXTURE_VALUES.some((v) => url.includes(v))) { leak! }`.
94
+ */
95
+ declare const FIXTURE_VALUES: ReadonlyArray<string>;
96
+
97
+ export { FIXTURE_VALUES, assertTransportContract, makeSecretFixture };
@@ -0,0 +1,97 @@
1
+ import { T as Transport } from './types-D-xVvmvX.js';
2
+
3
+ /**
4
+ * `assertTransportContract(transport)` — runs the documented Transport
5
+ * contract battery against a consumer-provided `Transport`. Throws on the
6
+ * first violation with a clear diagnostic message.
7
+ *
8
+ * Covers `contracts/transport.md`:
9
+ * - Structural: `name: string`, `send(event)` exists
10
+ * - T-S1: no `LogEvent` data appears in any `fetch` URL or
11
+ * `navigator.sendBeacon` URL the transport produces
12
+ * - T-S2: cross-origin delivery uses request body (POST/PUT/PATCH JSON
13
+ * or a `sendBeacon` `Blob`) — never URL params
14
+ * - T-S3: any URL with a scheme uses `https:`
15
+ * - T-S4: the transport does NOT mutate the event it receives
16
+ * - T-S5: `flush()` and `shutdown()` are idempotent (safe to call > 1x)
17
+ *
18
+ * The helper temporarily monkey-patches `globalThis.fetch` and
19
+ * `globalThis.navigator.sendBeacon` to capture invocations, then restores
20
+ * the originals when each assertion completes — even on failure. Tests
21
+ * can run multiple consumer transports in series safely.
22
+ *
23
+ * This module is reached only via the package's `./testing` subpath; the
24
+ * runtime entry does NOT re-export it.
25
+ */
26
+
27
+ /**
28
+ * Run the full Transport contract battery against `transport`. Resolves
29
+ * when all assertions pass; rejects with an `Error` carrying a
30
+ * diagnostic message on the first failure.
31
+ */
32
+ declare function assertTransportContract(transport: Transport): Promise<void>;
33
+
34
+ /**
35
+ * Stable bag of secret-looking values for testing. Every value here is
36
+ * fake but shaped like the real thing — long enough that finding it in
37
+ * a URL, query string, log payload, etc. is meaningful evidence of a
38
+ * leak (no false positives from short substrings).
39
+ *
40
+ * Consumers (and the package's own tests T028, T041, T058) place these
41
+ * values in attributes/context/etc. then assert downstream sinks never
42
+ * see any of them.
43
+ *
44
+ * The keys mirror the documented default redaction denylist in
45
+ * `contracts/redaction.md` so any new key here should track a denylist
46
+ * entry — making the fixture the canonical "things the package promises
47
+ * to mask" reference for consumers.
48
+ */
49
+ /**
50
+ * Concrete shape of {@link makeSecretFixture}'s return value. Named
51
+ * `string` fields (rather than `Record<string, string>`) so callers get
52
+ * `string` — not `string | undefined` — per-field under the package's
53
+ * `noUncheckedIndexedAccess` setting. Not part of the public `/testing`
54
+ * surface; it only types the helper's return.
55
+ *
56
+ * Declared as a `type` (not an `interface`) on purpose: a type alias
57
+ * whose properties are all `string` gets an *implicit* index signature,
58
+ * so the fixture stays assignable to `Record<string, string>` / OTel
59
+ * `Attributes` — while `keyof SecretFixture` remains the precise named-key
60
+ * union, keeping dynamic `fixture[someKey]` access typed `string`.
61
+ */
62
+ type SecretFixture = {
63
+ readonly password: string;
64
+ readonly passwd: string;
65
+ readonly token: string;
66
+ readonly accessToken: string;
67
+ readonly refreshToken: string;
68
+ readonly bearerToken: string;
69
+ readonly authorization: string;
70
+ readonly auth: string;
71
+ readonly cookie: string;
72
+ readonly setCookie: string;
73
+ readonly secret: string;
74
+ readonly apiKey: string;
75
+ readonly sessionId: string;
76
+ readonly sid: string;
77
+ readonly ssn: string;
78
+ readonly creditCard: string;
79
+ readonly cardNumber: string;
80
+ readonly cvv: string;
81
+ readonly jwt: string;
82
+ };
83
+ /**
84
+ * Return a stable record of secret-looking values keyed by category.
85
+ * Values are deterministic across calls so tests can assert against
86
+ * exact strings. Never mutate the returned object across tests — call
87
+ * `makeSecretFixture()` again to get a fresh copy.
88
+ */
89
+ declare function makeSecretFixture(): SecretFixture;
90
+ /**
91
+ * The complete list of fixture VALUES. Useful when scanning an
92
+ * arbitrary string (e.g., a captured URL or POST body) for any leaked
93
+ * fixture: `if (FIXTURE_VALUES.some((v) => url.includes(v))) { leak! }`.
94
+ */
95
+ declare const FIXTURE_VALUES: ReadonlyArray<string>;
96
+
97
+ export { FIXTURE_VALUES, assertTransportContract, makeSecretFixture };
@@ -0,0 +1,268 @@
1
+ // src/testing/secret-fixtures.ts
2
+ function makeSecretFixture() {
3
+ return {
4
+ password: "p4ssw0rd-correct-horse-battery-staple",
5
+ passwd: "p4ssw0rd-shadow-file-style",
6
+ token: "tok_AAAABBBBCCCCDDDD1234EEEEFFFFGGGG",
7
+ accessToken: "access_AAAA1111BBBB2222CCCC3333DDDD4444EEEE5555",
8
+ refreshToken: "refresh_FFFF6666GGGG7777HHHH8888IIII9999JJJJ0000",
9
+ bearerToken: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.bearerFixtureSignature",
10
+ authorization: "Basic dXNlcjpwYXNzOmZpeHR1cmUtbm90LXJlYWw",
11
+ auth: "auth_KKKK1111LLLL2222MMMM3333NNNN4444",
12
+ cookie: "sessionId=fixture-session-id-not-real-abc123; Secure; HttpOnly; SameSite=Strict",
13
+ setCookie: "auth=fixture-auth-cookie-not-real-xyz789; Path=/; Secure; HttpOnly",
14
+ secret: "sk_test_FIXTURE_4eC39HqLyjWDarjtT1zdp7dc_NOT_REAL",
15
+ apiKey: "pk_live_FIXTURE_ABCD1234EFGH5678IJKL9012MNOP_NOT_REAL",
16
+ sessionId: "sess_01HXYZ123ABCDEFGHIJKLMNOPQR",
17
+ sid: "sid_FIXTURE_QQQQ1111RRRR2222SSSS3333",
18
+ ssn: "123-45-6789",
19
+ creditCard: "4242 4242 4242 4242",
20
+ cardNumber: "5555555555554444",
21
+ cvv: "123",
22
+ jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.fixtureJwtSignatureNotReal"
23
+ };
24
+ }
25
+ var FIXTURE_VALUES = Object.values(
26
+ makeSecretFixture()
27
+ );
28
+
29
+ // src/testing/assert-transport-contract.ts
30
+ async function assertTransportContract(transport) {
31
+ assertStructural(transport);
32
+ await assertSendDoesNotThrow(transport);
33
+ await assertNoEventDataInURLs(transport);
34
+ await assertBodyOnlyDelivery(transport);
35
+ await assertHttpsForAbsoluteUrls(transport);
36
+ await assertEventImmutability(transport);
37
+ await assertFlushIdempotent(transport);
38
+ await assertShutdownIdempotent(transport);
39
+ }
40
+ function assertStructural(transport) {
41
+ if (typeof transport !== "object" || transport === null) {
42
+ throw fail("transport must be an object");
43
+ }
44
+ if (typeof transport.name !== "string" || transport.name.length === 0) {
45
+ throw fail("transport.name must be a non-empty string");
46
+ }
47
+ if (typeof transport.send !== "function") {
48
+ throw fail("transport.send must be a function");
49
+ }
50
+ }
51
+ async function assertSendDoesNotThrow(transport) {
52
+ const event = makeProbeEvent();
53
+ await withInterceptors(async () => {
54
+ try {
55
+ const result = transport.send(event);
56
+ if (result instanceof Promise) {
57
+ await result;
58
+ }
59
+ } catch (err) {
60
+ throw fail(
61
+ transport,
62
+ "send() threw to the caller \u2014 should fail silently or be wrapped",
63
+ err
64
+ );
65
+ }
66
+ });
67
+ }
68
+ async function assertNoEventDataInURLs(transport) {
69
+ const event = makeProbeEvent({ attributes: makeSecretFixture() });
70
+ await withInterceptors(async (captured) => {
71
+ await invokeSendSafely(transport, event);
72
+ for (const url of allUrls(captured)) {
73
+ const leakedValue = FIXTURE_VALUES.find((v) => url.includes(v));
74
+ if (leakedValue !== void 0) {
75
+ throw fail(
76
+ transport,
77
+ `URL contains a secret fixture value (T-S1 violation): url='${url}', leaked='${leakedValue}'`
78
+ );
79
+ }
80
+ if (url.includes(PROBE_MESSAGE)) {
81
+ throw fail(
82
+ transport,
83
+ `URL contains the probe event message (T-S1 violation): url='${url}'`
84
+ );
85
+ }
86
+ }
87
+ });
88
+ }
89
+ async function assertBodyOnlyDelivery(transport) {
90
+ const event = makeProbeEvent();
91
+ await withInterceptors(async (captured) => {
92
+ await invokeSendSafely(transport, event);
93
+ for (const call of captured.fetchCalls) {
94
+ const method = (call.init?.method ?? "GET").toUpperCase();
95
+ const allowedMethods = ["POST", "PUT", "PATCH"];
96
+ if (!allowedMethods.includes(method)) {
97
+ throw fail(
98
+ transport,
99
+ `fetch used HTTP method '${method}' for delivery \u2014 must use POST/PUT/PATCH with body (T-S2): url='${call.url}'`
100
+ );
101
+ }
102
+ if (call.init?.body === void 0 || call.init.body === null) {
103
+ throw fail(
104
+ transport,
105
+ `fetch was called with method='${method}' but no body \u2014 events must travel in the request body (T-S2): url='${call.url}'`
106
+ );
107
+ }
108
+ }
109
+ for (const call of captured.beaconCalls) {
110
+ if (call.data === null || call.data === void 0) {
111
+ throw fail(
112
+ transport,
113
+ `navigator.sendBeacon was called without data \u2014 events must travel in the body (T-S2): url='${call.url}'`
114
+ );
115
+ }
116
+ }
117
+ });
118
+ }
119
+ async function assertHttpsForAbsoluteUrls(transport) {
120
+ const event = makeProbeEvent();
121
+ await withInterceptors(async (captured) => {
122
+ await invokeSendSafely(transport, event);
123
+ for (const url of allUrls(captured)) {
124
+ if (!isAbsoluteUrl(url)) continue;
125
+ if (!url.toLowerCase().startsWith("https://")) {
126
+ throw fail(
127
+ transport,
128
+ `cross-origin URL is not HTTPS (T-S3): url='${url}'`
129
+ );
130
+ }
131
+ }
132
+ });
133
+ }
134
+ async function assertEventImmutability(transport) {
135
+ const event = makeProbeEvent({ attributes: { mutateMe: "before" } });
136
+ const before = JSON.stringify(event);
137
+ await withInterceptors(async () => {
138
+ await invokeSendSafely(transport, event);
139
+ });
140
+ const after = JSON.stringify(event);
141
+ if (before !== after) {
142
+ throw fail(
143
+ transport,
144
+ `transport mutated the received event (T-S4 violation):
145
+ before: ${before}
146
+ after: ${after}`
147
+ );
148
+ }
149
+ }
150
+ async function assertFlushIdempotent(transport) {
151
+ if (typeof transport.flush !== "function") return;
152
+ try {
153
+ await transport.flush();
154
+ await transport.flush();
155
+ await transport.flush();
156
+ } catch (err) {
157
+ throw fail(
158
+ transport,
159
+ "flush() is not idempotent \u2014 repeated calls must each resolve (T-S5)",
160
+ err
161
+ );
162
+ }
163
+ }
164
+ async function assertShutdownIdempotent(transport) {
165
+ if (typeof transport.shutdown !== "function") return;
166
+ try {
167
+ await transport.shutdown();
168
+ await transport.shutdown();
169
+ await transport.shutdown();
170
+ } catch (err) {
171
+ throw fail(
172
+ transport,
173
+ "shutdown() is not idempotent \u2014 repeated calls must each resolve (T-S5)",
174
+ err
175
+ );
176
+ }
177
+ }
178
+ async function withInterceptors(body) {
179
+ const captured = { fetchCalls: [], beaconCalls: [] };
180
+ const g = globalThis;
181
+ const originalFetch = g.fetch;
182
+ const originalBeacon = g.navigator?.sendBeacon?.bind(g.navigator);
183
+ g.fetch = async (input, init) => {
184
+ const url = extractFetchUrl(input);
185
+ captured.fetchCalls.push({ url, init });
186
+ return new Response("", { status: 204 });
187
+ };
188
+ if (g.navigator !== void 0) {
189
+ g.navigator.sendBeacon = (url, data) => {
190
+ captured.beaconCalls.push({
191
+ url: typeof url === "string" ? url : url.toString(),
192
+ data
193
+ });
194
+ return true;
195
+ };
196
+ }
197
+ try {
198
+ await body(captured);
199
+ } finally {
200
+ if (originalFetch === void 0) {
201
+ delete g.fetch;
202
+ } else {
203
+ g.fetch = originalFetch;
204
+ }
205
+ if (g.navigator !== void 0) {
206
+ if (originalBeacon === void 0) {
207
+ delete g.navigator.sendBeacon;
208
+ } else {
209
+ g.navigator.sendBeacon = originalBeacon;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ function extractFetchUrl(input) {
215
+ if (typeof input === "string") return input;
216
+ if (input instanceof URL) return input.toString();
217
+ return input.url;
218
+ }
219
+ function allUrls(captured) {
220
+ return [
221
+ ...captured.fetchCalls.map((c) => c.url),
222
+ ...captured.beaconCalls.map((c) => c.url)
223
+ ];
224
+ }
225
+ var PROBE_MESSAGE = "FLSDK-transport-contract-probe-message";
226
+ function makeProbeEvent(overrides = {}) {
227
+ return {
228
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
229
+ level: "info",
230
+ message: PROBE_MESSAGE,
231
+ attributes: {},
232
+ context: {
233
+ application: { name: "transport-contract-probe" },
234
+ environment: "test"
235
+ },
236
+ ...overrides
237
+ };
238
+ }
239
+ async function invokeSendSafely(transport, event) {
240
+ const result = transport.send(event);
241
+ if (result instanceof Promise) {
242
+ await result.catch(() => void 0);
243
+ }
244
+ }
245
+ function isAbsoluteUrl(url) {
246
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
247
+ }
248
+ function fail(...args) {
249
+ let message;
250
+ let cause;
251
+ if (typeof args[0] === "string") {
252
+ message = `[assertTransportContract] ${args[0]}`;
253
+ } else {
254
+ const transport = args[0];
255
+ const msg = args[1];
256
+ cause = args[2];
257
+ message = `[assertTransportContract] transport '${transport.name}': ${msg}`;
258
+ }
259
+ const err = new Error(message);
260
+ if (cause !== void 0) {
261
+ err.cause = cause;
262
+ }
263
+ return err;
264
+ }
265
+
266
+ export { FIXTURE_VALUES, assertTransportContract, makeSecretFixture };
267
+ //# sourceMappingURL=testing.mjs.map
268
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing/secret-fixtures.ts","../src/testing/assert-transport-contract.ts"],"names":[],"mappings":"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","file":"testing.mjs","sourcesContent":["/**\n * Stable bag of secret-looking values for testing. Every value here is\n * fake but shaped like the real thing — long enough that finding it in\n * a URL, query string, log payload, etc. is meaningful evidence of a\n * leak (no false positives from short substrings).\n *\n * Consumers (and the package's own tests T028, T041, T058) place these\n * values in attributes/context/etc. then assert downstream sinks never\n * see any of them.\n *\n * The keys mirror the documented default redaction denylist in\n * `contracts/redaction.md` so any new key here should track a denylist\n * entry — making the fixture the canonical \"things the package promises\n * to mask\" reference for consumers.\n */\n\n/**\n * Concrete shape of {@link makeSecretFixture}'s return value. Named\n * `string` fields (rather than `Record<string, string>`) so callers get\n * `string` — not `string | undefined` — per-field under the package's\n * `noUncheckedIndexedAccess` setting. Not part of the public `/testing`\n * surface; it only types the helper's return.\n *\n * Declared as a `type` (not an `interface`) on purpose: a type alias\n * whose properties are all `string` gets an *implicit* index signature,\n * so the fixture stays assignable to `Record<string, string>` / OTel\n * `Attributes` — while `keyof SecretFixture` remains the precise named-key\n * union, keeping dynamic `fixture[someKey]` access typed `string`.\n */\ntype SecretFixture = {\n readonly password: string;\n readonly passwd: string;\n readonly token: string;\n readonly accessToken: string;\n readonly refreshToken: string;\n readonly bearerToken: string;\n readonly authorization: string;\n readonly auth: string;\n readonly cookie: string;\n readonly setCookie: string;\n readonly secret: string;\n readonly apiKey: string;\n readonly sessionId: string;\n readonly sid: string;\n readonly ssn: string;\n readonly creditCard: string;\n readonly cardNumber: string;\n readonly cvv: string;\n readonly jwt: string;\n};\n\n/**\n * Return a stable record of secret-looking values keyed by category.\n * Values are deterministic across calls so tests can assert against\n * exact strings. Never mutate the returned object across tests — call\n * `makeSecretFixture()` again to get a fresh copy.\n */\nexport function makeSecretFixture(): SecretFixture {\n return {\n password: 'p4ssw0rd-correct-horse-battery-staple',\n passwd: 'p4ssw0rd-shadow-file-style',\n token: 'tok_AAAABBBBCCCCDDDD1234EEEEFFFFGGGG',\n accessToken: 'access_AAAA1111BBBB2222CCCC3333DDDD4444EEEE5555',\n refreshToken: 'refresh_FFFF6666GGGG7777HHHH8888IIII9999JJJJ0000',\n bearerToken:\n 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.bearerFixtureSignature',\n authorization: 'Basic dXNlcjpwYXNzOmZpeHR1cmUtbm90LXJlYWw',\n auth: 'auth_KKKK1111LLLL2222MMMM3333NNNN4444',\n cookie:\n 'sessionId=fixture-session-id-not-real-abc123; Secure; HttpOnly; SameSite=Strict',\n setCookie:\n 'auth=fixture-auth-cookie-not-real-xyz789; Path=/; Secure; HttpOnly',\n secret: 'sk_test_FIXTURE_4eC39HqLyjWDarjtT1zdp7dc_NOT_REAL',\n apiKey: 'pk_live_FIXTURE_ABCD1234EFGH5678IJKL9012MNOP_NOT_REAL',\n sessionId: 'sess_01HXYZ123ABCDEFGHIJKLMNOPQR',\n sid: 'sid_FIXTURE_QQQQ1111RRRR2222SSSS3333',\n ssn: '123-45-6789',\n creditCard: '4242 4242 4242 4242',\n cardNumber: '5555555555554444',\n cvv: '123',\n jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.fixtureJwtSignatureNotReal',\n };\n}\n\n/**\n * The complete list of fixture VALUES. Useful when scanning an\n * arbitrary string (e.g., a captured URL or POST body) for any leaked\n * fixture: `if (FIXTURE_VALUES.some((v) => url.includes(v))) { leak! }`.\n */\nexport const FIXTURE_VALUES: ReadonlyArray<string> = Object.values(\n makeSecretFixture(),\n);\n","/**\n * `assertTransportContract(transport)` — runs the documented Transport\n * contract battery against a consumer-provided `Transport`. Throws on the\n * first violation with a clear diagnostic message.\n *\n * Covers `contracts/transport.md`:\n * - Structural: `name: string`, `send(event)` exists\n * - T-S1: no `LogEvent` data appears in any `fetch` URL or\n * `navigator.sendBeacon` URL the transport produces\n * - T-S2: cross-origin delivery uses request body (POST/PUT/PATCH JSON\n * or a `sendBeacon` `Blob`) — never URL params\n * - T-S3: any URL with a scheme uses `https:`\n * - T-S4: the transport does NOT mutate the event it receives\n * - T-S5: `flush()` and `shutdown()` are idempotent (safe to call > 1x)\n *\n * The helper temporarily monkey-patches `globalThis.fetch` and\n * `globalThis.navigator.sendBeacon` to capture invocations, then restores\n * the originals when each assertion completes — even on failure. Tests\n * can run multiple consumer transports in series safely.\n *\n * This module is reached only via the package's `./testing` subpath; the\n * runtime entry does NOT re-export it.\n */\n\nimport type { LogEvent, Transport } from '../api/types.js';\nimport { FIXTURE_VALUES, makeSecretFixture } from './secret-fixtures.js';\n\n// ──────────────────────────────────────────────────────────────────────\n// Public entry point\n// ──────────────────────────────────────────────────────────────────────\n\n/**\n * Run the full Transport contract battery against `transport`. Resolves\n * when all assertions pass; rejects with an `Error` carrying a\n * diagnostic message on the first failure.\n */\nexport async function assertTransportContract(\n transport: Transport,\n): Promise<void> {\n assertStructural(transport);\n await assertSendDoesNotThrow(transport);\n await assertNoEventDataInURLs(transport);\n await assertBodyOnlyDelivery(transport);\n await assertHttpsForAbsoluteUrls(transport);\n await assertEventImmutability(transport);\n await assertFlushIdempotent(transport);\n await assertShutdownIdempotent(transport);\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Individual assertions\n// ──────────────────────────────────────────────────────────────────────\n\nfunction assertStructural(transport: Transport): void {\n if (typeof transport !== 'object' || transport === null) {\n throw fail('transport must be an object');\n }\n if (typeof transport.name !== 'string' || transport.name.length === 0) {\n throw fail('transport.name must be a non-empty string');\n }\n if (typeof transport.send !== 'function') {\n throw fail('transport.send must be a function');\n }\n}\n\nasync function assertSendDoesNotThrow(transport: Transport): Promise<void> {\n const event = makeProbeEvent();\n await withInterceptors(async () => {\n try {\n const result = transport.send(event);\n if (result instanceof Promise) {\n await result;\n }\n } catch (err) {\n throw fail(\n transport,\n 'send() threw to the caller — should fail silently or be wrapped',\n err,\n );\n }\n });\n}\n\nasync function assertNoEventDataInURLs(transport: Transport): Promise<void> {\n const event = makeProbeEvent({ attributes: makeSecretFixture() });\n await withInterceptors(async (captured) => {\n await invokeSendSafely(transport, event);\n\n for (const url of allUrls(captured)) {\n // 1. Any literal fixture value in the URL is a clear leak.\n const leakedValue = FIXTURE_VALUES.find((v) => url.includes(v));\n if (leakedValue !== undefined) {\n throw fail(\n transport,\n `URL contains a secret fixture value (T-S1 violation): ` +\n `url='${url}', leaked='${leakedValue}'`,\n );\n }\n // 2. The probe event's marker message in the URL also indicates\n // a leak — the consumer encoded event content there.\n if (url.includes(PROBE_MESSAGE)) {\n throw fail(\n transport,\n `URL contains the probe event message (T-S1 violation): ` +\n `url='${url}'`,\n );\n }\n }\n });\n}\n\nasync function assertBodyOnlyDelivery(transport: Transport): Promise<void> {\n const event = makeProbeEvent();\n await withInterceptors(async (captured) => {\n await invokeSendSafely(transport, event);\n\n for (const call of captured.fetchCalls) {\n const method = (call.init?.method ?? 'GET').toUpperCase();\n const allowedMethods = ['POST', 'PUT', 'PATCH'];\n if (!allowedMethods.includes(method)) {\n throw fail(\n transport,\n `fetch used HTTP method '${method}' for delivery — ` +\n `must use POST/PUT/PATCH with body (T-S2): url='${call.url}'`,\n );\n }\n if (call.init?.body === undefined || call.init.body === null) {\n throw fail(\n transport,\n `fetch was called with method='${method}' but no body — ` +\n `events must travel in the request body (T-S2): url='${call.url}'`,\n );\n }\n }\n\n for (const call of captured.beaconCalls) {\n if (call.data === null || call.data === undefined) {\n throw fail(\n transport,\n `navigator.sendBeacon was called without data — ` +\n `events must travel in the body (T-S2): url='${call.url}'`,\n );\n }\n }\n });\n}\n\nasync function assertHttpsForAbsoluteUrls(\n transport: Transport,\n): Promise<void> {\n const event = makeProbeEvent();\n await withInterceptors(async (captured) => {\n await invokeSendSafely(transport, event);\n\n for (const url of allUrls(captured)) {\n // Relative URLs are same-origin by definition and inherit the\n // page's scheme — skip them. Absolute URLs MUST be HTTPS.\n if (!isAbsoluteUrl(url)) continue;\n if (!url.toLowerCase().startsWith('https://')) {\n throw fail(\n transport,\n `cross-origin URL is not HTTPS (T-S3): url='${url}'`,\n );\n }\n }\n });\n}\n\nasync function assertEventImmutability(transport: Transport): Promise<void> {\n const event = makeProbeEvent({ attributes: { mutateMe: 'before' } });\n const before = JSON.stringify(event);\n await withInterceptors(async () => {\n await invokeSendSafely(transport, event);\n });\n const after = JSON.stringify(event);\n if (before !== after) {\n throw fail(\n transport,\n `transport mutated the received event (T-S4 violation):\\n` +\n ` before: ${before}\\n after: ${after}`,\n );\n }\n}\n\nasync function assertFlushIdempotent(transport: Transport): Promise<void> {\n if (typeof transport.flush !== 'function') return; // optional hook\n try {\n await transport.flush();\n await transport.flush();\n await transport.flush();\n } catch (err) {\n throw fail(\n transport,\n 'flush() is not idempotent — repeated calls must each resolve (T-S5)',\n err,\n );\n }\n}\n\nasync function assertShutdownIdempotent(transport: Transport): Promise<void> {\n if (typeof transport.shutdown !== 'function') return; // optional hook\n try {\n await transport.shutdown();\n await transport.shutdown();\n await transport.shutdown();\n } catch (err) {\n throw fail(\n transport,\n 'shutdown() is not idempotent — repeated calls must each resolve (T-S5)',\n err,\n );\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// fetch / sendBeacon interceptor\n// ──────────────────────────────────────────────────────────────────────\n\ninterface FetchCall {\n url: string;\n init: RequestInit | undefined;\n}\n\ninterface BeaconCall {\n url: string;\n data: BodyInit | null | undefined;\n}\n\ninterface CapturedCalls {\n fetchCalls: FetchCall[];\n beaconCalls: BeaconCall[];\n}\n\n/**\n * Run `body` with `globalThis.fetch` and `navigator.sendBeacon` replaced\n * by capturing stubs. Restores originals on success and failure.\n */\nasync function withInterceptors(\n body: (captured: CapturedCalls) => Promise<void>,\n): Promise<void> {\n const captured: CapturedCalls = { fetchCalls: [], beaconCalls: [] };\n\n const g = globalThis as unknown as {\n fetch?: typeof fetch;\n navigator?: { sendBeacon?: Navigator['sendBeacon'] };\n };\n\n const originalFetch = g.fetch;\n const originalBeacon = g.navigator?.sendBeacon?.bind(g.navigator);\n\n g.fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n const url = extractFetchUrl(input);\n captured.fetchCalls.push({ url, init });\n return new Response('', { status: 204 });\n };\n\n if (g.navigator !== undefined) {\n g.navigator.sendBeacon = (\n url: string | URL,\n data?: BodyInit | null,\n ): boolean => {\n captured.beaconCalls.push({\n url: typeof url === 'string' ? url : url.toString(),\n data,\n });\n return true;\n };\n }\n\n try {\n await body(captured);\n } finally {\n if (originalFetch === undefined) {\n delete g.fetch;\n } else {\n g.fetch = originalFetch;\n }\n if (g.navigator !== undefined) {\n if (originalBeacon === undefined) {\n delete g.navigator.sendBeacon;\n } else {\n g.navigator.sendBeacon = originalBeacon;\n }\n }\n }\n}\n\nfunction extractFetchUrl(input: RequestInfo | URL): string {\n if (typeof input === 'string') return input;\n if (input instanceof URL) return input.toString();\n // Request\n return input.url;\n}\n\nfunction allUrls(captured: CapturedCalls): string[] {\n return [\n ...captured.fetchCalls.map((c) => c.url),\n ...captured.beaconCalls.map((c) => c.url),\n ];\n}\n\n// ──────────────────────────────────────────────────────────────────────\n// Probe event + helpers\n// ──────────────────────────────────────────────────────────────────────\n\nconst PROBE_MESSAGE = 'FLSDK-transport-contract-probe-message';\n\nfunction makeProbeEvent(overrides: Partial<LogEvent> = {}): LogEvent {\n return {\n timestamp: new Date().toISOString(),\n level: 'info',\n message: PROBE_MESSAGE,\n attributes: {},\n context: {\n application: { name: 'transport-contract-probe' },\n environment: 'test',\n },\n ...overrides,\n };\n}\n\nasync function invokeSendSafely(\n transport: Transport,\n event: LogEvent,\n): Promise<void> {\n const result = transport.send(event);\n if (result instanceof Promise) {\n // Some intentionally-bad transports return rejected Promises; allow\n // the rejection to surface only as a contract-failure diagnostic,\n // not as an unhandled rejection in the test runner.\n await result.catch(() => undefined);\n }\n}\n\nfunction isAbsoluteUrl(url: string): boolean {\n // Match an URL scheme followed by `://` (http://, https://, ftp://, etc.)\n return /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url);\n}\n\nfunction fail(...args: unknown[]): Error;\nfunction fail(message: string): Error;\nfunction fail(transport: Transport, message: string, cause?: unknown): Error;\nfunction fail(...args: unknown[]): Error {\n let message: string;\n let cause: unknown;\n if (typeof args[0] === 'string') {\n message = `[assertTransportContract] ${args[0]}`;\n } else {\n const transport = args[0] as Transport;\n const msg = args[1] as string;\n cause = args[2];\n message = `[assertTransportContract] transport '${transport.name}': ${msg}`;\n }\n const err = new Error(message);\n if (cause !== undefined) {\n (err as Error & { cause?: unknown }).cause = cause;\n }\n return err;\n}\n"]}