@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.
- package/LICENSE +21 -0
- package/README.md +345 -0
- package/dist/index.cjs +1240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +167 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.mjs +1232 -0
- package/dist/index.mjs.map +1 -0
- package/dist/testing.cjs +272 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +97 -0
- package/dist/testing.d.ts +97 -0
- package/dist/testing.mjs +268 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/transport-beacon.cjs +452 -0
- package/dist/transport-beacon.cjs.map +1 -0
- package/dist/transport-beacon.d.cts +68 -0
- package/dist/transport-beacon.d.ts +68 -0
- package/dist/transport-beacon.mjs +450 -0
- package/dist/transport-beacon.mjs.map +1 -0
- package/dist/types-D-xVvmvX.d.cts +227 -0
- package/dist/types-D-xVvmvX.d.ts +227 -0
- package/package.json +79 -0
|
@@ -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 };
|
package/dist/testing.mjs
ADDED
|
@@ -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"]}
|