@graphorin/observability 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +115 -0
  4. package/dist/cost/cost-tracker.d.ts +28 -0
  5. package/dist/cost/cost-tracker.d.ts.map +1 -0
  6. package/dist/cost/cost-tracker.js +169 -0
  7. package/dist/cost/cost-tracker.js.map +1 -0
  8. package/dist/cost/index.d.ts +3 -0
  9. package/dist/cost/index.js +3 -0
  10. package/dist/cost/types.d.ts +104 -0
  11. package/dist/cost/types.d.ts.map +1 -0
  12. package/dist/eval/index.d.ts +3 -0
  13. package/dist/eval/index.js +3 -0
  14. package/dist/eval/runner.d.ts +11 -0
  15. package/dist/eval/runner.d.ts.map +1 -0
  16. package/dist/eval/runner.js +113 -0
  17. package/dist/eval/runner.js.map +1 -0
  18. package/dist/eval/types.d.ts +108 -0
  19. package/dist/eval/types.d.ts.map +1 -0
  20. package/dist/exporters/console.d.ts +27 -0
  21. package/dist/exporters/console.d.ts.map +1 -0
  22. package/dist/exporters/console.js +47 -0
  23. package/dist/exporters/console.js.map +1 -0
  24. package/dist/exporters/index.d.ts +6 -0
  25. package/dist/exporters/index.js +7 -0
  26. package/dist/exporters/jsonl.d.ts +57 -0
  27. package/dist/exporters/jsonl.d.ts.map +1 -0
  28. package/dist/exporters/jsonl.js +95 -0
  29. package/dist/exporters/jsonl.js.map +1 -0
  30. package/dist/exporters/otlp-http.d.ts +38 -0
  31. package/dist/exporters/otlp-http.d.ts.map +1 -0
  32. package/dist/exporters/otlp-http.js +115 -0
  33. package/dist/exporters/otlp-http.js.map +1 -0
  34. package/dist/exporters/types.d.ts +72 -0
  35. package/dist/exporters/types.d.ts.map +1 -0
  36. package/dist/exporters/types.js +13 -0
  37. package/dist/exporters/types.js.map +1 -0
  38. package/dist/exporters/with-validation.d.ts +45 -0
  39. package/dist/exporters/with-validation.d.ts.map +1 -0
  40. package/dist/exporters/with-validation.js +99 -0
  41. package/dist/exporters/with-validation.js.map +1 -0
  42. package/dist/gen-ai/emit.d.ts +27 -0
  43. package/dist/gen-ai/emit.d.ts.map +1 -0
  44. package/dist/gen-ai/emit.js +88 -0
  45. package/dist/gen-ai/emit.js.map +1 -0
  46. package/dist/gen-ai/index.d.ts +5 -0
  47. package/dist/gen-ai/index.js +5 -0
  48. package/dist/gen-ai/operation-mapping.d.ts +24 -0
  49. package/dist/gen-ai/operation-mapping.d.ts.map +1 -0
  50. package/dist/gen-ai/operation-mapping.js +67 -0
  51. package/dist/gen-ai/operation-mapping.js.map +1 -0
  52. package/dist/gen-ai/system-derivation.d.ts +41 -0
  53. package/dist/gen-ai/system-derivation.d.ts.map +1 -0
  54. package/dist/gen-ai/system-derivation.js +67 -0
  55. package/dist/gen-ai/system-derivation.js.map +1 -0
  56. package/dist/gen-ai/types.d.ts +87 -0
  57. package/dist/gen-ai/types.d.ts.map +1 -0
  58. package/dist/index.d.ts +58 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +60 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/logger/index.d.ts +2 -0
  63. package/dist/logger/index.js +3 -0
  64. package/dist/logger/logger.d.ts +53 -0
  65. package/dist/logger/logger.d.ts.map +1 -0
  66. package/dist/logger/logger.js +145 -0
  67. package/dist/logger/logger.js.map +1 -0
  68. package/dist/openinference/index.d.ts +45 -0
  69. package/dist/openinference/index.d.ts.map +1 -0
  70. package/dist/openinference/index.js +92 -0
  71. package/dist/openinference/index.js.map +1 -0
  72. package/dist/redaction/config.d.ts +39 -0
  73. package/dist/redaction/config.d.ts.map +1 -0
  74. package/dist/redaction/config.js +15 -0
  75. package/dist/redaction/config.js.map +1 -0
  76. package/dist/redaction/errors.d.ts +36 -0
  77. package/dist/redaction/errors.d.ts.map +1 -0
  78. package/dist/redaction/errors.js +42 -0
  79. package/dist/redaction/errors.js.map +1 -0
  80. package/dist/redaction/imperative-patterns.d.ts +108 -0
  81. package/dist/redaction/imperative-patterns.d.ts.map +1 -0
  82. package/dist/redaction/imperative-patterns.js +187 -0
  83. package/dist/redaction/imperative-patterns.js.map +1 -0
  84. package/dist/redaction/index.d.ts +7 -0
  85. package/dist/redaction/index.js +7 -0
  86. package/dist/redaction/patterns.d.ts +85 -0
  87. package/dist/redaction/patterns.d.ts.map +1 -0
  88. package/dist/redaction/patterns.js +172 -0
  89. package/dist/redaction/patterns.js.map +1 -0
  90. package/dist/redaction/types.d.ts +103 -0
  91. package/dist/redaction/types.d.ts.map +1 -0
  92. package/dist/redaction/validator.d.ts +26 -0
  93. package/dist/redaction/validator.d.ts.map +1 -0
  94. package/dist/redaction/validator.js +195 -0
  95. package/dist/redaction/validator.js.map +1 -0
  96. package/dist/replay/config.d.ts +56 -0
  97. package/dist/replay/config.d.ts.map +1 -0
  98. package/dist/replay/config.js +18 -0
  99. package/dist/replay/config.js.map +1 -0
  100. package/dist/replay/index.d.ts +5 -0
  101. package/dist/replay/index.js +5 -0
  102. package/dist/replay/log.d.ts +39 -0
  103. package/dist/replay/log.d.ts.map +1 -0
  104. package/dist/replay/log.js +59 -0
  105. package/dist/replay/log.js.map +1 -0
  106. package/dist/replay/replay.d.ts +25 -0
  107. package/dist/replay/replay.d.ts.map +1 -0
  108. package/dist/replay/replay.js +173 -0
  109. package/dist/replay/replay.js.map +1 -0
  110. package/dist/replay/types.d.ts +110 -0
  111. package/dist/replay/types.d.ts.map +1 -0
  112. package/dist/telemetry/index.d.ts +63 -0
  113. package/dist/telemetry/index.d.ts.map +1 -0
  114. package/dist/telemetry/index.js +49 -0
  115. package/dist/telemetry/index.js.map +1 -0
  116. package/dist/tracer/ids.d.ts +23 -0
  117. package/dist/tracer/ids.d.ts.map +1 -0
  118. package/dist/tracer/ids.js +30 -0
  119. package/dist/tracer/ids.js.map +1 -0
  120. package/dist/tracer/index.d.ts +6 -0
  121. package/dist/tracer/index.js +6 -0
  122. package/dist/tracer/sampling.d.ts +77 -0
  123. package/dist/tracer/sampling.d.ts.map +1 -0
  124. package/dist/tracer/sampling.js +67 -0
  125. package/dist/tracer/sampling.js.map +1 -0
  126. package/dist/tracer/span-names.d.ts +11 -0
  127. package/dist/tracer/span-names.d.ts.map +1 -0
  128. package/dist/tracer/span-names.js +11 -0
  129. package/dist/tracer/span-names.js.map +1 -0
  130. package/dist/tracer/span.d.ts +27 -0
  131. package/dist/tracer/span.d.ts.map +1 -0
  132. package/dist/tracer/span.js +80 -0
  133. package/dist/tracer/span.js.map +1 -0
  134. package/dist/tracer/tracer.d.ts +101 -0
  135. package/dist/tracer/tracer.d.ts.map +1 -0
  136. package/dist/tracer/tracer.js +150 -0
  137. package/dist/tracer/tracer.js.map +1 -0
  138. package/package.json +138 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patterns.js","names":["PATTERNS: readonly RedactionPattern[]","BUILT_IN_PATTERNS: readonly RedactionPattern[]","OPT_IN_PATTERNS: readonly RedactionPattern[]","ALL_BUILT_IN_PATTERNS: readonly RedactionPattern[]"],"sources":["../../src/redaction/patterns.ts"],"sourcesContent":["/**\n * Built-in PII / secret detection patterns. The catalogue is intentionally\n * conservative — every pattern has both positive and negative test\n * fixtures and is documented so operators understand exactly what is\n * matched.\n *\n * The catalogue is split into two groups:\n *\n * - **secret** — credentials, API tokens, JWTs, private keys. Matches\n * are always dropped + counted, regardless of the configured tier\n * floor.\n * - **pii** — email / phone / IBAN / credit card / SSN / IP address.\n * Subject to the configured tier floor + per-pattern enable / disable\n * knobs.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable pattern identifier. The catalogue is curated; user-supplied\n * patterns can use any identifier they want and will be passed through\n * the validator in addition to the built-ins.\n *\n * @stable\n */\nexport type BuiltInPatternName =\n | 'graphorin-token'\n | 'openai-key'\n | 'anthropic-key'\n | 'aws-access-key'\n | 'gcp-service-account'\n | 'github-pat'\n | 'jwt'\n | 'bearer-header'\n | 'basic-auth'\n | 'private-key-pem'\n | 'email'\n | 'creditcard'\n | 'us-ssn'\n | 'phone-e164'\n | 'iban'\n | 'ipv4'\n | 'ipv6';\n\n/**\n * Pattern category — `secret` matches always force a drop; `pii`\n * matches respect the configured `enabledPatterns` allow-list.\n *\n * @stable\n */\nexport type PatternCategory = 'secret' | 'pii';\n\n/**\n * One entry in the redaction catalogue.\n *\n * @stable\n */\nexport interface RedactionPattern {\n readonly name: string;\n readonly category: PatternCategory;\n readonly description: string;\n readonly regex: RegExp;\n /** Replacement string used when `mode === 'mask'`. */\n readonly mask?: string;\n /**\n * Optional per-match predicate (RP-21). When present, a regex hit is only\n * treated as a real match — and masked — when this returns `true` for the\n * matched substring. Used by the `creditcard` pattern to require a valid\n * Luhn checksum so look-alike digit runs (epoch-ms timestamps, order ids)\n * are not corrupted.\n */\n readonly verify?: (match: string) => boolean;\n /**\n * Optional opt-in flag. When `true` the pattern is **not** active by\n * default; operators must add it to `enabledPatterns` explicitly. Used\n * by the IPv4 / IPv6 patterns because raw IPs frequently appear in\n * non-PII log lines (host headers, debug traces, …).\n */\n readonly optIn?: boolean;\n}\n\nconst PATTERNS: readonly RedactionPattern[] = [\n {\n name: 'graphorin-token',\n category: 'secret',\n description: 'Graphorin server token (`kru_<env>_v1_<entropy>_<crc32>`).',\n regex: /kru_(?:dev|test|prod)_v1_[A-Za-z0-9]{20,80}_[A-Za-z0-9]{6}/g,\n mask: '[REDACTED graphorin-token]',\n },\n {\n name: 'openai-key',\n category: 'secret',\n description: 'OpenAI API key (`sk-...`).',\n regex: /\\bsk-[A-Za-z0-9_-]{20,}\\b/g,\n mask: '[REDACTED openai-key]',\n },\n {\n name: 'anthropic-key',\n category: 'secret',\n description: 'Anthropic API key (`sk-ant-...`).',\n regex: /\\bsk-ant-[A-Za-z0-9_-]{20,}\\b/g,\n mask: '[REDACTED anthropic-key]',\n },\n {\n name: 'aws-access-key',\n category: 'secret',\n description: 'AWS access-key id (`AKIA...`).',\n regex: /\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g,\n mask: '[REDACTED aws-access-key]',\n },\n {\n name: 'gcp-service-account',\n category: 'secret',\n description:\n 'GCP service-account email (`...@...iam.gserviceaccount.com`). Opt-in; the `email` pattern already redacts the address shape; this entry exists for operators wanting the explicit `gcp-service-account` counter label.',\n regex: /[A-Za-z0-9._-]+@[A-Za-z0-9-]+\\.iam\\.gserviceaccount\\.com\\b/g,\n mask: '[REDACTED gcp-service-account]',\n optIn: true,\n },\n {\n name: 'github-pat',\n category: 'secret',\n description: 'GitHub personal-access token / OAuth token / app token.',\n regex: /\\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,255}\\b/g,\n mask: '[REDACTED github-pat]',\n },\n {\n name: 'jwt',\n category: 'secret',\n description: 'JSON Web Token (`eyJ...`).',\n regex: /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\b/g,\n mask: '[REDACTED jwt]',\n },\n {\n name: 'bearer-header',\n category: 'secret',\n description: '`Authorization: Bearer ...` header values.',\n regex: /\\bBearer\\s+[A-Za-z0-9._-]{16,}\\b/gi,\n mask: 'Bearer [REDACTED]',\n },\n {\n name: 'basic-auth',\n category: 'secret',\n description: '`Authorization: Basic ...` header values.',\n regex: /\\bBasic\\s+[A-Za-z0-9+/=]{16,}\\b/gi,\n mask: 'Basic [REDACTED]',\n },\n {\n name: 'private-key-pem',\n category: 'secret',\n description: 'PEM private-key block (`-----BEGIN ... PRIVATE KEY-----`).',\n regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,\n mask: '[REDACTED private-key-pem]',\n },\n {\n name: 'email',\n category: 'pii',\n description: 'RFC-5322-ish email address.',\n regex: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n mask: '[REDACTED email]',\n },\n {\n name: 'creditcard',\n category: 'pii',\n description: 'Credit card number (13–19 digits, optional spaces / dashes; Luhn-checked).',\n regex: /\\b(?:\\d[\\s-]*?){13,19}\\b/g,\n mask: '[REDACTED creditcard]',\n // RP-21: require a valid Luhn checksum so a 13–19 digit run that is not a\n // real PAN (millisecond epoch timestamps, order numbers, …) is left alone.\n verify: isLuhnValid,\n },\n {\n name: 'us-ssn',\n category: 'pii',\n description: 'US Social Security Number (`123-45-6789`).',\n regex: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\n mask: '[REDACTED us-ssn]',\n },\n {\n name: 'phone-e164',\n category: 'pii',\n description: 'E.164 phone number (`+11234567890`).',\n regex: /\\+\\d{7,15}\\b/g,\n mask: '[REDACTED phone-e164]',\n },\n {\n name: 'iban',\n category: 'pii',\n description: 'IBAN (2-letter country + 2 check digits + up to 30 alphanumerics).',\n regex: /\\b[A-Z]{2}\\d{2}[A-Z0-9]{11,30}\\b/g,\n mask: '[REDACTED iban]',\n },\n {\n name: 'ipv4',\n category: 'pii',\n description: 'IPv4 dotted-quad address. Opt-in (often appears legitimately in logs).',\n regex: /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g,\n mask: '[REDACTED ipv4]',\n optIn: true,\n },\n {\n name: 'ipv6',\n category: 'pii',\n description: 'IPv6 address (full / compressed). Opt-in.',\n regex: /\\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\\b/g,\n mask: '[REDACTED ipv6]',\n optIn: true,\n },\n];\n\n/**\n * The 14 default-on built-in patterns (the IPv4 and IPv6 detectors are\n * opt-in and live in {@link OPT_IN_PATTERNS}).\n *\n * @stable\n */\nexport const BUILT_IN_PATTERNS: readonly RedactionPattern[] = PATTERNS.filter(\n (p) => p.optIn !== true,\n);\n\n/**\n * Patterns that are recognised by the validator but are NOT enabled by\n * default. Use them via `patterns: [...BUILT_IN_PATTERNS, ...OPT_IN_PATTERNS]`.\n *\n * @stable\n */\nexport const OPT_IN_PATTERNS: readonly RedactionPattern[] = PATTERNS.filter(\n (p) => p.optIn === true,\n);\n\n/**\n * Full registry — for tooling that wants to introspect every pattern\n * the framework knows about (e.g. CLI `graphorin redaction list`).\n *\n * @stable\n */\nexport const ALL_BUILT_IN_PATTERNS: readonly RedactionPattern[] = PATTERNS;\n\n/**\n * Luhn (mod-10) checksum validator used by the `creditcard` pattern (RP-21).\n * Strips spaces / dashes, bounds the length to 13–19 digits, and verifies the\n * checksum so a digit run that merely *looks* like a PAN is not redacted.\n */\nfunction isLuhnValid(value: string): boolean {\n const digits = value.replace(/\\D/g, '');\n if (digits.length < 13 || digits.length > 19) return false;\n let sum = 0;\n let double = false;\n for (let i = digits.length - 1; i >= 0; i -= 1) {\n let d = digits.charCodeAt(i) - 48; // '0'\n if (double) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n double = !double;\n }\n return sum % 10 === 0;\n}\n"],"mappings":";AAiFA,MAAMA,WAAwC;CAC5C;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aACE;EACF,OAAO;EACP,MAAM;EACN,OAAO;EACR;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EAGN,QAAQ;EACT;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACN,OAAO;EACR;CACD;EACE,MAAM;EACN,UAAU;EACV,aAAa;EACb,OAAO;EACP,MAAM;EACN,OAAO;EACR;CACF;;;;;;;AAQD,MAAaC,oBAAiD,SAAS,QACpE,MAAM,EAAE,UAAU,KACpB;;;;;;;AAQD,MAAaC,kBAA+C,SAAS,QAClE,MAAM,EAAE,UAAU,KACpB;;;;;;;AAQD,MAAaC,wBAAqD;;;;;;AAOlE,SAAS,YAAY,OAAwB;CAC3C,MAAM,SAAS,MAAM,QAAQ,OAAO,GAAG;AACvC,KAAI,OAAO,SAAS,MAAM,OAAO,SAAS,GAAI,QAAO;CACrD,IAAI,MAAM;CACV,IAAI,SAAS;AACb,MAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;EAC9C,IAAI,IAAI,OAAO,WAAW,EAAE,GAAG;AAC/B,MAAI,QAAQ;AACV,QAAK;AACL,OAAI,IAAI,EAAG,MAAK;;AAElB,SAAO;AACP,WAAS,CAAC;;AAEZ,QAAO,MAAM,OAAO"}
@@ -0,0 +1,103 @@
1
+ import { RedactionPattern } from "./patterns.js";
2
+ import { RedactionInput, RedactionInput as RedactionInput$1, RedactionOutput, RedactionOutput as RedactionOutput$1, RedactionValidator, RedactionValidator as RedactionValidator$1 } from "@graphorin/core";
3
+
4
+ //#region src/redaction/types.d.ts
5
+
6
+ /**
7
+ * Optional sink invoked every time the validator drops a value or
8
+ * masks a pattern. Useful for emitting custom metrics, audit entries,
9
+ * or alert hooks. The callback receives only sanitized data — secret
10
+ * values are never forwarded.
11
+ *
12
+ * @stable
13
+ */
14
+ type RedactionViolationCallback = (violation: RedactionViolation) => void;
15
+ /**
16
+ * Sanitized record describing a single redaction event. Never carries
17
+ * the secret value itself; only metadata that is safe to log.
18
+ *
19
+ * @stable
20
+ */
21
+ interface RedactionViolation {
22
+ readonly reason: 'sensitivity-tier-exceeded' | 'pii-pattern-match' | 'secret-pattern-match' | 'unredacted-secret-value' | 'invalid-input';
23
+ readonly attribute?: string;
24
+ readonly spanType?: string;
25
+ readonly origin?: string;
26
+ /** Pattern names that fired (if applicable). */
27
+ readonly patterns?: ReadonlyArray<string>;
28
+ /** Tier declared by the upstream caller. */
29
+ readonly declaredTier?: RedactionInput['tier'];
30
+ }
31
+ /**
32
+ * Counter exposed via {@link createRedactionValidator}. Implementations
33
+ * keep counters in-memory; downstream code can scrape them and convert
34
+ * to Prometheus metrics.
35
+ *
36
+ * @stable
37
+ */
38
+ interface RedactionCounters {
39
+ /** Total values dropped by the validator. */
40
+ droppedTotal: number;
41
+ /** Drops by reason; the four built-in reasons + any custom ones. */
42
+ droppedByReason: Readonly<Record<string, number>>;
43
+ /** Pattern-matching counters keyed by pattern name. */
44
+ matchesByPattern: Readonly<Record<string, number>>;
45
+ }
46
+ /**
47
+ * Configuration shape for {@link createRedactionValidator}.
48
+ *
49
+ * @stable
50
+ */
51
+ interface RedactionValidatorOptions {
52
+ /** Identifier reported via `validator.id`. Defaults to `'default'`. */
53
+ readonly id?: string;
54
+ /** Lowest tier that may pass through the validator. Default: `'public'`. */
55
+ readonly minTier?: RedactionInput['tier'];
56
+ /**
57
+ * When `true`, throw a {@link RedactionValidationError} on any drop.
58
+ * Useful in tests; production should keep this off so the validator
59
+ * silently drops + counts.
60
+ */
61
+ readonly failOnUnredactedSensitive?: boolean;
62
+ /**
63
+ * Pattern catalogue. Defaults to the 14 built-in default-on
64
+ * patterns. Custom patterns can extend or replace this list.
65
+ */
66
+ readonly patterns?: ReadonlyArray<RedactionPattern>;
67
+ /**
68
+ * Per-name allow-list. When provided, only patterns whose `name`
69
+ * appears here are evaluated. Empty array disables all pattern
70
+ * matching (tier filtering still applies).
71
+ */
72
+ readonly enabledPatterns?: ReadonlyArray<string>;
73
+ /**
74
+ * Per-name deny-list. Patterns listed here are skipped entirely.
75
+ * Applied **after** `enabledPatterns`.
76
+ */
77
+ readonly disabledPatterns?: ReadonlyArray<string>;
78
+ /**
79
+ * Optional sink invoked on every violation. Receives only sanitized
80
+ * data; never receives secret values.
81
+ */
82
+ readonly onViolation?: RedactionViolationCallback;
83
+ /**
84
+ * Optional pluggable additional check, fired after the pattern
85
+ * scan succeeds. The callback returns `null` to drop the value or a
86
+ * sanitized {@link RedactionOutput} to forward.
87
+ */
88
+ readonly customValidator?: (input: RedactionInput) => RedactionOutput | null;
89
+ }
90
+ /**
91
+ * Concrete validator returned by {@link createRedactionValidator}.
92
+ *
93
+ * @stable
94
+ */
95
+ interface RedactionValidatorInstance extends RedactionValidator {
96
+ /** Snapshot of internal counters. Returned object is a fresh copy. */
97
+ readonly counters: () => RedactionCounters;
98
+ /** Reset all counters back to zero. */
99
+ readonly resetCounters: () => void;
100
+ }
101
+ //#endregion
102
+ export { RedactionCounters, type RedactionInput$1 as RedactionInput, type RedactionOutput$1 as RedactionOutput, type RedactionValidator$1 as RedactionValidator, RedactionValidatorInstance, RedactionValidatorOptions, RedactionViolation, RedactionViolationCallback };
103
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/redaction/types.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAiEA;;;;AAqB6B,KAlEjB,0BAAA,GAkEiB,CAAA,SAAA,EAlEwB,kBAkExB,EAAA,GAAA,IAAA;;;;;;AAwB7B;UAlFiB,kBAAA;;;;;;sBAWK;;0BAEI;;;;;;;;;UAUT,iBAAA;;;;mBAIE,SAAS;;oBAER,SAAS;;;;;;;UAQZ,yBAAA;;;;qBAII;;;;;;;;;;;sBAWC,cAAc;;;;;;6BAMP;;;;;8BAKC;;;;;yBAKL;;;;;;qCAMY,mBAAmB;;;;;;;UAQvC,0BAAA,SAAmC;;2BAEzB"}
@@ -0,0 +1,26 @@
1
+ import { RedactionValidatorInstance, RedactionValidatorOptions } from "./types.js";
2
+ import { Sensitivity } from "@graphorin/core";
3
+
4
+ //#region src/redaction/validator.d.ts
5
+
6
+ /**
7
+ * Create a {@link RedactionValidator} configured against the supplied
8
+ * options. The result implements both the `RedactionValidator`
9
+ * contract from `@graphorin/core` and the
10
+ * {@link RedactionValidatorInstance} extension surface (counters +
11
+ * reset).
12
+ *
13
+ * @stable
14
+ */
15
+ declare function createRedactionValidator(opts?: RedactionValidatorOptions): RedactionValidatorInstance;
16
+ /**
17
+ * Quickly compute the relative ordering of two sensitivity tiers.
18
+ * Exposed because the tracer + replay layers need it without taking a
19
+ * full dependency on the validator implementation.
20
+ *
21
+ * @stable
22
+ */
23
+ declare function compareSensitivityTiers(a: Sensitivity, b: Sensitivity): number;
24
+ //#endregion
25
+ export { compareSensitivityTiers, createRedactionValidator };
26
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.d.ts","names":[],"sources":["../../src/redaction/validator.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;iBA0KgB,wBAAA,QACR,4BACL;;;;;;;;iBA+Ha,uBAAA,IAA2B,gBAAgB"}
@@ -0,0 +1,195 @@
1
+ import { RedactionValidationError } from "./errors.js";
2
+ import { BUILT_IN_PATTERNS } from "./patterns.js";
3
+ import { SENSITIVITY_ORDER } from "@graphorin/core";
4
+
5
+ //#region src/redaction/validator.ts
6
+ /**
7
+ * `createRedactionValidator(...)` — the building block for the
8
+ * mandatory `withValidation()` wrapper applied to every exporter.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ const TIER_INDEX = new Map(SENSITIVITY_ORDER.map((tier, idx) => [tier, idx]));
13
+ function rank(tier) {
14
+ return TIER_INDEX.get(tier) ?? 0;
15
+ }
16
+ function makeCounters() {
17
+ return {
18
+ droppedTotal: 0,
19
+ droppedByReason: /* @__PURE__ */ new Map(),
20
+ matchesByPattern: /* @__PURE__ */ new Map()
21
+ };
22
+ }
23
+ function bump(map, key) {
24
+ map.set(key, (map.get(key) ?? 0) + 1);
25
+ }
26
+ function freezeCounters(c) {
27
+ return {
28
+ droppedTotal: c.droppedTotal,
29
+ droppedByReason: Object.freeze(Object.fromEntries(c.droppedByReason)),
30
+ matchesByPattern: Object.freeze(Object.fromEntries(c.matchesByPattern))
31
+ };
32
+ }
33
+ function selectPatterns(patterns, enabled, disabled) {
34
+ const enabledSet = enabled === void 0 ? null : new Set(enabled);
35
+ const disabledSet = new Set(disabled ?? []);
36
+ const out = [];
37
+ for (const p of patterns) {
38
+ if (enabledSet !== null && !enabledSet.has(p.name)) continue;
39
+ if (disabledSet.has(p.name)) continue;
40
+ out.push(p);
41
+ }
42
+ return out;
43
+ }
44
+ /**
45
+ * Walk every string discovered in `value` and apply `fn`. Returns
46
+ * `{ output, matched }` where `output` is the rewritten payload and
47
+ * `matched` is the de-duplicated list of pattern names that fired.
48
+ *
49
+ * The walker handles strings, plain objects, and arrays. Other JS
50
+ * primitives are passed through untouched. Cycles are detected via a
51
+ * `WeakSet` and broken with `'[Circular]'`.
52
+ */
53
+ function walk(value, fn, matched, seen) {
54
+ if (typeof value === "string") return fn(value, matched);
55
+ if (value === null || typeof value !== "object") return value;
56
+ if (seen.has(value)) return "[Circular]";
57
+ seen.add(value);
58
+ if (Array.isArray(value)) {
59
+ const out$1 = new Array(value.length);
60
+ for (let i = 0; i < value.length; i++) out$1[i] = walk(value[i], fn, matched, seen);
61
+ return out$1;
62
+ }
63
+ const obj = value;
64
+ const out = {};
65
+ for (const key of Object.keys(obj)) out[key] = walk(obj[key], fn, matched, seen);
66
+ return out;
67
+ }
68
+ /**
69
+ * Apply every active pattern to `s`. Mutates `matched` with the names
70
+ * that fired and returns the rewritten string.
71
+ */
72
+ function applyPatterns(s, patterns, matched) {
73
+ let out = s;
74
+ for (const p of patterns) {
75
+ const mask = p.mask ?? `[REDACTED ${p.name}]`;
76
+ p.regex.lastIndex = 0;
77
+ if (p.verify === void 0) {
78
+ if (p.regex.test(out)) {
79
+ matched.add(p.name);
80
+ p.regex.lastIndex = 0;
81
+ out = out.replace(p.regex, mask);
82
+ }
83
+ continue;
84
+ }
85
+ const verify = p.verify;
86
+ p.regex.lastIndex = 0;
87
+ out = out.replace(p.regex, (m) => {
88
+ if (verify(m)) {
89
+ matched.add(p.name);
90
+ return mask;
91
+ }
92
+ return m;
93
+ });
94
+ }
95
+ return out;
96
+ }
97
+ function violationFor(reason, input, patterns) {
98
+ const baseContext = input.context;
99
+ return {
100
+ reason,
101
+ declaredTier: input.tier,
102
+ ...baseContext?.attribute === void 0 ? {} : { attribute: baseContext.attribute },
103
+ ...baseContext?.spanType === void 0 ? {} : { spanType: baseContext.spanType },
104
+ ...baseContext?.origin === void 0 ? {} : { origin: baseContext.origin },
105
+ ...patterns === void 0 || patterns.length === 0 ? {} : { patterns }
106
+ };
107
+ }
108
+ /**
109
+ * Create a {@link RedactionValidator} configured against the supplied
110
+ * options. The result implements both the `RedactionValidator`
111
+ * contract from `@graphorin/core` and the
112
+ * {@link RedactionValidatorInstance} extension surface (counters +
113
+ * reset).
114
+ *
115
+ * @stable
116
+ */
117
+ function createRedactionValidator(opts = {}) {
118
+ const id = opts.id ?? "default";
119
+ const minTier = opts.minTier ?? "public";
120
+ const failOnUnredacted = opts.failOnUnredactedSensitive === true;
121
+ const patterns = selectPatterns(opts.patterns ?? BUILT_IN_PATTERNS, opts.enabledPatterns, opts.disabledPatterns);
122
+ const onViolation = opts.onViolation;
123
+ const custom = opts.customValidator;
124
+ let counters = makeCounters();
125
+ function recordViolation(violation) {
126
+ counters.droppedTotal += 1;
127
+ bump(counters.droppedByReason, violation.reason);
128
+ for (const name of violation.patterns ?? []) bump(counters.matchesByPattern, name);
129
+ if (onViolation !== void 0) try {
130
+ onViolation(violation);
131
+ } catch {}
132
+ }
133
+ function dropOrThrow(_input, violation) {
134
+ recordViolation(violation);
135
+ if (failOnUnredacted) throw new RedactionValidationError(`RedactionValidator dropped value (${violation.reason}) — set failOnUnredactedSensitive: false in production to keep the pipeline running.`, violation);
136
+ return null;
137
+ }
138
+ return {
139
+ id,
140
+ minTier,
141
+ validate(input) {
142
+ if (input === null || input === void 0 || typeof input !== "object") return dropOrThrow(input, violationFor("invalid-input", input));
143
+ const declared = input.tier ?? "internal";
144
+ if (rank(declared) > rank(minTier)) return dropOrThrow(input, violationFor("sensitivity-tier-exceeded", input));
145
+ const matched = /* @__PURE__ */ new Set();
146
+ const rewritten = walk(input.value, (s, m) => applyPatterns(s, patterns, m), matched, /* @__PURE__ */ new WeakSet());
147
+ if (matched.size > 0) {
148
+ const matchedList = [...matched];
149
+ for (const name of matchedList) bump(counters.matchesByPattern, name);
150
+ if (matchedList.some((name) => patterns.find((p) => p.name === name)?.category === "secret")) {
151
+ if (failOnUnredacted) {
152
+ const violation = violationFor("secret-pattern-match", input, matchedList);
153
+ counters.droppedTotal += 1;
154
+ bump(counters.droppedByReason, violation.reason);
155
+ if (onViolation !== void 0) try {
156
+ onViolation(violation);
157
+ } catch {}
158
+ throw new RedactionValidationError(`RedactionValidator detected secret pattern(s) in attribute (${matchedList.join(", ")}).`, violation);
159
+ }
160
+ }
161
+ }
162
+ const out = matched.size === 0 ? {
163
+ value: rewritten,
164
+ tier: declared
165
+ } : {
166
+ value: rewritten,
167
+ tier: declared,
168
+ matched: [...matched]
169
+ };
170
+ if (custom !== void 0) return custom({
171
+ ...input,
172
+ value: rewritten
173
+ });
174
+ return out;
175
+ },
176
+ counters: () => freezeCounters(counters),
177
+ resetCounters: () => {
178
+ counters = makeCounters();
179
+ }
180
+ };
181
+ }
182
+ /**
183
+ * Quickly compute the relative ordering of two sensitivity tiers.
184
+ * Exposed because the tracer + replay layers need it without taking a
185
+ * full dependency on the validator implementation.
186
+ *
187
+ * @stable
188
+ */
189
+ function compareSensitivityTiers(a, b) {
190
+ return rank(a) - rank(b);
191
+ }
192
+
193
+ //#endregion
194
+ export { compareSensitivityTiers, createRedactionValidator };
195
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","names":["out: RedactionPattern[]","out","out: Record<string, unknown>","minTier: Sensitivity","out: RedactionOutput"],"sources":["../../src/redaction/validator.ts"],"sourcesContent":["/**\n * `createRedactionValidator(...)` — the building block for the\n * mandatory `withValidation()` wrapper applied to every exporter.\n *\n * @packageDocumentation\n */\n\nimport { SENSITIVITY_ORDER, type Sensitivity } from '@graphorin/core';\n\nimport { RedactionValidationError } from './errors.js';\nimport { BUILT_IN_PATTERNS, type RedactionPattern } from './patterns.js';\nimport type {\n RedactionCounters,\n RedactionInput,\n RedactionOutput,\n RedactionValidator,\n RedactionValidatorInstance,\n RedactionValidatorOptions,\n RedactionViolation,\n} from './types.js';\n\nconst TIER_INDEX = new Map<Sensitivity, number>(\n SENSITIVITY_ORDER.map((tier, idx) => [tier, idx] as const),\n);\n\nfunction rank(tier: Sensitivity): number {\n const idx = TIER_INDEX.get(tier);\n return idx ?? 0;\n}\n\ninterface CountersInternal {\n droppedTotal: number;\n droppedByReason: Map<string, number>;\n matchesByPattern: Map<string, number>;\n}\n\nfunction makeCounters(): CountersInternal {\n return {\n droppedTotal: 0,\n droppedByReason: new Map(),\n matchesByPattern: new Map(),\n };\n}\n\nfunction bump(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) ?? 0) + 1);\n}\n\nfunction freezeCounters(c: CountersInternal): RedactionCounters {\n return {\n droppedTotal: c.droppedTotal,\n droppedByReason: Object.freeze(Object.fromEntries(c.droppedByReason)),\n matchesByPattern: Object.freeze(Object.fromEntries(c.matchesByPattern)),\n };\n}\n\nfunction selectPatterns(\n patterns: ReadonlyArray<RedactionPattern>,\n enabled?: ReadonlyArray<string>,\n disabled?: ReadonlyArray<string>,\n): RedactionPattern[] {\n const enabledSet = enabled === undefined ? null : new Set(enabled);\n const disabledSet = new Set(disabled ?? []);\n const out: RedactionPattern[] = [];\n for (const p of patterns) {\n if (enabledSet !== null && !enabledSet.has(p.name)) continue;\n if (disabledSet.has(p.name)) continue;\n out.push(p);\n }\n return out;\n}\n\n/**\n * Walk every string discovered in `value` and apply `fn`. Returns\n * `{ output, matched }` where `output` is the rewritten payload and\n * `matched` is the de-duplicated list of pattern names that fired.\n *\n * The walker handles strings, plain objects, and arrays. Other JS\n * primitives are passed through untouched. Cycles are detected via a\n * `WeakSet` and broken with `'[Circular]'`.\n */\nfunction walk(\n value: unknown,\n fn: (s: string, matched: Set<string>) => string,\n matched: Set<string>,\n seen: WeakSet<object>,\n): unknown {\n if (typeof value === 'string') return fn(value, matched);\n if (value === null || typeof value !== 'object') return value;\n if (seen.has(value as object)) return '[Circular]';\n seen.add(value as object);\n\n if (Array.isArray(value)) {\n const out = new Array<unknown>(value.length);\n for (let i = 0; i < value.length; i++) {\n out[i] = walk(value[i], fn, matched, seen);\n }\n return out;\n }\n\n const obj = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(obj)) {\n out[key] = walk(obj[key], fn, matched, seen);\n }\n return out;\n}\n\n/**\n * Apply every active pattern to `s`. Mutates `matched` with the names\n * that fired and returns the rewritten string.\n */\nfunction applyPatterns(\n s: string,\n patterns: ReadonlyArray<RedactionPattern>,\n matched: Set<string>,\n): string {\n let out = s;\n for (const p of patterns) {\n const mask = p.mask ?? `[REDACTED ${p.name}]`;\n p.regex.lastIndex = 0;\n if (p.verify === undefined) {\n if (p.regex.test(out)) {\n matched.add(p.name);\n p.regex.lastIndex = 0;\n out = out.replace(p.regex, mask);\n }\n continue;\n }\n // RP-21: per-match predicate — only mask hits the verifier accepts (e.g.\n // Luhn-valid PANs), and only count the pattern as matched when one did.\n const verify = p.verify;\n p.regex.lastIndex = 0;\n out = out.replace(p.regex, (m) => {\n if (verify(m)) {\n matched.add(p.name);\n return mask;\n }\n return m;\n });\n }\n return out;\n}\n\nfunction violationFor(\n reason: RedactionViolation['reason'],\n input: RedactionInput,\n patterns?: ReadonlyArray<string>,\n): RedactionViolation {\n const baseContext = input.context;\n const base: RedactionViolation = {\n reason,\n declaredTier: input.tier,\n ...(baseContext?.attribute === undefined ? {} : { attribute: baseContext.attribute }),\n ...(baseContext?.spanType === undefined ? {} : { spanType: baseContext.spanType }),\n ...(baseContext?.origin === undefined ? {} : { origin: baseContext.origin }),\n ...(patterns === undefined || patterns.length === 0 ? {} : { patterns }),\n };\n return base;\n}\n\n/**\n * Create a {@link RedactionValidator} configured against the supplied\n * options. The result implements both the `RedactionValidator`\n * contract from `@graphorin/core` and the\n * {@link RedactionValidatorInstance} extension surface (counters +\n * reset).\n *\n * @stable\n */\nexport function createRedactionValidator(\n opts: RedactionValidatorOptions = {},\n): RedactionValidatorInstance {\n const id = opts.id ?? 'default';\n const minTier: Sensitivity = opts.minTier ?? 'public';\n const failOnUnredacted = opts.failOnUnredactedSensitive === true;\n const patterns = selectPatterns(\n opts.patterns ?? BUILT_IN_PATTERNS,\n opts.enabledPatterns,\n opts.disabledPatterns,\n );\n const onViolation = opts.onViolation;\n const custom = opts.customValidator;\n\n let counters = makeCounters();\n\n function recordViolation(violation: RedactionViolation): void {\n counters.droppedTotal += 1;\n bump(counters.droppedByReason, violation.reason);\n for (const name of violation.patterns ?? []) {\n bump(counters.matchesByPattern, name);\n }\n if (onViolation !== undefined) {\n try {\n onViolation(violation);\n } catch {\n // Listener errors must never break the exporter.\n }\n }\n }\n\n function dropOrThrow(_input: RedactionInput, violation: RedactionViolation): null {\n recordViolation(violation);\n if (failOnUnredacted) {\n throw new RedactionValidationError(\n `RedactionValidator dropped value (${violation.reason}) — set ` +\n 'failOnUnredactedSensitive: false in production to keep the ' +\n 'pipeline running.',\n violation,\n );\n }\n return null;\n }\n\n const validator: RedactionValidator = {\n id,\n minTier,\n validate(input: RedactionInput): RedactionOutput | null {\n if (input === null || input === undefined || typeof input !== 'object') {\n return dropOrThrow(\n input as RedactionInput,\n violationFor('invalid-input', input as RedactionInput),\n );\n }\n\n const declared = input.tier ?? 'internal';\n\n if (rank(declared) > rank(minTier)) {\n return dropOrThrow(input, violationFor('sensitivity-tier-exceeded', input));\n }\n\n const matched = new Set<string>();\n const rewritten = walk(\n input.value,\n (s, m) => applyPatterns(s, patterns, m),\n matched,\n new WeakSet(),\n );\n\n if (matched.size > 0) {\n const matchedList = [...matched];\n for (const name of matchedList) {\n bump(counters.matchesByPattern, name);\n }\n const containsSecret = matchedList.some(\n (name) => patterns.find((p) => p.name === name)?.category === 'secret',\n );\n\n if (containsSecret) {\n if (failOnUnredacted) {\n const violation = violationFor('secret-pattern-match', input, matchedList);\n counters.droppedTotal += 1;\n bump(counters.droppedByReason, violation.reason);\n if (onViolation !== undefined) {\n try {\n onViolation(violation);\n } catch {\n /* listener errors swallowed */\n }\n }\n throw new RedactionValidationError(\n `RedactionValidator detected secret pattern(s) in attribute (${matchedList.join(', ')}).`,\n violation,\n );\n }\n }\n }\n\n const out: RedactionOutput =\n matched.size === 0\n ? { value: rewritten, tier: declared }\n : { value: rewritten, tier: declared, matched: [...matched] };\n\n if (custom !== undefined) {\n return custom({ ...input, value: rewritten });\n }\n\n return out;\n },\n };\n\n const instance: RedactionValidatorInstance = {\n ...validator,\n counters: () => freezeCounters(counters),\n resetCounters: () => {\n counters = makeCounters();\n },\n };\n\n return instance;\n}\n\n/**\n * Quickly compute the relative ordering of two sensitivity tiers.\n * Exposed because the tracer + replay layers need it without taking a\n * full dependency on the validator implementation.\n *\n * @stable\n */\nexport function compareSensitivityTiers(a: Sensitivity, b: Sensitivity): number {\n return rank(a) - rank(b);\n}\n"],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ //#region src/replay/config.d.ts
2
+ /**
3
+ * Configuration shapes for the replay log. The shapes mirror the
4
+ * canonical `observability.replayLog.*` settings so consumer
5
+ * configuration files can use a single typed structure.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /**
10
+ * Shape consumed by `observability.replayLog.*`.
11
+ *
12
+ * @stable
13
+ */
14
+ interface ReplayLogConfig {
15
+ /**
16
+ * Root directory for the JSONL trace files. Required when the
17
+ * replay log is enabled.
18
+ */
19
+ readonly path: string;
20
+ /**
21
+ * Retention window in days. `0` keeps every file forever.
22
+ *
23
+ * @default 30
24
+ */
25
+ readonly retentionDays?: number;
26
+ /**
27
+ * Auto-prune hint. `enabled` + `schedule` describe a daily prune of files
28
+ * older than `retentionDays`, but **no built-in scheduler consumes this
29
+ * yet** (RP-19) — it is a declarative intent. Until a host wires it to a
30
+ * trigger, callers must run `pruneTraces(...)` themselves; the default is
31
+ * therefore `enabled: false` so the option is not an inert default-on.
32
+ *
33
+ * @default { enabled: false, schedule: '0 4 * * *' }
34
+ */
35
+ readonly autoPrune?: {
36
+ readonly enabled: boolean;
37
+ readonly schedule?: string;
38
+ };
39
+ /**
40
+ * Encryption-at-rest toggle. `'off'` (default) writes plain JSONL;
41
+ * the opt-in `'aes256gcm'` mode hooks into the encryption-at-rest
42
+ * passphrase chain (Phase 16 deliverable).
43
+ *
44
+ * @default 'off'
45
+ */
46
+ readonly encryption?: 'off' | 'aes256gcm';
47
+ }
48
+ /**
49
+ * Default values for the replay log configuration.
50
+ *
51
+ * @stable
52
+ */
53
+ declare const DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'>;
54
+ //#endregion
55
+ export { DEFAULT_REPLAY_LOG_CONFIG, ReplayLogConfig };
56
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","names":[],"sources":["../../src/replay/config.ts"],"sourcesContent":[],"mappings":";;AAaA;AAwCA;;;;;;;;;;UAxCiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCJ,2BAA2B,KAAK,SAAS"}
@@ -0,0 +1,18 @@
1
+ //#region src/replay/config.ts
2
+ /**
3
+ * Default values for the replay log configuration.
4
+ *
5
+ * @stable
6
+ */
7
+ const DEFAULT_REPLAY_LOG_CONFIG = Object.freeze({
8
+ retentionDays: 30,
9
+ autoPrune: Object.freeze({
10
+ enabled: false,
11
+ schedule: "0 4 * * *"
12
+ }),
13
+ encryption: "off"
14
+ });
15
+
16
+ //#endregion
17
+ export { DEFAULT_REPLAY_LOG_CONFIG };
18
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":["DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'>"],"sources":["../../src/replay/config.ts"],"sourcesContent":["/**\n * Configuration shapes for the replay log. The shapes mirror the\n * canonical `observability.replayLog.*` settings so consumer\n * configuration files can use a single typed structure.\n *\n * @packageDocumentation\n */\n\n/**\n * Shape consumed by `observability.replayLog.*`.\n *\n * @stable\n */\nexport interface ReplayLogConfig {\n /**\n * Root directory for the JSONL trace files. Required when the\n * replay log is enabled.\n */\n readonly path: string;\n /**\n * Retention window in days. `0` keeps every file forever.\n *\n * @default 30\n */\n readonly retentionDays?: number;\n /**\n * Auto-prune hint. `enabled` + `schedule` describe a daily prune of files\n * older than `retentionDays`, but **no built-in scheduler consumes this\n * yet** (RP-19) — it is a declarative intent. Until a host wires it to a\n * trigger, callers must run `pruneTraces(...)` themselves; the default is\n * therefore `enabled: false` so the option is not an inert default-on.\n *\n * @default { enabled: false, schedule: '0 4 * * *' }\n */\n readonly autoPrune?: {\n readonly enabled: boolean;\n readonly schedule?: string;\n };\n /**\n * Encryption-at-rest toggle. `'off'` (default) writes plain JSONL;\n * the opt-in `'aes256gcm'` mode hooks into the encryption-at-rest\n * passphrase chain (Phase 16 deliverable).\n *\n * @default 'off'\n */\n readonly encryption?: 'off' | 'aes256gcm';\n}\n\n/**\n * Default values for the replay log configuration.\n *\n * @stable\n */\nexport const DEFAULT_REPLAY_LOG_CONFIG: Omit<Required<ReplayLogConfig>, 'path'> = Object.freeze({\n retentionDays: 30,\n autoPrune: Object.freeze({ enabled: false, schedule: '0 4 * * *' }),\n encryption: 'off' as const,\n});\n"],"mappings":";;;;;;AAqDA,MAAaA,4BAAqE,OAAO,OAAO;CAC9F,eAAe;CACf,WAAW,OAAO,OAAO;EAAE,SAAS;EAAO,UAAU;EAAa,CAAC;CACnE,YAAY;CACb,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { DEFAULT_REPLAY_LOG_CONFIG, ReplayLogConfig } from "./config.js";
2
+ import { PruneTracesOptions, getTraceLog, pruneTraces } from "./log.js";
3
+ import { ReplayAuditBridge, ReplayAuditEvent, ReplayEvent, ReplayMode, ReplayOptions, ReplayRunInput } from "./types.js";
4
+ import { Replay, createReplay } from "./replay.js";
5
+ export { DEFAULT_REPLAY_LOG_CONFIG, type PruneTracesOptions, type Replay, type ReplayAuditBridge, type ReplayAuditEvent, type ReplayEvent, type ReplayLogConfig, type ReplayMode, type ReplayOptions, type ReplayRunInput, createReplay, getTraceLog, pruneTraces };
@@ -0,0 +1,5 @@
1
+ import { DEFAULT_REPLAY_LOG_CONFIG } from "./config.js";
2
+ import { getTraceLog, pruneTraces } from "./log.js";
3
+ import { createReplay } from "./replay.js";
4
+
5
+ export { DEFAULT_REPLAY_LOG_CONFIG, createReplay, getTraceLog, pruneTraces };
@@ -0,0 +1,39 @@
1
+ import { SpanRecord } from "../exporters/types.js";
2
+
3
+ //#region src/replay/log.d.ts
4
+
5
+ /**
6
+ * Read every span record from a JSONL trace log. Lines that fail to parse are
7
+ * skipped (the iterator keeps going); the generator only ever yields parsed
8
+ * {@link SpanRecord} values, never `null`.
9
+ *
10
+ * @stable
11
+ */
12
+ declare function getTraceLog(filePath: string): AsyncIterable<SpanRecord>;
13
+ /**
14
+ * Configuration shape for {@link pruneTraces}.
15
+ *
16
+ * @stable
17
+ */
18
+ interface PruneTracesOptions {
19
+ /** Root directory housing the JSONL files. */
20
+ readonly root: string;
21
+ /** Files older than `olderThanDays` are deleted. `0` keeps every file. */
22
+ readonly olderThanDays: number;
23
+ /**
24
+ * Wall clock used to compute the threshold. Defaults to `Date.now`.
25
+ *
26
+ * @internal
27
+ */
28
+ readonly now?: () => number;
29
+ }
30
+ /**
31
+ * Remove every JSONL file that is older than the configured retention
32
+ * window. Returns the deleted files for caller-side accounting.
33
+ *
34
+ * @stable
35
+ */
36
+ declare function pruneTraces(opts: PruneTracesOptions): Promise<ReadonlyArray<string>>;
37
+ //#endregion
38
+ export { PruneTracesOptions, getTraceLog, pruneTraces };
39
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","names":[],"sources":["../../src/replay/log.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;iBAoBuB,WAAA,oBAA+B,cAAc;;;;;;UAkBnD,kBAAA;;;;;;;;;;;;;;;;;;iBAmBK,WAAA,OAAkB,qBAAqB,QAAQ"}
@@ -0,0 +1,59 @@
1
+ import { readFile, readdir, stat, unlink } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+
4
+ //#region src/replay/log.ts
5
+ /**
6
+ * `getTraceLog(...)` / `pruneTraces(...)` — minimal helpers for reading
7
+ * back the JSONL files produced by {@link createJSONLExporter} and
8
+ * pruning old files based on retention policy.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ /**
13
+ * Read every span record from a JSONL trace log. Lines that fail to parse are
14
+ * skipped (the iterator keeps going); the generator only ever yields parsed
15
+ * {@link SpanRecord} values, never `null`.
16
+ *
17
+ * @stable
18
+ */
19
+ async function* getTraceLog(filePath) {
20
+ const data = await readFile(filePath, "utf8");
21
+ for (const line of data.split("\n")) {
22
+ if (line.trim() === "") continue;
23
+ try {
24
+ yield JSON.parse(line);
25
+ } catch {}
26
+ }
27
+ }
28
+ /**
29
+ * Remove every JSONL file that is older than the configured retention
30
+ * window. Returns the deleted files for caller-side accounting.
31
+ *
32
+ * @stable
33
+ */
34
+ async function pruneTraces(opts) {
35
+ if (opts.olderThanDays <= 0) return [];
36
+ const threshold = (opts.now ?? (() => Date.now()))() - opts.olderThanDays * 24 * 60 * 60 * 1e3;
37
+ const root = resolve(opts.root);
38
+ const removed = [];
39
+ for await (const filePath of walkJsonl(root)) if ((await stat(filePath)).mtimeMs < threshold) {
40
+ await unlink(filePath);
41
+ removed.push(filePath);
42
+ }
43
+ return removed;
44
+ }
45
+ async function* walkJsonl(root) {
46
+ const entries = await readdir(root, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ const full = join(root, entry.name);
49
+ if (entry.isDirectory()) {
50
+ yield* walkJsonl(full);
51
+ continue;
52
+ }
53
+ if (entry.isFile() && full.endsWith(".jsonl")) yield full;
54
+ }
55
+ }
56
+
57
+ //#endregion
58
+ export { getTraceLog, pruneTraces };
59
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","names":["removed: string[]"],"sources":["../../src/replay/log.ts"],"sourcesContent":["/**\n * `getTraceLog(...)` / `pruneTraces(...)` — minimal helpers for reading\n * back the JSONL files produced by {@link createJSONLExporter} and\n * pruning old files based on retention policy.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat, unlink } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nimport type { SpanRecord } from '../exporters/types.js';\n\n/**\n * Read every span record from a JSONL trace log. Lines that fail to parse are\n * skipped (the iterator keeps going); the generator only ever yields parsed\n * {@link SpanRecord} values, never `null`.\n *\n * @stable\n */\nexport async function* getTraceLog(filePath: string): AsyncIterable<SpanRecord> {\n const data = await readFile(filePath, 'utf8');\n for (const line of data.split('\\n')) {\n if (line.trim() === '') continue;\n try {\n const parsed = JSON.parse(line) as SpanRecord;\n yield parsed;\n } catch {\n // Skip malformed lines — replay must keep going.\n }\n }\n}\n\n/**\n * Configuration shape for {@link pruneTraces}.\n *\n * @stable\n */\nexport interface PruneTracesOptions {\n /** Root directory housing the JSONL files. */\n readonly root: string;\n /** Files older than `olderThanDays` are deleted. `0` keeps every file. */\n readonly olderThanDays: number;\n /**\n * Wall clock used to compute the threshold. Defaults to `Date.now`.\n *\n * @internal\n */\n readonly now?: () => number;\n}\n\n/**\n * Remove every JSONL file that is older than the configured retention\n * window. Returns the deleted files for caller-side accounting.\n *\n * @stable\n */\nexport async function pruneTraces(opts: PruneTracesOptions): Promise<ReadonlyArray<string>> {\n if (opts.olderThanDays <= 0) return [];\n\n const now = opts.now ?? (() => Date.now());\n const threshold = now() - opts.olderThanDays * 24 * 60 * 60 * 1000;\n const root = resolve(opts.root);\n const removed: string[] = [];\n\n for await (const filePath of walkJsonl(root)) {\n const info = await stat(filePath);\n if (info.mtimeMs < threshold) {\n await unlink(filePath);\n removed.push(filePath);\n }\n }\n\n return removed;\n}\n\nasync function* walkJsonl(root: string): AsyncIterable<string> {\n const entries = await readdir(root, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(root, entry.name);\n if (entry.isDirectory()) {\n yield* walkJsonl(full);\n continue;\n }\n if (entry.isFile() && full.endsWith('.jsonl')) {\n yield full;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,gBAAuB,YAAY,UAA6C;CAC9E,MAAM,OAAO,MAAM,SAAS,UAAU,OAAO;AAC7C,MAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,EAAE;AACnC,MAAI,KAAK,MAAM,KAAK,GAAI;AACxB,MAAI;AAEF,SADe,KAAK,MAAM,KAAK;UAEzB;;;;;;;;;AA8BZ,eAAsB,YAAY,MAA0D;AAC1F,KAAI,KAAK,iBAAiB,EAAG,QAAO,EAAE;CAGtC,MAAM,aADM,KAAK,cAAc,KAAK,KAAK,IAClB,GAAG,KAAK,gBAAgB,KAAK,KAAK,KAAK;CAC9D,MAAM,OAAO,QAAQ,KAAK,KAAK;CAC/B,MAAMA,UAAoB,EAAE;AAE5B,YAAW,MAAM,YAAY,UAAU,KAAK,CAE1C,MADa,MAAM,KAAK,SAAS,EACxB,UAAU,WAAW;AAC5B,QAAM,OAAO,SAAS;AACtB,UAAQ,KAAK,SAAS;;AAI1B,QAAO;;AAGT,gBAAgB,UAAU,MAAqC;CAC7D,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,eAAe,MAAM,CAAC;AAC5D,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,OAAO,KAAK,MAAM,MAAM,KAAK;AACnC,MAAI,MAAM,aAAa,EAAE;AACvB,UAAO,UAAU,KAAK;AACtB;;AAEF,MAAI,MAAM,QAAQ,IAAI,KAAK,SAAS,SAAS,CAC3C,OAAM"}
@@ -0,0 +1,25 @@
1
+ import { ReplayEvent, ReplayOptions, ReplayRunInput } from "./types.js";
2
+
3
+ //#region src/replay/replay.d.ts
4
+
5
+ /**
6
+ * @stable
7
+ */
8
+ interface Replay {
9
+ run(input: ReplayRunInput): AsyncIterable<ReplayEvent>;
10
+ }
11
+ /**
12
+ * Build a replay primitive. The returned object exposes a single
13
+ * `run(...)` async iterator that yields {@link ReplayEvent} records.
14
+ *
15
+ * Sanitized mode is the default and applies the configured
16
+ * {@link RedactionValidatorInstance} to every record. Raw mode
17
+ * requires the `canReadRaw` callback to return `true` AND emits an
18
+ * audit log entry on every invocation.
19
+ *
20
+ * @stable
21
+ */
22
+ declare function createReplay(opts?: ReplayOptions): Replay;
23
+ //#endregion
24
+ export { Replay, createReplay };
25
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","names":[],"sources":["../../src/replay/replay.ts"],"sourcesContent":[],"mappings":";;;;;;;UAgCiB,MAAA;aACJ,iBAAiB,cAAc;;;;;;;;;;;;;iBAc5B,YAAA,QAAmB,gBAAqB"}