@7h3/protocol 0.1.2 → 0.5.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 (276) hide show
  1. package/.dockerignore +19 -0
  2. package/.github/dependabot.yml +32 -0
  3. package/.github/workflows/ci.yml +31 -0
  4. package/.github/workflows/publish.yml +59 -0
  5. package/.github/workflows/scorecard.yml +37 -0
  6. package/7h3.example.yaml +125 -0
  7. package/CHANGELOG.md +152 -0
  8. package/CONTRIBUTING.md +82 -0
  9. package/Dockerfile +73 -0
  10. package/GOVERNANCE.md +62 -0
  11. package/README.md +1317 -6
  12. package/SECURITY.md +70 -0
  13. package/bench-results/replay-cache-full-1777891033256.json +10 -0
  14. package/bench-results/replay-cache-full-1777896317488.json +10 -0
  15. package/bench-results/replay-cache-full-1777900993184.json +10 -0
  16. package/bench-results/replay-cache-full-1777901019285.json +10 -0
  17. package/bench-results/replay-cache-quick-1777870170126.json +10 -0
  18. package/bench-results/signature-profiles-quick-1775875160079.json +85 -0
  19. package/bench-results/signature-profiles-quick-1775983539716.json +85 -0
  20. package/bench-results/signature-profiles-quick-1776237913190.json +85 -0
  21. package/bench-results/wire-codecs-full-1777891019803.json +93 -0
  22. package/bench-results/wire-codecs-full-1777896260964.json +93 -0
  23. package/bench-results/wire-codecs-full-1777901004247.json +93 -0
  24. package/bench-results/wire-codecs-quick-1775972879056.json +93 -0
  25. package/bench-results/wire-codecs-quick-1775983541111.json +93 -0
  26. package/bench-results/wire-codecs-quick-1776237914299.json +93 -0
  27. package/bench-results/wire-codecs-quick-1777841285236.json +93 -0
  28. package/bench-results/wire-codecs-quick-1777841321772.json +93 -0
  29. package/bench-results/wire-codecs-quick-1777841330408.json +93 -0
  30. package/bench-results/wire-codecs-quick-1777852886082.json +93 -0
  31. package/bench-results/wire-codecs-quick-1777852988773.json +93 -0
  32. package/bench-results/wire-codecs-quick-1777870188095.json +93 -0
  33. package/bench-results/wire-codecs-quick-1777870263918.json +93 -0
  34. package/bench-results/wire-codecs-quick-1777870455034.json +93 -0
  35. package/bench-results/wire-codecs-quick-1778816163081.json +93 -0
  36. package/bench-results/wire-codecs-quick-1778843936130.json +93 -0
  37. package/bin/7h3.ts +406 -0
  38. package/conformance/7h3_v0_1.json +77 -0
  39. package/conformance/7h3_v0_1_binary.json +20 -0
  40. package/conformance/aip_v0_1_binary.json +20 -0
  41. package/docker-compose.yaml +77 -0
  42. package/docs/ADOPTION_PLAN.md +120 -0
  43. package/docs/AGENTS.md +77 -0
  44. package/docs/AIP_RFC_v0.1.md +97 -0
  45. package/docs/AI_DECISION_CARD.md +122 -0
  46. package/docs/AI_RUNTIME_POLICY.json +126 -0
  47. package/docs/AI_RUNTIME_POLICY.yaml +110 -0
  48. package/docs/BACKPRESSURE_TUNING.md +65 -0
  49. package/docs/BENCHMARK_CLAIM_MATRIX.md +42 -0
  50. package/docs/BENCHMARK_REPORT_TEMPLATE.md +169 -0
  51. package/docs/BINARY_CODEC_BENCH.md +23 -0
  52. package/docs/CLEAN_CLONE_RUNBOOK.md +36 -0
  53. package/docs/CLOCK_SKEW_POLICY.md +30 -0
  54. package/docs/DISTRIBUTED_REPLAY.md +142 -0
  55. package/docs/FUZZ_CAMPAIGN.md +121 -0
  56. package/docs/GATEWAY.md +195 -0
  57. package/docs/KEY_MANAGEMENT_POLICY.md +53 -0
  58. package/docs/KEY_REVOCATION.md +69 -0
  59. package/docs/MCP_WRAPPER.md +159 -0
  60. package/docs/MIGRATION_GUIDE.md +40 -0
  61. package/docs/OPERATORS.md +184 -0
  62. package/docs/PERF_REGRESSION_POLICY.md +34 -0
  63. package/docs/PROJECT_EXAMINATION_2026-05-31.md +219 -0
  64. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +135 -0
  65. package/docs/RELEASE_GATE.md +25 -0
  66. package/docs/RELEASE_NOTES_v0.1.0.md +54 -0
  67. package/docs/SECURITY_REVIEW_2026-06-05.md +165 -0
  68. package/docs/TELEMETRY.md +41 -0
  69. package/docs/THREAT_MODEL.md +89 -0
  70. package/docs/VERSIONING_POLICY.md +30 -0
  71. package/docs/assets/banner-github.png +0 -0
  72. package/docs/assets/banner.png +0 -0
  73. package/docs/assets/banner.svg +123 -0
  74. package/eslint.config.js +15 -0
  75. package/fuzz/ts/harness-decode.ts +136 -0
  76. package/fuzz/ts/harness-verify.ts +121 -0
  77. package/fuzz/ts/run.ts +35 -0
  78. package/mcp-server/README.md +38 -0
  79. package/mcp-server/package-lock.json +1187 -0
  80. package/mcp-server/package.json +35 -0
  81. package/mcp-server/src/index.ts +236 -0
  82. package/mcp-server/tsconfig.json +14 -0
  83. package/package.json +121 -13
  84. package/scripts/aip-framework-quickstart.ts +110 -0
  85. package/scripts/aip-mcp-gateway.ts +38 -0
  86. package/scripts/aip-mcp-wrap-demo.ts +72 -0
  87. package/scripts/aip-quickstart.ts +60 -0
  88. package/scripts/bench-diff.ts +118 -0
  89. package/scripts/bench-protocol-e2e.ts +937 -0
  90. package/scripts/bench-protocol-openloop.ts +1397 -0
  91. package/scripts/bench-replay-cache.ts +76 -0
  92. package/scripts/bench-signature-profiles.ts +180 -0
  93. package/scripts/bench-wire-codecs.ts +161 -0
  94. package/scripts/build-binary-conformance.ts +36 -0
  95. package/scripts/build-release-dashboard.ts +175 -0
  96. package/scripts/canary-rollout.ts +38 -0
  97. package/scripts/mcpGatewayCli.test.ts +116 -0
  98. package/scripts/prepare-aip-package.ts +88 -0
  99. package/scripts/regen-conformance-sigs.ts +18 -0
  100. package/scripts/release-gate.ts +19 -0
  101. package/scripts/validate-runtime-policy.ts +18 -0
  102. package/sdk/browser/index.test.ts +162 -0
  103. package/sdk/browser/index.ts +257 -0
  104. package/sdk/browser/package.json +13 -0
  105. package/sdk/go/cbor.go +551 -0
  106. package/sdk/go/cbor_test.go +232 -0
  107. package/sdk/go/encryption.go +280 -0
  108. package/sdk/go/encryption_test.go +318 -0
  109. package/sdk/go/go.mod +7 -0
  110. package/sdk/go/go.sum +4 -0
  111. package/sdk/go/http.go +135 -0
  112. package/sdk/go/protocol.go +324 -0
  113. package/sdk/go/protocol_test.go +334 -0
  114. package/sdk/go/replay.go +121 -0
  115. package/sdk/go/replay_test.go +149 -0
  116. package/sdk/go/webhook.go +136 -0
  117. package/sdk/pq/package-lock.json +1358 -0
  118. package/sdk/pq/package.json +42 -0
  119. package/sdk/pq/src/index.test.ts +143 -0
  120. package/sdk/pq/src/index.ts +166 -0
  121. package/sdk/pq/tsconfig.json +14 -0
  122. package/sdk/pq/vitest.config.ts +7 -0
  123. package/sdk/python/README.md +18 -0
  124. package/sdk/python/protocol_7h3/__init__.py +46 -0
  125. package/sdk/python/protocol_7h3/encryption.py +252 -0
  126. package/sdk/python/protocol_7h3/http.py +212 -0
  127. package/sdk/python/protocol_7h3/keys.py +149 -0
  128. package/sdk/python/protocol_7h3/pq.py +244 -0
  129. package/sdk/python/protocol_7h3/protocol.py +525 -0
  130. package/sdk/python/protocol_7h3/queue.py +118 -0
  131. package/sdk/python/protocol_7h3/replay.py +98 -0
  132. package/sdk/python/protocol_7h3/webhook.py +116 -0
  133. package/sdk/python/pyproject.toml +40 -0
  134. package/sdk/python/tests/test_conformance.py +110 -0
  135. package/sdk/python/tests/test_encryption.py +206 -0
  136. package/sdk/python/tests/test_http.py +305 -0
  137. package/sdk/python/tests/test_keys.py +417 -0
  138. package/sdk/python/tests/test_queue.py +120 -0
  139. package/sdk/python/tests/test_webhook.py +345 -0
  140. package/sdk/rust/Cargo.lock +371 -0
  141. package/sdk/rust/Cargo.toml +25 -0
  142. package/sdk/rust/README.md +31 -0
  143. package/sdk/rust/fuzz/Cargo.toml +29 -0
  144. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
  145. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
  146. package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
  147. package/sdk/rust/src/http.rs +145 -0
  148. package/sdk/rust/src/keys.rs +161 -0
  149. package/sdk/rust/src/lib.rs +688 -0
  150. package/sdk/rust/src/queue.rs +79 -0
  151. package/sdk/rust/src/webhook.rs +86 -0
  152. package/sdk/rust/tests/conformance.rs +148 -0
  153. package/sdk/rust/tests/gateway.rs +130 -0
  154. package/sdk/rust/tests/http_webhook_queue.rs +201 -0
  155. package/sdk/rust/tests/keys.rs +189 -0
  156. package/sdk/threshold/index.d.ts +68 -0
  157. package/sdk/threshold/index.d.ts.map +1 -0
  158. package/sdk/threshold/index.js +254 -0
  159. package/sdk/threshold/package-lock.json +1361 -0
  160. package/sdk/threshold/package.json +39 -0
  161. package/sdk/threshold/src/index.d.ts +68 -0
  162. package/sdk/threshold/src/index.d.ts.map +1 -0
  163. package/sdk/threshold/src/index.js +254 -0
  164. package/sdk/threshold/src/index.test.ts +238 -0
  165. package/sdk/threshold/src/index.ts +355 -0
  166. package/sdk/threshold/tsconfig.json +19 -0
  167. package/sdk/threshold/vitest.config.ts +12 -0
  168. package/src/agentAdapter.test.ts +48 -0
  169. package/src/agentAdapter.ts +56 -0
  170. package/src/auditLog.test.ts +145 -0
  171. package/src/auditLog.ts +147 -0
  172. package/src/capability.test.ts +504 -0
  173. package/src/capability.ts +380 -0
  174. package/src/cborCodec.test.ts +263 -0
  175. package/src/cborCodec.ts +339 -0
  176. package/src/conformance.test.ts +136 -0
  177. package/src/conformanceVectors.ts +99 -0
  178. package/src/encryption.test.ts +206 -0
  179. package/src/encryption.ts +245 -0
  180. package/src/envelopeCbor.ts +140 -0
  181. package/src/frameworkAdapters.test.ts +290 -0
  182. package/src/frameworkAdapters.ts +261 -0
  183. package/src/gateway.test.ts +343 -0
  184. package/src/gateway.ts +246 -0
  185. package/src/grpcBinding.test.ts +211 -0
  186. package/src/grpcBinding.ts +103 -0
  187. package/src/httpBinding.test.ts +376 -0
  188. package/src/httpBinding.ts +189 -0
  189. package/src/index.ts +39 -0
  190. package/src/keyInfra.test.ts +278 -0
  191. package/src/keyInfra.ts +228 -0
  192. package/src/keyRegistry.ts +59 -0
  193. package/src/keyRotation.test.ts +78 -0
  194. package/src/keyRotation.ts +72 -0
  195. package/src/mcpGateway.test.ts +129 -0
  196. package/src/mcpGateway.ts +250 -0
  197. package/src/mcpTransports.test.ts +92 -0
  198. package/src/mcpTransports.ts +169 -0
  199. package/src/mcpWrapper.test.ts +179 -0
  200. package/src/mcpWrapper.ts +206 -0
  201. package/src/otel.ts +136 -0
  202. package/src/policyEnforcer.test.ts +99 -0
  203. package/src/policyEnforcer.ts +169 -0
  204. package/src/policyTelemetryFeedback.test.ts +25 -0
  205. package/src/policyTelemetryFeedback.ts +38 -0
  206. package/src/protocol.bench.ts +37 -0
  207. package/{protocol.d.ts → src/protocol.d.ts} +2 -1
  208. package/src/protocol.d.ts.map +1 -0
  209. package/src/protocol.js +294 -0
  210. package/src/protocol.test.ts +155 -0
  211. package/src/protocol.ts +414 -0
  212. package/src/protocolAgent.test.ts +105 -0
  213. package/src/protocolAgent.ts +169 -0
  214. package/src/protocolBinary.test.ts +165 -0
  215. package/src/protocolBinary.ts +312 -0
  216. package/src/protocolCapabilities.ts +70 -0
  217. package/src/protocolFuzz.advanced.test.ts +235 -0
  218. package/src/protocolFuzz.test.ts +111 -0
  219. package/src/protocolNegative.test.ts +97 -0
  220. package/src/protocolReplay.test.ts +71 -0
  221. package/src/protocolReplay.ts +194 -0
  222. package/src/protocolTransport.test.ts +556 -0
  223. package/src/protocolTransport.ts +483 -0
  224. package/src/queueBinding.test.ts +130 -0
  225. package/src/queueBinding.ts +102 -0
  226. package/src/rateLimiter.test.ts +96 -0
  227. package/src/rateLimiter.ts +46 -0
  228. package/src/redisClient.ts +140 -0
  229. package/src/redisIntegration.test.ts +134 -0
  230. package/src/replayStores.test.ts +273 -0
  231. package/src/replayStores.ts +215 -0
  232. package/src/revocation.test.ts +98 -0
  233. package/src/revocation.ts +0 -0
  234. package/src/routePolicy.test.ts +87 -0
  235. package/src/routePolicy.ts +72 -0
  236. package/src/runtimePolicy.test.ts +49 -0
  237. package/src/runtimePolicy.ts +81 -0
  238. package/src/runtimePolicyManager.test.ts +29 -0
  239. package/src/runtimePolicyManager.ts +50 -0
  240. package/src/runtimePolicyPresets.ts +43 -0
  241. package/src/signedResponse.test.ts +111 -0
  242. package/src/signedResponse.ts +83 -0
  243. package/src/stream.test.ts +254 -0
  244. package/src/stream.ts +417 -0
  245. package/src/telemetry.test.ts +251 -0
  246. package/src/telemetry.ts +299 -0
  247. package/src/webhookBinding.test.ts +144 -0
  248. package/src/webhookBinding.ts +115 -0
  249. package/src/wsBinding.test.ts +221 -0
  250. package/src/wsBinding.ts +200 -0
  251. package/tsconfig.json +15 -0
  252. package/tsconfig.lib.json +23 -0
  253. package/vite.lib.config.ts +16 -0
  254. package/vitest.config.ts +11 -0
  255. package/agentAdapter.d.ts +0 -26
  256. package/conformanceVectors.d.ts +0 -20
  257. package/frameworkAdapters.d.ts +0 -72
  258. package/index.d.ts +0 -20
  259. package/index.js +0 -1702
  260. package/keyRotation.d.ts +0 -20
  261. package/mcpGateway.d.ts +0 -37
  262. package/mcpTransports.d.ts +0 -62
  263. package/mcpWrapper.d.ts +0 -83
  264. package/policyEnforcer.d.ts +0 -50
  265. package/policyTelemetryFeedback.d.ts +0 -11
  266. package/protocolAgent.d.ts +0 -58
  267. package/protocolBinary.d.ts +0 -8
  268. package/protocolCapabilities.d.ts +0 -24
  269. package/protocolReplay.d.ts +0 -35
  270. package/protocolTransport.d.ts +0 -73
  271. package/redisClient.d.ts +0 -49
  272. package/replayStores.d.ts +0 -32
  273. package/revocation.d.ts +0 -71
  274. package/runtimePolicy.d.ts +0 -24
  275. package/runtimePolicyManager.d.ts +0 -15
  276. package/runtimePolicyPresets.d.ts +0 -11
