@7h3/protocol 0.1.2 → 0.4.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 (230) 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 +92 -0
  8. package/CONTRIBUTING.md +82 -0
  9. package/Dockerfile +73 -0
  10. package/GOVERNANCE.md +62 -0
  11. package/README.md +323 -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 +385 -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.png +0 -0
  72. package/eslint.config.js +15 -0
  73. package/fuzz/ts/harness-decode.ts +136 -0
  74. package/fuzz/ts/harness-verify.ts +121 -0
  75. package/fuzz/ts/run.ts +35 -0
  76. package/mcp-server/README.md +38 -0
  77. package/mcp-server/package-lock.json +1187 -0
  78. package/mcp-server/package.json +35 -0
  79. package/mcp-server/src/index.ts +236 -0
  80. package/mcp-server/tsconfig.json +14 -0
  81. package/package.json +79 -13
  82. package/scripts/aip-framework-quickstart.ts +110 -0
  83. package/scripts/aip-mcp-gateway.ts +38 -0
  84. package/scripts/aip-mcp-wrap-demo.ts +72 -0
  85. package/scripts/aip-quickstart.ts +60 -0
  86. package/scripts/bench-diff.ts +118 -0
  87. package/scripts/bench-protocol-e2e.ts +937 -0
  88. package/scripts/bench-protocol-openloop.ts +1397 -0
  89. package/scripts/bench-replay-cache.ts +76 -0
  90. package/scripts/bench-signature-profiles.ts +180 -0
  91. package/scripts/bench-wire-codecs.ts +161 -0
  92. package/scripts/build-binary-conformance.ts +36 -0
  93. package/scripts/build-release-dashboard.ts +175 -0
  94. package/scripts/canary-rollout.ts +38 -0
  95. package/scripts/mcpGatewayCli.test.ts +116 -0
  96. package/scripts/prepare-aip-package.ts +88 -0
  97. package/scripts/regen-conformance-sigs.ts +18 -0
  98. package/scripts/release-gate.ts +19 -0
  99. package/scripts/validate-runtime-policy.ts +18 -0
  100. package/sdk/browser/index.test.ts +162 -0
  101. package/sdk/browser/index.ts +257 -0
  102. package/sdk/browser/package.json +13 -0
  103. package/sdk/go/go.mod +3 -0
  104. package/sdk/go/http.go +135 -0
  105. package/sdk/go/protocol.go +324 -0
  106. package/sdk/go/protocol_test.go +334 -0
  107. package/sdk/go/webhook.go +136 -0
  108. package/sdk/python/README.md +18 -0
  109. package/sdk/python/protocol_7h3/__init__.py +46 -0
  110. package/sdk/python/protocol_7h3/http.py +212 -0
  111. package/sdk/python/protocol_7h3/keys.py +149 -0
  112. package/sdk/python/protocol_7h3/protocol.py +525 -0
  113. package/sdk/python/protocol_7h3/queue.py +118 -0
  114. package/sdk/python/protocol_7h3/webhook.py +116 -0
  115. package/sdk/python/pyproject.toml +40 -0
  116. package/sdk/python/tests/test_conformance.py +110 -0
  117. package/sdk/python/tests/test_http.py +305 -0
  118. package/sdk/python/tests/test_keys.py +417 -0
  119. package/sdk/python/tests/test_queue.py +120 -0
  120. package/sdk/python/tests/test_webhook.py +345 -0
  121. package/sdk/rust/Cargo.lock +371 -0
  122. package/sdk/rust/Cargo.toml +25 -0
  123. package/sdk/rust/README.md +31 -0
  124. package/sdk/rust/fuzz/Cargo.toml +29 -0
  125. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
  126. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
  127. package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
  128. package/sdk/rust/src/http.rs +145 -0
  129. package/sdk/rust/src/keys.rs +161 -0
  130. package/sdk/rust/src/lib.rs +688 -0
  131. package/sdk/rust/src/queue.rs +79 -0
  132. package/sdk/rust/src/webhook.rs +86 -0
  133. package/sdk/rust/tests/conformance.rs +148 -0
  134. package/sdk/rust/tests/gateway.rs +130 -0
  135. package/sdk/rust/tests/http_webhook_queue.rs +201 -0
  136. package/sdk/rust/tests/keys.rs +189 -0
  137. package/src/agentAdapter.test.ts +48 -0
  138. package/src/agentAdapter.ts +56 -0
  139. package/src/auditLog.test.ts +145 -0
  140. package/src/auditLog.ts +147 -0
  141. package/src/conformance.test.ts +136 -0
  142. package/src/conformanceVectors.ts +99 -0
  143. package/src/frameworkAdapters.test.ts +290 -0
  144. package/src/frameworkAdapters.ts +261 -0
  145. package/src/gateway.test.ts +343 -0
  146. package/src/gateway.ts +171 -0
  147. package/src/grpcBinding.test.ts +211 -0
  148. package/src/grpcBinding.ts +103 -0
  149. package/src/httpBinding.test.ts +376 -0
  150. package/src/httpBinding.ts +163 -0
  151. package/src/index.ts +32 -0
  152. package/src/keyInfra.test.ts +278 -0
  153. package/src/keyInfra.ts +228 -0
  154. package/src/keyRegistry.ts +59 -0
  155. package/src/keyRotation.test.ts +78 -0
  156. package/src/keyRotation.ts +72 -0
  157. package/src/mcpGateway.test.ts +129 -0
  158. package/src/mcpGateway.ts +250 -0
  159. package/src/mcpTransports.test.ts +92 -0
  160. package/src/mcpTransports.ts +169 -0
  161. package/src/mcpWrapper.test.ts +179 -0
  162. package/src/mcpWrapper.ts +206 -0
  163. package/src/policyEnforcer.test.ts +99 -0
  164. package/src/policyEnforcer.ts +169 -0
  165. package/src/policyTelemetryFeedback.test.ts +25 -0
  166. package/src/policyTelemetryFeedback.ts +38 -0
  167. package/src/protocol.bench.ts +37 -0
  168. package/src/protocol.test.ts +155 -0
  169. package/src/protocol.ts +413 -0
  170. package/src/protocolAgent.test.ts +105 -0
  171. package/src/protocolAgent.ts +169 -0
  172. package/src/protocolBinary.test.ts +165 -0
  173. package/src/protocolBinary.ts +312 -0
  174. package/src/protocolCapabilities.ts +70 -0
  175. package/src/protocolFuzz.advanced.test.ts +235 -0
  176. package/src/protocolFuzz.test.ts +111 -0
  177. package/src/protocolNegative.test.ts +97 -0
  178. package/src/protocolReplay.test.ts +71 -0
  179. package/src/protocolReplay.ts +194 -0
  180. package/src/protocolTransport.test.ts +556 -0
  181. package/src/protocolTransport.ts +483 -0
  182. package/src/queueBinding.test.ts +130 -0
  183. package/src/queueBinding.ts +102 -0
  184. package/src/rateLimiter.test.ts +96 -0
  185. package/src/rateLimiter.ts +46 -0
  186. package/src/redisClient.ts +140 -0
  187. package/src/redisIntegration.test.ts +134 -0
  188. package/src/replayStores.test.ts +141 -0
  189. package/src/replayStores.ts +82 -0
  190. package/src/revocation.test.ts +98 -0
  191. package/src/revocation.ts +0 -0
  192. package/src/routePolicy.test.ts +87 -0
  193. package/src/routePolicy.ts +72 -0
  194. package/src/runtimePolicy.test.ts +49 -0
  195. package/src/runtimePolicy.ts +81 -0
  196. package/src/runtimePolicyManager.test.ts +29 -0
  197. package/src/runtimePolicyManager.ts +50 -0
  198. package/src/runtimePolicyPresets.ts +43 -0
  199. package/src/signedResponse.test.ts +111 -0
  200. package/src/signedResponse.ts +83 -0
  201. package/src/webhookBinding.test.ts +144 -0
  202. package/src/webhookBinding.ts +115 -0
  203. package/src/wsBinding.test.ts +221 -0
  204. package/src/wsBinding.ts +100 -0
  205. package/tsconfig.json +15 -0
  206. package/tsconfig.lib.json +23 -0
  207. package/vite.lib.config.ts +16 -0
  208. package/agentAdapter.d.ts +0 -26
  209. package/conformanceVectors.d.ts +0 -20
  210. package/frameworkAdapters.d.ts +0 -72
  211. package/index.d.ts +0 -20
  212. package/index.js +0 -1702
  213. package/keyRotation.d.ts +0 -20
  214. package/mcpGateway.d.ts +0 -37
  215. package/mcpTransports.d.ts +0 -62
  216. package/mcpWrapper.d.ts +0 -83
  217. package/policyEnforcer.d.ts +0 -50
  218. package/policyTelemetryFeedback.d.ts +0 -11
  219. package/protocol.d.ts +0 -66
  220. package/protocolAgent.d.ts +0 -58
  221. package/protocolBinary.d.ts +0 -8
  222. package/protocolCapabilities.d.ts +0 -24
  223. package/protocolReplay.d.ts +0 -35
  224. package/protocolTransport.d.ts +0 -73
  225. package/redisClient.d.ts +0 -49
  226. package/replayStores.d.ts +0 -32
  227. package/revocation.d.ts +0 -71
  228. package/runtimePolicy.d.ts +0 -24
  229. package/runtimePolicyManager.d.ts +0 -15
  230. package/runtimePolicyPresets.d.ts +0 -11
