gavio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +95 -0
  2. package/dist/cjs/context.js +47 -0
  3. package/dist/cjs/errors.js +57 -0
  4. package/dist/cjs/gateway.js +127 -0
  5. package/dist/cjs/ids.js +60 -0
  6. package/dist/cjs/index.js +49 -0
  7. package/dist/cjs/interceptors/audit/index.js +12 -0
  8. package/dist/cjs/interceptors/audit/interceptor.js +77 -0
  9. package/dist/cjs/interceptors/audit/record.js +107 -0
  10. package/dist/cjs/interceptors/audit/sink.js +3 -0
  11. package/dist/cjs/interceptors/audit/sinks/index.js +5 -0
  12. package/dist/cjs/interceptors/audit/sinks/stdout.js +33 -0
  13. package/dist/cjs/interceptors/base.js +7 -0
  14. package/dist/cjs/interceptors/cache/backend.js +9 -0
  15. package/dist/cjs/interceptors/cache/backends/index.js +5 -0
  16. package/dist/cjs/interceptors/cache/backends/memory.js +53 -0
  17. package/dist/cjs/interceptors/cache/index.js +9 -0
  18. package/dist/cjs/interceptors/chain.js +57 -0
  19. package/dist/cjs/interceptors/index.js +18 -0
  20. package/dist/cjs/interceptors/pii/context.js +25 -0
  21. package/dist/cjs/interceptors/pii/guard.js +161 -0
  22. package/dist/cjs/interceptors/pii/index.js +28 -0
  23. package/dist/cjs/interceptors/pii/match.js +21 -0
  24. package/dist/cjs/interceptors/pii/scanner.js +31 -0
  25. package/dist/cjs/interceptors/pii/scanners/bsn.js +41 -0
  26. package/dist/cjs/interceptors/pii/scanners/credit-card.js +51 -0
  27. package/dist/cjs/interceptors/pii/scanners/email.js +26 -0
  28. package/dist/cjs/interceptors/pii/scanners/iban.js +58 -0
  29. package/dist/cjs/interceptors/pii/scanners/index.js +45 -0
  30. package/dist/cjs/interceptors/pii/scanners/ip-address.js +36 -0
  31. package/dist/cjs/interceptors/pii/scanners/phone.js +37 -0
  32. package/dist/cjs/interceptors/pii/scanners/secret.js +46 -0
  33. package/dist/cjs/interceptors/pii/scanners/ssn.js +28 -0
  34. package/dist/cjs/interceptors/reliability/fallback.js +53 -0
  35. package/dist/cjs/interceptors/reliability/index.js +11 -0
  36. package/dist/cjs/interceptors/reliability/retry.js +69 -0
  37. package/dist/cjs/interceptors/reliability/timeout.js +41 -0
  38. package/dist/cjs/package.json +3 -0
  39. package/dist/cjs/pricing.js +70 -0
  40. package/dist/cjs/providers/anthropic.js +80 -0
  41. package/dist/cjs/providers/base.js +30 -0
  42. package/dist/cjs/providers/http.js +42 -0
  43. package/dist/cjs/providers/index.js +34 -0
  44. package/dist/cjs/providers/mock.js +54 -0
  45. package/dist/cjs/providers/openai.js +63 -0
  46. package/dist/cjs/request.js +60 -0
  47. package/dist/cjs/response.js +55 -0
  48. package/dist/cjs/testing/harness.js +70 -0
  49. package/dist/cjs/testing/index.js +8 -0
  50. package/dist/cjs/types.js +61 -0
  51. package/dist/esm/context.d.ts +33 -0
  52. package/dist/esm/context.js +43 -0
  53. package/dist/esm/errors.d.ts +36 -0
  54. package/dist/esm/errors.js +44 -0
  55. package/dist/esm/gateway.d.ts +54 -0
  56. package/dist/esm/gateway.js +123 -0
  57. package/dist/esm/ids.d.ts +11 -0
  58. package/dist/esm/ids.js +56 -0
  59. package/dist/esm/index.d.ts +25 -0
  60. package/dist/esm/index.js +20 -0
  61. package/dist/esm/interceptors/audit/index.d.ts +7 -0
  62. package/dist/esm/interceptors/audit/index.js +3 -0
  63. package/dist/esm/interceptors/audit/interceptor.d.ts +11 -0
  64. package/dist/esm/interceptors/audit/interceptor.js +72 -0
  65. package/dist/esm/interceptors/audit/record.d.ts +66 -0
  66. package/dist/esm/interceptors/audit/record.js +103 -0
  67. package/dist/esm/interceptors/audit/sink.d.ts +8 -0
  68. package/dist/esm/interceptors/audit/sink.js +2 -0
  69. package/dist/esm/interceptors/audit/sinks/index.d.ts +2 -0
  70. package/dist/esm/interceptors/audit/sinks/index.js +1 -0
  71. package/dist/esm/interceptors/audit/sinks/stdout.d.ts +8 -0
  72. package/dist/esm/interceptors/audit/sinks/stdout.js +30 -0
  73. package/dist/esm/interceptors/base.d.ts +37 -0
  74. package/dist/esm/interceptors/base.js +4 -0
  75. package/dist/esm/interceptors/cache/backend.d.ts +14 -0
  76. package/dist/esm/interceptors/cache/backend.js +8 -0
  77. package/dist/esm/interceptors/cache/backends/index.d.ts +2 -0
  78. package/dist/esm/interceptors/cache/backends/index.js +1 -0
  79. package/dist/esm/interceptors/cache/backends/memory.d.ts +7 -0
  80. package/dist/esm/interceptors/cache/backends/memory.js +50 -0
  81. package/dist/esm/interceptors/cache/index.d.ts +7 -0
  82. package/dist/esm/interceptors/cache/index.js +5 -0
  83. package/dist/esm/interceptors/chain.d.ts +17 -0
  84. package/dist/esm/interceptors/chain.js +53 -0
  85. package/dist/esm/interceptors/index.d.ts +8 -0
  86. package/dist/esm/interceptors/index.js +7 -0
  87. package/dist/esm/interceptors/pii/context.d.ts +15 -0
  88. package/dist/esm/interceptors/pii/context.js +21 -0
  89. package/dist/esm/interceptors/pii/guard.d.ts +30 -0
  90. package/dist/esm/interceptors/pii/guard.js +157 -0
  91. package/dist/esm/interceptors/pii/index.d.ts +10 -0
  92. package/dist/esm/interceptors/pii/index.js +7 -0
  93. package/dist/esm/interceptors/pii/match.d.ts +26 -0
  94. package/dist/esm/interceptors/pii/match.js +17 -0
  95. package/dist/esm/interceptors/pii/scanner.d.ts +32 -0
  96. package/dist/esm/interceptors/pii/scanner.js +26 -0
  97. package/dist/esm/interceptors/pii/scanners/bsn.d.ts +5 -0
  98. package/dist/esm/interceptors/pii/scanners/bsn.js +37 -0
  99. package/dist/esm/interceptors/pii/scanners/credit-card.d.ts +4 -0
  100. package/dist/esm/interceptors/pii/scanners/credit-card.js +47 -0
  101. package/dist/esm/interceptors/pii/scanners/email.d.ts +3 -0
  102. package/dist/esm/interceptors/pii/scanners/email.js +23 -0
  103. package/dist/esm/interceptors/pii/scanners/iban.d.ts +5 -0
  104. package/dist/esm/interceptors/pii/scanners/iban.js +54 -0
  105. package/dist/esm/interceptors/pii/scanners/index.d.ts +13 -0
  106. package/dist/esm/interceptors/pii/scanners/index.js +30 -0
  107. package/dist/esm/interceptors/pii/scanners/ip-address.d.ts +3 -0
  108. package/dist/esm/interceptors/pii/scanners/ip-address.js +33 -0
  109. package/dist/esm/interceptors/pii/scanners/phone.d.ts +6 -0
  110. package/dist/esm/interceptors/pii/scanners/phone.js +34 -0
  111. package/dist/esm/interceptors/pii/scanners/secret.d.ts +9 -0
  112. package/dist/esm/interceptors/pii/scanners/secret.js +43 -0
  113. package/dist/esm/interceptors/pii/scanners/ssn.d.ts +3 -0
  114. package/dist/esm/interceptors/pii/scanners/ssn.js +25 -0
  115. package/dist/esm/interceptors/reliability/fallback.d.ts +9 -0
  116. package/dist/esm/interceptors/reliability/fallback.js +50 -0
  117. package/dist/esm/interceptors/reliability/index.d.ts +7 -0
  118. package/dist/esm/interceptors/reliability/index.js +4 -0
  119. package/dist/esm/interceptors/reliability/retry.d.ts +13 -0
  120. package/dist/esm/interceptors/reliability/retry.js +66 -0
  121. package/dist/esm/interceptors/reliability/timeout.d.ts +9 -0
  122. package/dist/esm/interceptors/reliability/timeout.js +37 -0
  123. package/dist/esm/package.json +3 -0
  124. package/dist/esm/pricing.d.ts +19 -0
  125. package/dist/esm/pricing.js +65 -0
  126. package/dist/esm/providers/anthropic.d.ts +30 -0
  127. package/dist/esm/providers/anthropic.js +77 -0
  128. package/dist/esm/providers/base.d.ts +23 -0
  129. package/dist/esm/providers/base.js +28 -0
  130. package/dist/esm/providers/http.d.ts +8 -0
  131. package/dist/esm/providers/http.js +39 -0
  132. package/dist/esm/providers/index.d.ts +15 -0
  133. package/dist/esm/providers/index.js +25 -0
  134. package/dist/esm/providers/mock.d.ts +31 -0
  135. package/dist/esm/providers/mock.js +51 -0
  136. package/dist/esm/providers/openai.d.ts +26 -0
  137. package/dist/esm/providers/openai.js +60 -0
  138. package/dist/esm/request.d.ts +36 -0
  139. package/dist/esm/request.js +56 -0
  140. package/dist/esm/response.d.ts +38 -0
  141. package/dist/esm/response.js +51 -0
  142. package/dist/esm/testing/harness.d.ts +37 -0
  143. package/dist/esm/testing/harness.js +66 -0
  144. package/dist/esm/testing/index.d.ts +5 -0
  145. package/dist/esm/testing/index.js +3 -0
  146. package/dist/esm/types.d.ts +58 -0
  147. package/dist/esm/types.js +56 -0
  148. package/package.json +115 -0
  149. package/src/context.ts +57 -0
  150. package/src/errors.ts +47 -0
  151. package/src/gateway.ts +174 -0
  152. package/src/ids.ts +69 -0
  153. package/src/index.ts +52 -0
  154. package/src/interceptors/audit/index.ts +7 -0
  155. package/src/interceptors/audit/interceptor.ts +93 -0
  156. package/src/interceptors/audit/record.ts +138 -0
  157. package/src/interceptors/audit/sink.ts +10 -0
  158. package/src/interceptors/audit/sinks/index.ts +2 -0
  159. package/src/interceptors/audit/sinks/stdout.ts +42 -0
  160. package/src/interceptors/base.ts +58 -0
  161. package/src/interceptors/cache/backend.ts +15 -0
  162. package/src/interceptors/cache/backends/index.ts +2 -0
  163. package/src/interceptors/cache/backends/memory.ts +68 -0
  164. package/src/interceptors/cache/index.ts +8 -0
  165. package/src/interceptors/chain.ts +65 -0
  166. package/src/interceptors/index.ts +9 -0
  167. package/src/interceptors/pii/context.ts +24 -0
  168. package/src/interceptors/pii/guard.ts +201 -0
  169. package/src/interceptors/pii/index.ts +21 -0
  170. package/src/interceptors/pii/match.ts +43 -0
  171. package/src/interceptors/pii/scanner.ts +54 -0
  172. package/src/interceptors/pii/scanners/bsn.ts +44 -0
  173. package/src/interceptors/pii/scanners/credit-card.ts +52 -0
  174. package/src/interceptors/pii/scanners/email.ts +31 -0
  175. package/src/interceptors/pii/scanners/iban.ts +60 -0
  176. package/src/interceptors/pii/scanners/index.ts +35 -0
  177. package/src/interceptors/pii/scanners/ip-address.ts +41 -0
  178. package/src/interceptors/pii/scanners/phone.ts +46 -0
  179. package/src/interceptors/pii/scanners/secret.ts +51 -0
  180. package/src/interceptors/pii/scanners/ssn.ts +33 -0
  181. package/src/interceptors/reliability/fallback.ts +66 -0
  182. package/src/interceptors/reliability/index.ts +8 -0
  183. package/src/interceptors/reliability/retry.ts +97 -0
  184. package/src/interceptors/reliability/timeout.ts +53 -0
  185. package/src/pricing.ts +72 -0
  186. package/src/providers/anthropic.ts +113 -0
  187. package/src/providers/base.ts +52 -0
  188. package/src/providers/http.ts +50 -0
  189. package/src/providers/index.ts +39 -0
  190. package/src/providers/mock.ts +73 -0
  191. package/src/providers/openai.ts +94 -0
  192. package/src/request.ts +76 -0
  193. package/src/response.ts +73 -0
  194. package/src/testing/harness.ts +98 -0
  195. package/src/testing/index.ts +6 -0
  196. package/src/types.ts +83 -0
