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
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Gavio — the open standard AI gateway for production systems.
3
+ *
4
+ * Public API surface (v0.1.0):
5
+ *
6
+ * import { Gateway, GavioRequest, GavioResponse, Provider } from 'gavio'
7
+ *
8
+ * See https://gavio.io for documentation. MIT licensed.
9
+ */
10
+
11
+ export const VERSION = '0.1.0'
12
+
13
+ export { Gateway } from './gateway.js'
14
+ export type { GatewayOptions, CompleteOptions } from './gateway.js'
15
+
16
+ export { GavioRequest } from './request.js'
17
+ export type { GavioRequestInit } from './request.js'
18
+ export { GavioResponse } from './response.js'
19
+ export type { GavioResponseInit } from './response.js'
20
+ export { InterceptorContext } from './context.js'
21
+ export type { InterceptorContextInit } from './context.js'
22
+
23
+ export { uuid7, newTraceId } from './ids.js'
24
+ export { PricingProvider, estimateTokens } from './pricing.js'
25
+
26
+ export {
27
+ Provider,
28
+ CacheType,
29
+ PiiMode,
30
+ Sensitivity,
31
+ GuardrailOutcome,
32
+ TokenUsage,
33
+ coerceProvider,
34
+ } from './types.js'
35
+ export type { Message } from './types.js'
36
+
37
+ export type { Interceptor, Executor, ExecutorPolicy } from './interceptors/base.js'
38
+ export { InterceptorChain } from './interceptors/chain.js'
39
+
40
+ // errors
41
+ export {
42
+ GavioError,
43
+ ConfigurationError,
44
+ ProviderError,
45
+ ProviderUnavailableError,
46
+ RateLimitError,
47
+ ServerError,
48
+ TimeoutError,
49
+ PiiBlockedError,
50
+ BudgetExceededError,
51
+ GuardrailViolationError,
52
+ } from './errors.js'
@@ -0,0 +1,7 @@
1
+ export { auditInterceptor, isAuditInterceptor, AUDIT_NAME } from './interceptor.js'
2
+ export type { AuditInterceptorOptions } from './interceptor.js'
3
+ export { AuditRecord, SCHEMA_VERSION } from './record.js'
4
+ export type { AuditRecordInit } from './record.js'
5
+ export type { AuditSink } from './sink.js'
6
+ export { stdoutSink } from './sinks/stdout.js'
7
+ export type { StdoutSinkOptions } from './sinks/stdout.js'
@@ -0,0 +1,93 @@
1
+ /** auditInterceptor (F-OBS-01) — captures a full record of every call. */
2
+
3
+ import type { InterceptorContext } from '../../context.js'
4
+ import type { GavioRequest } from '../../request.js'
5
+ import type { GavioResponse } from '../../response.js'
6
+ import type { Interceptor } from '../base.js'
7
+ import { AuditRecord } from './record.js'
8
+ import type { AuditSink } from './sink.js'
9
+ import { stdoutSink } from './sinks/stdout.js'
10
+
11
+ const PROMPT_HASH_KEY = 'audit_prompt_hash'
12
+
13
+ export const AUDIT_NAME = 'audit'
14
+
15
+ export interface AuditInterceptorOptions {
16
+ sink?: AuditSink | 'stdout'
17
+ }
18
+
19
+ /**
20
+ * Build an AuditRecord per request and write it to a sink.
21
+ *
22
+ * Register this as the outermost interceptor so its `after` runs last and sees
23
+ * the final, fully-processed response. It hashes the (already PII-redacted)
24
+ * prompt in `before` and the response in `after` — content is never stored,
25
+ * only digests and metadata.
26
+ */
27
+ class AuditInterceptor implements Interceptor {
28
+ readonly name = AUDIT_NAME
29
+ readonly dryRunSafe = true // auditing is observation-only, so it always runs
30
+
31
+ private readonly sink: AuditSink
32
+
33
+ constructor(options: AuditInterceptorOptions = {}) {
34
+ this.sink = resolveSink(options.sink)
35
+ }
36
+
37
+ async before(request: GavioRequest, ctx: InterceptorContext): Promise<GavioRequest> {
38
+ ctx.state[PROMPT_HASH_KEY] = AuditRecord.hashText(request.promptText())
39
+ return request
40
+ }
41
+
42
+ async after(
43
+ response: GavioResponse,
44
+ ctx: InterceptorContext,
45
+ ): Promise<GavioResponse> {
46
+ const record = new AuditRecord({
47
+ traceId: response.traceId,
48
+ parentTraceId: ctx.parentTraceId,
49
+ agentId: ctx.agentId,
50
+ sessionId: ctx.sessionId,
51
+ timestampUtc: AuditRecord.nowUtc(),
52
+ provider: response.provider,
53
+ model: response.model,
54
+ modelVersion: response.modelVersion,
55
+ promptHash: (ctx.state[PROMPT_HASH_KEY] as string | undefined) ?? '',
56
+ responseHash: AuditRecord.hashText(response.content),
57
+ tokenUsage: response.usage,
58
+ costUsd: response.costUsd,
59
+ latencyMs: response.latencyMs,
60
+ piiEntityTypes: [...ctx.piiEntityTypes],
61
+ piiEntityCounts: { ...ctx.piiEntityCounts },
62
+ interceptorsFired: [...ctx.interceptorsFired],
63
+ cacheHit: response.cacheHit,
64
+ cacheType: response.cacheType,
65
+ guardrailOutcome: ctx.guardrailOutcome,
66
+ riskScore: ctx.riskScore,
67
+ })
68
+ response.audit = record
69
+ try {
70
+ await this.sink.write(record)
71
+ } catch {
72
+ // Auditing must never break the call.
73
+ // eslint-disable-next-line no-console
74
+ console.error(`[gavio:audit] sink write failed for trace ${record.traceId}`)
75
+ }
76
+ return response
77
+ }
78
+ }
79
+
80
+ function resolveSink(sink: AuditSink | 'stdout' | undefined): AuditSink {
81
+ if (sink === undefined || sink === 'stdout') return stdoutSink()
82
+ return sink
83
+ }
84
+
85
+ /** Factory: build an audit interceptor. */
86
+ export function auditInterceptor(options: AuditInterceptorOptions = {}): Interceptor {
87
+ return new AuditInterceptor(options)
88
+ }
89
+
90
+ /** True if an interceptor is the audit interceptor (used by dev-mode auto-wiring). */
91
+ export function isAuditInterceptor(i: Interceptor): boolean {
92
+ return i.name === AUDIT_NAME
93
+ }
@@ -0,0 +1,138 @@
1
+ /** AuditRecord — the immutable, per-request audit entry. */
2
+
3
+ import { createHash } from 'node:crypto'
4
+ import { TokenUsage } from '../../types.js'
5
+
6
+ export const SCHEMA_VERSION = '1.0'
7
+
8
+ function sha256(text: string): string {
9
+ return createHash('sha256').update(text, 'utf-8').digest('hex')
10
+ }
11
+
12
+ export interface AuditRecordInit {
13
+ traceId: string
14
+ provider: string
15
+ model: string
16
+ timestampUtc: string
17
+ parentTraceId?: string | null
18
+ agentId?: string | null
19
+ sessionId?: string | null
20
+ modelVersion?: string
21
+ promptHash?: string
22
+ responseHash?: string
23
+ tokenUsage?: TokenUsage
24
+ costUsd?: number
25
+ latencyMs?: number
26
+ piiEntityTypes?: string[]
27
+ piiEntityCounts?: Record<string, number>
28
+ interceptorsFired?: string[]
29
+ cacheHit?: boolean
30
+ cacheType?: string | null
31
+ guardrailOutcome?: string | null
32
+ riskScore?: number | null
33
+ previousHash?: string
34
+ schemaVersion?: string
35
+ }
36
+
37
+ /**
38
+ * One append-only audit entry. Carries metadata only — never raw content.
39
+ *
40
+ * `promptHash` / `responseHash` are SHA-256 digests so the entry is verifiable
41
+ * without storing sensitive text. `previousHash` is reserved for the v0.2.0
42
+ * hash-chain (F-OBS-02); empty in v0.1.0.
43
+ */
44
+ export class AuditRecord {
45
+ traceId: string
46
+ provider: string
47
+ model: string
48
+ timestampUtc: string
49
+ parentTraceId: string | null
50
+ agentId: string | null
51
+ sessionId: string | null
52
+ modelVersion: string
53
+ promptHash: string
54
+ responseHash: string
55
+ tokenUsage: TokenUsage
56
+ costUsd: number
57
+ latencyMs: number
58
+ piiEntityTypes: string[]
59
+ piiEntityCounts: Record<string, number>
60
+ interceptorsFired: string[]
61
+ cacheHit: boolean
62
+ cacheType: string | null
63
+ guardrailOutcome: string | null
64
+ riskScore: number | null
65
+ previousHash: string
66
+ schemaVersion: string
67
+
68
+ constructor(init: AuditRecordInit) {
69
+ this.traceId = init.traceId
70
+ this.provider = init.provider
71
+ this.model = init.model
72
+ this.timestampUtc = init.timestampUtc
73
+ this.parentTraceId = init.parentTraceId ?? null
74
+ this.agentId = init.agentId ?? null
75
+ this.sessionId = init.sessionId ?? null
76
+ this.modelVersion = init.modelVersion ?? ''
77
+ this.promptHash = init.promptHash ?? ''
78
+ this.responseHash = init.responseHash ?? ''
79
+ this.tokenUsage = init.tokenUsage ?? new TokenUsage()
80
+ this.costUsd = init.costUsd ?? 0.0
81
+ this.latencyMs = init.latencyMs ?? 0
82
+ this.piiEntityTypes = init.piiEntityTypes ?? []
83
+ this.piiEntityCounts = init.piiEntityCounts ?? {}
84
+ this.interceptorsFired = init.interceptorsFired ?? []
85
+ this.cacheHit = init.cacheHit ?? false
86
+ this.cacheType = init.cacheType ?? null
87
+ this.guardrailOutcome = init.guardrailOutcome ?? null
88
+ this.riskScore = init.riskScore ?? null
89
+ this.previousHash = init.previousHash ?? ''
90
+ this.schemaVersion = init.schemaVersion ?? SCHEMA_VERSION
91
+ }
92
+
93
+ static nowUtc(): string {
94
+ return new Date().toISOString()
95
+ }
96
+
97
+ static hashText(text: string): string {
98
+ return sha256(text)
99
+ }
100
+
101
+ toJSON(): Record<string, unknown> {
102
+ return {
103
+ traceId: this.traceId,
104
+ parentTraceId: this.parentTraceId,
105
+ agentId: this.agentId,
106
+ sessionId: this.sessionId,
107
+ provider: this.provider,
108
+ model: this.model,
109
+ modelVersion: this.modelVersion,
110
+ timestampUtc: this.timestampUtc,
111
+ promptHash: this.promptHash,
112
+ responseHash: this.responseHash,
113
+ tokenUsage: this.tokenUsage.toJSON(),
114
+ costUsd: this.costUsd,
115
+ latencyMs: this.latencyMs,
116
+ piiEntityTypes: this.piiEntityTypes,
117
+ piiEntityCounts: this.piiEntityCounts,
118
+ interceptorsFired: this.interceptorsFired,
119
+ cacheHit: this.cacheHit,
120
+ cacheType: this.cacheType,
121
+ guardrailOutcome: this.guardrailOutcome,
122
+ riskScore: this.riskScore,
123
+ previousHash: this.previousHash,
124
+ schemaVersion: this.schemaVersion,
125
+ }
126
+ }
127
+
128
+ /** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
129
+ toCanonicalJson(): string {
130
+ const data = this.toJSON()
131
+ return JSON.stringify(data, Object.keys(data).sort())
132
+ }
133
+
134
+ /** Hash of this record's content — used to build the v0.2.0 chain. */
135
+ contentHash(): string {
136
+ return sha256(this.toCanonicalJson())
137
+ }
138
+ }
@@ -0,0 +1,10 @@
1
+ /** AuditSink — the extensible destination for audit records. */
2
+
3
+ import type { AuditRecord } from './record.js'
4
+
5
+ /** Where audit records go. Implement `write` to add a backend. */
6
+ export interface AuditSink {
7
+ write(record: AuditRecord): Promise<void>
8
+ /** Flush/close any resources. Optional. */
9
+ close?(): Promise<void>
10
+ }
@@ -0,0 +1,2 @@
1
+ export { stdoutSink } from './stdout.js'
2
+ export type { StdoutSinkOptions } from './stdout.js'
@@ -0,0 +1,42 @@
1
+ /** stdoutSink — human-readable audit output for development (F-OBS-05). */
2
+
3
+ import type { AuditRecord } from '../record.js'
4
+ import type { AuditSink } from '../sink.js'
5
+
6
+ export interface StdoutSinkOptions {
7
+ pretty?: boolean
8
+ write?: (line: string) => void
9
+ }
10
+
11
+ /** Print each audit record to stdout. Zero dependencies. */
12
+ export function stdoutSink(options: StdoutSinkOptions = {}): AuditSink {
13
+ const pretty = options.pretty ?? true
14
+ // eslint-disable-next-line no-console
15
+ const emit = options.write ?? ((line: string) => console.log(line))
16
+ return {
17
+ async write(record: AuditRecord): Promise<void> {
18
+ const data = record.toJSON()
19
+ emit(pretty ? formatPretty(data) : JSON.stringify(data))
20
+ },
21
+ }
22
+ }
23
+
24
+ function formatPretty(data: Record<string, unknown>): string {
25
+ const usage = data['tokenUsage'] as { totalTokens: number }
26
+ const piiTypes = data['piiEntityTypes'] as string[]
27
+ const pii = piiTypes.length > 0 ? piiTypes : ['none']
28
+ const interceptors = data['interceptorsFired'] as string[]
29
+ const traceId = String(data['traceId'])
30
+ const cost = Number(data['costUsd'])
31
+ return (
32
+ '[gavio:audit] ' +
33
+ `trace=${traceId.slice(0, 18)}… ` +
34
+ `${String(data['provider'])}/${String(data['model'])} ` +
35
+ `tokens=${usage.totalTokens} ` +
36
+ `cost=$${cost.toFixed(6)} ` +
37
+ `latency=${String(data['latencyMs'])}ms ` +
38
+ `cache=${data['cacheHit'] ? 'HIT' : 'miss'} ` +
39
+ `pii=${pii.join(',')} ` +
40
+ `interceptors=[${interceptors.join(',')}]`
41
+ )
42
+ }
@@ -0,0 +1,58 @@
1
+ /** The Interceptor interface — the unit of composition in Gavio. */
2
+
3
+ import type { InterceptorContext } from '../context.js'
4
+ import type { GavioRequest } from '../request.js'
5
+ import type { GavioResponse } from '../response.js'
6
+
7
+ /**
8
+ * A pre/post hook around the provider call.
9
+ *
10
+ * `before` runs in registration order on the request; `after` runs in reverse
11
+ * order on the response (onion model). Either may be omitted. Throwing from
12
+ * `before` aborts the call.
13
+ */
14
+ export interface Interceptor {
15
+ /** Unique name used in audit logs and metrics. */
16
+ readonly name: string
17
+
18
+ /** Pre-interceptor: runs before the provider call. */
19
+ before?(
20
+ request: GavioRequest,
21
+ ctx: InterceptorContext,
22
+ ): Promise<GavioRequest> | GavioRequest
23
+
24
+ /** Post-interceptor: runs after the provider call. */
25
+ after?(
26
+ response: GavioResponse,
27
+ ctx: InterceptorContext,
28
+ ): Promise<GavioResponse> | GavioResponse
29
+
30
+ /** Called if the provider call or a downstream interceptor throws. */
31
+ onError?(error: Error, ctx: InterceptorContext): void | Promise<void>
32
+
33
+ /** If true (default), participates in dry-run mode (logs only). */
34
+ readonly dryRunSafe?: boolean
35
+ }
36
+
37
+ /** A function that takes the final request and returns a response (the provider call). */
38
+ export type Executor = (request: GavioRequest) => Promise<GavioResponse>
39
+
40
+ /**
41
+ * Base class for executor-wrapping reliability policies.
42
+ *
43
+ * Retry, timeout, and fallback can't be expressed as plain before/after hooks:
44
+ * they need to re-invoke (or race) the executor. They implement `around` so the
45
+ * Gateway composes them *around* the provider call, innermost-last.
46
+ */
47
+ export interface ExecutorPolicy extends Interceptor {
48
+ readonly isExecutorPolicy: true
49
+ around(
50
+ request: GavioRequest,
51
+ ctx: InterceptorContext,
52
+ callNext: Executor,
53
+ ): Promise<GavioResponse>
54
+ }
55
+
56
+ export function isExecutorPolicy(i: Interceptor): i is ExecutorPolicy {
57
+ return (i as Partial<ExecutorPolicy>).isExecutorPolicy === true
58
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CacheBackend — the key/value contract behind the cache interceptors.
3
+ *
4
+ * The full SemanticCache interceptor lands in v0.2.0 (F-CACHE-01/02). v0.1.0
5
+ * ships the backend interface and the in-memory backend so dev mode has a
6
+ * working, dependency-free cache substrate.
7
+ */
8
+
9
+ /** A minimal async key/value store. */
10
+ export interface CacheBackend {
11
+ get(key: string): Promise<unknown | null>
12
+ set(key: string, value: unknown, ttlSeconds?: number | null): Promise<void>
13
+ delete(key: string): Promise<void>
14
+ clear(): Promise<void>
15
+ }
@@ -0,0 +1,2 @@
1
+ export { memoryCacheBackend } from './memory.js'
2
+ export type { MemoryCacheBackendOptions } from './memory.js'
@@ -0,0 +1,68 @@
1
+ /** In-memory cache backend (F-CACHE-03) — default zero-dependency dev backend. */
2
+
3
+ import type { CacheBackend } from '../backend.js'
4
+
5
+ interface Entry {
6
+ value: unknown
7
+ expiresAt: number | null
8
+ }
9
+
10
+ export interface MemoryCacheBackendOptions {
11
+ maxSize?: number
12
+ }
13
+
14
+ /** LRU-bounded, optionally TTL'd in-process cache. Not shared across processes. */
15
+ class MemoryBackend implements CacheBackend {
16
+ readonly maxSize: number
17
+ // Map preserves insertion order, which we use for LRU eviction.
18
+ private store = new Map<string, Entry>()
19
+
20
+ constructor(maxSize = 1000) {
21
+ this.maxSize = maxSize
22
+ }
23
+
24
+ async get(key: string): Promise<unknown | null> {
25
+ const entry = this.store.get(key)
26
+ if (entry === undefined) return null
27
+ if (entry.expiresAt !== null && now() > entry.expiresAt) {
28
+ this.store.delete(key)
29
+ return null
30
+ }
31
+ // Move to end (most-recently-used).
32
+ this.store.delete(key)
33
+ this.store.set(key, entry)
34
+ return entry.value
35
+ }
36
+
37
+ async set(key: string, value: unknown, ttlSeconds?: number | null): Promise<void> {
38
+ const expiresAt = ttlSeconds ? now() + ttlSeconds * 1000 : null
39
+ this.store.delete(key)
40
+ this.store.set(key, { value, expiresAt })
41
+ while (this.store.size > this.maxSize) {
42
+ const oldest = this.store.keys().next().value
43
+ if (oldest === undefined) break
44
+ this.store.delete(oldest)
45
+ }
46
+ }
47
+
48
+ async delete(key: string): Promise<void> {
49
+ this.store.delete(key)
50
+ }
51
+
52
+ async clear(): Promise<void> {
53
+ this.store.clear()
54
+ }
55
+
56
+ get size(): number {
57
+ return this.store.size
58
+ }
59
+ }
60
+
61
+ function now(): number {
62
+ return Date.now()
63
+ }
64
+
65
+ /** Factory: build an in-memory cache backend. */
66
+ export function memoryCacheBackend(options: MemoryCacheBackendOptions = {}): CacheBackend {
67
+ return new MemoryBackend(options.maxSize ?? 1000)
68
+ }
@@ -0,0 +1,8 @@
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
+
6
+ export type { CacheBackend } from './backend.js'
7
+ export { memoryCacheBackend } from './backends/memory.js'
8
+ export type { MemoryCacheBackendOptions } from './backends/memory.js'
@@ -0,0 +1,65 @@
1
+ /** InterceptorChain — runs the pre/post pipeline around the provider call. */
2
+
3
+ import type { InterceptorContext } from '../context.js'
4
+ import type { GavioRequest } from '../request.js'
5
+ import type { GavioResponse } from '../response.js'
6
+ import type { Executor, Interceptor } from './base.js'
7
+
8
+ const dryRunSafe = (i: Interceptor): boolean => i.dryRunSafe !== false
9
+
10
+ /**
11
+ * Ordered list of interceptors wrapping an executor.
12
+ *
13
+ * `before` hooks fire in order; the executor runs; `after` hooks fire in
14
+ * reverse order (onion model). If any stage throws, every interceptor's
15
+ * `onError` is invoked before the error propagates.
16
+ */
17
+ export class InterceptorChain {
18
+ private readonly interceptors: Interceptor[]
19
+
20
+ constructor(interceptors: Interceptor[]) {
21
+ this.interceptors = [...interceptors]
22
+ }
23
+
24
+ async execute(
25
+ request: GavioRequest,
26
+ ctx: InterceptorContext,
27
+ executor: Executor,
28
+ ): Promise<GavioResponse> {
29
+ try {
30
+ let req = request
31
+ for (const interceptor of this.interceptors) {
32
+ if (ctx.dryRun && !dryRunSafe(interceptor)) continue
33
+ if (interceptor.before) {
34
+ req = await interceptor.before(req, ctx)
35
+ }
36
+ ctx.markFired(interceptor.name)
37
+ }
38
+
39
+ let response = await executor(req)
40
+
41
+ for (let i = this.interceptors.length - 1; i >= 0; i--) {
42
+ const interceptor = this.interceptors[i]!
43
+ if (ctx.dryRun && !dryRunSafe(interceptor)) continue
44
+ if (interceptor.after) {
45
+ response = await interceptor.after(response, ctx)
46
+ }
47
+ }
48
+
49
+ response.interceptorsFired = [...ctx.interceptorsFired]
50
+ return response
51
+ } catch (error) {
52
+ const err = error instanceof Error ? error : new Error(String(error))
53
+ for (const interceptor of this.interceptors) {
54
+ if (interceptor.onError) {
55
+ try {
56
+ await interceptor.onError(err, ctx)
57
+ } catch {
58
+ // on_error must never break the propagation of the original error.
59
+ }
60
+ }
61
+ }
62
+ throw error
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ /** Interceptor barrel. */
2
+
3
+ export type { Interceptor, Executor, ExecutorPolicy } from './base.js'
4
+ export { isExecutorPolicy } from './base.js'
5
+ export { InterceptorChain } from './chain.js'
6
+ export { piiGuard } from './pii/index.js'
7
+ export { auditInterceptor } from './audit/index.js'
8
+ export { retryInterceptor, timeoutPolicy, fallbackChain } from './reliability/index.js'
9
+ export { memoryCacheBackend } from './cache/index.js'
@@ -0,0 +1,24 @@
1
+ /** ScanContext — per-request state shared across PII scanners. */
2
+
3
+ /**
4
+ * Context threaded through every scanner for one request.
5
+ *
6
+ * Tracks a monotonic per-entity-type index so repeated entities get stable,
7
+ * distinct placeholders (`[EMAIL_1]`, `[EMAIL_2]`).
8
+ */
9
+ export class ScanContext {
10
+ readonly language: string
11
+ readonly locale: string
12
+ private counters: Record<string, number> = {}
13
+
14
+ constructor(language = 'en', locale = 'NL') {
15
+ this.language = language
16
+ this.locale = locale
17
+ }
18
+
19
+ /** Return the next 1-based index for an entity type. */
20
+ nextIndex(entityType: string): number {
21
+ this.counters[entityType] = (this.counters[entityType] ?? 0) + 1
22
+ return this.counters[entityType]
23
+ }
24
+ }