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,33 @@
1
+ "use strict";
2
+ /** stdoutSink — human-readable audit output for development (F-OBS-05). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.stdoutSink = stdoutSink;
5
+ /** Print each audit record to stdout. Zero dependencies. */
6
+ function stdoutSink(options = {}) {
7
+ const pretty = options.pretty ?? true;
8
+ // eslint-disable-next-line no-console
9
+ const emit = options.write ?? ((line) => console.log(line));
10
+ return {
11
+ async write(record) {
12
+ const data = record.toJSON();
13
+ emit(pretty ? formatPretty(data) : JSON.stringify(data));
14
+ },
15
+ };
16
+ }
17
+ function formatPretty(data) {
18
+ const usage = data['tokenUsage'];
19
+ const piiTypes = data['piiEntityTypes'];
20
+ const pii = piiTypes.length > 0 ? piiTypes : ['none'];
21
+ const interceptors = data['interceptorsFired'];
22
+ const traceId = String(data['traceId']);
23
+ const cost = Number(data['costUsd']);
24
+ return ('[gavio:audit] ' +
25
+ `trace=${traceId.slice(0, 18)}… ` +
26
+ `${String(data['provider'])}/${String(data['model'])} ` +
27
+ `tokens=${usage.totalTokens} ` +
28
+ `cost=$${cost.toFixed(6)} ` +
29
+ `latency=${String(data['latencyMs'])}ms ` +
30
+ `cache=${data['cacheHit'] ? 'HIT' : 'miss'} ` +
31
+ `pii=${pii.join(',')} ` +
32
+ `interceptors=[${interceptors.join(',')}]`);
33
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /** The Interceptor interface — the unit of composition in Gavio. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.isExecutorPolicy = isExecutorPolicy;
5
+ function isExecutorPolicy(i) {
6
+ return i.isExecutorPolicy === true;
7
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * CacheBackend — the key/value contract behind the cache interceptors.
4
+ *
5
+ * The full SemanticCache interceptor lands in v0.2.0 (F-CACHE-01/02). v0.1.0
6
+ * ships the backend interface and the in-memory backend so dev mode has a
7
+ * working, dependency-free cache substrate.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryCacheBackend = void 0;
4
+ var memory_js_1 = require("./memory.js");
5
+ Object.defineProperty(exports, "memoryCacheBackend", { enumerable: true, get: function () { return memory_js_1.memoryCacheBackend; } });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /** In-memory cache backend (F-CACHE-03) — default zero-dependency dev backend. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.memoryCacheBackend = memoryCacheBackend;
5
+ /** LRU-bounded, optionally TTL'd in-process cache. Not shared across processes. */
6
+ class MemoryBackend {
7
+ maxSize;
8
+ // Map preserves insertion order, which we use for LRU eviction.
9
+ store = new Map();
10
+ constructor(maxSize = 1000) {
11
+ this.maxSize = maxSize;
12
+ }
13
+ async get(key) {
14
+ const entry = this.store.get(key);
15
+ if (entry === undefined)
16
+ return null;
17
+ if (entry.expiresAt !== null && now() > entry.expiresAt) {
18
+ this.store.delete(key);
19
+ return null;
20
+ }
21
+ // Move to end (most-recently-used).
22
+ this.store.delete(key);
23
+ this.store.set(key, entry);
24
+ return entry.value;
25
+ }
26
+ async set(key, value, ttlSeconds) {
27
+ const expiresAt = ttlSeconds ? now() + ttlSeconds * 1000 : null;
28
+ this.store.delete(key);
29
+ this.store.set(key, { value, expiresAt });
30
+ while (this.store.size > this.maxSize) {
31
+ const oldest = this.store.keys().next().value;
32
+ if (oldest === undefined)
33
+ break;
34
+ this.store.delete(oldest);
35
+ }
36
+ }
37
+ async delete(key) {
38
+ this.store.delete(key);
39
+ }
40
+ async clear() {
41
+ this.store.clear();
42
+ }
43
+ get size() {
44
+ return this.store.size;
45
+ }
46
+ }
47
+ function now() {
48
+ return Date.now();
49
+ }
50
+ /** Factory: build an in-memory cache backend. */
51
+ function memoryCacheBackend(options = {}) {
52
+ return new MemoryBackend(options.maxSize ?? 1000);
53
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Caching substrate. The SemanticCache interceptor ships in v0.2.0; v0.1.0
4
+ * exposes the CacheBackend interface and the in-memory backend only.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.memoryCacheBackend = void 0;
8
+ var memory_js_1 = require("./backends/memory.js");
9
+ Object.defineProperty(exports, "memoryCacheBackend", { enumerable: true, get: function () { return memory_js_1.memoryCacheBackend; } });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /** InterceptorChain — runs the pre/post pipeline around the provider call. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.InterceptorChain = void 0;
5
+ const dryRunSafe = (i) => i.dryRunSafe !== false;
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
+ class InterceptorChain {
14
+ interceptors;
15
+ constructor(interceptors) {
16
+ this.interceptors = [...interceptors];
17
+ }
18
+ async execute(request, ctx, executor) {
19
+ try {
20
+ let req = request;
21
+ for (const interceptor of this.interceptors) {
22
+ if (ctx.dryRun && !dryRunSafe(interceptor))
23
+ continue;
24
+ if (interceptor.before) {
25
+ req = await interceptor.before(req, ctx);
26
+ }
27
+ ctx.markFired(interceptor.name);
28
+ }
29
+ let response = await executor(req);
30
+ for (let i = this.interceptors.length - 1; i >= 0; i--) {
31
+ const interceptor = this.interceptors[i];
32
+ if (ctx.dryRun && !dryRunSafe(interceptor))
33
+ continue;
34
+ if (interceptor.after) {
35
+ response = await interceptor.after(response, ctx);
36
+ }
37
+ }
38
+ response.interceptorsFired = [...ctx.interceptorsFired];
39
+ return response;
40
+ }
41
+ catch (error) {
42
+ const err = error instanceof Error ? error : new Error(String(error));
43
+ for (const interceptor of this.interceptors) {
44
+ if (interceptor.onError) {
45
+ try {
46
+ await interceptor.onError(err, ctx);
47
+ }
48
+ catch {
49
+ // on_error must never break the propagation of the original error.
50
+ }
51
+ }
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ }
57
+ exports.InterceptorChain = InterceptorChain;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /** Interceptor barrel. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.memoryCacheBackend = exports.fallbackChain = exports.timeoutPolicy = exports.retryInterceptor = exports.auditInterceptor = exports.piiGuard = exports.InterceptorChain = exports.isExecutorPolicy = void 0;
5
+ var base_js_1 = require("./base.js");
6
+ Object.defineProperty(exports, "isExecutorPolicy", { enumerable: true, get: function () { return base_js_1.isExecutorPolicy; } });
7
+ var chain_js_1 = require("./chain.js");
8
+ Object.defineProperty(exports, "InterceptorChain", { enumerable: true, get: function () { return chain_js_1.InterceptorChain; } });
9
+ var index_js_1 = require("./pii/index.js");
10
+ Object.defineProperty(exports, "piiGuard", { enumerable: true, get: function () { return index_js_1.piiGuard; } });
11
+ var index_js_2 = require("./audit/index.js");
12
+ Object.defineProperty(exports, "auditInterceptor", { enumerable: true, get: function () { return index_js_2.auditInterceptor; } });
13
+ var index_js_3 = require("./reliability/index.js");
14
+ Object.defineProperty(exports, "retryInterceptor", { enumerable: true, get: function () { return index_js_3.retryInterceptor; } });
15
+ Object.defineProperty(exports, "timeoutPolicy", { enumerable: true, get: function () { return index_js_3.timeoutPolicy; } });
16
+ Object.defineProperty(exports, "fallbackChain", { enumerable: true, get: function () { return index_js_3.fallbackChain; } });
17
+ var index_js_4 = require("./cache/index.js");
18
+ Object.defineProperty(exports, "memoryCacheBackend", { enumerable: true, get: function () { return index_js_4.memoryCacheBackend; } });
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /** ScanContext — per-request state shared across PII scanners. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ScanContext = void 0;
5
+ /**
6
+ * Context threaded through every scanner for one request.
7
+ *
8
+ * Tracks a monotonic per-entity-type index so repeated entities get stable,
9
+ * distinct placeholders (`[EMAIL_1]`, `[EMAIL_2]`).
10
+ */
11
+ class ScanContext {
12
+ language;
13
+ locale;
14
+ counters = {};
15
+ constructor(language = 'en', locale = 'NL') {
16
+ this.language = language;
17
+ this.locale = locale;
18
+ }
19
+ /** Return the next 1-based index for an entity type. */
20
+ nextIndex(entityType) {
21
+ this.counters[entityType] = (this.counters[entityType] ?? 0) + 1;
22
+ return this.counters[entityType];
23
+ }
24
+ }
25
+ exports.ScanContext = ScanContext;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * PiiGuard — the pre/post interceptor that detects and redacts PII.
4
+ *
5
+ * Pipeline rule (privacy): PII is scanned on every request before it reaches
6
+ * the provider. Detected entities are redacted/masked/tagged or blocked. In
7
+ * REDACT mode the original values are restored in the response.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.resolveOverlaps = resolveOverlaps;
11
+ exports.piiGuard = piiGuard;
12
+ const errors_js_1 = require("../../errors.js");
13
+ const types_js_1 = require("../../types.js");
14
+ const context_js_1 = require("./context.js");
15
+ const match_js_1 = require("./match.js");
16
+ const scanner_js_1 = require("./scanner.js");
17
+ const index_js_1 = require("./scanners/index.js");
18
+ const STATE_KEY = 'pii_replacements';
19
+ // Confidence floor per sensitivity level — matches below the floor are ignored.
20
+ const CONFIDENCE_FLOOR = {
21
+ [types_js_1.Sensitivity.STRICT]: 0.0,
22
+ [types_js_1.Sensitivity.BALANCED]: 0.6,
23
+ [types_js_1.Sensitivity.PERMISSIVE]: 0.9,
24
+ };
25
+ class PiiGuard {
26
+ name = 'pii_guard';
27
+ dryRunSafe = true;
28
+ scanners;
29
+ sensitivity;
30
+ mode;
31
+ restoreOnResponse;
32
+ logEntityTypes;
33
+ ownDryRun;
34
+ locale;
35
+ language;
36
+ constructor(options = {}) {
37
+ this.scanners = options.scanners ?? (0, index_js_1.defaultScanners)();
38
+ this.sensitivity = options.sensitivity ?? types_js_1.Sensitivity.STRICT;
39
+ this.mode = options.mode ?? types_js_1.PiiMode.REDACT;
40
+ this.restoreOnResponse = options.restoreOnResponse ?? true;
41
+ this.logEntityTypes = options.logEntityTypes ?? true;
42
+ this.ownDryRun = options.dryRun ?? false;
43
+ this.locale = options.locale ?? 'NL';
44
+ this.language = options.language ?? 'en';
45
+ }
46
+ async before(request, ctx) {
47
+ const scanCtx = new context_js_1.ScanContext(this.language, this.locale);
48
+ const floor = CONFIDENCE_FLOOR[this.sensitivity];
49
+ const newMessages = [];
50
+ const allTypes = [];
51
+ const replacements = ctx.state[STATE_KEY] ?? {};
52
+ const isDryRun = this.ownDryRun || ctx.dryRun;
53
+ for (const message of request.messages) {
54
+ const content = message.content ?? '';
55
+ const matches = await this.scanText(content, scanCtx, floor);
56
+ for (const m of matches)
57
+ allTypes.push(m.entityType);
58
+ if (matches.length > 0 && this.mode === types_js_1.PiiMode.BLOCK) {
59
+ const types = matches.map((m) => m.entityType);
60
+ throw new errors_js_1.PiiBlockedError(types);
61
+ }
62
+ let redacted = content;
63
+ if (matches.length > 0 && !isDryRun) {
64
+ redacted = this.apply(content, matches, replacements);
65
+ }
66
+ newMessages.push({ ...message, content: redacted });
67
+ }
68
+ if (allTypes.length > 0) {
69
+ ctx.recordPii(allTypes);
70
+ if (this.logEntityTypes) {
71
+ const unique = Array.from(new Set(allTypes)).sort();
72
+ // eslint-disable-next-line no-console
73
+ console.info(`[gavio:pii] detected entity types: ${unique.join(', ')}`);
74
+ }
75
+ }
76
+ if (this.restoreOnResponse && Object.keys(replacements).length > 0) {
77
+ ctx.state[STATE_KEY] = replacements;
78
+ }
79
+ if (isDryRun)
80
+ return request;
81
+ return request.copyWithMessages(newMessages);
82
+ }
83
+ async after(response, ctx) {
84
+ if (!this.restoreOnResponse || this.mode !== types_js_1.PiiMode.REDACT)
85
+ return response;
86
+ const replacements = ctx.state[STATE_KEY];
87
+ if (!replacements || Object.keys(replacements).length === 0)
88
+ return response;
89
+ let content = response.content;
90
+ for (const [token, original] of Object.entries(replacements)) {
91
+ content = content.split(token).join(original);
92
+ }
93
+ if (content === response.content)
94
+ return response;
95
+ return response.copyWithContent(content);
96
+ }
97
+ async scanText(text, scanCtx, floor) {
98
+ const raw = [];
99
+ const ordered = [...this.scanners].sort((a, b) => (0, scanner_js_1.scannerTier)(a) - (0, scanner_js_1.scannerTier)(b));
100
+ for (const scanner of ordered) {
101
+ const found = await scanner.scan(text, scanCtx);
102
+ for (const match of found) {
103
+ if (match.confidence >= floor)
104
+ raw.push(match);
105
+ }
106
+ }
107
+ return resolveOverlaps(raw);
108
+ }
109
+ apply(text, matches, replacements) {
110
+ // Replace right-to-left so earlier offsets stay valid.
111
+ const ordered = [...matches].sort((a, b) => b.start - a.start);
112
+ let out = text;
113
+ for (const match of ordered) {
114
+ const token = this.tokenFor(match);
115
+ if (this.mode === types_js_1.PiiMode.REDACT) {
116
+ replacements[token] = match.value;
117
+ }
118
+ out = out.slice(0, match.start) + token + out.slice(match.end);
119
+ }
120
+ return out;
121
+ }
122
+ tokenFor(match) {
123
+ if (this.mode === types_js_1.PiiMode.MASK) {
124
+ return '*'.repeat(Math.max((0, match_js_1.matchLength)(match), 1));
125
+ }
126
+ if (this.mode === types_js_1.PiiMode.TAG) {
127
+ return `<${match.entityType}>${match.value}</${match.entityType}>`;
128
+ }
129
+ // REDACT (default)
130
+ return match.replacement || `[${match.entityType}]`;
131
+ }
132
+ }
133
+ /**
134
+ * Drop lower-priority matches that overlap a kept one.
135
+ *
136
+ * Sort by start, then by descending span length (prefer the longer match),
137
+ * then by confidence. Greedily keep non-overlapping matches.
138
+ */
139
+ function resolveOverlaps(matches) {
140
+ const ordered = [...matches].sort((a, b) => {
141
+ if (a.start !== b.start)
142
+ return a.start - b.start;
143
+ const lenDiff = (0, match_js_1.matchLength)(b) - (0, match_js_1.matchLength)(a);
144
+ if (lenDiff !== 0)
145
+ return lenDiff;
146
+ return b.confidence - a.confidence;
147
+ });
148
+ const kept = [];
149
+ let occupiedEnd = -1;
150
+ for (const match of ordered) {
151
+ if (match.start >= occupiedEnd) {
152
+ kept.push(match);
153
+ occupiedEnd = match.end;
154
+ }
155
+ }
156
+ return kept;
157
+ }
158
+ /** Factory: build a PiiGuard interceptor. */
159
+ function piiGuard(options = {}) {
160
+ return new PiiGuard(options);
161
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /** PII guard public surface. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.defaultScanners = exports.ssnScanner = exports.secretScanner = exports.phoneScanner = exports.ipAddressScanner = exports.ibanScanner = exports.emailScanner = exports.creditCardScanner = exports.bsnScanner = exports.Sensitivity = exports.PiiMode = exports.matchLength = exports.makeMatch = exports.scannerTier = exports.ScannerRegistry = exports.ScanContext = exports.resolveOverlaps = exports.piiGuard = void 0;
5
+ var guard_js_1 = require("./guard.js");
6
+ Object.defineProperty(exports, "piiGuard", { enumerable: true, get: function () { return guard_js_1.piiGuard; } });
7
+ Object.defineProperty(exports, "resolveOverlaps", { enumerable: true, get: function () { return guard_js_1.resolveOverlaps; } });
8
+ var context_js_1 = require("./context.js");
9
+ Object.defineProperty(exports, "ScanContext", { enumerable: true, get: function () { return context_js_1.ScanContext; } });
10
+ var scanner_js_1 = require("./scanner.js");
11
+ Object.defineProperty(exports, "ScannerRegistry", { enumerable: true, get: function () { return scanner_js_1.ScannerRegistry; } });
12
+ Object.defineProperty(exports, "scannerTier", { enumerable: true, get: function () { return scanner_js_1.scannerTier; } });
13
+ var match_js_1 = require("./match.js");
14
+ Object.defineProperty(exports, "makeMatch", { enumerable: true, get: function () { return match_js_1.makeMatch; } });
15
+ Object.defineProperty(exports, "matchLength", { enumerable: true, get: function () { return match_js_1.matchLength; } });
16
+ var types_js_1 = require("../../types.js");
17
+ Object.defineProperty(exports, "PiiMode", { enumerable: true, get: function () { return types_js_1.PiiMode; } });
18
+ Object.defineProperty(exports, "Sensitivity", { enumerable: true, get: function () { return types_js_1.Sensitivity; } });
19
+ var index_js_1 = require("./scanners/index.js");
20
+ Object.defineProperty(exports, "bsnScanner", { enumerable: true, get: function () { return index_js_1.bsnScanner; } });
21
+ Object.defineProperty(exports, "creditCardScanner", { enumerable: true, get: function () { return index_js_1.creditCardScanner; } });
22
+ Object.defineProperty(exports, "emailScanner", { enumerable: true, get: function () { return index_js_1.emailScanner; } });
23
+ Object.defineProperty(exports, "ibanScanner", { enumerable: true, get: function () { return index_js_1.ibanScanner; } });
24
+ Object.defineProperty(exports, "ipAddressScanner", { enumerable: true, get: function () { return index_js_1.ipAddressScanner; } });
25
+ Object.defineProperty(exports, "phoneScanner", { enumerable: true, get: function () { return index_js_1.phoneScanner; } });
26
+ Object.defineProperty(exports, "secretScanner", { enumerable: true, get: function () { return index_js_1.secretScanner; } });
27
+ Object.defineProperty(exports, "ssnScanner", { enumerable: true, get: function () { return index_js_1.ssnScanner; } });
28
+ Object.defineProperty(exports, "defaultScanners", { enumerable: true, get: function () { return index_js_1.defaultScanners; } });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /** PiiMatch — a single detected PII entity within a span of text. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.matchLength = matchLength;
5
+ exports.makeMatch = makeMatch;
6
+ function matchLength(m) {
7
+ return m.end - m.start;
8
+ }
9
+ function makeMatch(init) {
10
+ if (init.start < 0 || init.end < init.start) {
11
+ throw new Error(`Invalid PiiMatch span: start=${init.start}, end=${init.end}`);
12
+ }
13
+ return {
14
+ entityType: init.entityType,
15
+ start: init.start,
16
+ end: init.end,
17
+ value: init.value,
18
+ confidence: init.confidence ?? 1.0,
19
+ replacement: init.replacement,
20
+ };
21
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /** PiiScanner interface and ScannerRegistry. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ScannerRegistry = void 0;
5
+ exports.scannerTier = scannerTier;
6
+ function scannerTier(s) {
7
+ return s.tier ?? 1;
8
+ }
9
+ /** Registry of scanners, discoverable by entity type at runtime. */
10
+ class ScannerRegistry {
11
+ scanners = [];
12
+ constructor(scanners) {
13
+ for (const s of scanners ?? [])
14
+ this.register(s);
15
+ }
16
+ register(scanner) {
17
+ this.scanners.push(scanner);
18
+ return this;
19
+ }
20
+ /** Return scanners sorted by tier (lowest first). */
21
+ all() {
22
+ return [...this.scanners].sort((a, b) => scannerTier(a) - scannerTier(b));
23
+ }
24
+ byEntityType(entityType) {
25
+ return this.scanners.filter((s) => s.entityType === entityType);
26
+ }
27
+ get size() {
28
+ return this.scanners.length;
29
+ }
30
+ }
31
+ exports.ScannerRegistry = ScannerRegistry;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /** Dutch BSN scanner — regex + 11-proef (eleven-test) checksum. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.validBsn = validBsn;
5
+ exports.bsnScanner = bsnScanner;
6
+ const match_js_1 = require("../match.js");
7
+ // BSN is 8 or 9 digits; we validate the 9-digit form with the 11-proef.
8
+ const BSN = /\b\d{9}\b/g;
9
+ /** 11-proef: sum of digit*weight (9,8,...,2,-1) must be divisible by 11. */
10
+ function validBsn(digits) {
11
+ if (digits.length !== 9)
12
+ return false;
13
+ const weights = [9, 8, 7, 6, 5, 4, 3, 2, -1];
14
+ let total = 0;
15
+ for (let i = 0; i < 9; i++) {
16
+ total += Number(digits[i]) * weights[i];
17
+ }
18
+ return total % 11 === 0;
19
+ }
20
+ function bsnScanner() {
21
+ return {
22
+ entityType: 'BSN',
23
+ tier: 1,
24
+ scan(text, ctx) {
25
+ const out = [];
26
+ for (const m of text.matchAll(BSN)) {
27
+ if (!validBsn(m[0]))
28
+ continue;
29
+ const idx = ctx.nextIndex('BSN');
30
+ out.push((0, match_js_1.makeMatch)({
31
+ entityType: 'BSN',
32
+ start: m.index,
33
+ end: m.index + m[0].length,
34
+ value: m[0],
35
+ replacement: `[BSN_${idx}]`,
36
+ }));
37
+ }
38
+ return out;
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /** Credit card scanner — regex candidate + Luhn checksum validation. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.luhnValid = luhnValid;
5
+ exports.creditCardScanner = creditCardScanner;
6
+ const match_js_1 = require("../match.js");
7
+ // 13–19 digits, optionally separated by single spaces or hyphens.
8
+ const CARD = /\b(?:\d[ -]?){12,18}\d\b/g;
9
+ function luhnValid(number) {
10
+ const digits = [];
11
+ for (const c of number) {
12
+ if (c >= '0' && c <= '9')
13
+ digits.push(c.charCodeAt(0) - 48);
14
+ }
15
+ if (digits.length < 13 || digits.length > 19)
16
+ return false;
17
+ let checksum = 0;
18
+ const parity = digits.length % 2;
19
+ for (let i = 0; i < digits.length; i++) {
20
+ let d = digits[i];
21
+ if (i % 2 === parity) {
22
+ d *= 2;
23
+ if (d > 9)
24
+ d -= 9;
25
+ }
26
+ checksum += d;
27
+ }
28
+ return checksum % 10 === 0;
29
+ }
30
+ function creditCardScanner() {
31
+ return {
32
+ entityType: 'CREDIT_CARD',
33
+ tier: 1,
34
+ scan(text, ctx) {
35
+ const out = [];
36
+ for (const m of text.matchAll(CARD)) {
37
+ if (!luhnValid(m[0]))
38
+ continue;
39
+ const idx = ctx.nextIndex('CREDIT_CARD');
40
+ out.push((0, match_js_1.makeMatch)({
41
+ entityType: 'CREDIT_CARD',
42
+ start: m.index,
43
+ end: m.index + m[0].length,
44
+ value: m[0],
45
+ replacement: `[CREDIT_CARD_${idx}]`,
46
+ }));
47
+ }
48
+ return out;
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /** Email address scanner (RFC 5322 pragmatic subset). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.emailScanner = emailScanner;
5
+ const match_js_1 = require("../match.js");
6
+ const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
7
+ function emailScanner() {
8
+ return {
9
+ entityType: 'EMAIL',
10
+ tier: 1,
11
+ scan(text, ctx) {
12
+ const out = [];
13
+ for (const m of text.matchAll(EMAIL)) {
14
+ const idx = ctx.nextIndex('EMAIL');
15
+ out.push((0, match_js_1.makeMatch)({
16
+ entityType: 'EMAIL',
17
+ start: m.index,
18
+ end: m.index + m[0].length,
19
+ value: m[0],
20
+ replacement: `[EMAIL_${idx}]`,
21
+ }));
22
+ }
23
+ return out;
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /** IBAN scanner — regex candidate + ISO 13616 mod-97 checksum validation. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.validIban = validIban;
5
+ exports.ibanScanner = ibanScanner;
6
+ const match_js_1 = require("../match.js");
7
+ // Candidate: 2 letters, 2 check digits, 11–30 alphanumerics (optionally spaced).
8
+ const IBAN = /\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}\b/g;
9
+ /** ISO 13616 mod-97: rearrange, convert letters to numbers, check %97 == 1. */
10
+ function validIban(candidate) {
11
+ const cleaned = candidate.replace(/ /g, '').toUpperCase();
12
+ if (cleaned.length < 15)
13
+ return false;
14
+ const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
15
+ let digits = '';
16
+ for (const ch of rearranged) {
17
+ if (ch >= 'A' && ch <= 'Z') {
18
+ digits += (ch.charCodeAt(0) - 55).toString();
19
+ }
20
+ else if (ch >= '0' && ch <= '9') {
21
+ digits += ch;
22
+ }
23
+ else {
24
+ return false;
25
+ }
26
+ }
27
+ return mod97(digits) === 1;
28
+ }
29
+ /** Compute n % 97 over a large numeric string without BigInt overflow concerns. */
30
+ function mod97(numeric) {
31
+ let remainder = 0;
32
+ for (const ch of numeric) {
33
+ remainder = (remainder * 10 + (ch.charCodeAt(0) - 48)) % 97;
34
+ }
35
+ return remainder;
36
+ }
37
+ function ibanScanner() {
38
+ return {
39
+ entityType: 'IBAN',
40
+ tier: 1,
41
+ scan(text, ctx) {
42
+ const out = [];
43
+ for (const m of text.matchAll(IBAN)) {
44
+ if (!validIban(m[0]))
45
+ continue;
46
+ const idx = ctx.nextIndex('IBAN');
47
+ out.push((0, match_js_1.makeMatch)({
48
+ entityType: 'IBAN',
49
+ start: m.index,
50
+ end: m.index + m[0].length,
51
+ value: m[0],
52
+ replacement: `[IBAN_${idx}]`,
53
+ }));
54
+ }
55
+ return out;
56
+ },
57
+ };
58
+ }