@@ -0,0 +1,50 @@
1
+ /** In-memory cache backend (F-CACHE-03) — default zero-dependency dev backend. */
2
+ /** LRU-bounded, optionally TTL'd in-process cache. Not shared across processes. */
3
+ class MemoryBackend {
4
+ maxSize;
5
+ // Map preserves insertion order, which we use for LRU eviction.
6
+ store = new Map();
7
+ constructor(maxSize = 1000) {
8
+ this.maxSize = maxSize;
9
+ }
10
+ async get(key) {
11
+ const entry = this.store.get(key);
12
+ if (entry === undefined)
13
+ return null;
14
+ if (entry.expiresAt !== null && now() > entry.expiresAt) {
15
+ this.store.delete(key);
16
+ return null;
17
+ }
18
+ // Move to end (most-recently-used).
19
+ this.store.delete(key);
20
+ this.store.set(key, entry);
21
+ return entry.value;
22
+ }
23
+ async set(key, value, ttlSeconds) {
24
+ const expiresAt = ttlSeconds ? now() + ttlSeconds * 1000 : null;
25
+ this.store.delete(key);
26
+ this.store.set(key, { value, expiresAt });
27
+ while (this.store.size > this.maxSize) {
28
+ const oldest = this.store.keys().next().value;
29
+ if (oldest === undefined)
30
+ break;
31
+ this.store.delete(oldest);
32
+ }
33
+ }
34
+ async delete(key) {
35
+ this.store.delete(key);
36
+ }
37
+ async clear() {
38
+ this.store.clear();
39
+ }
40
+ get size() {
41
+ return this.store.size;
42
+ }
43
+ }
44
+ function now() {
45
+ return Date.now();
46
+ }
47
+ /** Factory: build an in-memory cache backend. */
48
+ export function memoryCacheBackend(options = {}) {
49
+ return new MemoryBackend(options.maxSize ?? 1000);
50
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Caching substrate. The SemanticCache interceptor ships in v0.2.0; v0.1.0
3
+ * exposes the CacheBackend interface and the in-memory backend only.
4
+ */
5
+ export type { CacheBackend } from './backend.js';
6
+ export { memoryCacheBackend } from './backends/memory.js';
7
+ export type { MemoryCacheBackendOptions } from './backends/memory.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Caching substrate. The SemanticCache interceptor ships in v0.2.0; v0.1.0
3
+ * exposes the CacheBackend interface and the in-memory backend only.
4
+ */
5
+ export { memoryCacheBackend } from './backends/memory.js';
@@ -0,0 +1,17 @@
1
+ /** InterceptorChain — runs the pre/post pipeline around the provider call. */
2
+ import type { InterceptorContext } from '../context.js';
3
+ import type { GavioRequest } from '../request.js';
4
+ import type { GavioResponse } from '../response.js';
5
+ import type { Executor, Interceptor } from './base.js';
6
+ /**
7
+ * Ordered list of interceptors wrapping an executor.
8
+ *
9
+ * `before` hooks fire in order; the executor runs; `after` hooks fire in
10
+ * reverse order (onion model). If any stage throws, every interceptor's
11
+ * `onError` is invoked before the error propagates.
12
+ */
13
+ export declare class InterceptorChain {
14
+ private readonly interceptors;
15
+ constructor(interceptors: Interceptor[]);
16
+ execute(request: GavioRequest, ctx: InterceptorContext, executor: Executor): Promise<GavioResponse>;
17
+ }
@@ -0,0 +1,53 @@
1
+ /** InterceptorChain — runs the pre/post pipeline around the provider call. */
2
+ const dryRunSafe = (i) => i.dryRunSafe !== false;
3
+ /**
4
+ * Ordered list of interceptors wrapping an executor.
5
+ *
6
+ * `before` hooks fire in order; the executor runs; `after` hooks fire in
7
+ * reverse order (onion model). If any stage throws, every interceptor's
8
+ * `onError` is invoked before the error propagates.
9
+ */
10
+ export class InterceptorChain {
11
+ interceptors;
12
+ constructor(interceptors) {
13
+ this.interceptors = [...interceptors];
14
+ }
15
+ async execute(request, ctx, executor) {
16
+ try {
17
+ let req = request;
18
+ for (const interceptor of this.interceptors) {
19
+ if (ctx.dryRun && !dryRunSafe(interceptor))
20
+ continue;
21
+ if (interceptor.before) {
22
+ req = await interceptor.before(req, ctx);
23
+ }
24
+ ctx.markFired(interceptor.name);
25
+ }
26
+ let response = await executor(req);
27
+ for (let i = this.interceptors.length - 1; i >= 0; i--) {
28
+ const interceptor = this.interceptors[i];
29
+ if (ctx.dryRun && !dryRunSafe(interceptor))
30
+ continue;
31
+ if (interceptor.after) {
32
+ response = await interceptor.after(response, ctx);
33
+ }
34
+ }
35
+ response.interceptorsFired = [...ctx.interceptorsFired];
36
+ return response;
37
+ }
38
+ catch (error) {
39
+ const err = error instanceof Error ? error : new Error(String(error));
40
+ for (const interceptor of this.interceptors) {
41
+ if (interceptor.onError) {
42
+ try {
43
+ await interceptor.onError(err, ctx);
44
+ }
45
+ catch {
46
+ // on_error must never break the propagation of the original error.
47
+ }
48
+ }
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,8 @@
1
+ /** Interceptor barrel. */
2
+ export type { Interceptor, Executor, ExecutorPolicy } from './base.js';
3
+ export { isExecutorPolicy } from './base.js';
4
+ export { InterceptorChain } from './chain.js';
5
+ export { piiGuard } from './pii/index.js';
6
+ export { auditInterceptor } from './audit/index.js';
7
+ export { retryInterceptor, timeoutPolicy, fallbackChain } from './reliability/index.js';
8
+ export { memoryCacheBackend } from './cache/index.js';
@@ -0,0 +1,7 @@
1
+ /** Interceptor barrel. */
2
+ export { isExecutorPolicy } from './base.js';
3
+ export { InterceptorChain } from './chain.js';
4
+ export { piiGuard } from './pii/index.js';
5
+ export { auditInterceptor } from './audit/index.js';
6
+ export { retryInterceptor, timeoutPolicy, fallbackChain } from './reliability/index.js';
7
+ export { memoryCacheBackend } from './cache/index.js';
@@ -0,0 +1,15 @@
1
+ /** ScanContext — per-request state shared across PII scanners. */
2
+ /**
3
+ * Context threaded through every scanner for one request.
4
+ *
5
+ * Tracks a monotonic per-entity-type index so repeated entities get stable,
6
+ * distinct placeholders (`[EMAIL_1]`, `[EMAIL_2]`).
7
+ */
8
+ export declare class ScanContext {
9
+ readonly language: string;
10
+ readonly locale: string;
11
+ private counters;
12
+ constructor(language?: string, locale?: string);
13
+ /** Return the next 1-based index for an entity type. */
14
+ nextIndex(entityType: string): number;
15
+ }
@@ -0,0 +1,21 @@
1
+ /** ScanContext — per-request state shared across PII scanners. */
2
+ /**
3
+ * Context threaded through every scanner for one request.
4
+ *
5
+ * Tracks a monotonic per-entity-type index so repeated entities get stable,
6
+ * distinct placeholders (`[EMAIL_1]`, `[EMAIL_2]`).
7
+ */
8
+ export class ScanContext {
9
+ language;
10
+ locale;
11
+ counters = {};
12
+ constructor(language = 'en', locale = 'NL') {
13
+ this.language = language;
14
+ this.locale = locale;
15
+ }
16
+ /** Return the next 1-based index for an entity type. */
17
+ nextIndex(entityType) {
18
+ this.counters[entityType] = (this.counters[entityType] ?? 0) + 1;
19
+ return this.counters[entityType];
20
+ }
21
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * PiiGuard — the pre/post interceptor that detects and redacts PII.
3
+ *
4
+ * Pipeline rule (privacy): PII is scanned on every request before it reaches
5
+ * the provider. Detected entities are redacted/masked/tagged or blocked. In
6
+ * REDACT mode the original values are restored in the response.
7
+ */
8
+ import { PiiMode, Sensitivity } from '../../types.js';
9
+ import type { Interceptor } from '../base.js';
10
+ import type { PiiMatch } from './match.js';
11
+ import type { PiiScanner } from './scanner.js';
12
+ export interface PiiGuardOptions {
13
+ scanners?: PiiScanner[];
14
+ sensitivity?: Sensitivity;
15
+ mode?: PiiMode;
16
+ restoreOnResponse?: boolean;
17
+ logEntityTypes?: boolean;
18
+ dryRun?: boolean;
19
+ locale?: string;
20
+ language?: string;
21
+ }
22
+ /**
23
+ * Drop lower-priority matches that overlap a kept one.
24
+ *
25
+ * Sort by start, then by descending span length (prefer the longer match),
26
+ * then by confidence. Greedily keep non-overlapping matches.
27
+ */
28
+ export declare function resolveOverlaps(matches: PiiMatch[]): PiiMatch[];
29
+ /** Factory: build a PiiGuard interceptor. */
30
+ export declare function piiGuard(options?: PiiGuardOptions): Interceptor;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * PiiGuard — the pre/post interceptor that detects and redacts PII.
3
+ *
4
+ * Pipeline rule (privacy): PII is scanned on every request before it reaches
5
+ * the provider. Detected entities are redacted/masked/tagged or blocked. In
6
+ * REDACT mode the original values are restored in the response.
7
+ */
8
+ import { PiiBlockedError } from '../../errors.js';
9
+ import { PiiMode, Sensitivity } from '../../types.js';
10
+ import { ScanContext } from './context.js';
11
+ import { matchLength } from './match.js';
12
+ import { scannerTier } from './scanner.js';
13
+ import { defaultScanners } from './scanners/index.js';
14
+ const STATE_KEY = 'pii_replacements';
15
+ // Confidence floor per sensitivity level — matches below the floor are ignored.
16
+ const CONFIDENCE_FLOOR = {
17
+ [Sensitivity.STRICT]: 0.0,
18
+ [Sensitivity.BALANCED]: 0.6,
19
+ [Sensitivity.PERMISSIVE]: 0.9,
20
+ };
21
+ class PiiGuard {
22
+ name = 'pii_guard';
23
+ dryRunSafe = true;
24
+ scanners;
25
+ sensitivity;
26
+ mode;
27
+ restoreOnResponse;
28
+ logEntityTypes;
29
+ ownDryRun;
30
+ locale;
31
+ language;
32
+ constructor(options = {}) {
33
+ this.scanners = options.scanners ?? defaultScanners();
34
+ this.sensitivity = options.sensitivity ?? Sensitivity.STRICT;
35
+ this.mode = options.mode ?? PiiMode.REDACT;
36
+ this.restoreOnResponse = options.restoreOnResponse ?? true;
37
+ this.logEntityTypes = options.logEntityTypes ?? true;
38
+ this.ownDryRun = options.dryRun ?? false;
39
+ this.locale = options.locale ?? 'NL';
40
+ this.language = options.language ?? 'en';
41
+ }
42
+ async before(request, ctx) {
43
+ const scanCtx = new ScanContext(this.language, this.locale);
44
+ const floor = CONFIDENCE_FLOOR[this.sensitivity];
45
+ const newMessages = [];
46
+ const allTypes = [];
47
+ const replacements = ctx.state[STATE_KEY] ?? {};
48
+ const isDryRun = this.ownDryRun || ctx.dryRun;
49
+ for (const message of request.messages) {
50
+ const content = message.content ?? '';
51
+ const matches = await this.scanText(content, scanCtx, floor);
52
+ for (const m of matches)
53
+ allTypes.push(m.entityType);
54
+ if (matches.length > 0 && this.mode === PiiMode.BLOCK) {
55
+ const types = matches.map((m) => m.entityType);
56
+ throw new PiiBlockedError(types);
57
+ }
58
+ let redacted = content;
59
+ if (matches.length > 0 && !isDryRun) {
60
+ redacted = this.apply(content, matches, replacements);
61
+ }
62
+ newMessages.push({ ...message, content: redacted });
63
+ }
64
+ if (allTypes.length > 0) {
65
+ ctx.recordPii(allTypes);
66
+ if (this.logEntityTypes) {
67
+ const unique = Array.from(new Set(allTypes)).sort();
68
+ // eslint-disable-next-line no-console
69
+ console.info(`[gavio:pii] detected entity types: ${unique.join(', ')}`);
70
+ }
71
+ }
72
+ if (this.restoreOnResponse && Object.keys(replacements).length > 0) {
73
+ ctx.state[STATE_KEY] = replacements;
74
+ }
75
+ if (isDryRun)
76
+ return request;
77
+ return request.copyWithMessages(newMessages);
78
+ }
79
+ async after(response, ctx) {
80
+ if (!this.restoreOnResponse || this.mode !== PiiMode.REDACT)
81
+ return response;
82
+ const replacements = ctx.state[STATE_KEY];
83
+ if (!replacements || Object.keys(replacements).length === 0)
84
+ return response;
85
+ let content = response.content;
86
+ for (const [token, original] of Object.entries(replacements)) {
87
+ content = content.split(token).join(original);
88
+ }
89
+ if (content === response.content)
90
+ return response;
91
+ return response.copyWithContent(content);
92
+ }
93
+ async scanText(text, scanCtx, floor) {
94
+ const raw = [];
95
+ const ordered = [...this.scanners].sort((a, b) => scannerTier(a) - scannerTier(b));
96
+ for (const scanner of ordered) {
97
+ const found = await scanner.scan(text, scanCtx);
98
+ for (const match of found) {
99
+ if (match.confidence >= floor)
100
+ raw.push(match);
101
+ }
102
+ }
103
+ return resolveOverlaps(raw);
104
+ }
105
+ apply(text, matches, replacements) {
106
+ // Replace right-to-left so earlier offsets stay valid.
107
+ const ordered = [...matches].sort((a, b) => b.start - a.start);
108
+ let out = text;
109
+ for (const match of ordered) {
110
+ const token = this.tokenFor(match);
111
+ if (this.mode === PiiMode.REDACT) {
112
+ replacements[token] = match.value;
113
+ }
114
+ out = out.slice(0, match.start) + token + out.slice(match.end);
115
+ }
116
+ return out;
117
+ }
118
+ tokenFor(match) {
119
+ if (this.mode === PiiMode.MASK) {
120
+ return '*'.repeat(Math.max(matchLength(match), 1));
121
+ }
122
+ if (this.mode === PiiMode.TAG) {
123
+ return `<${match.entityType}>${match.value}</${match.entityType}>`;
124
+ }
125
+ // REDACT (default)
126
+ return match.replacement || `[${match.entityType}]`;
127
+ }
128
+ }
129
+ /**
130
+ * Drop lower-priority matches that overlap a kept one.
131
+ *
132
+ * Sort by start, then by descending span length (prefer the longer match),
133
+ * then by confidence. Greedily keep non-overlapping matches.
134
+ */
135
+ export function resolveOverlaps(matches) {
136
+ const ordered = [...matches].sort((a, b) => {
137
+ if (a.start !== b.start)
138
+ return a.start - b.start;
139
+ const lenDiff = matchLength(b) - matchLength(a);
140
+ if (lenDiff !== 0)
141
+ return lenDiff;
142
+ return b.confidence - a.confidence;
143
+ });
144
+ const kept = [];
145
+ let occupiedEnd = -1;
146
+ for (const match of ordered) {
147
+ if (match.start >= occupiedEnd) {
148
+ kept.push(match);
149
+ occupiedEnd = match.end;
150
+ }
151
+ }
152
+ return kept;
153
+ }
154
+ /** Factory: build a PiiGuard interceptor. */
155
+ export function piiGuard(options = {}) {
156
+ return new PiiGuard(options);
157
+ }
@@ -0,0 +1,10 @@
1
+ /** PII guard public surface. */
2
+ export { piiGuard, resolveOverlaps } from './guard.js';
3
+ export type { PiiGuardOptions } from './guard.js';
4
+ export { ScanContext } from './context.js';
5
+ export { ScannerRegistry, scannerTier } from './scanner.js';
6
+ export type { PiiScanner } from './scanner.js';
7
+ export { makeMatch, matchLength } from './match.js';
8
+ export type { PiiMatch } from './match.js';
9
+ export { PiiMode, Sensitivity } from '../../types.js';
10
+ export { bsnScanner, creditCardScanner, emailScanner, ibanScanner, ipAddressScanner, phoneScanner, secretScanner, ssnScanner, defaultScanners, } from './scanners/index.js';
@@ -0,0 +1,7 @@
1
+ /** PII guard public surface. */
2
+ export { piiGuard, resolveOverlaps } from './guard.js';
3
+ export { ScanContext } from './context.js';
4
+ export { ScannerRegistry, scannerTier } from './scanner.js';
5
+ export { makeMatch, matchLength } from './match.js';
6
+ export { PiiMode, Sensitivity } from '../../types.js';
7
+ export { bsnScanner, creditCardScanner, emailScanner, ibanScanner, ipAddressScanner, phoneScanner, secretScanner, ssnScanner, defaultScanners, } from './scanners/index.js';
@@ -0,0 +1,26 @@
1
+ /** PiiMatch — a single detected PII entity within a span of text. */
2
+ /**
3
+ * One detected entity.
4
+ *
5
+ * `start`/`end` are half-open character offsets into the scanned text.
6
+ * `replacement` is the placeholder used in REDACT mode; `value` is the
7
+ * original text (never logged — used only for restore).
8
+ */
9
+ export interface PiiMatch {
10
+ entityType: string;
11
+ start: number;
12
+ end: number;
13
+ value: string;
14
+ confidence: number;
15
+ /** e.g. '[EMAIL_1]'. */
16
+ replacement: string;
17
+ }
18
+ export declare function matchLength(m: PiiMatch): number;
19
+ export declare function makeMatch(init: {
20
+ entityType: string;
21
+ start: number;
22
+ end: number;
23
+ value: string;
24
+ confidence?: number;
25
+ replacement: string;
26
+ }): PiiMatch;
@@ -0,0 +1,17 @@
1
+ /** PiiMatch — a single detected PII entity within a span of text. */
2
+ export function matchLength(m) {
3
+ return m.end - m.start;
4
+ }
5
+ export function makeMatch(init) {
6
+ if (init.start < 0 || init.end < init.start) {
7
+ throw new Error(`Invalid PiiMatch span: start=${init.start}, end=${init.end}`);
8
+ }
9
+ return {
10
+ entityType: init.entityType,
11
+ start: init.start,
12
+ end: init.end,
13
+ value: init.value,
14
+ confidence: init.confidence ?? 1.0,
15
+ replacement: init.replacement,
16
+ };
17
+ }
@@ -0,0 +1,32 @@
1
+ /** PiiScanner interface and ScannerRegistry. */
2
+ import type { ScanContext } from './context.js';
3
+ import type { PiiMatch } from './match.js';
4
+ /**
5
+ * Detects one class of PII entity within text.
6
+ *
7
+ * Scanners are tiered: tier 1 = regex, tier 2 = NER/ML, tier 3 = LLM. Lower
8
+ * tiers run first so cheap deterministic matches are found before expensive
9
+ * ones. v0.1.0 ships only tier-1 regex scanners.
10
+ */
11
+ export interface PiiScanner {
12
+ /** e.g. 'EMAIL', 'IBAN', 'BSN'. */
13
+ readonly entityType: string;
14
+ /** default: 1 */
15
+ readonly tier?: 1 | 2 | 3;
16
+ scan(text: string, ctx: ScanContext): PiiMatch[] | Promise<PiiMatch[]>;
17
+ /** default: 1.0 */
18
+ readonly confidence?: number;
19
+ supportsLanguage?(lang: string): boolean;
20
+ supportsLocale?(locale: string): boolean;
21
+ }
22
+ export declare function scannerTier(s: PiiScanner): number;
23
+ /** Registry of scanners, discoverable by entity type at runtime. */
24
+ export declare class ScannerRegistry {
25
+ private scanners;
26
+ constructor(scanners?: PiiScanner[]);
27
+ register(scanner: PiiScanner): this;
28
+ /** Return scanners sorted by tier (lowest first). */
29
+ all(): PiiScanner[];
30
+ byEntityType(entityType: string): PiiScanner[];
31
+ get size(): number;
32
+ }
@@ -0,0 +1,26 @@
1
+ /** PiiScanner interface and ScannerRegistry. */
2
+ export function scannerTier(s) {
3
+ return s.tier ?? 1;
4
+ }
5
+ /** Registry of scanners, discoverable by entity type at runtime. */
6
+ export class ScannerRegistry {
7
+ scanners = [];
8
+ constructor(scanners) {
9
+ for (const s of scanners ?? [])
10
+ this.register(s);
11
+ }
12
+ register(scanner) {
13
+ this.scanners.push(scanner);
14
+ return this;
15
+ }
16
+ /** Return scanners sorted by tier (lowest first). */
17
+ all() {
18
+ return [...this.scanners].sort((a, b) => scannerTier(a) - scannerTier(b));
19
+ }
20
+ byEntityType(entityType) {
21
+ return this.scanners.filter((s) => s.entityType === entityType);
22
+ }
23
+ get size() {
24
+ return this.scanners.length;
25
+ }
26
+ }
@@ -0,0 +1,5 @@
1
+ /** Dutch BSN scanner — regex + 11-proef (eleven-test) checksum. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ /** 11-proef: sum of digit*weight (9,8,...,2,-1) must be divisible by 11. */
4
+ export declare function validBsn(digits: string): boolean;
5
+ export declare function bsnScanner(): PiiScanner;
@@ -0,0 +1,37 @@
1
+ /** Dutch BSN scanner — regex + 11-proef (eleven-test) checksum. */
2
+ import { makeMatch } from '../match.js';
3
+ // BSN is 8 or 9 digits; we validate the 9-digit form with the 11-proef.
4
+ const BSN = /\b\d{9}\b/g;
5
+ /** 11-proef: sum of digit*weight (9,8,...,2,-1) must be divisible by 11. */
6
+ export function validBsn(digits) {
7
+ if (digits.length !== 9)
8
+ return false;
9
+ const weights = [9, 8, 7, 6, 5, 4, 3, 2, -1];
10
+ let total = 0;
11
+ for (let i = 0; i < 9; i++) {
12
+ total += Number(digits[i]) * weights[i];
13
+ }
14
+ return total % 11 === 0;
15
+ }
16
+ export function bsnScanner() {
17
+ return {
18
+ entityType: 'BSN',
19
+ tier: 1,
20
+ scan(text, ctx) {
21
+ const out = [];
22
+ for (const m of text.matchAll(BSN)) {
23
+ if (!validBsn(m[0]))
24
+ continue;
25
+ const idx = ctx.nextIndex('BSN');
26
+ out.push(makeMatch({
27
+ entityType: 'BSN',
28
+ start: m.index,
29
+ end: m.index + m[0].length,
30
+ value: m[0],
31
+ replacement: `[BSN_${idx}]`,
32
+ }));
33
+ }
34
+ return out;
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,4 @@
1
+ /** Credit card scanner — regex candidate + Luhn checksum validation. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ export declare function luhnValid(number: string): boolean;
4
+ export declare function creditCardScanner(): PiiScanner;
@@ -0,0 +1,47 @@
1
+ /** Credit card scanner — regex candidate + Luhn checksum validation. */
2
+ import { makeMatch } from '../match.js';
3
+ // 13–19 digits, optionally separated by single spaces or hyphens.
4
+ const CARD = /\b(?:\d[ -]?){12,18}\d\b/g;
5
+ export function luhnValid(number) {
6
+ const digits = [];
7
+ for (const c of number) {
8
+ if (c >= '0' && c <= '9')
9
+ digits.push(c.charCodeAt(0) - 48);
10
+ }
11
+ if (digits.length < 13 || digits.length > 19)
12
+ return false;
13
+ let checksum = 0;
14
+ const parity = digits.length % 2;
15
+ for (let i = 0; i < digits.length; i++) {
16
+ let d = digits[i];
17
+ if (i % 2 === parity) {
18
+ d *= 2;
19
+ if (d > 9)
20
+ d -= 9;
21
+ }
22
+ checksum += d;
23
+ }
24
+ return checksum % 10 === 0;
25
+ }
26
+ export function creditCardScanner() {
27
+ return {
28
+ entityType: 'CREDIT_CARD',
29
+ tier: 1,
30
+ scan(text, ctx) {
31
+ const out = [];
32
+ for (const m of text.matchAll(CARD)) {
33
+ if (!luhnValid(m[0]))
34
+ continue;
35
+ const idx = ctx.nextIndex('CREDIT_CARD');
36
+ out.push(makeMatch({
37
+ entityType: 'CREDIT_CARD',
38
+ start: m.index,
39
+ end: m.index + m[0].length,
40
+ value: m[0],
41
+ replacement: `[CREDIT_CARD_${idx}]`,
42
+ }));
43
+ }
44
+ return out;
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,3 @@
1
+ /** Email address scanner (RFC 5322 pragmatic subset). */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ export declare function emailScanner(): PiiScanner;
@@ -0,0 +1,23 @@
1
+ /** Email address scanner (RFC 5322 pragmatic subset). */
2
+ import { makeMatch } from '../match.js';
3
+ const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
4
+ export function emailScanner() {
5
+ return {
6
+ entityType: 'EMAIL',
7
+ tier: 1,
8
+ scan(text, ctx) {
9
+ const out = [];
10
+ for (const m of text.matchAll(EMAIL)) {
11
+ const idx = ctx.nextIndex('EMAIL');
12
+ out.push(makeMatch({
13
+ entityType: 'EMAIL',
14
+ start: m.index,
15
+ end: m.index + m[0].length,
16
+ value: m[0],
17
+ replacement: `[EMAIL_${idx}]`,
18
+ }));
19
+ }
20
+ return out;
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,5 @@
1
+ /** IBAN scanner — regex candidate + ISO 13616 mod-97 checksum validation. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ /** ISO 13616 mod-97: rearrange, convert letters to numbers, check %97 == 1. */
4
+ export declare function validIban(candidate: string): boolean;
5
+ export declare function ibanScanner(): PiiScanner;