@tallyrow/safesignal 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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"]}
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(transport: Transport): 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"]}
@@ -1,4 +1,4 @@
1
- import { T as Transport } from './types-D-xVvmvX.cjs';
1
+ import { j as Transport } from './types-BiRyHi1e.cjs';
2
2
 
3
3
  /**
4
4
  * `assertTransportContract(transport)` — runs the documented Transport
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as Transport } from './types-D-xVvmvX.js';
1
+ import { j as Transport } from './types-BiRyHi1e.js';
2
2
 
3
3
  /**
4
4
  * `assertTransportContract(transport)` — runs the documented Transport
@@ -1 +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"]}
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(transport: Transport): 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"]}
@@ -55,23 +55,37 @@ function createBatcher(opts) {
55
55
  };
56
56
  }
57
57
 
58
- // src/transport-beacon/errors.ts
59
- var BeaconError = class extends Error {
60
- constructor(code, transportName, message, cause) {
61
- super(message);
62
- this.name = "BeaconError";
63
- this.code = code;
64
- this.transportName = transportName;
65
- if (cause !== void 0) {
66
- Object.defineProperty(this, "cause", {
67
- value: cause,
68
- enumerable: true,
69
- writable: false,
70
- configurable: false
71
- });
72
- }
58
+ // src/transport-beacon/delivery.ts
59
+ var BEACON_SIZE_LIMIT_BYTES = 65536;
60
+ function getPayloadByteLength(payload) {
61
+ return new TextEncoder().encode(payload).length;
62
+ }
63
+ function tryBeacon(endpoint, payload) {
64
+ const nav = globalThis.navigator;
65
+ if (nav === void 0 || typeof nav.sendBeacon !== "function") {
66
+ return false;
73
67
  }
74
- };
68
+ try {
69
+ const blob = new Blob([payload], { type: "application/json" });
70
+ return nav.sendBeacon(endpoint, blob);
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ async function tryFetchKeepalive(endpoint, payload) {
76
+ const fetchFn = globalThis.fetch;
77
+ if (typeof fetchFn !== "function") {
78
+ return false;
79
+ }
80
+ const response = await fetchFn(endpoint, {
81
+ method: "POST",
82
+ body: payload,
83
+ headers: { "content-type": "application/json" },
84
+ keepalive: true,
85
+ credentials: "same-origin"
86
+ });
87
+ return response.ok;
88
+ }
75
89
 
76
90
  // src/transport-beacon/endpoint-validation.ts
77
91
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
@@ -118,37 +132,23 @@ function typeName(value) {
118
132
  return typeof value;
119
133
  }
120
134
 
121
- // src/transport-beacon/delivery.ts
122
- var BEACON_SIZE_LIMIT_BYTES = 65536;
123
- function getPayloadByteLength(payload) {
124
- return new TextEncoder().encode(payload).length;
125
- }
126
- function tryBeacon(endpoint, payload) {
127
- const nav = globalThis.navigator;
128
- if (nav === void 0 || typeof nav.sendBeacon !== "function") {
129
- return false;
130
- }
131
- try {
132
- const blob = new Blob([payload], { type: "application/json" });
133
- return nav.sendBeacon(endpoint, blob);
134
- } catch {
135
- return false;
136
- }
137
- }
138
- async function tryFetchKeepalive(endpoint, payload) {
139
- const fetchFn = globalThis.fetch;
140
- if (typeof fetchFn !== "function") {
141
- return false;
135
+ // src/transport-beacon/errors.ts
136
+ var BeaconError = class extends Error {
137
+ constructor(code, transportName, message, cause) {
138
+ super(message);
139
+ this.name = "BeaconError";
140
+ this.code = code;
141
+ this.transportName = transportName;
142
+ if (cause !== void 0) {
143
+ Object.defineProperty(this, "cause", {
144
+ value: cause,
145
+ enumerable: true,
146
+ writable: false,
147
+ configurable: false
148
+ });
149
+ }
142
150
  }
143
- const response = await fetchFn(endpoint, {
144
- method: "POST",
145
- body: payload,
146
- headers: { "content-type": "application/json" },
147
- keepalive: true,
148
- credentials: "same-origin"
149
- });
150
- return response.ok;
151
- }
151
+ };
152
152
 
153
153
  // src/transport-beacon/lifecycle.ts
154
154
  function installPagehideHandler(handler) {
@@ -173,7 +173,9 @@ function validateOptions(options) {
173
173
  }
174
174
  if (options.batching !== void 0) {
175
175
  if (typeof options.batching !== "object" || options.batching === null) {
176
- throw new TypeError("beacon transport: options.batching must be an object");
176
+ throw new TypeError(
177
+ "beacon transport: options.batching must be an object"
178
+ );
177
179
  }
178
180
  const { maxBatchSize, maxBatchAgeMs } = options.batching;
179
181
  if (!Number.isInteger(maxBatchSize) || maxBatchSize < 1 || maxBatchSize > 1e3) {
@@ -241,7 +243,10 @@ function notifyBatchDrop(state, droppedCount, reason, cause) {
241
243
  function createBeaconTransport(options) {
242
244
  validateOptions(options);
243
245
  const allowInsecureLoopback = options.allowInsecureLoopback ?? false;
244
- validateEndpoint(options.endpoint, allowInsecureLoopback);
246
+ validateEndpoint(
247
+ options.endpoint,
248
+ allowInsecureLoopback
249
+ );
245
250
  const state = {
246
251
  endpoint: options.endpoint,
247
252
  name: options.name ?? "beacon",
@@ -384,7 +389,12 @@ function createBeaconTransport(options) {
384
389
  try {
385
390
  envelope = JSON.stringify({ events });
386
391
  } catch (cause) {
387
- notifyBatchDrop(state2, events.length, "JSON.stringify threw on the envelope", cause);
392
+ notifyBatchDrop(
393
+ state2,
394
+ events.length,
395
+ "JSON.stringify threw on the envelope",
396
+ cause
397
+ );
388
398
  return;
389
399
  }
390
400
  const bytes = getPayloadByteLength(envelope);
@@ -420,7 +430,12 @@ function createBeaconTransport(options) {
420
430
  }
421
431
  },
422
432
  (cause) => {
423
- notifyBatchDrop(state2, events.length, `fetch fallback rejected`, cause);
433
+ notifyBatchDrop(
434
+ state2,
435
+ events.length,
436
+ `fetch fallback rejected`,
437
+ cause
438
+ );
424
439
  }
425
440
  );
426
441
  return;