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,54 @@
1
+ /** IBAN scanner — regex candidate + ISO 13616 mod-97 checksum validation. */
2
+ import { makeMatch } from '../match.js';
3
+ // Candidate: 2 letters, 2 check digits, 11–30 alphanumerics (optionally spaced).
4
+ const IBAN = /\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}\b/g;
5
+ /** ISO 13616 mod-97: rearrange, convert letters to numbers, check %97 == 1. */
6
+ export function validIban(candidate) {
7
+ const cleaned = candidate.replace(/ /g, '').toUpperCase();
8
+ if (cleaned.length < 15)
9
+ return false;
10
+ const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
11
+ let digits = '';
12
+ for (const ch of rearranged) {
13
+ if (ch >= 'A' && ch <= 'Z') {
14
+ digits += (ch.charCodeAt(0) - 55).toString();
15
+ }
16
+ else if (ch >= '0' && ch <= '9') {
17
+ digits += ch;
18
+ }
19
+ else {
20
+ return false;
21
+ }
22
+ }
23
+ return mod97(digits) === 1;
24
+ }
25
+ /** Compute n % 97 over a large numeric string without BigInt overflow concerns. */
26
+ function mod97(numeric) {
27
+ let remainder = 0;
28
+ for (const ch of numeric) {
29
+ remainder = (remainder * 10 + (ch.charCodeAt(0) - 48)) % 97;
30
+ }
31
+ return remainder;
32
+ }
33
+ export function ibanScanner() {
34
+ return {
35
+ entityType: 'IBAN',
36
+ tier: 1,
37
+ scan(text, ctx) {
38
+ const out = [];
39
+ for (const m of text.matchAll(IBAN)) {
40
+ if (!validIban(m[0]))
41
+ continue;
42
+ const idx = ctx.nextIndex('IBAN');
43
+ out.push(makeMatch({
44
+ entityType: 'IBAN',
45
+ start: m.index,
46
+ end: m.index + m[0].length,
47
+ value: m[0],
48
+ replacement: `[IBAN_${idx}]`,
49
+ }));
50
+ }
51
+ return out;
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,13 @@
1
+ /** Built-in tier-1 (regex) PII scanners. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ export { bsnScanner, validBsn } from './bsn.js';
4
+ export { creditCardScanner, luhnValid } from './credit-card.js';
5
+ export { emailScanner } from './email.js';
6
+ export { ibanScanner, validIban } from './iban.js';
7
+ export { ipAddressScanner } from './ip-address.js';
8
+ export { phoneScanner } from './phone.js';
9
+ export type { PhoneScannerOptions } from './phone.js';
10
+ export { secretScanner } from './secret.js';
11
+ export { ssnScanner } from './ssn.js';
12
+ /** The default scanner set wired into PiiGuard when none is supplied. */
13
+ export declare function defaultScanners(): PiiScanner[];
@@ -0,0 +1,30 @@
1
+ /** Built-in tier-1 (regex) PII scanners. */
2
+ import { bsnScanner } from './bsn.js';
3
+ import { creditCardScanner } from './credit-card.js';
4
+ import { emailScanner } from './email.js';
5
+ import { ibanScanner } from './iban.js';
6
+ import { ipAddressScanner } from './ip-address.js';
7
+ import { phoneScanner } from './phone.js';
8
+ import { secretScanner } from './secret.js';
9
+ import { ssnScanner } from './ssn.js';
10
+ export { bsnScanner, validBsn } from './bsn.js';
11
+ export { creditCardScanner, luhnValid } from './credit-card.js';
12
+ export { emailScanner } from './email.js';
13
+ export { ibanScanner, validIban } from './iban.js';
14
+ export { ipAddressScanner } from './ip-address.js';
15
+ export { phoneScanner } from './phone.js';
16
+ export { secretScanner } from './secret.js';
17
+ export { ssnScanner } from './ssn.js';
18
+ /** The default scanner set wired into PiiGuard when none is supplied. */
19
+ export function defaultScanners() {
20
+ return [
21
+ secretScanner(),
22
+ emailScanner(),
23
+ ibanScanner(),
24
+ bsnScanner(),
25
+ creditCardScanner(),
26
+ ssnScanner(),
27
+ phoneScanner(),
28
+ ipAddressScanner(),
29
+ ];
30
+ }
@@ -0,0 +1,3 @@
1
+ /** IP address scanner — IPv4 and IPv6, validated via node:net isIP. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ export declare function ipAddressScanner(): PiiScanner;
@@ -0,0 +1,33 @@
1
+ /** IP address scanner — IPv4 and IPv6, validated via node:net isIP. */
2
+ import { isIP } from 'node:net';
3
+ import { makeMatch } from '../match.js';
4
+ const IPV4 = String.raw `(?:\d{1,3}\.){3}\d{1,3}`;
5
+ // Permissive IPv6 candidate — allows empty groups for "::" compression. False
6
+ // positives are filtered by isIP validation below.
7
+ const IPV6 = String.raw `(?:[A-Fa-f0-9]{0,4}:){2,7}[A-Fa-f0-9]{0,4}`;
8
+ const IP = new RegExp(String.raw `(?<![\w.])(?:${IPV6}|${IPV4})(?![\w.])`, 'g');
9
+ function validIp(candidate) {
10
+ return isIP(candidate) !== 0;
11
+ }
12
+ export function ipAddressScanner() {
13
+ return {
14
+ entityType: 'IP_ADDRESS',
15
+ tier: 1,
16
+ scan(text, ctx) {
17
+ const out = [];
18
+ for (const m of text.matchAll(IP)) {
19
+ if (!validIp(m[0]))
20
+ continue;
21
+ const idx = ctx.nextIndex('IP_ADDRESS');
22
+ out.push(makeMatch({
23
+ entityType: 'IP_ADDRESS',
24
+ start: m.index,
25
+ end: m.index + m[0].length,
26
+ value: m[0],
27
+ replacement: `[IP_ADDRESS_${idx}]`,
28
+ }));
29
+ }
30
+ return out;
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,6 @@
1
+ /** Phone number scanner — E.164 and common national formats. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ export interface PhoneScannerOptions {
4
+ locales?: string[];
5
+ }
6
+ export declare function phoneScanner(options?: PhoneScannerOptions): PiiScanner;
@@ -0,0 +1,34 @@
1
+ /** Phone number scanner — E.164 and common national formats. */
2
+ import { makeMatch } from '../match.js';
3
+ // E.164 (+CC...) or national groupings with separators. A digit-count filter
4
+ // (7–15) below avoids matching short numbers / years.
5
+ const PHONE = /(?<![\w.])(?:\+?\d{1,3}[ .-]?)?(?:\(\d{1,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){2,4}(?![\w])/g;
6
+ export function phoneScanner(options = {}) {
7
+ const locales = options.locales ?? ['NL', 'DE', 'GB', 'US'];
8
+ return {
9
+ entityType: 'PHONE',
10
+ tier: 1,
11
+ confidence: 0.85,
12
+ scan(text, ctx) {
13
+ const out = [];
14
+ for (const m of text.matchAll(PHONE)) {
15
+ const digitCount = (m[0].match(/\d/g) ?? []).length;
16
+ if (digitCount < 7 || digitCount > 15)
17
+ continue;
18
+ const idx = ctx.nextIndex('PHONE');
19
+ out.push(makeMatch({
20
+ entityType: 'PHONE',
21
+ start: m.index,
22
+ end: m.index + m[0].length,
23
+ value: m[0],
24
+ confidence: 0.85,
25
+ replacement: `[PHONE_${idx}]`,
26
+ }));
27
+ }
28
+ return out;
29
+ },
30
+ supportsLocale(locale) {
31
+ return locales.includes(locale.toUpperCase());
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Secret / credential scanner (F-SEC-04).
3
+ *
4
+ * Detects API keys, tokens, JWTs, PEM private keys, and database connection
5
+ * strings. These must never leave the device, so SecretScanner is tier 1 and
6
+ * runs by default.
7
+ */
8
+ import type { PiiScanner } from '../scanner.js';
9
+ export declare function secretScanner(): PiiScanner;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Secret / credential scanner (F-SEC-04).
3
+ *
4
+ * Detects API keys, tokens, JWTs, PEM private keys, and database connection
5
+ * strings. These must never leave the device, so SecretScanner is tier 1 and
6
+ * runs by default.
7
+ */
8
+ import { makeMatch } from '../match.js';
9
+ // [label, pattern] — ordered most-specific first. All patterns are global.
10
+ const PATTERNS = [
11
+ ['ANTHROPIC_KEY', /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g],
12
+ ['OPENAI_KEY', /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
13
+ ['AWS_ACCESS_KEY', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g],
14
+ ['GITHUB_TOKEN', /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g],
15
+ ['JWT', /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g],
16
+ ['PRIVATE_KEY', /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g],
17
+ [
18
+ 'DB_CONNECTION_STRING',
19
+ /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s"']+/g,
20
+ ],
21
+ ];
22
+ export function secretScanner() {
23
+ return {
24
+ entityType: 'SECRET',
25
+ tier: 1,
26
+ scan(text, ctx) {
27
+ const out = [];
28
+ for (const [, pattern] of PATTERNS) {
29
+ for (const m of text.matchAll(pattern)) {
30
+ const idx = ctx.nextIndex('SECRET');
31
+ out.push(makeMatch({
32
+ entityType: 'SECRET',
33
+ start: m.index,
34
+ end: m.index + m[0].length,
35
+ value: m[0],
36
+ replacement: `[SECRET_${idx}]`,
37
+ }));
38
+ }
39
+ }
40
+ return out;
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,3 @@
1
+ /** US Social Security Number scanner. */
2
+ import type { PiiScanner } from '../scanner.js';
3
+ export declare function ssnScanner(): PiiScanner;
@@ -0,0 +1,25 @@
1
+ /** US Social Security Number scanner. */
2
+ import { makeMatch } from '../match.js';
3
+ // AAA-GG-SSSS with hyphens or spaces. Requires a separator to avoid colliding
4
+ // with bare 9-digit numbers (handled by the BSN scanner / others).
5
+ const SSN = /\b(?!000|666|9\d\d)\d{3}[ -](?!00)\d{2}[ -](?!0000)\d{4}\b/g;
6
+ export function ssnScanner() {
7
+ return {
8
+ entityType: 'SSN',
9
+ tier: 1,
10
+ scan(text, ctx) {
11
+ const out = [];
12
+ for (const m of text.matchAll(SSN)) {
13
+ const idx = ctx.nextIndex('SSN');
14
+ out.push(makeMatch({
15
+ entityType: 'SSN',
16
+ start: m.index,
17
+ end: m.index + m[0].length,
18
+ value: m[0],
19
+ replacement: `[SSN_${idx}]`,
20
+ }));
21
+ }
22
+ return out;
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,9 @@
1
+ /** fallbackChain (F-REL-02) — route to a secondary provider on failure. */
2
+ import type { ProviderAdapter } from '../../providers/base.js';
3
+ import type { ExecutorPolicy } from '../base.js';
4
+ export interface FallbackChainOptions {
5
+ /** Provider adapters to try, in order, when the primary call fails. */
6
+ fallbacks: ProviderAdapter[];
7
+ }
8
+ /** Factory: build a fallback policy. */
9
+ export declare function fallbackChain(options: FallbackChainOptions): ExecutorPolicy;
@@ -0,0 +1,50 @@
1
+ /** fallbackChain (F-REL-02) — route to a secondary provider on failure. */
2
+ import { ProviderError } from '../../errors.js';
3
+ import { coerceProvider } from '../../types.js';
4
+ /**
5
+ * Try the primary executor; on a provider error, try fallback adapters.
6
+ *
7
+ * Each fallback is a provider adapter (anything with an async `complete`). The
8
+ * request's provider/model are rewritten per fallback so the audit record
9
+ * reflects which provider actually answered.
10
+ */
11
+ class FallbackChain {
12
+ name = 'fallback';
13
+ isExecutorPolicy = true;
14
+ dryRunSafe = true;
15
+ fallbacks;
16
+ constructor(options) {
17
+ if (!options.fallbacks || options.fallbacks.length === 0) {
18
+ throw new Error('fallbackChain requires at least one fallback adapter');
19
+ }
20
+ this.fallbacks = options.fallbacks;
21
+ }
22
+ async around(request, ctx, callNext) {
23
+ ctx.markFired(this.name);
24
+ try {
25
+ return await callNext(request);
26
+ }
27
+ catch (primaryError) {
28
+ if (!(primaryError instanceof ProviderError))
29
+ throw primaryError;
30
+ let lastError = primaryError;
31
+ for (const adapter of this.fallbacks) {
32
+ try {
33
+ const rerouted = request.copyWithMessages(request.messages);
34
+ rerouted.provider = coerceProvider(adapter.providerName);
35
+ return await adapter.complete(rerouted);
36
+ }
37
+ catch (error) {
38
+ if (!(error instanceof ProviderError))
39
+ throw error;
40
+ lastError = error;
41
+ }
42
+ }
43
+ throw lastError;
44
+ }
45
+ }
46
+ }
47
+ /** Factory: build a fallback policy. */
48
+ export function fallbackChain(options) {
49
+ return new FallbackChain(options);
50
+ }
@@ -0,0 +1,7 @@
1
+ /** Reliability policies (F-REL-01, F-REL-02, F-REL-07). */
2
+ export { retryInterceptor } from './retry.js';
3
+ export type { RetryInterceptorOptions } from './retry.js';
4
+ export { timeoutPolicy, timeout } from './timeout.js';
5
+ export type { TimeoutPolicyOptions } from './timeout.js';
6
+ export { fallbackChain } from './fallback.js';
7
+ export type { FallbackChainOptions } from './fallback.js';
@@ -0,0 +1,4 @@
1
+ /** Reliability policies (F-REL-01, F-REL-02, F-REL-07). */
2
+ export { retryInterceptor } from './retry.js';
3
+ export { timeoutPolicy, timeout } from './timeout.js';
4
+ export { fallbackChain } from './fallback.js';
@@ -0,0 +1,13 @@
1
+ /** retryInterceptor (F-REL-01) — exponential backoff with jitter. */
2
+ import type { ExecutorPolicy } from '../base.js';
3
+ export interface RetryInterceptorOptions {
4
+ maxAttempts?: number;
5
+ baseDelayMs?: number;
6
+ maxDelayMs?: number;
7
+ jitter?: boolean;
8
+ retryOn?: (error: unknown) => boolean;
9
+ /** Override the sleep implementation (used in tests). */
10
+ sleep?: (ms: number) => Promise<void>;
11
+ }
12
+ /** Factory: build a retry policy. */
13
+ export declare function retryInterceptor(options?: RetryInterceptorOptions): ExecutorPolicy;
@@ -0,0 +1,66 @@
1
+ /** retryInterceptor (F-REL-01) — exponential backoff with jitter. */
2
+ import { randomBytes } from 'node:crypto';
3
+ import { ProviderUnavailableError, RateLimitError, ServerError, TimeoutError, } from '../../errors.js';
4
+ /** Default predicate: retry transient provider errors. */
5
+ function defaultRetryable(error) {
6
+ return (error instanceof RateLimitError ||
7
+ error instanceof TimeoutError ||
8
+ error instanceof ServerError ||
9
+ error instanceof ProviderUnavailableError);
10
+ }
11
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ class RetryInterceptor {
13
+ name = 'retry';
14
+ isExecutorPolicy = true;
15
+ dryRunSafe = true;
16
+ maxAttempts;
17
+ baseDelayMs;
18
+ maxDelayMs;
19
+ jitter;
20
+ retryOn;
21
+ sleep;
22
+ constructor(options = {}) {
23
+ const maxAttempts = options.maxAttempts ?? 3;
24
+ if (maxAttempts < 1)
25
+ throw new Error('maxAttempts must be >= 1');
26
+ this.maxAttempts = maxAttempts;
27
+ this.baseDelayMs = options.baseDelayMs ?? 500;
28
+ this.maxDelayMs = options.maxDelayMs ?? 10_000;
29
+ this.jitter = options.jitter ?? true;
30
+ this.retryOn = options.retryOn ?? defaultRetryable;
31
+ this.sleep = options.sleep ?? defaultSleep;
32
+ }
33
+ async around(request, ctx, callNext) {
34
+ ctx.markFired(this.name);
35
+ let lastError;
36
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
37
+ try {
38
+ return await callNext(request);
39
+ }
40
+ catch (error) {
41
+ if (!this.retryOn(error))
42
+ throw error;
43
+ lastError = error;
44
+ if (attempt >= this.maxAttempts)
45
+ break;
46
+ await this.sleep(this.delayMs(attempt));
47
+ }
48
+ }
49
+ throw lastError;
50
+ }
51
+ delayMs(attempt) {
52
+ // Exponential: base * 2^(attempt-1), capped, with optional full jitter.
53
+ const raw = this.baseDelayMs * 2 ** (attempt - 1);
54
+ let capped = Math.min(raw, this.maxDelayMs);
55
+ if (this.jitter) {
56
+ // full jitter in [0, capped] using crypto bytes (no global RNG state)
57
+ const frac = randomBytes(2).readUInt16BE(0) / 0xffff;
58
+ capped *= frac;
59
+ }
60
+ return capped;
61
+ }
62
+ }
63
+ /** Factory: build a retry policy. */
64
+ export function retryInterceptor(options = {}) {
65
+ return new RetryInterceptor(options);
66
+ }
@@ -0,0 +1,9 @@
1
+ /** timeoutPolicy (F-REL-07) — per-request timeout enforcement. */
2
+ import type { ExecutorPolicy } from '../base.js';
3
+ export interface TimeoutPolicyOptions {
4
+ timeoutSeconds?: number;
5
+ }
6
+ /** Factory: build a timeout policy. */
7
+ export declare function timeoutPolicy(options?: TimeoutPolicyOptions): ExecutorPolicy;
8
+ /** Alias matching the SDK plan naming (`timeout`). */
9
+ export declare const timeout: typeof timeoutPolicy;
@@ -0,0 +1,37 @@
1
+ /** timeoutPolicy (F-REL-07) — per-request timeout enforcement. */
2
+ import { TimeoutError } from '../../errors.js';
3
+ class TimeoutPolicy {
4
+ name = 'timeout';
5
+ isExecutorPolicy = true;
6
+ dryRunSafe = true;
7
+ timeoutSeconds;
8
+ constructor(options = {}) {
9
+ const timeoutSeconds = options.timeoutSeconds ?? 30.0;
10
+ if (timeoutSeconds <= 0)
11
+ throw new Error('timeoutSeconds must be > 0');
12
+ this.timeoutSeconds = timeoutSeconds;
13
+ }
14
+ async around(request, ctx, callNext) {
15
+ ctx.markFired(this.name);
16
+ const ms = this.timeoutSeconds * 1000;
17
+ let timer;
18
+ const timeout = new Promise((_, reject) => {
19
+ timer = setTimeout(() => {
20
+ reject(new TimeoutError(`Request exceeded ${this.timeoutSeconds}s timeout`));
21
+ }, ms);
22
+ });
23
+ try {
24
+ return await Promise.race([callNext(request), timeout]);
25
+ }
26
+ finally {
27
+ if (timer !== undefined)
28
+ clearTimeout(timer);
29
+ }
30
+ }
31
+ }
32
+ /** Factory: build a timeout policy. */
33
+ export function timeoutPolicy(options = {}) {
34
+ return new TimeoutPolicy(options);
35
+ }
36
+ /** Alias matching the SDK plan naming (`timeout`). */
37
+ export const timeout = timeoutPolicy;
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Token cost tracking (F-GOV-01).
3
+ *
4
+ * Prices are USD per 1,000 tokens, sourced from public provider pricing and
5
+ * overridable. Unknown models price at zero (warned once) rather than guessing.
6
+ * Prices are intentionally data, not code — update the table, not the estimator.
7
+ */
8
+ import type { TokenUsage } from './types.js';
9
+ /** Estimates request cost from token usage and a model price table. */
10
+ export declare class PricingProvider {
11
+ private prices;
12
+ private warned;
13
+ constructor(prices?: Record<string, [number, number]>);
14
+ setPrice(model: string, inputPer1k: number, outputPer1k: number): void;
15
+ rates(model: string): [number, number];
16
+ estimate(model: string, usage: TokenUsage): number;
17
+ }
18
+ /** Rough token estimate (~4 chars/token) for providers without a tokenizer. */
19
+ export declare function estimateTokens(text: string): number;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Token cost tracking (F-GOV-01).
3
+ *
4
+ * Prices are USD per 1,000 tokens, sourced from public provider pricing and
5
+ * overridable. Unknown models price at zero (warned once) rather than guessing.
6
+ * Prices are intentionally data, not code — update the table, not the estimator.
7
+ */
8
+ /** model -> [inputPer1kUsd, outputPer1kUsd] */
9
+ const DEFAULT_PRICES = {
10
+ // OpenAI
11
+ 'gpt-4o': [0.0025, 0.01],
12
+ 'gpt-4o-mini': [0.00015, 0.0006],
13
+ o1: [0.015, 0.06],
14
+ 'o1-mini': [0.0011, 0.0044],
15
+ // Anthropic
16
+ 'claude-sonnet-4-6': [0.003, 0.015],
17
+ 'claude-sonnet-4-20250514': [0.003, 0.015],
18
+ 'claude-haiku-4-5': [0.0008, 0.004],
19
+ 'claude-opus-4-1': [0.015, 0.075],
20
+ // Local / mock are free.
21
+ mock: [0.0, 0.0],
22
+ };
23
+ /** Estimates request cost from token usage and a model price table. */
24
+ export class PricingProvider {
25
+ prices;
26
+ warned = new Set();
27
+ constructor(prices) {
28
+ this.prices = { ...DEFAULT_PRICES, ...(prices ?? {}) };
29
+ }
30
+ setPrice(model, inputPer1k, outputPer1k) {
31
+ this.prices[model] = [inputPer1k, outputPer1k];
32
+ }
33
+ rates(model) {
34
+ const rate = this.prices[model];
35
+ if (rate !== undefined)
36
+ return rate;
37
+ // try a prefix match (e.g. "gpt-4o-2024-..." -> "gpt-4o")
38
+ for (const [known, value] of Object.entries(this.prices)) {
39
+ if (model.startsWith(known))
40
+ return value;
41
+ }
42
+ if (!this.warned.has(model)) {
43
+ // eslint-disable-next-line no-console
44
+ console.warn(`[gavio:pricing] no pricing for model '${model}'; treating as free`);
45
+ this.warned.add(model);
46
+ }
47
+ return [0.0, 0.0];
48
+ }
49
+ estimate(model, usage) {
50
+ const [inRate, outRate] = this.rates(model);
51
+ let cost = (usage.promptTokens / 1000.0) * inRate;
52
+ cost += (usage.completionTokens / 1000.0) * outRate;
53
+ return roundTo(cost, 8);
54
+ }
55
+ }
56
+ function roundTo(value, decimals) {
57
+ const factor = 10 ** decimals;
58
+ return Math.round(value * factor) / factor;
59
+ }
60
+ /** Rough token estimate (~4 chars/token) for providers without a tokenizer. */
61
+ export function estimateTokens(text) {
62
+ if (!text)
63
+ return 0;
64
+ return Math.max(1, Math.floor(text.length / 4));
65
+ }
@@ -0,0 +1,30 @@
1
+ /** anthropicAdapter — Messages API (Claude Sonnet, Haiku, Opus). */
2
+ import type { PricingProvider } from '../pricing.js';
3
+ import type { GavioRequest } from '../request.js';
4
+ import type { GavioResponse } from '../response.js';
5
+ import { BaseProviderAdapter } from './base.js';
6
+ export interface AnthropicAdapterOptions {
7
+ apiKey?: string;
8
+ baseUrl?: string;
9
+ timeoutMs?: number;
10
+ pricing?: PricingProvider;
11
+ }
12
+ /**
13
+ * Talks to the Anthropic Messages endpoint. Anthropic splits the system prompt
14
+ * from the message list, so any `role === "system"` messages are extracted into
15
+ * the `system` field.
16
+ */
17
+ declare class AnthropicAdapter extends BaseProviderAdapter {
18
+ private readonly apiKey;
19
+ private readonly baseUrl;
20
+ private readonly timeoutSeconds;
21
+ constructor(options?: AnthropicAdapterOptions);
22
+ get providerName(): string;
23
+ private headers;
24
+ private static splitSystem;
25
+ complete(request: GavioRequest): Promise<GavioResponse>;
26
+ healthCheck(): Promise<boolean>;
27
+ }
28
+ /** Factory: build an Anthropic provider adapter. */
29
+ export declare function anthropicAdapter(options?: AnthropicAdapterOptions): AnthropicAdapter;
30
+ export {};