@@ -0,0 +1,116 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ interface JsonRpcEnvelope {
5
+ jsonrpc: '2.0'
6
+ id: string | number | null
7
+ result?: unknown
8
+ error?: {
9
+ code: number
10
+ message: string
11
+ }
12
+ }
13
+
14
+ function waitForLine(child: ChildProcessWithoutNullStreams, timeoutMs = 8_000): Promise<string> {
15
+ return new Promise((resolve, reject) => {
16
+ let buffer = ''
17
+ const timer = setTimeout(() => {
18
+ cleanup()
19
+ reject(new Error('Timed out waiting for gateway output'))
20
+ }, timeoutMs)
21
+
22
+ const onData = (chunk: Buffer) => {
23
+ buffer += chunk.toString('utf8')
24
+ const newlineIndex = buffer.indexOf('\n')
25
+ if (newlineIndex >= 0) {
26
+ const line = buffer.slice(0, newlineIndex).trim()
27
+ cleanup()
28
+ resolve(line)
29
+ }
30
+ }
31
+
32
+ const onError = (error: Error) => {
33
+ cleanup()
34
+ reject(error)
35
+ }
36
+
37
+ const onExit = () => {
38
+ cleanup()
39
+ reject(new Error('Gateway process exited before producing output'))
40
+ }
41
+
42
+ const cleanup = () => {
43
+ clearTimeout(timer)
44
+ child.stdout.off('data', onData)
45
+ child.off('error', onError)
46
+ child.off('exit', onExit)
47
+ }
48
+
49
+ child.stdout.on('data', onData)
50
+ child.on('error', onError)
51
+ child.on('exit', onExit)
52
+ })
53
+ }
54
+
55
+ function startGateway(extraEnv: Record<string, string> = {}): ChildProcessWithoutNullStreams {
56
+ const child = spawn('npm', ['run', '-s', 'aip:mcp:gateway'], {
57
+ cwd: process.cwd(),
58
+ env: {
59
+ ...process.env,
60
+ ...extraEnv,
61
+ },
62
+ stdio: ['pipe', 'pipe', 'pipe'],
63
+ })
64
+
65
+ child.stderr.on('data', () => {
66
+ // suppress stderr accumulation in test output
67
+ })
68
+
69
+ return child
70
+ }
71
+
72
+ async function stopGateway(child: ChildProcessWithoutNullStreams): Promise<void> {
73
+ if (child.killed) return
74
+ child.kill('SIGTERM')
75
+ await new Promise<void>((resolve) => {
76
+ child.once('exit', () => resolve())
77
+ setTimeout(() => resolve(), 2_000)
78
+ })
79
+ }
80
+
81
+ describe('AIP MCP gateway CLI', () => {
82
+ it('returns JSON-RPC result for allowed method', async () => {
83
+ const child = startGateway()
84
+ try {
85
+ child.stdin.write(
86
+ `${JSON.stringify({ jsonrpc: '2.0', id: 99, method: 'tools/call', params: { name: 'planner' } })}\n`,
87
+ )
88
+ const line = await waitForLine(child)
89
+ const response = JSON.parse(line) as JsonRpcEnvelope
90
+
91
+ expect(response.jsonrpc).toBe('2.0')
92
+ expect(response.id).toBe(99)
93
+ expect(response.error).toBeUndefined()
94
+ expect(response.result).toBeTruthy()
95
+ } finally {
96
+ await stopGateway(child)
97
+ }
98
+ })
99
+
100
+ it('returns method-not-found for disallowed method from env policy', async () => {
101
+ const child = startGateway({ AIP_ALLOWED_METHODS: 'tools/call' })
102
+ try {
103
+ child.stdin.write(
104
+ `${JSON.stringify({ jsonrpc: '2.0', id: 'abc', method: 'resources/read', params: { uri: 'file://x' } })}\n`,
105
+ )
106
+ const line = await waitForLine(child)
107
+ const response = JSON.parse(line) as JsonRpcEnvelope
108
+
109
+ expect(response.id).toBe('abc')
110
+ expect(response.result).toBeUndefined()
111
+ expect(response.error?.code).toBe(-32601)
112
+ } finally {
113
+ await stopGateway(child)
114
+ }
115
+ })
116
+ })
@@ -0,0 +1,88 @@
1
+ import { mkdir, readFile, writeFile, copyFile, readdir } from 'node:fs/promises'
2
+ import process from 'node:process'
3
+
4
+ interface RootPackageJson {
5
+ version?: string
6
+ }
7
+
8
+ function parseVersionArg(argv: string[]): string | undefined {
9
+ const idx = argv.indexOf('--version')
10
+ if (idx < 0) return undefined
11
+ return argv[idx + 1]
12
+ }
13
+
14
+ async function readRootVersion(): Promise<string> {
15
+ const raw = await readFile('package.json', 'utf8')
16
+ const parsed = JSON.parse(raw) as RootPackageJson
17
+ return parsed.version ?? '0.0.0'
18
+ }
19
+
20
+ async function main(): Promise<void> {
21
+ const version = parseVersionArg(process.argv) ?? (await readRootVersion())
22
+ const outDir = 'dist/npm-protocol'
23
+
24
+ await mkdir(outDir, { recursive: true })
25
+ await copyFile('dist/protocol/index.js', `${outDir}/index.js`)
26
+
27
+ // Copy all .d.ts files — index.d.ts re-exports from the individual module files,
28
+ // so all of them must be present for TypeScript consumers to resolve types.
29
+ const distFiles = await readdir('dist/protocol')
30
+ for (const file of distFiles) {
31
+ if (file.endsWith('.d.ts')) {
32
+ await copyFile(`dist/protocol/${file}`, `${outDir}/${file}`)
33
+ }
34
+ }
35
+
36
+ const packageJson = {
37
+ name: '@7h3/protocol',
38
+ version,
39
+ description: '7h3 Protocol: deterministic, signed, replay-safe AI-to-AI message envelopes (wire 7h3/0.1).',
40
+ type: 'module',
41
+ main: './index.js',
42
+ types: './index.d.ts',
43
+ exports: {
44
+ '.': {
45
+ import: './index.js',
46
+ types: './index.d.ts',
47
+ },
48
+ },
49
+ files: ['index.js', '*.d.ts', 'README.md'],
50
+ license: 'MIT',
51
+ repository: {
52
+ type: 'git',
53
+ url: 'https://github.com/IceMasterT/7h3-protocol-aip.git',
54
+ },
55
+ }
56
+
57
+ const readme = [
58
+ '# @7h3/protocol',
59
+ '',
60
+ '7h3 Protocol (wire version `7h3/0.1`):',
61
+ 'deterministic, signed, replay-safe AI-to-AI message envelopes.',
62
+ '',
63
+ 'Install:',
64
+ '',
65
+ '```bash',
66
+ 'npm install @7h3/protocol',
67
+ '```',
68
+ '',
69
+ 'Import:',
70
+ '',
71
+ '```ts',
72
+ "import { createAipAgentAdapter, receiveEnvelope } from '@7h3/protocol'",
73
+ '```',
74
+ '',
75
+ ].join('\n')
76
+
77
+ await writeFile(`${outDir}/package.json`, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
78
+ await writeFile(`${outDir}/README.md`, readme, 'utf8')
79
+
80
+ console.log(`Prepared publishable package in ${outDir}`)
81
+ console.log(`Version: ${version}`)
82
+ }
83
+
84
+ main().catch((error: unknown) => {
85
+ const message = error instanceof Error ? error.stack ?? error.message : String(error)
86
+ console.error(message)
87
+ process.exitCode = 1
88
+ })
@@ -0,0 +1,18 @@
1
+ import { signCanonicalPayloadHmac, signCanonicalPayloadEd25519 } from '../src/protocol'
2
+ import { AIP_V01_CONFORMANCE_VECTORS, AIP_V01_ED25519_CONFORMANCE_VECTORS } from '../src/conformanceVectors'
3
+
4
+ async function main() {
5
+ console.log('=== HMAC vectors ===')
6
+ for (const v of AIP_V01_CONFORMANCE_VECTORS) {
7
+ const sig = await signCanonicalPayloadHmac(v.canonical, v.secret)
8
+ console.log(`${v.id}: '${sig}'`)
9
+ }
10
+
11
+ console.log('\n=== Ed25519 vectors ===')
12
+ for (const v of AIP_V01_ED25519_CONFORMANCE_VECTORS) {
13
+ const sig = await signCanonicalPayloadEd25519(v.canonical, v.privateKey)
14
+ console.log(`${v.id}: '${sig}'`)
15
+ }
16
+ }
17
+
18
+ main().catch(e => { console.error(e); process.exit(1) })
@@ -0,0 +1,19 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ const commands: Array<[string, string[]]> = [
4
+ ['npm', ['run', 'policy:validate']],
5
+ ['npm', ['run', 'test']],
6
+ ['npm', ['run', 'build:protocol']],
7
+ ['npm', ['run', 'lint']],
8
+ ['npm', ['run', 'bench:wire:quick']],
9
+ ['npm', ['run', 'bench:openloop:adaptive:ci']],
10
+ ]
11
+
12
+ for (const [command, args] of commands) {
13
+ console.log(`\n$ ${command} ${args.join(' ')}`)
14
+ const result = spawnSync(command, args, { stdio: 'inherit', shell: false })
15
+ if (result.status !== 0) {
16
+ process.exitCode = result.status ?? 1
17
+ break
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { parseRuntimePolicyJson } from '../src/runtimePolicy'
3
+
4
+ async function main(): Promise<void> {
5
+ const path = process.argv[2] ?? 'AI_RUNTIME_POLICY.json'
6
+ const text = await readFile(path, 'utf8')
7
+ const policy = parseRuntimePolicyJson(text)
8
+ if (!policy.hard_invariants || !policy.slo_defaults || !policy.tuning) {
9
+ throw new Error('Runtime policy is missing one or more required sections: hard_invariants, slo_defaults, tuning')
10
+ }
11
+ console.log(`Runtime policy validated: ${policy.name}@${policy.version} (${policy.status})`)
12
+ }
13
+
14
+ main().catch((error: unknown) => {
15
+ const message = error instanceof Error ? error.stack ?? error.message : String(error)
16
+ console.error(message)
17
+ process.exitCode = 1
18
+ })
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ generateKeypair,
4
+ canonicalizeEnvelope,
5
+ createEnvelope,
6
+ signEnvelope,
7
+ verifyEnvelope,
8
+ signRequest,
9
+ isEnvelopeExpired,
10
+ ENVELOPE_HEADER,
11
+ WIRE_VERSION,
12
+ type BrowserEnvelopeHeader,
13
+ type BrowserEnvelopeBody,
14
+ } from './index.ts'
15
+
16
+ describe('Browser SDK', () => {
17
+ // Test 1: generateKeypair produces non-empty strings
18
+ it('generateKeypair produces non-empty strings', async () => {
19
+ const kp = await generateKeypair()
20
+ expect(typeof kp.publicKey).toBe('string')
21
+ expect(kp.publicKey.length).toBeGreaterThan(0)
22
+ expect(typeof kp.privateKey).toBe('string')
23
+ expect(kp.privateKey.length).toBeGreaterThan(0)
24
+ })
25
+
26
+ // Test 2: canonicalizeEnvelope matches exact conformance vector
27
+ it('canonicalizeEnvelope matches conformance vector', () => {
28
+ const header: BrowserEnvelopeHeader = {
29
+ version: '7h3/0.1',
30
+ messageId: 'vec-1',
31
+ nonce: 'nonce-vec-1',
32
+ sender: 'agent.alpha',
33
+ recipient: 'agent.beta',
34
+ timestampMs: 1712500000000,
35
+ ttlMs: 60000,
36
+ }
37
+ const body: BrowserEnvelopeBody = {
38
+ intent: 'TASK',
39
+ content: 'route:alpha->beta',
40
+ capability: 'task.plan',
41
+ correlationId: 'corr-1',
42
+ }
43
+ const expected =
44
+ '{"body":{"capability":"task.plan","content":"route:alpha->beta","correlationId":"corr-1","intent":"TASK"},"header":{"messageId":"vec-1","nonce":"nonce-vec-1","recipient":"agent.beta","sender":"agent.alpha","timestampMs":1712500000000,"ttlMs":60000,"version":"7h3/0.1"}}'
45
+ expect(canonicalizeEnvelope({ header, body })).toBe(expected)
46
+ })
47
+
48
+ // Test 3: canonicalizeEnvelope with no optional fields
49
+ it('canonicalizeEnvelope omits undefined optional fields', () => {
50
+ const header: BrowserEnvelopeHeader = {
51
+ version: '7h3/0.1',
52
+ messageId: 'msg-1',
53
+ nonce: 'n1',
54
+ sender: 'agent.x',
55
+ timestampMs: 1712500000000,
56
+ ttlMs: 30000,
57
+ }
58
+ const body: BrowserEnvelopeBody = {
59
+ intent: 'PING',
60
+ content: 'hello',
61
+ }
62
+ const result = canonicalizeEnvelope({ header, body })
63
+ const parsed = JSON.parse(result)
64
+ expect(parsed.header.recipient).toBeUndefined()
65
+ expect(parsed.body.capability).toBeUndefined()
66
+ expect(parsed.body.correlationId).toBeUndefined()
67
+ // Verify the raw string doesn't include those keys
68
+ expect(result).not.toContain('"recipient"')
69
+ expect(result).not.toContain('"capability"')
70
+ expect(result).not.toContain('"correlationId"')
71
+ })
72
+
73
+ // Test 4: createEnvelope produces correct structure
74
+ it('createEnvelope produces correct structure', () => {
75
+ const env = createEnvelope({
76
+ sender: 'agent.test',
77
+ body: { intent: 'TASK', content: 'do work' },
78
+ })
79
+ expect(env.header.version).toBe(WIRE_VERSION)
80
+ expect(typeof env.header.messageId).toBe('string')
81
+ expect(env.header.messageId.length).toBeGreaterThan(0)
82
+ expect(env.body.intent).toBe('TASK')
83
+ expect(env.header.sender).toBe('agent.test')
84
+ expect(env.header.ttlMs).toBe(60_000)
85
+ })
86
+
87
+ // Test 5: signEnvelope + verifyEnvelope round trip
88
+ it('signEnvelope + verifyEnvelope round trip', async () => {
89
+ const kp = await generateKeypair()
90
+ const env = createEnvelope({
91
+ sender: 'agent.a',
92
+ body: { intent: 'PING', content: 'test' },
93
+ })
94
+ const signed = await signEnvelope(env, kp.privateKey)
95
+ expect(signed.signature).toBeDefined()
96
+ expect(signed.signature?.alg).toBe('ED25519')
97
+ const valid = await verifyEnvelope(signed, kp.publicKey)
98
+ expect(valid).toBe(true)
99
+ })
100
+
101
+ // Test 6: verifyEnvelope: tampered body returns false
102
+ it('verifyEnvelope: tampered body returns false', async () => {
103
+ const kp = await generateKeypair()
104
+ const env = createEnvelope({
105
+ sender: 'agent.a',
106
+ body: { intent: 'PING', content: 'original' },
107
+ })
108
+ const signed = await signEnvelope(env, kp.privateKey)
109
+ const tampered = {
110
+ ...signed,
111
+ body: { ...signed.body, content: 'tampered' },
112
+ }
113
+ const valid = await verifyEnvelope(tampered, kp.publicKey)
114
+ expect(valid).toBe(false)
115
+ })
116
+
117
+ // Test 7: verifyEnvelope: wrong key returns false
118
+ it('verifyEnvelope: wrong key returns false', async () => {
119
+ const kp1 = await generateKeypair()
120
+ const kp2 = await generateKeypair()
121
+ const env = createEnvelope({
122
+ sender: 'agent.a',
123
+ body: { intent: 'PING', content: 'test' },
124
+ })
125
+ const signed = await signEnvelope(env, kp1.privateKey)
126
+ const valid = await verifyEnvelope(signed, kp2.publicKey)
127
+ expect(valid).toBe(false)
128
+ })
129
+
130
+ // Test 8: signRequest: produces Request with x-7h3-envelope header set
131
+ it('signRequest: produces Request with x-7h3-envelope header', async () => {
132
+ const kp = await generateKeypair()
133
+ const req = new Request('https://example.com/api')
134
+ const signed = await signRequest(req, {
135
+ sender: 'agent.client',
136
+ privateKey: kp.privateKey,
137
+ })
138
+ const headerVal = signed.headers.get(ENVELOPE_HEADER)
139
+ expect(headerVal).not.toBeNull()
140
+ expect(typeof headerVal).toBe('string')
141
+ const parsed = JSON.parse(headerVal!)
142
+ expect(parsed.signature).toBeDefined()
143
+ expect(parsed.header.sender).toBe('agent.client')
144
+ })
145
+
146
+ // Test 9: isEnvelopeExpired: returns true for past timestamp
147
+ it('isEnvelopeExpired: returns true for past timestamp', () => {
148
+ const past = Date.now() - 120_000
149
+ const env = {
150
+ header: {
151
+ version: '7h3/0.1' as const,
152
+ messageId: 'x',
153
+ nonce: 'y',
154
+ sender: 'agent.z',
155
+ timestampMs: past,
156
+ ttlMs: 60_000,
157
+ },
158
+ body: { intent: 'PING' as const, content: 'old' },
159
+ }
160
+ expect(isEnvelopeExpired(env)).toBe(true)
161
+ })
162
+ })
@@ -0,0 +1,257 @@
1
+ // 7h3 Protocol - Browser/Edge SDK
2
+ // Pure Web Crypto API. Zero dependencies.
3
+ // Works in browsers, Deno, Cloudflare Workers, Bun, and Node 20+.
4
+
5
+ export const WIRE_VERSION = '7h3/0.1'
6
+ export const ENVELOPE_HEADER = 'x-7h3-envelope'
7
+ export const RESPONSE_HEADER = 'x-7h3-response'
8
+
9
+ export type IntentKind = 'PING' | 'PONG' | 'CAPS' | 'TASK' | 'RESULT' | 'ERROR'
10
+
11
+ export interface BrowserEnvelopeHeader {
12
+ version: typeof WIRE_VERSION
13
+ messageId: string
14
+ timestampMs: number
15
+ ttlMs: number
16
+ sender: string
17
+ recipient?: string
18
+ nonce: string
19
+ }
20
+
21
+ export interface BrowserEnvelopeBody {
22
+ intent: IntentKind
23
+ content: string
24
+ capability?: string
25
+ correlationId?: string
26
+ }
27
+
28
+ export interface BrowserSignature {
29
+ alg: 'ED25519'
30
+ keyId: string
31
+ value: string
32
+ }
33
+
34
+ export interface BrowserEnvelope {
35
+ header: BrowserEnvelopeHeader
36
+ body: BrowserEnvelopeBody
37
+ signature?: BrowserSignature
38
+ }
39
+
40
+ export interface BrowserKeyPair {
41
+ publicKey: string // base64url SPKI, no padding
42
+ privateKey: string // base64url PKCS8, no padding
43
+ }
44
+
45
+ // ─── Internal helpers (NOT exported) ─────────────────────────────────────────
46
+
47
+ function toBase64Url(buf: ArrayBuffer | Uint8Array): string {
48
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
49
+ let binary = ''
50
+ for (let i = 0; i < bytes.length; i++) {
51
+ binary += String.fromCharCode(bytes[i])
52
+ }
53
+ return btoa(binary)
54
+ .replace(/\+/g, '-')
55
+ .replace(/\//g, '_')
56
+ .replace(/=+$/g, '')
57
+ }
58
+
59
+ function fromBase64Url(str: string): Uint8Array {
60
+ const padded = str
61
+ .replace(/-/g, '+')
62
+ .replace(/_/g, '/')
63
+ .padEnd(Math.ceil(str.length / 4) * 4, '=')
64
+ const binary = atob(padded)
65
+ const bytes = new Uint8Array(binary.length)
66
+ for (let i = 0; i < binary.length; i++) {
67
+ bytes[i] = binary.charCodeAt(i)
68
+ }
69
+ return bytes
70
+ }
71
+
72
+ function generateNonce(): string {
73
+ const bytes = new Uint8Array(16)
74
+ crypto.getRandomValues(bytes)
75
+ return toBase64Url(bytes)
76
+ }
77
+
78
+ function canonicalBody(b: BrowserEnvelopeBody): string {
79
+ const parts: string[] = []
80
+ if (b.capability !== undefined) {
81
+ parts.push(`"capability":${JSON.stringify(b.capability)}`)
82
+ }
83
+ parts.push(`"content":${JSON.stringify(b.content)}`)
84
+ if (b.correlationId !== undefined) {
85
+ parts.push(`"correlationId":${JSON.stringify(b.correlationId)}`)
86
+ }
87
+ parts.push(`"intent":${JSON.stringify(b.intent)}`)
88
+ return `{${parts.join(',')}}`
89
+ }
90
+
91
+ function canonicalHeader(h: BrowserEnvelopeHeader): string {
92
+ const parts: string[] = [
93
+ `"messageId":${JSON.stringify(h.messageId)}`,
94
+ `"nonce":${JSON.stringify(h.nonce)}`,
95
+ ]
96
+ if (h.recipient !== undefined) {
97
+ parts.push(`"recipient":${JSON.stringify(h.recipient)}`)
98
+ }
99
+ parts.push(`"sender":${JSON.stringify(h.sender)}`)
100
+ parts.push(`"timestampMs":${h.timestampMs}`)
101
+ parts.push(`"ttlMs":${h.ttlMs}`)
102
+ parts.push(`"version":${JSON.stringify(h.version)}`)
103
+ return `{${parts.join(',')}}`
104
+ }
105
+
106
+ // ─── Public API ───────────────────────────────────────────────────────────────
107
+
108
+ export function canonicalizeEnvelope(envelope: Omit<BrowserEnvelope, 'signature'>): string {
109
+ return `{"body":${canonicalBody(envelope.body)},"header":${canonicalHeader(envelope.header)}}`
110
+ }
111
+
112
+ export async function generateKeypair(): Promise<BrowserKeyPair> {
113
+ const pair = await crypto.subtle.generateKey(
114
+ { name: 'Ed25519' } as AlgorithmIdentifier,
115
+ true,
116
+ ['sign', 'verify'],
117
+ ) as CryptoKeyPair
118
+
119
+ const privateKeyRaw = await crypto.subtle.exportKey('pkcs8', pair.privateKey)
120
+ const publicKeyRaw = await crypto.subtle.exportKey('spki', pair.publicKey)
121
+
122
+ return {
123
+ publicKey: toBase64Url(publicKeyRaw),
124
+ privateKey: toBase64Url(privateKeyRaw),
125
+ }
126
+ }
127
+
128
+ export async function signEnvelope(
129
+ envelope: Omit<BrowserEnvelope, 'signature'>,
130
+ privateKeyPkcs8Base64Url: string,
131
+ keyId?: string,
132
+ ): Promise<BrowserEnvelope> {
133
+ const der = fromBase64Url(privateKeyPkcs8Base64Url)
134
+ const cryptoKey = await crypto.subtle.importKey(
135
+ 'pkcs8',
136
+ der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer,
137
+ { name: 'Ed25519' } as AlgorithmIdentifier,
138
+ false,
139
+ ['sign'],
140
+ )
141
+
142
+ const payload = canonicalizeEnvelope(envelope)
143
+ const sigBuffer = await crypto.subtle.sign(
144
+ 'Ed25519',
145
+ cryptoKey,
146
+ new TextEncoder().encode(payload),
147
+ )
148
+
149
+ return {
150
+ ...envelope,
151
+ signature: {
152
+ alg: 'ED25519',
153
+ keyId: keyId ?? generateNonce().slice(0, 16),
154
+ value: toBase64Url(sigBuffer),
155
+ },
156
+ }
157
+ }
158
+
159
+ export async function verifyEnvelope(
160
+ envelope: BrowserEnvelope,
161
+ publicKeySpkiBase64Url: string,
162
+ ): Promise<boolean> {
163
+ if (!envelope.signature) return false
164
+ if (envelope.signature.alg !== 'ED25519') return false
165
+
166
+ const der = fromBase64Url(publicKeySpkiBase64Url)
167
+ const cryptoKey = await crypto.subtle.importKey(
168
+ 'spki',
169
+ der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer,
170
+ { name: 'Ed25519' } as AlgorithmIdentifier,
171
+ false,
172
+ ['verify'],
173
+ )
174
+
175
+ const unsigned: Omit<BrowserEnvelope, 'signature'> = {
176
+ header: envelope.header,
177
+ body: envelope.body,
178
+ }
179
+ const payload = canonicalizeEnvelope(unsigned)
180
+ const sigBytes = fromBase64Url(envelope.signature.value)
181
+
182
+ return crypto.subtle.verify(
183
+ 'Ed25519',
184
+ cryptoKey,
185
+ sigBytes.buffer.slice(sigBytes.byteOffset, sigBytes.byteOffset + sigBytes.byteLength) as ArrayBuffer,
186
+ new TextEncoder().encode(payload),
187
+ )
188
+ }
189
+
190
+ export function createEnvelope(opts: {
191
+ sender: string
192
+ recipient?: string
193
+ ttlMs?: number
194
+ body: BrowserEnvelopeBody
195
+ }): Omit<BrowserEnvelope, 'signature'> {
196
+ return {
197
+ header: {
198
+ version: WIRE_VERSION,
199
+ messageId: generateNonce(),
200
+ timestampMs: Date.now(),
201
+ ttlMs: opts.ttlMs ?? 60_000,
202
+ sender: opts.sender,
203
+ recipient: opts.recipient,
204
+ nonce: generateNonce(),
205
+ },
206
+ body: opts.body,
207
+ }
208
+ }
209
+
210
+ export async function signRequest(
211
+ request: Request,
212
+ opts: {
213
+ sender: string
214
+ recipient?: string
215
+ ttlMs?: number
216
+ privateKey: string
217
+ keyId?: string
218
+ },
219
+ ): Promise<Request> {
220
+ const envelope = createEnvelope({
221
+ sender: opts.sender,
222
+ recipient: opts.recipient,
223
+ ttlMs: opts.ttlMs,
224
+ body: {
225
+ intent: 'TASK',
226
+ content: request.url,
227
+ },
228
+ })
229
+
230
+ const signed = await signEnvelope(envelope, opts.privateKey, opts.keyId)
231
+
232
+ const headers = new Headers(request.headers)
233
+ headers.set(ENVELOPE_HEADER, JSON.stringify(signed))
234
+
235
+ return new Request(request, { headers })
236
+ }
237
+
238
+ export async function verifyResponseHeader(
239
+ response: Response,
240
+ publicKey: string,
241
+ ): Promise<boolean> {
242
+ const headerValue = response.headers.get(RESPONSE_HEADER)
243
+ if (!headerValue) return false
244
+
245
+ let parsed: BrowserEnvelope
246
+ try {
247
+ parsed = JSON.parse(headerValue) as BrowserEnvelope
248
+ } catch {
249
+ return false
250
+ }
251
+
252
+ return verifyEnvelope(parsed, publicKey)
253
+ }
254
+
255
+ export function isEnvelopeExpired(envelope: BrowserEnvelope): boolean {
256
+ return envelope.header.timestampMs + envelope.header.ttlMs < Date.now()
257
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@7h3/protocol-browser",
3
+ "version": "0.4.0",
4
+ "description": "7h3 Protocol - Browser/Edge SDK. Pure Web Crypto API. Zero dependencies.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "module": "./index.js",
8
+ "types": "./index.d.ts",
9
+ "exports": { ".": { "types": "./index.d.ts", "import": "./index.js" } },
10
+ "keywords": ["7h3","protocol","browser","webcrypto","ed25519","signing","mcp"],
11
+ "license": "MIT",
12
+ "sideEffects": false
13
+ }
package/sdk/go/go.mod ADDED
@@ -0,0 +1,3 @@
1
+ module github.com/IceMasterT/7h3-protocol/sdk/go
2
+
3
+ go 1.21