@@ -0,0 +1,115 @@
1
+ import {
2
+ signCanonicalPayloadEd25519,
3
+ verifyCanonicalPayloadEd25519,
4
+ signCanonicalPayloadHmac,
5
+ verifyCanonicalPayloadHmac,
6
+ generateEd25519KeypairBase64Url,
7
+ } from './protocol'
8
+
9
+ export const WEBHOOK_SIG_HEADER = 'x-7h3-sig'
10
+ export const WEBHOOK_TS_HEADER = 'x-7h3-ts'
11
+ export const WEBHOOK_DEFAULT_TTL_MS = 300_000 // 5 minutes
12
+
13
+ // What gets signed: "${timestampMs}.${rawBody}" — binds time AND content
14
+ function webhookSigningPayload(timestampMs: number, body: string): string {
15
+ return `${timestampMs}.${body}`
16
+ }
17
+
18
+ export interface WebhookSignOptions {
19
+ privateKey: string // Ed25519 PKCS8 base64url
20
+ ttlMs?: number // default 5 min
21
+ }
22
+
23
+ export interface WebhookSignHmacOptions {
24
+ secret: string
25
+ ttlMs?: number
26
+ }
27
+
28
+ export interface WebhookVerifyOptions {
29
+ publicKey: string // Ed25519 SPKI base64url
30
+ maxAgeMs?: number // default 5 min
31
+ }
32
+
33
+ export interface WebhookVerifyHmacOptions {
34
+ secret: string
35
+ maxAgeMs?: number
36
+ }
37
+
38
+ export interface WebhookHeaders {
39
+ [WEBHOOK_SIG_HEADER]: string
40
+ [WEBHOOK_TS_HEADER]: string
41
+ }
42
+
43
+ // Sign a webhook payload with Ed25519
44
+ export async function signWebhook(
45
+ payload: string | Uint8Array,
46
+ opts: WebhookSignOptions
47
+ ): Promise<WebhookHeaders> {
48
+ const body = typeof payload === 'string' ? payload : new TextDecoder().decode(payload)
49
+ const timestampMs = Date.now()
50
+ const signingPayload = webhookSigningPayload(timestampMs, body)
51
+ const sig = await signCanonicalPayloadEd25519(signingPayload, opts.privateKey)
52
+ return { [WEBHOOK_SIG_HEADER]: sig, [WEBHOOK_TS_HEADER]: String(timestampMs) }
53
+ }
54
+
55
+ // Sign with HMAC shared secret
56
+ export async function signWebhookHmac(
57
+ payload: string | Uint8Array,
58
+ opts: WebhookSignHmacOptions
59
+ ): Promise<WebhookHeaders> {
60
+ const body = typeof payload === 'string' ? payload : new TextDecoder().decode(payload)
61
+ const timestampMs = Date.now()
62
+ const signingPayload = webhookSigningPayload(timestampMs, body)
63
+ const sig = await signCanonicalPayloadHmac(signingPayload, opts.secret)
64
+ return { [WEBHOOK_SIG_HEADER]: sig, [WEBHOOK_TS_HEADER]: String(timestampMs) }
65
+ }
66
+
67
+ // Verify Ed25519 webhook signature
68
+ export async function verifyWebhook(
69
+ payload: string | Uint8Array,
70
+ headers: WebhookHeaders | Record<string, string>,
71
+ opts: WebhookVerifyOptions
72
+ ): Promise<boolean> {
73
+ const body = typeof payload === 'string' ? payload : new TextDecoder().decode(payload)
74
+ const sig = headers[WEBHOOK_SIG_HEADER]
75
+ const tsStr = headers[WEBHOOK_TS_HEADER]
76
+ if (!sig || !tsStr) return false
77
+ const ts = Number(tsStr)
78
+ if (!Number.isFinite(ts)) return false
79
+ const maxAge = opts.maxAgeMs ?? WEBHOOK_DEFAULT_TTL_MS
80
+ if (Date.now() - ts > maxAge) return false
81
+ const signingPayload = webhookSigningPayload(ts, body)
82
+ return verifyCanonicalPayloadEd25519(signingPayload, sig, opts.publicKey)
83
+ }
84
+
85
+ // Verify HMAC webhook signature
86
+ export async function verifyWebhookHmac(
87
+ payload: string | Uint8Array,
88
+ headers: WebhookHeaders | Record<string, string>,
89
+ opts: WebhookVerifyHmacOptions
90
+ ): Promise<boolean> {
91
+ const body = typeof payload === 'string' ? payload : new TextDecoder().decode(payload)
92
+ const sig = headers[WEBHOOK_SIG_HEADER]
93
+ const tsStr = headers[WEBHOOK_TS_HEADER]
94
+ if (!sig || !tsStr) return false
95
+ const ts = Number(tsStr)
96
+ if (!Number.isFinite(ts)) return false
97
+ const maxAge = opts.maxAgeMs ?? WEBHOOK_DEFAULT_TTL_MS
98
+ if (Date.now() - ts > maxAge) return false
99
+ const signingPayload = webhookSigningPayload(ts, body)
100
+ return verifyCanonicalPayloadHmac(signingPayload, sig, opts.secret)
101
+ }
102
+
103
+ // Parse and verify in one call, throws on failure
104
+ export async function consumeWebhook<T = unknown>(
105
+ payload: string,
106
+ headers: Record<string, string>,
107
+ opts: WebhookVerifyOptions
108
+ ): Promise<T> {
109
+ const valid = await verifyWebhook(payload, headers, opts)
110
+ if (!valid) throw new Error('7h3: webhook signature verification failed')
111
+ return JSON.parse(payload) as T
112
+ }
113
+
114
+ // Re-export key generation for convenience
115
+ export { generateEd25519KeypairBase64Url }
@@ -0,0 +1,221 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { wrapWebSocket, generateEd25519KeypairBase64Url, type WebSocketLike } from './wsBinding'
3
+ import { signEnvelopeEd25519, createEnvelope, type ProtocolEnvelope } from './protocol'
4
+ import { createStaticKeyRegistry } from './keyRegistry'
5
+
6
+ // Mock WebSocket class that buffers sent messages and lets you inject received messages
7
+ class MockWebSocket implements WebSocketLike {
8
+ public sent: string[] = []
9
+ private messageHandlers: Array<(e: { data: string }) => void> = []
10
+ private closeHandlers: Array<() => void> = []
11
+ readyState = 1
12
+
13
+ send(data: string): void {
14
+ this.sent.push(data)
15
+ }
16
+
17
+ addEventListener(event: string, handler: (...args: any[]) => void): void {
18
+ if (event === 'message') {
19
+ this.messageHandlers.push(handler as (e: { data: string }) => void)
20
+ } else if (event === 'close') {
21
+ this.closeHandlers.push(handler as () => void)
22
+ }
23
+ }
24
+
25
+ removeEventListener(event: string, handler: (...args: unknown[]) => void): void {
26
+ if (event === 'message') {
27
+ const idx = this.messageHandlers.indexOf(handler as any)
28
+ if (idx !== -1) this.messageHandlers.splice(idx, 1)
29
+ } else if (event === 'close') {
30
+ const idx = this.closeHandlers.indexOf(handler as any)
31
+ if (idx !== -1) this.closeHandlers.splice(idx, 1)
32
+ }
33
+ }
34
+
35
+ // Inject a received message
36
+ receive(data: string): void {
37
+ for (const h of this.messageHandlers) h({ data })
38
+ }
39
+
40
+ close(): void {
41
+ for (const h of this.closeHandlers) h()
42
+ }
43
+ }
44
+
45
+ describe('wrapWebSocket', () => {
46
+ it('send() produces a signed JSON frame', async () => {
47
+ const keypair = await generateEd25519KeypairBase64Url()
48
+ const ws = new MockWebSocket()
49
+ const registry = createStaticKeyRegistry({ 'agent-A': keypair.publicKey })
50
+
51
+ const pws = wrapWebSocket(ws, {
52
+ privateKey: keypair.privateKey,
53
+ sender: 'agent-A',
54
+ keyRegistry: registry,
55
+ })
56
+
57
+ await pws.send({ hello: 'world' })
58
+
59
+ expect(ws.sent).toHaveLength(1)
60
+ const frame = JSON.parse(ws.sent[0]) as ProtocolEnvelope
61
+ expect(frame.header.sender).toBe('agent-A')
62
+ expect(frame.signature).toBeDefined()
63
+ expect(frame.signature?.alg).toBe('ED25519')
64
+ expect(frame.body.intent).toBe('TASK')
65
+ expect(frame.body.content).toBe(JSON.stringify({ hello: 'world' }))
66
+ })
67
+
68
+ it('incoming signed frame calls onMessage handler', async () => {
69
+ const keypair = await generateEd25519KeypairBase64Url()
70
+ const ws = new MockWebSocket()
71
+ const registry = createStaticKeyRegistry({ 'agent-B': keypair.publicKey })
72
+
73
+ const pws = wrapWebSocket(ws, {
74
+ privateKey: keypair.privateKey,
75
+ sender: 'agent-A',
76
+ keyRegistry: registry,
77
+ })
78
+
79
+ const received: Array<{ payload: unknown; envelope: ProtocolEnvelope }> = []
80
+ pws.onMessage((payload, envelope) => received.push({ payload, envelope }))
81
+
82
+ // Create a valid signed frame from agent-B
83
+ const envelope = createEnvelope({
84
+ sender: 'agent-B',
85
+ intent: 'TASK',
86
+ content: JSON.stringify({ ping: true }),
87
+ ttlMs: 30_000,
88
+ })
89
+ const signed = await signEnvelopeEd25519(envelope, keypair.privateKey)
90
+
91
+ // Wait for async message processing
92
+ await new Promise<void>(resolve => {
93
+ pws.onMessage(() => resolve())
94
+ ws.receive(JSON.stringify(signed))
95
+ })
96
+
97
+ expect(received).toHaveLength(1)
98
+ expect(received[0].payload).toEqual({ ping: true })
99
+ expect(received[0].envelope.header.sender).toBe('agent-B')
100
+ })
101
+
102
+ it('tampered frame calls onVerifyFail', async () => {
103
+ const keypair = await generateEd25519KeypairBase64Url()
104
+ const ws = new MockWebSocket()
105
+ const registry = createStaticKeyRegistry({ 'agent-B': keypair.publicKey })
106
+
107
+ const pws = wrapWebSocket(ws, {
108
+ privateKey: keypair.privateKey,
109
+ sender: 'agent-A',
110
+ keyRegistry: registry,
111
+ })
112
+
113
+ const errors: Error[] = []
114
+ pws.onVerifyFail((err) => errors.push(err))
115
+
116
+ // Create a valid signed frame, then tamper with body content
117
+ const envelope = createEnvelope({
118
+ sender: 'agent-B',
119
+ intent: 'TASK',
120
+ content: 'original content',
121
+ ttlMs: 30_000,
122
+ })
123
+ const signed = await signEnvelopeEd25519(envelope, keypair.privateKey)
124
+ // Tamper with content after signing
125
+ const tampered = { ...signed, body: { ...signed.body, content: 'tampered content' } }
126
+
127
+ await new Promise<void>(resolve => {
128
+ pws.onVerifyFail(() => resolve())
129
+ ws.receive(JSON.stringify(tampered))
130
+ })
131
+
132
+ expect(errors).toHaveLength(1)
133
+ expect(errors[0].message).toContain('invalid signature')
134
+ })
135
+
136
+ it('unknown sender calls onVerifyFail', async () => {
137
+ const keypair = await generateEd25519KeypairBase64Url()
138
+ const ws = new MockWebSocket()
139
+ // Registry has no entry for 'unknown-agent'
140
+ const registry = createStaticKeyRegistry({})
141
+
142
+ const pws = wrapWebSocket(ws, {
143
+ privateKey: keypair.privateKey,
144
+ sender: 'agent-A',
145
+ keyRegistry: registry,
146
+ })
147
+
148
+ const errors: Error[] = []
149
+ pws.onVerifyFail((err) => errors.push(err))
150
+
151
+ const envelope = createEnvelope({
152
+ sender: 'unknown-agent',
153
+ intent: 'TASK',
154
+ content: 'hello',
155
+ ttlMs: 30_000,
156
+ })
157
+ const signed = await signEnvelopeEd25519(envelope, keypair.privateKey)
158
+
159
+ await new Promise<void>(resolve => {
160
+ pws.onVerifyFail(() => resolve())
161
+ ws.receive(JSON.stringify(signed))
162
+ })
163
+
164
+ expect(errors).toHaveLength(1)
165
+ expect(errors[0].message).toContain('unknown sender')
166
+ })
167
+
168
+ it('malformed JSON calls onVerifyFail', async () => {
169
+ const keypair = await generateEd25519KeypairBase64Url()
170
+ const ws = new MockWebSocket()
171
+ const registry = createStaticKeyRegistry({})
172
+
173
+ const pws = wrapWebSocket(ws, {
174
+ privateKey: keypair.privateKey,
175
+ sender: 'agent-A',
176
+ keyRegistry: registry,
177
+ })
178
+
179
+ const errors: Error[] = []
180
+ pws.onVerifyFail((err) => errors.push(err))
181
+
182
+ await new Promise<void>(resolve => {
183
+ pws.onVerifyFail(() => resolve())
184
+ ws.receive('this is not valid JSON {{{')
185
+ })
186
+
187
+ expect(errors).toHaveLength(1)
188
+ expect(errors[0].message).toContain('malformed frame')
189
+ })
190
+
191
+ it('seq increments on each send', async () => {
192
+ const keypair = await generateEd25519KeypairBase64Url()
193
+ const ws = new MockWebSocket()
194
+ const registry = createStaticKeyRegistry({ 'agent-A': keypair.publicKey })
195
+
196
+ const pws = wrapWebSocket(ws, {
197
+ privateKey: keypair.privateKey,
198
+ sender: 'agent-A',
199
+ keyRegistry: registry,
200
+ })
201
+
202
+ expect(pws.seq).toBe(0)
203
+
204
+ await pws.send('first')
205
+ expect(pws.seq).toBe(1)
206
+
207
+ await pws.send('second')
208
+ expect(pws.seq).toBe(2)
209
+
210
+ await pws.send('third')
211
+ expect(pws.seq).toBe(3)
212
+
213
+ // Verify correlationId carries the seq number
214
+ const frame1 = JSON.parse(ws.sent[0]) as ProtocolEnvelope
215
+ const frame2 = JSON.parse(ws.sent[1]) as ProtocolEnvelope
216
+ const frame3 = JSON.parse(ws.sent[2]) as ProtocolEnvelope
217
+ expect(frame1.body.correlationId).toBe('1')
218
+ expect(frame2.body.correlationId).toBe('2')
219
+ expect(frame3.body.correlationId).toBe('3')
220
+ })
221
+ })
@@ -0,0 +1,200 @@
1
+ import {
2
+ signEnvelopeEd25519, verifyEnvelopeEd25519,
3
+ createEnvelope, validateEnvelope,
4
+ generateEd25519KeypairBase64Url, type ProtocolEnvelope
5
+ } from './protocol'
6
+ import type { KeyRegistry } from './keyRegistry'
7
+ import {
8
+ SignedStreamWriter,
9
+ verifyStream,
10
+ decodeStreamChunk,
11
+ encodeStreamChunk,
12
+ type StreamSignerOpts,
13
+ type StreamVerifierOpts,
14
+ type StreamVerifyResult,
15
+ type StreamChunk,
16
+ } from './stream'
17
+
18
+ export { type KeyRegistry, generateEd25519KeypairBase64Url }
19
+ export type { StreamSignerOpts, StreamVerifierOpts, StreamVerifyResult, StreamChunk }
20
+
21
+ // Minimal WebSocket interface (works with browser WebSocket, ws library, etc.)
22
+ export interface WebSocketLike {
23
+ send(data: string): void
24
+ addEventListener(event: 'message', handler: (e: { data: string | Buffer }) => void): void
25
+ addEventListener(event: 'close', handler: () => void): void
26
+ removeEventListener(event: string, handler: (...args: unknown[]) => void): void
27
+ readyState?: number
28
+ }
29
+
30
+ export interface WsBindingOptions {
31
+ privateKey: string
32
+ sender: string
33
+ recipient?: string
34
+ keyRegistry: KeyRegistry // to look up peer public keys by sender ID
35
+ ttlMs?: number // per-frame TTL, default 30_000
36
+ onVerifyFail?: (err: Error, rawData: string) => void
37
+ }
38
+
39
+ export interface ProtectedWebSocket {
40
+ // Send a payload — automatically signs it
41
+ send(payload: unknown): Promise<void>
42
+ // Register handler for verified incoming messages
43
+ onMessage(handler: (payload: unknown, envelope: ProtocolEnvelope) => void): void
44
+ // Register handler for verify failures (optional)
45
+ onVerifyFail(handler: (err: Error, rawData: string) => void): void
46
+ // Sequence number of last sent message
47
+ readonly seq: number
48
+ }
49
+
50
+ export function wrapWebSocket(ws: WebSocketLike, opts: WsBindingOptions): ProtectedWebSocket {
51
+ let seq = 0
52
+ const messageHandlers: Array<(payload: unknown, envelope: ProtocolEnvelope) => void> = []
53
+ const failHandlers: Array<(err: Error, rawData: string) => void> = []
54
+
55
+ function fireVerifyFail(err: Error, raw: string) {
56
+ opts.onVerifyFail?.(err, raw)
57
+ for (const h of failHandlers) h(err, raw)
58
+ }
59
+
60
+ // Listen for incoming frames
61
+ const messageListener = async (e: { data: string | Buffer }) => {
62
+ const raw = typeof e.data === 'string' ? e.data : e.data.toString('utf8')
63
+ let envelope: ProtocolEnvelope
64
+ try { envelope = JSON.parse(raw) as ProtocolEnvelope }
65
+ catch (err) { fireVerifyFail(new Error('malformed frame'), raw); return }
66
+
67
+ const sender = envelope?.header?.sender
68
+ if (!sender) { fireVerifyFail(new Error('missing sender'), raw); return }
69
+
70
+ const publicKey = await opts.keyRegistry.getPublicKey(sender)
71
+ if (!publicKey) { fireVerifyFail(new Error(`unknown sender: ${sender}`), raw); return }
72
+
73
+ const valid = await verifyEnvelopeEd25519(envelope, publicKey).catch(() => false)
74
+ if (!valid) { fireVerifyFail(new Error('invalid signature'), raw); return }
75
+
76
+ const diags = validateEnvelope(envelope)
77
+ if (diags.some(d => d.level === 'error')) {
78
+ fireVerifyFail(new Error(`envelope invalid: ${diags.map(d => d.message).join('; ')}`), raw)
79
+ return
80
+ }
81
+
82
+ let payload: unknown = envelope.body.content
83
+ try { payload = JSON.parse(envelope.body.content) } catch { /* keep as string */ }
84
+ for (const h of messageHandlers) h(payload, envelope)
85
+ }
86
+
87
+ ws.addEventListener('message', messageListener as any)
88
+ ws.addEventListener('close', () => {
89
+ ws.removeEventListener('message', messageListener as any)
90
+ })
91
+
92
+ return {
93
+ get seq() { return seq },
94
+ async send(payload: unknown): Promise<void> {
95
+ const content = typeof payload === 'string' ? payload : JSON.stringify(payload)
96
+ const envelope = createEnvelope({
97
+ sender: opts.sender,
98
+ recipient: opts.recipient,
99
+ ttlMs: opts.ttlMs ?? 30_000,
100
+ intent: 'TASK',
101
+ content,
102
+ })
103
+ // embed sequence number in correlationId to help downstream ordering
104
+ ;(envelope.body as any).correlationId = String(++seq)
105
+ const signed = await signEnvelopeEd25519(envelope, opts.privateKey)
106
+ ws.send(JSON.stringify(signed))
107
+ },
108
+ onMessage(handler) { messageHandlers.push(handler) },
109
+ onVerifyFail(handler) { failHandlers.push(handler) },
110
+ }
111
+ }
112
+
113
+ // ─────────────────── Stream signing over WebSocket ────────────────────────────
114
+
115
+ /**
116
+ * createSignedWebSocketStream
117
+ *
118
+ * Attaches a SignedStreamWriter to a WebSocket:
119
+ * - Each incoming WebSocket message is signed as a stream chunk and sent back.
120
+ * - On connection close, the final signed frame is sent.
121
+ *
122
+ * Returns the underlying SignedStreamWriter so callers can also call writeChunk
123
+ * directly (e.g. when driving the stream from application code rather than
124
+ * forwarding received messages).
125
+ */
126
+ export function createSignedWebSocketStream(
127
+ ws: WebSocketLike,
128
+ opts: StreamSignerOpts,
129
+ ): SignedStreamWriter {
130
+ const writer = new SignedStreamWriter(opts)
131
+
132
+ const messageListener = async (e: { data: string | Buffer }) => {
133
+ const data = typeof e.data === 'string' ? e.data : e.data.toString('utf8')
134
+ const chunk = await writer.writeChunk(data)
135
+ ws.send(encodeStreamChunk(chunk))
136
+ }
137
+
138
+ const closeListener = async () => {
139
+ ws.removeEventListener('message', messageListener as any)
140
+ ws.removeEventListener('close', closeListener as any)
141
+ const finalChunk = await writer.finalize()
142
+ ws.send(encodeStreamChunk(finalChunk))
143
+ }
144
+
145
+ ws.addEventListener('message', messageListener as any)
146
+ ws.addEventListener('close', closeListener as any)
147
+
148
+ return writer
149
+ }
150
+
151
+ /**
152
+ * receiveSignedWebSocketStream
153
+ *
154
+ * Listens for StreamChunk JSON frames on a WebSocket, accumulates them, and
155
+ * when the final frame (f=true) arrives calls verifyStream over the complete
156
+ * set of collected chunks.
157
+ *
158
+ * Resolves with StreamVerifyResult when the final frame is received.
159
+ * Rejects if a frame cannot be decoded.
160
+ */
161
+ export function receiveSignedWebSocketStream(
162
+ ws: WebSocketLike,
163
+ opts: StreamVerifierOpts,
164
+ ): Promise<StreamVerifyResult> {
165
+ return new Promise<StreamVerifyResult>((resolve, reject) => {
166
+ const collected: StreamChunk[] = []
167
+
168
+ const messageListener = async (e: { data: string | Buffer }) => {
169
+ const raw = typeof e.data === 'string' ? e.data : e.data.toString('utf8')
170
+ let chunk: StreamChunk
171
+ try {
172
+ chunk = decodeStreamChunk(raw)
173
+ } catch (err) {
174
+ cleanup()
175
+ reject(err)
176
+ return
177
+ }
178
+ collected.push(chunk)
179
+ if (chunk.f) {
180
+ cleanup()
181
+ const result = await verifyStream(collected, opts)
182
+ resolve(result)
183
+ }
184
+ }
185
+
186
+ const closeListener = () => {
187
+ // Connection closed before receiving final frame
188
+ cleanup()
189
+ resolve({ ok: false, reason: 'connection closed before final frame' })
190
+ }
191
+
192
+ function cleanup() {
193
+ ws.removeEventListener('message', messageListener as any)
194
+ ws.removeEventListener('close', closeListener as any)
195
+ }
196
+
197
+ ws.addEventListener('message', messageListener as any)
198
+ ws.addEventListener('close', closeListener as any)
199
+ })
200
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2023", "DOM"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "verbatimModuleSyntax": false
13
+ },
14
+ "include": ["src/**/*.ts", "scripts/**/*.ts"]
15
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "emitDeclarationOnly": true,
8
+ "declarationMap": false,
9
+ "sourceMap": false,
10
+ "outDir": "./dist/protocol",
11
+ "rootDir": "./src",
12
+ "stripInternal": true,
13
+ "strict": true,
14
+ "types": ["node"],
15
+ "skipLibCheck": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noUncheckedSideEffectImports": true
20
+ },
21
+ "include": ["src/**/*.ts"],
22
+ "exclude": ["src/**/*.test.ts", "src/**/*.bench.ts"]
23
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite'
2
+
3
+ export default defineConfig({
4
+ build: {
5
+ lib: {
6
+ entry: 'src/index.ts',
7
+ formats: ['es'],
8
+ fileName: () => 'index.js',
9
+ },
10
+ outDir: 'dist/protocol',
11
+ emptyOutDir: false,
12
+ rollupOptions: {
13
+ external: [],
14
+ },
15
+ },
16
+ })
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ exclude: [
6
+ 'sdk/pq/dist/**',
7
+ 'sdk/threshold/dist/**',
8
+ '**/node_modules/**',
9
+ ],
10
+ },
11
+ })
package/agentAdapter.d.ts DELETED
@@ -1,26 +0,0 @@
1
- import { type AgentReceiveResult, type AgentSessionConfig } from './protocolAgent';
2
- import { type IntentKind, type ProtocolEnvelope } from './protocol';
3
- import { type WireEnvelope, type WireFormat } from './protocolTransport';
4
- export interface AipAdapterConfig extends AgentSessionConfig {
5
- wireFormat?: WireFormat;
6
- }
7
- export declare class AipAgentAdapter {
8
- private readonly session;
9
- private readonly wireFormat;
10
- constructor(config: AipAdapterConfig);
11
- toRaw(envelope: ProtocolEnvelope): WireEnvelope;
12
- createRawIntent(input: {
13
- recipient?: string;
14
- intent: IntentKind;
15
- content: string;
16
- capability?: string;
17
- correlationId?: string;
18
- ttlMs?: number;
19
- messageId?: string;
20
- nonce?: string;
21
- nowMs?: number;
22
- }): Promise<WireEnvelope>;
23
- receiveRaw(raw: WireEnvelope, nowMs?: number): Promise<AgentReceiveResult>;
24
- handleRaw(raw: WireEnvelope, emitResponseRaw: (rawResponse: WireEnvelope, responseEnvelope: ProtocolEnvelope) => void | Promise<void>, nowMs?: number): Promise<AgentReceiveResult>;
25
- }
26
- export declare function createAipAgentAdapter(config: AipAdapterConfig): AipAgentAdapter;
@@ -1,20 +0,0 @@
1
- import type { ProtocolEnvelope } from './protocol';
2
- export interface ConformanceVector {
3
- id: string;
4
- secret: string;
5
- keyId: string;
6
- envelope: Omit<ProtocolEnvelope, 'signature'>;
7
- canonical: string;
8
- signature: string;
9
- }
10
- export interface Ed25519ConformanceVector {
11
- id: string;
12
- keyId: string;
13
- publicKey: string;
14
- privateKey: string;
15
- envelope: Omit<ProtocolEnvelope, 'signature'>;
16
- canonical: string;
17
- signature: string;
18
- }
19
- export declare const AIP_V01_CONFORMANCE_VECTORS: ConformanceVector[];
20
- export declare const AIP_V01_ED25519_CONFORMANCE_VECTORS: Ed25519ConformanceVector[];