@7h3/protocol 0.5.0 → 0.5.2

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 (294) hide show
  1. package/README.md +6 -1317
  2. package/agentAdapter.d.ts +26 -0
  3. package/auditLog.d.ts +54 -0
  4. package/capability.d.ts +67 -0
  5. package/cborCodec.d.ts +34 -0
  6. package/conformanceVectors.d.ts +20 -0
  7. package/encryption.d.ts +85 -0
  8. package/envelopeCbor.d.ts +33 -0
  9. package/frameworkAdapters.d.ts +72 -0
  10. package/gateway.d.ts +53 -0
  11. package/grpcBinding.d.ts +29 -0
  12. package/httpBinding.d.ts +50 -0
  13. package/index.d.ts +39 -0
  14. package/index.js +3670 -0
  15. package/keyInfra.d.ts +64 -0
  16. package/keyRegistry.d.ts +9 -0
  17. package/keyRotation.d.ts +20 -0
  18. package/mcpGateway.d.ts +37 -0
  19. package/mcpTransports.d.ts +62 -0
  20. package/mcpWrapper.d.ts +83 -0
  21. package/otel.d.ts +65 -0
  22. package/package.json +13 -121
  23. package/policyEnforcer.d.ts +50 -0
  24. package/policyTelemetryFeedback.d.ts +11 -0
  25. package/{src/protocol.d.ts → protocol.d.ts} +6 -2
  26. package/protocolAgent.d.ts +58 -0
  27. package/protocolBinary.d.ts +8 -0
  28. package/protocolCapabilities.d.ts +24 -0
  29. package/protocolReplay.d.ts +35 -0
  30. package/protocolTransport.d.ts +73 -0
  31. package/queueBinding.d.ts +43 -0
  32. package/rateLimiter.d.ts +18 -0
  33. package/{src/redisClient.ts → redisClient.d.ts} +25 -75
  34. package/replayStores.d.ts +98 -0
  35. package/revocation.d.ts +71 -0
  36. package/routePolicy.d.ts +30 -0
  37. package/runtimePolicy.d.ts +24 -0
  38. package/runtimePolicyManager.d.ts +15 -0
  39. package/runtimePolicyPresets.d.ts +11 -0
  40. package/signedResponse.d.ts +21 -0
  41. package/stream.d.ts +59 -0
  42. package/telemetry.d.ts +79 -0
  43. package/webhookBinding.d.ts +30 -0
  44. package/wsBinding.d.ts +52 -0
  45. package/.dockerignore +0 -19
  46. package/.github/dependabot.yml +0 -32
  47. package/.github/workflows/ci.yml +0 -31
  48. package/.github/workflows/publish.yml +0 -59
  49. package/.github/workflows/scorecard.yml +0 -37
  50. package/7h3.example.yaml +0 -125
  51. package/CHANGELOG.md +0 -152
  52. package/CONTRIBUTING.md +0 -82
  53. package/Dockerfile +0 -73
  54. package/GOVERNANCE.md +0 -62
  55. package/SECURITY.md +0 -70
  56. package/bench-results/replay-cache-full-1777891033256.json +0 -10
  57. package/bench-results/replay-cache-full-1777896317488.json +0 -10
  58. package/bench-results/replay-cache-full-1777900993184.json +0 -10
  59. package/bench-results/replay-cache-full-1777901019285.json +0 -10
  60. package/bench-results/replay-cache-quick-1777870170126.json +0 -10
  61. package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
  62. package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
  63. package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
  64. package/bench-results/wire-codecs-full-1777891019803.json +0 -93
  65. package/bench-results/wire-codecs-full-1777896260964.json +0 -93
  66. package/bench-results/wire-codecs-full-1777901004247.json +0 -93
  67. package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
  68. package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
  69. package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
  70. package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
  71. package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
  72. package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
  73. package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
  74. package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
  75. package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
  76. package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
  77. package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
  78. package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
  79. package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
  80. package/bin/7h3.ts +0 -406
  81. package/conformance/7h3_v0_1.json +0 -77
  82. package/conformance/7h3_v0_1_binary.json +0 -20
  83. package/conformance/aip_v0_1_binary.json +0 -20
  84. package/docker-compose.yaml +0 -77
  85. package/docs/ADOPTION_PLAN.md +0 -120
  86. package/docs/AGENTS.md +0 -77
  87. package/docs/AIP_RFC_v0.1.md +0 -97
  88. package/docs/AI_DECISION_CARD.md +0 -122
  89. package/docs/AI_RUNTIME_POLICY.json +0 -126
  90. package/docs/AI_RUNTIME_POLICY.yaml +0 -110
  91. package/docs/BACKPRESSURE_TUNING.md +0 -65
  92. package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
  93. package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
  94. package/docs/BINARY_CODEC_BENCH.md +0 -23
  95. package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
  96. package/docs/CLOCK_SKEW_POLICY.md +0 -30
  97. package/docs/DISTRIBUTED_REPLAY.md +0 -142
  98. package/docs/FUZZ_CAMPAIGN.md +0 -121
  99. package/docs/GATEWAY.md +0 -195
  100. package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
  101. package/docs/KEY_REVOCATION.md +0 -69
  102. package/docs/MCP_WRAPPER.md +0 -159
  103. package/docs/MIGRATION_GUIDE.md +0 -40
  104. package/docs/OPERATORS.md +0 -184
  105. package/docs/PERF_REGRESSION_POLICY.md +0 -34
  106. package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
  107. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
  108. package/docs/RELEASE_GATE.md +0 -25
  109. package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
  110. package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
  111. package/docs/TELEMETRY.md +0 -41
  112. package/docs/THREAT_MODEL.md +0 -89
  113. package/docs/VERSIONING_POLICY.md +0 -30
  114. package/docs/assets/banner-github.png +0 -0
  115. package/docs/assets/banner.png +0 -0
  116. package/docs/assets/banner.svg +0 -123
  117. package/eslint.config.js +0 -15
  118. package/fuzz/ts/harness-decode.ts +0 -136
  119. package/fuzz/ts/harness-verify.ts +0 -121
  120. package/fuzz/ts/run.ts +0 -35
  121. package/mcp-server/README.md +0 -38
  122. package/mcp-server/package-lock.json +0 -1187
  123. package/mcp-server/package.json +0 -35
  124. package/mcp-server/src/index.ts +0 -236
  125. package/mcp-server/tsconfig.json +0 -14
  126. package/scripts/aip-framework-quickstart.ts +0 -110
  127. package/scripts/aip-mcp-gateway.ts +0 -38
  128. package/scripts/aip-mcp-wrap-demo.ts +0 -72
  129. package/scripts/aip-quickstart.ts +0 -60
  130. package/scripts/bench-diff.ts +0 -118
  131. package/scripts/bench-protocol-e2e.ts +0 -937
  132. package/scripts/bench-protocol-openloop.ts +0 -1397
  133. package/scripts/bench-replay-cache.ts +0 -76
  134. package/scripts/bench-signature-profiles.ts +0 -180
  135. package/scripts/bench-wire-codecs.ts +0 -161
  136. package/scripts/build-binary-conformance.ts +0 -36
  137. package/scripts/build-release-dashboard.ts +0 -175
  138. package/scripts/canary-rollout.ts +0 -38
  139. package/scripts/mcpGatewayCli.test.ts +0 -116
  140. package/scripts/prepare-aip-package.ts +0 -88
  141. package/scripts/regen-conformance-sigs.ts +0 -18
  142. package/scripts/release-gate.ts +0 -19
  143. package/scripts/validate-runtime-policy.ts +0 -18
  144. package/sdk/browser/index.test.ts +0 -162
  145. package/sdk/browser/index.ts +0 -257
  146. package/sdk/browser/package.json +0 -13
  147. package/sdk/go/cbor.go +0 -551
  148. package/sdk/go/cbor_test.go +0 -232
  149. package/sdk/go/encryption.go +0 -280
  150. package/sdk/go/encryption_test.go +0 -318
  151. package/sdk/go/go.mod +0 -7
  152. package/sdk/go/go.sum +0 -4
  153. package/sdk/go/http.go +0 -135
  154. package/sdk/go/protocol.go +0 -324
  155. package/sdk/go/protocol_test.go +0 -334
  156. package/sdk/go/replay.go +0 -121
  157. package/sdk/go/replay_test.go +0 -149
  158. package/sdk/go/webhook.go +0 -136
  159. package/sdk/pq/package-lock.json +0 -1358
  160. package/sdk/pq/package.json +0 -42
  161. package/sdk/pq/src/index.test.ts +0 -143
  162. package/sdk/pq/src/index.ts +0 -166
  163. package/sdk/pq/tsconfig.json +0 -14
  164. package/sdk/pq/vitest.config.ts +0 -7
  165. package/sdk/python/README.md +0 -18
  166. package/sdk/python/protocol_7h3/__init__.py +0 -46
  167. package/sdk/python/protocol_7h3/encryption.py +0 -252
  168. package/sdk/python/protocol_7h3/http.py +0 -212
  169. package/sdk/python/protocol_7h3/keys.py +0 -149
  170. package/sdk/python/protocol_7h3/pq.py +0 -244
  171. package/sdk/python/protocol_7h3/protocol.py +0 -525
  172. package/sdk/python/protocol_7h3/queue.py +0 -118
  173. package/sdk/python/protocol_7h3/replay.py +0 -98
  174. package/sdk/python/protocol_7h3/webhook.py +0 -116
  175. package/sdk/python/pyproject.toml +0 -40
  176. package/sdk/python/tests/test_conformance.py +0 -110
  177. package/sdk/python/tests/test_encryption.py +0 -206
  178. package/sdk/python/tests/test_http.py +0 -305
  179. package/sdk/python/tests/test_keys.py +0 -417
  180. package/sdk/python/tests/test_queue.py +0 -120
  181. package/sdk/python/tests/test_webhook.py +0 -345
  182. package/sdk/rust/Cargo.lock +0 -371
  183. package/sdk/rust/Cargo.toml +0 -25
  184. package/sdk/rust/README.md +0 -31
  185. package/sdk/rust/fuzz/Cargo.toml +0 -29
  186. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
  187. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
  188. package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
  189. package/sdk/rust/src/http.rs +0 -145
  190. package/sdk/rust/src/keys.rs +0 -161
  191. package/sdk/rust/src/lib.rs +0 -688
  192. package/sdk/rust/src/queue.rs +0 -79
  193. package/sdk/rust/src/webhook.rs +0 -86
  194. package/sdk/rust/tests/conformance.rs +0 -148
  195. package/sdk/rust/tests/gateway.rs +0 -130
  196. package/sdk/rust/tests/http_webhook_queue.rs +0 -201
  197. package/sdk/rust/tests/keys.rs +0 -189
  198. package/sdk/threshold/index.d.ts +0 -68
  199. package/sdk/threshold/index.d.ts.map +0 -1
  200. package/sdk/threshold/index.js +0 -254
  201. package/sdk/threshold/package-lock.json +0 -1361
  202. package/sdk/threshold/package.json +0 -39
  203. package/sdk/threshold/src/index.d.ts +0 -68
  204. package/sdk/threshold/src/index.d.ts.map +0 -1
  205. package/sdk/threshold/src/index.js +0 -254
  206. package/sdk/threshold/src/index.test.ts +0 -238
  207. package/sdk/threshold/src/index.ts +0 -355
  208. package/sdk/threshold/tsconfig.json +0 -19
  209. package/sdk/threshold/vitest.config.ts +0 -12
  210. package/src/agentAdapter.test.ts +0 -48
  211. package/src/agentAdapter.ts +0 -56
  212. package/src/auditLog.test.ts +0 -145
  213. package/src/auditLog.ts +0 -147
  214. package/src/capability.test.ts +0 -504
  215. package/src/capability.ts +0 -380
  216. package/src/cborCodec.test.ts +0 -263
  217. package/src/cborCodec.ts +0 -339
  218. package/src/conformance.test.ts +0 -136
  219. package/src/conformanceVectors.ts +0 -99
  220. package/src/encryption.test.ts +0 -206
  221. package/src/encryption.ts +0 -245
  222. package/src/envelopeCbor.ts +0 -140
  223. package/src/frameworkAdapters.test.ts +0 -290
  224. package/src/frameworkAdapters.ts +0 -261
  225. package/src/gateway.test.ts +0 -343
  226. package/src/gateway.ts +0 -246
  227. package/src/grpcBinding.test.ts +0 -211
  228. package/src/grpcBinding.ts +0 -103
  229. package/src/httpBinding.test.ts +0 -376
  230. package/src/httpBinding.ts +0 -189
  231. package/src/index.ts +0 -39
  232. package/src/keyInfra.test.ts +0 -278
  233. package/src/keyInfra.ts +0 -228
  234. package/src/keyRegistry.ts +0 -59
  235. package/src/keyRotation.test.ts +0 -78
  236. package/src/keyRotation.ts +0 -72
  237. package/src/mcpGateway.test.ts +0 -129
  238. package/src/mcpGateway.ts +0 -250
  239. package/src/mcpTransports.test.ts +0 -92
  240. package/src/mcpTransports.ts +0 -169
  241. package/src/mcpWrapper.test.ts +0 -179
  242. package/src/mcpWrapper.ts +0 -206
  243. package/src/otel.ts +0 -136
  244. package/src/policyEnforcer.test.ts +0 -99
  245. package/src/policyEnforcer.ts +0 -169
  246. package/src/policyTelemetryFeedback.test.ts +0 -25
  247. package/src/policyTelemetryFeedback.ts +0 -38
  248. package/src/protocol.bench.ts +0 -37
  249. package/src/protocol.d.ts.map +0 -1
  250. package/src/protocol.js +0 -294
  251. package/src/protocol.test.ts +0 -155
  252. package/src/protocol.ts +0 -414
  253. package/src/protocolAgent.test.ts +0 -105
  254. package/src/protocolAgent.ts +0 -169
  255. package/src/protocolBinary.test.ts +0 -165
  256. package/src/protocolBinary.ts +0 -312
  257. package/src/protocolCapabilities.ts +0 -70
  258. package/src/protocolFuzz.advanced.test.ts +0 -235
  259. package/src/protocolFuzz.test.ts +0 -111
  260. package/src/protocolNegative.test.ts +0 -97
  261. package/src/protocolReplay.test.ts +0 -71
  262. package/src/protocolReplay.ts +0 -194
  263. package/src/protocolTransport.test.ts +0 -556
  264. package/src/protocolTransport.ts +0 -483
  265. package/src/queueBinding.test.ts +0 -130
  266. package/src/queueBinding.ts +0 -102
  267. package/src/rateLimiter.test.ts +0 -96
  268. package/src/rateLimiter.ts +0 -46
  269. package/src/redisIntegration.test.ts +0 -134
  270. package/src/replayStores.test.ts +0 -273
  271. package/src/replayStores.ts +0 -215
  272. package/src/revocation.test.ts +0 -98
  273. package/src/revocation.ts +0 -0
  274. package/src/routePolicy.test.ts +0 -87
  275. package/src/routePolicy.ts +0 -72
  276. package/src/runtimePolicy.test.ts +0 -49
  277. package/src/runtimePolicy.ts +0 -81
  278. package/src/runtimePolicyManager.test.ts +0 -29
  279. package/src/runtimePolicyManager.ts +0 -50
  280. package/src/runtimePolicyPresets.ts +0 -43
  281. package/src/signedResponse.test.ts +0 -111
  282. package/src/signedResponse.ts +0 -83
  283. package/src/stream.test.ts +0 -254
  284. package/src/stream.ts +0 -417
  285. package/src/telemetry.test.ts +0 -251
  286. package/src/telemetry.ts +0 -299
  287. package/src/webhookBinding.test.ts +0 -144
  288. package/src/webhookBinding.ts +0 -115
  289. package/src/wsBinding.test.ts +0 -221
  290. package/src/wsBinding.ts +0 -200
  291. package/tsconfig.json +0 -15
  292. package/tsconfig.lib.json +0 -23
  293. package/vite.lib.config.ts +0 -16
  294. package/vitest.config.ts +0 -11
@@ -1,215 +0,0 @@
1
- import type { DistributedReplayStore } from './protocolReplay'
2
- import { InMemoryRedisLikeClient, type RedisLikeClient } from './redisClient'
3
-
4
- // ---------------------------------------------------------------------------
5
- // ReplayStore — high-level check() interface (returns true = REPLAY)
6
- // ---------------------------------------------------------------------------
7
-
8
- /**
9
- * Minimal Redis client surface required by RedisReplayStore.
10
- *
11
- * Distinct from the internal RedisLikeClient: this interface uses a `quit()`
12
- * lifecycle method and does not require pipeline support, making it easy to
13
- * adapt any Redis client (ioredis, node-redis, Upstash, etc.) without coupling
14
- * to the internal pipeline abstraction.
15
- */
16
- export interface RedisClientLike {
17
- set(key: string, value: string, opts?: { nx?: boolean; px?: number }): Promise<string | null>
18
- get(key: string): Promise<string | null>
19
- del(key: string): Promise<number | any>
20
- quit(): Promise<any>
21
- }
22
-
23
- /**
24
- * Gateway-facing replay store interface.
25
- *
26
- * `check()` atomically marks a key as seen and returns whether it was already
27
- * present: `false` = fresh (first time seen), `true` = replay (already seen).
28
- */
29
- export interface ReplayStore {
30
- check(key: string, ttlMs: number): Promise<boolean>
31
- }
32
-
33
- /**
34
- * Redis-backed implementation of {@link ReplayStore}.
35
- *
36
- * Uses SET … NX PX for an atomic "set if not exists with TTL" operation.
37
- * The operation is single-round-trip and safe under concurrent access from
38
- * multiple gateway instances.
39
- */
40
- export class RedisReplayStore implements ReplayStore {
41
- private readonly keyPrefix: string
42
- private readonly client: RedisClientLike
43
-
44
- constructor(opts: { redisUrl?: string; keyPrefix?: string; client?: RedisClientLike } = {}) {
45
- this.keyPrefix = opts.keyPrefix ?? '7h3:nonce:'
46
- if (opts.client) {
47
- this.client = opts.client
48
- } else {
49
- // Lazily adapt InMemoryRedisLikeClient to RedisClientLike for zero-dep default
50
- const inner = new InMemoryRedisLikeClient()
51
- this.client = {
52
- set: (key, value, setOpts) =>
53
- inner.set(key, value, { nx: setOpts?.nx, pxMs: setOpts?.px }),
54
- get: (key) => inner.get ? inner.get(key) : Promise.resolve(null),
55
- del: (key) => inner.del ? inner.del(key) : Promise.resolve(0),
56
- quit: () => Promise.resolve(),
57
- }
58
- }
59
- }
60
-
61
- /**
62
- * Returns `false` if the key is fresh (first time seen — key was set in Redis),
63
- * or `true` if the key is a replay (key already existed — SET NX returned null).
64
- */
65
- async check(key: string, ttlMs: number): Promise<boolean> {
66
- const redisKey = `${this.keyPrefix}${key}`
67
- const result = await this.client.set(redisKey, '1', { nx: true, px: Math.max(1, ttlMs) })
68
- // SET NX returns 'OK' when the key was newly set (fresh), null when already present (replay)
69
- return result === null
70
- }
71
- }
72
-
73
- /**
74
- * Cluster-aware replay store that wraps multiple {@link RedisReplayStore}
75
- * instances — one per Redis shard / node.
76
- *
77
- * A message is treated as a replay if ANY node has already seen the nonce.
78
- * All nodes are checked in parallel via Promise.all.
79
- */
80
- export class ClusterRedisReplayStore implements ReplayStore {
81
- private readonly nodes: RedisReplayStore[]
82
-
83
- constructor(nodes: RedisReplayStore[]) {
84
- this.nodes = nodes
85
- }
86
-
87
- async check(key: string, ttlMs: number): Promise<boolean> {
88
- const results = await Promise.all(this.nodes.map((node) => node.check(key, ttlMs)))
89
- // If any node reports replay (true), the request is a replay
90
- return results.some((seen) => seen)
91
- }
92
- }
93
-
94
- /**
95
- * Convenience factory that creates a {@link RedisReplayStore} from a URL
96
- * or options object.
97
- */
98
- export function createRedisReplayStore(
99
- clientOrOpts: RedisLikeClient | { redisUrl?: string; keyPrefix?: string; client?: RedisClientLike },
100
- options?: RedisReplayStoreOptions,
101
- ): DistributedReplayStore
102
-
103
- export function createRedisReplayStore(
104
- clientOrOpts: RedisLikeClient | { redisUrl?: string; keyPrefix?: string; client?: RedisClientLike },
105
- options: RedisReplayStoreOptions = {},
106
- ): DistributedReplayStore | RedisReplayStore {
107
- // Detect old (client-first) vs new (opts-object) call style
108
- if (isRedisLikeClient(clientOrOpts)) {
109
- return _createDistributedReplayStore(clientOrOpts, options)
110
- }
111
- // New style: return a RedisReplayStore instance
112
- return new RedisReplayStore(clientOrOpts as { redisUrl?: string; keyPrefix?: string; client?: RedisClientLike })
113
- }
114
-
115
- function isRedisLikeClient(val: unknown): val is RedisLikeClient {
116
- return (
117
- val !== null &&
118
- typeof val === 'object' &&
119
- typeof (val as any).set === 'function' &&
120
- // RedisLikeClient does NOT have a `quit` method — RedisClientLike does
121
- // Pipeline presence distinguishes internal RedisLikeClient
122
- (typeof (val as any).pipeline === 'function' || !('quit' in val))
123
- )
124
- }
125
-
126
- /**
127
- * Creates a {@link ClusterRedisReplayStore} from an array of Redis URLs.
128
- * Each URL is used to create an independent {@link RedisReplayStore} node.
129
- */
130
- export function createClusterReplayStore(redisUrls: string[]): ClusterRedisReplayStore {
131
- const nodes = redisUrls.map((url) => new RedisReplayStore({ redisUrl: url }))
132
- return new ClusterRedisReplayStore(nodes)
133
- }
134
-
135
- /**
136
- * What to do when the Redis client throws (network blip, server down).
137
- *
138
- * - `fallback` (default): degrade to a local in-memory store so traffic keeps
139
- * flowing with single-node replay protection still in force. Quality +
140
- * reliability + scalability: Redis is never a fleet-wide kill switch.
141
- * - `reject`: fail closed — deny the message because uniqueness cannot be confirmed.
142
- * - `allow`: fail open — accept the message (skip the check) to maximise uptime.
143
- */
144
- export type ReplayErrorBehavior = 'fallback' | 'reject' | 'allow'
145
-
146
- export interface RedisReplayStoreOptions {
147
- /** Key namespace in Redis. Default `aip:replay:`. */
148
- keyPrefix?: string
149
- /** Behavior when the Redis client throws. Default `fallback`. */
150
- errorBehavior?: ReplayErrorBehavior
151
- /** Local store used when `errorBehavior` is `fallback`. Defaults to an in-memory-backed store. */
152
- fallback?: DistributedReplayStore
153
- /** Observability hook fired whenever a Redis error forces degraded handling. */
154
- onDegraded?: (error: unknown, context: { key: string; behavior: ReplayErrorBehavior }) => void
155
- }
156
-
157
- /**
158
- * Internal factory — creates a {@link DistributedReplayStore} backed by
159
- * Redis-style `SET NX PX`. Used by the overloaded `createRedisReplayStore`
160
- * when a `RedisLikeClient` is passed as the first argument.
161
- *
162
- * Drop the returned store into `new DistributedReplayCache(store)` (or pass a
163
- * `DistributedReplayCache` as the transport `replayCache`).
164
- */
165
- function _createDistributedReplayStore(
166
- client: RedisLikeClient,
167
- options: RedisReplayStoreOptions = {},
168
- ): DistributedReplayStore {
169
- const keyPrefix = options.keyPrefix ?? 'aip:replay:'
170
- const errorBehavior = options.errorBehavior ?? 'fallback'
171
- const onDegraded = options.onDegraded
172
- const fallback =
173
- options.fallback ??
174
- (errorBehavior === 'fallback'
175
- ? // The local fallback runs over a non-throwing in-memory client, so it
176
- // needs no fallback of its own — `reject` terminates the chain.
177
- _createDistributedReplayStore(new InMemoryRedisLikeClient(), { keyPrefix, errorBehavior: 'reject' })
178
- : undefined)
179
-
180
- function degrade(error: unknown, key: string, expiresAtMs: number, nowMs: number): Promise<boolean> | boolean {
181
- onDegraded?.(error, { key, behavior: errorBehavior })
182
- if (errorBehavior === 'allow') return true
183
- if (errorBehavior === 'reject') return false
184
- return fallback ? fallback.reserve(key, expiresAtMs, nowMs) : false
185
- }
186
-
187
- return {
188
- async reserve(key: string, expiresAtMs: number, nowMs: number): Promise<boolean> {
189
- const pxMs = Math.max(1, expiresAtMs - nowMs)
190
- try {
191
- const result = await client.set(`${keyPrefix}${key}`, '1', { nx: true, pxMs })
192
- return result === 'OK'
193
- } catch (error) {
194
- return degrade(error, key, expiresAtMs, nowMs)
195
- }
196
- },
197
-
198
- async reserveMany(entries: Array<{ key: string; expiresAtMs: number }>, nowMs: number): Promise<boolean[]> {
199
- if (entries.length === 0) return []
200
- if (client.pipeline) {
201
- try {
202
- const batch = client.pipeline()
203
- for (const { key, expiresAtMs } of entries) {
204
- batch.set(`${keyPrefix}${key}`, '1', { nx: true, pxMs: Math.max(1, expiresAtMs - nowMs) })
205
- }
206
- const results = await batch.exec()
207
- return results.map((result) => result === 'OK')
208
- } catch (error) {
209
- return Promise.all(entries.map((entry) => degrade(error, entry.key, entry.expiresAtMs, nowMs)))
210
- }
211
- }
212
- return Promise.all(entries.map((entry) => this.reserve(entry.key, entry.expiresAtMs, nowMs)))
213
- },
214
- }
215
- }
@@ -1,98 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import { InMemoryRedisLikeClient } from './redisClient'
3
- import {
4
- InMemoryRevocationStore,
5
- createRedisRevocationStore,
6
- withRevocationCheck,
7
- } from './revocation'
8
- import type { SignatureVerificationMaterial } from './protocol'
9
-
10
- describe('InMemoryRevocationStore', () => {
11
- it('reports a key as not revoked until it is revoked', async () => {
12
- const store = new InMemoryRevocationStore()
13
- expect(await store.isRevoked('agent.a', 'k1')).toBe(false)
14
- await store.revoke('agent.a', 'k1')
15
- expect(await store.isRevoked('agent.a', 'k1')).toBe(true)
16
- })
17
-
18
- it('scopes revocation to the exact (sender, keyId) pair', async () => {
19
- const store = new InMemoryRevocationStore()
20
- await store.revoke('agent.a', 'k1')
21
- expect(await store.isRevoked('agent.a', 'k2')).toBe(false)
22
- expect(await store.isRevoked('agent.b', 'k1')).toBe(false)
23
- })
24
-
25
- it('honors a time-bounded revocation (untilMs)', async () => {
26
- let now = 1000
27
- const store = new InMemoryRevocationStore({ now: () => now })
28
- await store.revoke('agent.a', 'k1', { untilMs: 2000 })
29
- expect(await store.isRevoked('agent.a', 'k1')).toBe(true)
30
- now = 2500
31
- expect(await store.isRevoked('agent.a', 'k1')).toBe(false)
32
- })
33
- })
34
-
35
- describe('createRedisRevocationStore', () => {
36
- it('revoke writes a key that isRevoked reads back', async () => {
37
- const store = createRedisRevocationStore(new InMemoryRedisLikeClient())
38
- expect(await store.isRevoked('agent.a', 'k1')).toBe(false)
39
- await store.revoke('agent.a', 'k1')
40
- expect(await store.isRevoked('agent.a', 'k1')).toBe(true)
41
- })
42
-
43
- it('caches reads so repeated checks within the freshness window hit Redis once', async () => {
44
- let now = 1000
45
- const client = new InMemoryRedisLikeClient({ now: () => now })
46
- const getSpy = vi.spyOn(client, 'get')
47
- const store = createRedisRevocationStore(client, { cacheTtlMs: 5000, now: () => now })
48
- await store.isRevoked('agent.a', 'k1')
49
- await store.isRevoked('agent.a', 'k1')
50
- expect(getSpy).toHaveBeenCalledOnce()
51
- now = 7000 // past the cache window
52
- await store.isRevoked('agent.a', 'k1')
53
- expect(getSpy).toHaveBeenCalledTimes(2)
54
- })
55
-
56
- it('fails closed by default: a Redis error means the key is treated as revoked', async () => {
57
- const onDegraded = vi.fn()
58
- const throwing = {
59
- set: async () => 'OK' as const,
60
- get: async () => {
61
- throw new Error('redis unreachable')
62
- },
63
- }
64
- const store = createRedisRevocationStore(throwing, { onDegraded })
65
- expect(await store.isRevoked('agent.a', 'k1')).toBe(true)
66
- expect(onDegraded).toHaveBeenCalledOnce()
67
- })
68
-
69
- it('errorBehavior "allow" treats a Redis error as not revoked', async () => {
70
- const throwing = {
71
- set: async () => 'OK' as const,
72
- get: async () => {
73
- throw new Error('redis unreachable')
74
- },
75
- }
76
- const store = createRedisRevocationStore(throwing, { errorBehavior: 'allow' })
77
- expect(await store.isRevoked('agent.a', 'k1')).toBe(false)
78
- })
79
- })
80
-
81
- describe('withRevocationCheck (signature resolver wrapper)', () => {
82
- const material: SignatureVerificationMaterial = { alg: 'HS256', secret: 's3cret' }
83
- const innerResolver = async () => material
84
- const signature = { alg: 'HS256' as const, keyId: 'k1', value: 'sig' }
85
-
86
- it('returns material for a key that is not revoked', async () => {
87
- const store = new InMemoryRevocationStore()
88
- const resolver = withRevocationCheck(innerResolver, store)
89
- expect(await resolver(signature, 'agent.a')).toEqual(material)
90
- })
91
-
92
- it('returns undefined (verification fails) for a revoked key', async () => {
93
- const store = new InMemoryRevocationStore()
94
- await store.revoke('agent.a', 'k1')
95
- const resolver = withRevocationCheck(innerResolver, store)
96
- expect(await resolver(signature, 'agent.a')).toBeUndefined()
97
- })
98
- })
package/src/revocation.ts DELETED
Binary file
@@ -1,87 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { matchGlob, matchPolicy, isAllowedSender, type RoutePolicy } from './routePolicy'
3
-
4
- describe('matchGlob', () => {
5
- it('exact match', () => {
6
- expect(matchGlob('/health', '/health')).toBe(true)
7
- expect(matchGlob('/health', '/healthz')).toBe(false)
8
- })
9
-
10
- it('single * does not cross segments', () => {
11
- expect(matchGlob('/api/*', '/api/users')).toBe(true)
12
- expect(matchGlob('/api/*', '/api/users/list')).toBe(false)
13
- expect(matchGlob('/api/*', '/api/')).toBe(true) // empty segment is still "within"
14
- })
15
-
16
- it('double ** matches any depth', () => {
17
- expect(matchGlob('/api/**', '/api/users')).toBe(true)
18
- expect(matchGlob('/api/**', '/api/users/list')).toBe(true)
19
- expect(matchGlob('/api/**', '/api/a/b/c/d')).toBe(true)
20
- expect(matchGlob('/api/**', '/other/users')).toBe(false)
21
- })
22
-
23
- it('? matches single non-slash char', () => {
24
- expect(matchGlob('/v?/api', '/v1/api')).toBe(true)
25
- expect(matchGlob('/v?/api', '/v12/api')).toBe(false)
26
- expect(matchGlob('/v?/api', '/v//api')).toBe(false)
27
- })
28
-
29
- it('handles regex metacharacters in literal path', () => {
30
- expect(matchGlob('/api/v1.0/health', '/api/v1.0/health')).toBe(true)
31
- expect(matchGlob('/api/v1.0/health', '/api/v100/health')).toBe(false)
32
- })
33
- })
34
-
35
- describe('matchPolicy', () => {
36
- const policies: RoutePolicy[] = [
37
- { path: '/health', require: 'none' },
38
- { path: '/api/admin/**', require: 'ed25519', allowedSenders: ['admin-agent'] },
39
- { path: '/api/*', require: 'any' },
40
- { path: '/webhook/**', require: 'hmac' },
41
- ]
42
-
43
- it('returns first matching policy (exact before wildcard)', () => {
44
- const result = matchPolicy(policies, '/health')
45
- expect(result?.require).toBe('none')
46
- })
47
-
48
- it('matches /api/users with /api/*', () => {
49
- const result = matchPolicy(policies, '/api/users')
50
- expect(result?.require).toBe('any')
51
- })
52
-
53
- it('/api/admin/delete matches /api/admin/** before /api/*', () => {
54
- const result = matchPolicy(policies, '/api/admin/delete')
55
- expect(result?.require).toBe('ed25519')
56
- })
57
-
58
- it('matches deep webhook path', () => {
59
- const result = matchPolicy(policies, '/webhook/stripe/events')
60
- expect(result?.require).toBe('hmac')
61
- })
62
-
63
- it('returns null for no match', () => {
64
- const result = matchPolicy(policies, '/unknown')
65
- expect(result).toBeNull()
66
- })
67
- })
68
-
69
- describe('isAllowedSender', () => {
70
- it('allows all senders when allowedSenders not set', () => {
71
- const policy: RoutePolicy = { path: '/api/*', require: 'any' }
72
- expect(isAllowedSender(policy, 'any-agent')).toBe(true)
73
- expect(isAllowedSender(policy, 'another-agent')).toBe(true)
74
- })
75
-
76
- it('allows only listed senders', () => {
77
- const policy: RoutePolicy = { path: '/api/*', require: 'ed25519', allowedSenders: ['trusted-agent', 'another-agent'] }
78
- expect(isAllowedSender(policy, 'trusted-agent')).toBe(true)
79
- expect(isAllowedSender(policy, 'another-agent')).toBe(true)
80
- expect(isAllowedSender(policy, 'unknown-agent')).toBe(false)
81
- })
82
-
83
- it('empty allowedSenders array allows all', () => {
84
- const policy: RoutePolicy = { path: '/api/*', require: 'any', allowedSenders: [] }
85
- expect(isAllowedSender(policy, 'anyone')).toBe(true)
86
- })
87
- })
@@ -1,72 +0,0 @@
1
- export type PolicyRequirement = 'ed25519' | 'hmac' | 'any' | 'none'
2
-
3
- export interface RoutePolicy {
4
- path: string
5
- require: PolicyRequirement
6
- rateLimit?: { requests: number; windowMs: number }
7
- allowedSenders?: string[]
8
- signResponse?: boolean
9
- }
10
-
11
- /**
12
- * Translate a glob pattern to a RegExp.
13
- * Supported:
14
- * - exact: '/health'
15
- * - '?' → any single non-slash char
16
- * - '*' → any chars within a single segment (no slash)
17
- * - '**' → any chars at any depth (including slashes)
18
- */
19
- export function matchGlob(pattern: string, path: string): boolean {
20
- // Build regex from pattern, handling ** before * to avoid double-expanding
21
- let regexStr = '^'
22
- let i = 0
23
- while (i < pattern.length) {
24
- const ch = pattern[i]
25
- if (ch === '*') {
26
- if (pattern[i + 1] === '*') {
27
- // Double star: matches anything including /
28
- regexStr += '.*'
29
- i += 2
30
- } else {
31
- // Single star: matches any non-slash sequence
32
- regexStr += '[^/]*'
33
- i += 1
34
- }
35
- } else if (ch === '?') {
36
- // Single char, not slash
37
- regexStr += '[^/]'
38
- i += 1
39
- } else {
40
- // Escape regex metacharacters in literal segments
41
- regexStr += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&')
42
- i += 1
43
- }
44
- }
45
- regexStr += '$'
46
-
47
- return new RegExp(regexStr).test(path)
48
- }
49
-
50
- /**
51
- * Find the first matching policy for a given path.
52
- * Returns null if no policy matches.
53
- */
54
- export function matchPolicy(policies: RoutePolicy[], path: string): RoutePolicy | null {
55
- for (const policy of policies) {
56
- if (matchGlob(policy.path, path)) {
57
- return policy
58
- }
59
- }
60
- return null
61
- }
62
-
63
- /**
64
- * Check whether a sender is permitted by a policy.
65
- * If allowedSenders is not set, all senders are allowed.
66
- */
67
- export function isAllowedSender(policy: RoutePolicy, sender: string): boolean {
68
- if (!policy.allowedSenders || policy.allowedSenders.length === 0) {
69
- return true
70
- }
71
- return policy.allowedSenders.includes(sender)
72
- }
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { loadRuntimePolicy, parseRuntimePolicyJson, validateRuntimePolicy } from './runtimePolicy'
3
-
4
- describe('runtime policy loader', () => {
5
- it('parses valid policy JSON', () => {
6
- const policy = parseRuntimePolicyJson(
7
- JSON.stringify({
8
- version: '1.0',
9
- name: 'gluv-runtime-policy',
10
- status: 'active',
11
- }),
12
- )
13
-
14
- expect(policy.version).toBe('1.0')
15
- expect(policy.name).toBe('gluv-runtime-policy')
16
- expect(policy.status).toBe('active')
17
- })
18
-
19
- it('rejects invalid JSON', () => {
20
- expect(() => parseRuntimePolicyJson('{')).toThrow(/Invalid runtime policy JSON/i)
21
- })
22
-
23
- it('rejects invalid root object', () => {
24
- expect(() => validateRuntimePolicy('not-an-object')).toThrow(/root must be an object/i)
25
- })
26
-
27
- it('rejects missing required fields', () => {
28
- expect(() => validateRuntimePolicy({ name: 'x', status: 'active' })).toThrow(/version/i)
29
- expect(() => validateRuntimePolicy({ version: '1.0', status: 'active' })).toThrow(/name/i)
30
- expect(() => validateRuntimePolicy({ version: '1.0', name: 'x' })).toThrow(/status/i)
31
- })
32
-
33
- it('loads policy using injected reader and path', async () => {
34
- const policy = await loadRuntimePolicy({
35
- path: '/virtual/AI_RUNTIME_POLICY.json',
36
- readTextFile: async (filePath) => {
37
- expect(filePath).toBe('/virtual/AI_RUNTIME_POLICY.json')
38
- return JSON.stringify({
39
- version: '1.0',
40
- name: 'gluv-runtime-policy',
41
- status: 'active',
42
- owner: 'platform-ai-ops',
43
- })
44
- },
45
- })
46
-
47
- expect(policy.owner).toBe('platform-ai-ops')
48
- })
49
- })
@@ -1,81 +0,0 @@
1
- export interface RuntimePolicy {
2
- version: string
3
- name: string
4
- status: string
5
- owner?: string
6
- updated_at?: string
7
- hard_invariants?: Record<string, unknown>
8
- slo_defaults?: Record<string, unknown>
9
- mode_selection?: Record<string, unknown>
10
- guardrails?: Record<string, unknown>
11
- tuning?: Record<string, unknown>
12
- adaptive_flow?: Record<string, unknown>
13
- benchmark_policy?: Record<string, unknown>
14
- runtime_decision_tree?: unknown[]
15
- operator_handoff_required_fields?: string[]
16
- [key: string]: unknown
17
- }
18
-
19
- export interface LoadRuntimePolicyOptions {
20
- path?: string
21
- readTextFile?: (filePath: string) => Promise<string>
22
- }
23
-
24
- function isRecord(value: unknown): value is Record<string, unknown> {
25
- return typeof value === 'object' && value !== null && !Array.isArray(value)
26
- }
27
-
28
- function asNonEmptyString(value: unknown, field: string): string {
29
- if (typeof value !== 'string' || value.trim().length === 0) {
30
- throw new Error(`Invalid runtime policy: '${field}' must be a non-empty string`)
31
- }
32
- return value
33
- }
34
-
35
- export function validateRuntimePolicy(value: unknown): RuntimePolicy {
36
- if (!isRecord(value)) {
37
- throw new Error('Invalid runtime policy: root must be an object')
38
- }
39
-
40
- const version = asNonEmptyString(value.version, 'version')
41
- const name = asNonEmptyString(value.name, 'name')
42
- const status = asNonEmptyString(value.status, 'status')
43
-
44
- return {
45
- ...value,
46
- version,
47
- name,
48
- status,
49
- }
50
- }
51
-
52
- export function parseRuntimePolicyJson(jsonText: string): RuntimePolicy {
53
- let parsed: unknown
54
- try {
55
- parsed = JSON.parse(jsonText)
56
- } catch (error) {
57
- const message = error instanceof Error ? error.message : String(error)
58
- throw new Error(`Invalid runtime policy JSON: ${message}`)
59
- }
60
- return validateRuntimePolicy(parsed)
61
- }
62
-
63
- async function defaultReadTextFile(filePath: string): Promise<string> {
64
- if (typeof fetch === 'function') {
65
- const response = await fetch(filePath)
66
- if (!response.ok) {
67
- throw new Error(`Failed to fetch runtime policy at '${filePath}': ${response.status} ${response.statusText}`)
68
- }
69
- return response.text()
70
- }
71
- throw new Error(
72
- "No default runtime file reader available in this environment. Provide 'readTextFile' in loadRuntimePolicy(options).",
73
- )
74
- }
75
-
76
- export async function loadRuntimePolicy(options: LoadRuntimePolicyOptions = {}): Promise<RuntimePolicy> {
77
- const filePath = options.path ?? 'AI_RUNTIME_POLICY.json'
78
- const readTextFile = options.readTextFile ?? defaultReadTextFile
79
- const text = await readTextFile(filePath)
80
- return parseRuntimePolicyJson(text)
81
- }
@@ -1,29 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { RuntimePolicyManager } from './runtimePolicyManager'
3
-
4
- describe('runtime policy manager', () => {
5
- it('loads initial policy and exposes snapshot', async () => {
6
- const manager = new RuntimePolicyManager({
7
- path: '/virtual/AI_RUNTIME_POLICY.json',
8
- readTextFile: async () =>
9
- JSON.stringify({
10
- version: '1.0',
11
- name: 'gluv-runtime-policy',
12
- status: 'active',
13
- hard_invariants: {},
14
- slo_defaults: {},
15
- tuning: {
16
- batch_size: { low_traffic: [4, 8], medium_traffic: [8, 32], high_traffic: [32, 64] },
17
- inflight_cap: { low_traffic: [16, 64], medium_traffic: [64, 256], high_traffic: [256, 1024] },
18
- retry_backoff_ms: { low_traffic: [1, 2], medium_traffic: [2, 8], high_traffic: [4, 16] },
19
- max_retry_attempts: 3,
20
- retriable_status_codes: [503],
21
- },
22
- }),
23
- })
24
-
25
- const loaded = await manager.loadInitial()
26
- expect(loaded.policy.name).toBe('gluv-runtime-policy')
27
- expect(manager.current().policy.version).toBe('1.0')
28
- })
29
- })
@@ -1,50 +0,0 @@
1
- import { bootstrapRuntimePolicyEnforcer, type BootstrapRuntimePolicyEnforcerOptions, type PolicyEnforcer } from './policyEnforcer'
2
- import type { RuntimePolicy } from './runtimePolicy'
3
-
4
- export interface RuntimePolicySnapshot {
5
- policy: RuntimePolicy
6
- enforcer: PolicyEnforcer
7
- loadedAtMs: number
8
- }
9
-
10
- export class RuntimePolicyManager {
11
- private snapshot: RuntimePolicySnapshot | null = null
12
- private readonly options: BootstrapRuntimePolicyEnforcerOptions
13
-
14
- constructor(options: BootstrapRuntimePolicyEnforcerOptions = {}) {
15
- this.options = options
16
- }
17
-
18
- async loadInitial(): Promise<RuntimePolicySnapshot> {
19
- const loaded = await bootstrapRuntimePolicyEnforcer(this.options)
20
- this.snapshot = {
21
- ...loaded,
22
- loadedAtMs: Date.now(),
23
- }
24
- return this.snapshot
25
- }
26
-
27
- current(): RuntimePolicySnapshot {
28
- if (!this.snapshot) {
29
- throw new Error('Runtime policy not loaded. Call loadInitial() first.')
30
- }
31
- return this.snapshot
32
- }
33
-
34
- async refresh(): Promise<RuntimePolicySnapshot> {
35
- const previous = this.snapshot
36
- try {
37
- const loaded = await bootstrapRuntimePolicyEnforcer(this.options)
38
- this.snapshot = {
39
- ...loaded,
40
- loadedAtMs: Date.now(),
41
- }
42
- return this.snapshot
43
- } catch (error) {
44
- if (previous) {
45
- return previous
46
- }
47
- throw error
48
- }
49
- }
50
- }