@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
package/src/capability.ts DELETED
@@ -1,380 +0,0 @@
1
- import {
2
- generateEd25519KeypairBase64Url,
3
- signCanonicalPayloadEd25519,
4
- verifyCanonicalPayloadEd25519,
5
- } from './protocol'
6
- import { matchGlob } from './routePolicy'
7
-
8
- // Re-export for test convenience
9
- export { generateEd25519KeypairBase64Url }
10
-
11
- export interface CapabilityScope {
12
- pathGlob: string
13
- methods?: string[]
14
- maxDelegations?: number
15
- }
16
-
17
- export interface CapabilityToken {
18
- id: string
19
- version: '7h3-cap/1'
20
- issuer: string
21
- subject: string
22
- scopes: CapabilityScope[]
23
- issuedAt: number
24
- expiresAt: number
25
- delegationDepth: number
26
- parentTokenId?: string
27
- /** How many more times this token can be re-delegated. undefined = unlimited, 0 = no further delegation. */
28
- maxDelegations: number | undefined
29
- signature: string
30
- keyId: string
31
- }
32
-
33
- export type CapabilityVerifyResult =
34
- | { ok: true; token: CapabilityToken; chain: CapabilityToken[] }
35
- | { ok: false; reason: string }
36
-
37
- export const CAP_HEADER = 'x-7h3-capability'
38
-
39
- // ---------------------------------------------------------------------------
40
- // Canonicalization
41
- // ---------------------------------------------------------------------------
42
-
43
- function canonicalizeScope(scope: CapabilityScope): string {
44
- const parts: string[] = []
45
- if (scope.maxDelegations !== undefined) {
46
- parts.push(`"maxDelegations":${scope.maxDelegations}`)
47
- }
48
- if (scope.methods !== undefined) {
49
- parts.push(`"methods":${JSON.stringify(scope.methods)}`)
50
- }
51
- parts.push(`"pathGlob":${JSON.stringify(scope.pathGlob)}`)
52
- return `{${parts.join(',')}}`
53
- }
54
-
55
- /**
56
- * Deterministic canonical form for signing: alphabetically sorted top-level fields,
57
- * each scope also sorted. The `signature` field is excluded.
58
- */
59
- export function canonicalizeCapabilityToken(token: Omit<CapabilityToken, 'signature'>): string {
60
- const parts: string[] = []
61
-
62
- parts.push(`"delegationDepth":${token.delegationDepth}`)
63
- parts.push(`"expiresAt":${token.expiresAt}`)
64
- parts.push(`"id":${JSON.stringify(token.id)}`)
65
- parts.push(`"issuedAt":${token.issuedAt}`)
66
- parts.push(`"issuer":${JSON.stringify(token.issuer)}`)
67
- parts.push(`"keyId":${JSON.stringify(token.keyId)}`)
68
-
69
- if (token.maxDelegations !== undefined) {
70
- parts.push(`"maxDelegations":${token.maxDelegations}`)
71
- }
72
-
73
- if (token.parentTokenId !== undefined) {
74
- parts.push(`"parentTokenId":${JSON.stringify(token.parentTokenId)}`)
75
- }
76
-
77
- const scopesStr = `[${token.scopes.map(canonicalizeScope).join(',')}]`
78
- parts.push(`"scopes":${scopesStr}`)
79
-
80
- parts.push(`"subject":${JSON.stringify(token.subject)}`)
81
- parts.push(`"version":${JSON.stringify(token.version)}`)
82
-
83
- return `{${parts.join(',')}}`
84
- }
85
-
86
- // ---------------------------------------------------------------------------
87
- // Issue
88
- // ---------------------------------------------------------------------------
89
-
90
- export async function issueCapabilityToken(opts: {
91
- issuerPrivateKey: string
92
- issuerId: string
93
- subject: string
94
- scopes: CapabilityScope[]
95
- ttlMs: number
96
- maxDelegations?: number
97
- keyId?: string
98
- }): Promise<CapabilityToken> {
99
- const now = Date.now()
100
- const id = `cap-${now}-${Math.random().toString(36).slice(2, 12)}`
101
- const keyId = opts.keyId ?? `${opts.issuerId}-key`
102
- const maxDelegations = opts.maxDelegations ?? 0
103
-
104
- const unsigned: Omit<CapabilityToken, 'signature'> = {
105
- id,
106
- version: '7h3-cap/1',
107
- issuer: opts.issuerId,
108
- subject: opts.subject,
109
- scopes: opts.scopes,
110
- issuedAt: now,
111
- expiresAt: now + opts.ttlMs,
112
- delegationDepth: 0,
113
- maxDelegations,
114
- keyId,
115
- }
116
-
117
- const payload = canonicalizeCapabilityToken(unsigned)
118
- const signature = await signCanonicalPayloadEd25519(payload, opts.issuerPrivateKey)
119
-
120
- return { ...unsigned, signature }
121
- }
122
-
123
- // ---------------------------------------------------------------------------
124
- // Delegate
125
- // ---------------------------------------------------------------------------
126
-
127
- /**
128
- * Check whether childScope is a subset of parentScope.
129
- * - pathGlob: the parent must matchGlob the child's pathGlob literal (or the child's glob must be
130
- * at least as restrictive). We check that every path that matches child also matches parent by
131
- * testing the child pathGlob against the parent pathGlob pattern.
132
- * - methods: parent undefined = any (child is allowed anything); child undefined = any =
133
- * broader than a defined parent → reject.
134
- */
135
- function scopeIsSubset(child: CapabilityScope, parent: CapabilityScope): boolean {
136
- // Path: child path must be covered by parent path pattern
137
- if (!matchGlob(parent.pathGlob, child.pathGlob)) {
138
- // Also try: child glob should be at most as broad as parent
139
- // For glob subset we check that child's pathGlob matches under the parent glob pattern
140
- // This covers the common case of /api/payments/** under /api/**
141
- // We fall back: if parent.pathGlob doesn't match child.pathGlob directly,
142
- // test by checking child is a "narrower" glob — just use matchGlob both ways
143
- return false
144
- }
145
-
146
- // Methods: parent undefined = allows all; child must not exceed parent
147
- if (parent.methods !== undefined) {
148
- if (child.methods === undefined) {
149
- // child wants all methods, parent is restricted — not a subset
150
- return false
151
- }
152
- // Every child method must be in parent methods
153
- for (const m of child.methods) {
154
- if (!parent.methods.includes(m)) {
155
- return false
156
- }
157
- }
158
- }
159
- // parent.methods === undefined → parent allows all; child can be anything
160
-
161
- return true
162
- }
163
-
164
- function scopesAreSubset(childScopes: CapabilityScope[], parentScopes: CapabilityScope[]): boolean {
165
- // Every child scope must be covered by at least one parent scope
166
- for (const child of childScopes) {
167
- const covered = parentScopes.some((parent) => scopeIsSubset(child, parent))
168
- if (!covered) return false
169
- }
170
- return true
171
- }
172
-
173
- export async function delegateCapabilityToken(opts: {
174
- parentToken: CapabilityToken
175
- delegatorPrivateKey: string
176
- delegatorId: string
177
- newSubject: string
178
- scopes?: CapabilityScope[]
179
- ttlMs: number
180
- keyId?: string
181
- }): Promise<CapabilityToken> {
182
- const { parentToken, delegatorId, newSubject } = opts
183
-
184
- // Validate delegator is parent token's subject
185
- if (delegatorId !== parentToken.subject) {
186
- throw new Error(`delegatorId '${delegatorId}' does not match parentToken.subject '${parentToken.subject}'`)
187
- }
188
-
189
- // Validate parent not expired
190
- const now = Date.now()
191
- if (now >= parentToken.expiresAt) {
192
- throw new Error('Parent token is expired')
193
- }
194
-
195
- // Validate delegation is still allowed
196
- if (parentToken.maxDelegations !== undefined && parentToken.maxDelegations <= 0) {
197
- throw new Error('Parent token does not allow further delegation (maxDelegations=0)')
198
- }
199
-
200
- // Determine new scopes
201
- const newScopes = opts.scopes ?? parentToken.scopes
202
-
203
- // Validate new scopes are a subset of parent scopes
204
- if (!scopesAreSubset(newScopes, parentToken.scopes)) {
205
- throw new Error('Delegated scopes exceed parent token scopes')
206
- }
207
-
208
- // Validate TTL does not exceed parent remaining TTL
209
- const parentRemainingMs = parentToken.expiresAt - now
210
- if (opts.ttlMs > parentRemainingMs) {
211
- throw new Error(
212
- `Delegated ttlMs (${opts.ttlMs}) exceeds parent token remaining TTL (${parentRemainingMs})`,
213
- )
214
- }
215
-
216
- const id = `cap-${now}-${Math.random().toString(36).slice(2, 12)}`
217
- const keyId = opts.keyId ?? `${delegatorId}-key`
218
-
219
- // Compute new maxDelegations: decrement from parent (undefined = unlimited stays unlimited)
220
- const newMaxDelegations =
221
- parentToken.maxDelegations === undefined ? undefined : parentToken.maxDelegations - 1
222
-
223
- const unsigned: Omit<CapabilityToken, 'signature'> = {
224
- id,
225
- version: '7h3-cap/1',
226
- issuer: delegatorId,
227
- subject: newSubject,
228
- scopes: newScopes,
229
- issuedAt: now,
230
- expiresAt: now + opts.ttlMs,
231
- delegationDepth: parentToken.delegationDepth + 1,
232
- parentTokenId: parentToken.id,
233
- maxDelegations: newMaxDelegations,
234
- keyId,
235
- }
236
-
237
- const payload = canonicalizeCapabilityToken(unsigned)
238
- const signature = await signCanonicalPayloadEd25519(payload, opts.delegatorPrivateKey)
239
-
240
- return { ...unsigned, signature }
241
- }
242
-
243
- // ---------------------------------------------------------------------------
244
- // Verify single token
245
- // ---------------------------------------------------------------------------
246
-
247
- export async function verifyCapabilityToken(
248
- token: CapabilityToken,
249
- publicKey: string,
250
- opts?: { now?: number },
251
- ): Promise<boolean> {
252
- const now = opts?.now ?? Date.now()
253
-
254
- // Check expiry
255
- if (now >= token.expiresAt) {
256
- return false
257
- }
258
-
259
- // Reconstruct unsigned form
260
- const { signature, ...unsigned } = token
261
-
262
- const payload = canonicalizeCapabilityToken(unsigned)
263
- return verifyCanonicalPayloadEd25519(payload, signature, publicKey)
264
- }
265
-
266
- // ---------------------------------------------------------------------------
267
- // Verify chain
268
- // ---------------------------------------------------------------------------
269
-
270
- export async function verifyCapabilityChain(
271
- chain: CapabilityToken[],
272
- keyRegistry: { getPublicKey(id: string): Promise<string | null> },
273
- opts?: { requiredPathGlob?: string; requiredMethod?: string; now?: number },
274
- ): Promise<CapabilityVerifyResult> {
275
- if (chain.length === 0) {
276
- return { ok: false, reason: 'empty-chain' }
277
- }
278
-
279
- const now = opts?.now ?? Date.now()
280
-
281
- // Verify each token in the chain
282
- for (let i = 0; i < chain.length; i++) {
283
- const token = chain[i]
284
-
285
- // Structural check: root must have depth 0
286
- if (i === 0 && token.delegationDepth !== 0) {
287
- return { ok: false, reason: 'root-token-delegation-depth-must-be-zero' }
288
- }
289
-
290
- // Chain linkage: issuer of token[i] must be subject of token[i-1]
291
- if (i > 0) {
292
- const prev = chain[i - 1]
293
- if (token.issuer !== prev.subject) {
294
- return {
295
- ok: false,
296
- reason: `chain-broken: token[${i}].issuer '${token.issuer}' !== token[${i - 1}].subject '${prev.subject}'`,
297
- }
298
- }
299
- if (token.parentTokenId !== prev.id) {
300
- return {
301
- ok: false,
302
- reason: `chain-broken: token[${i}].parentTokenId '${token.parentTokenId}' !== token[${i - 1}].id '${prev.id}'`,
303
- }
304
- }
305
- if (token.delegationDepth !== prev.delegationDepth + 1) {
306
- return {
307
- ok: false,
308
- reason: `chain-broken: token[${i}].delegationDepth ${token.delegationDepth} !== ${prev.delegationDepth + 1}`,
309
- }
310
- }
311
- }
312
-
313
- // Expiry check
314
- if (now >= token.expiresAt) {
315
- return { ok: false, reason: `token[${i}]-expired` }
316
- }
317
-
318
- // Get public key for issuer
319
- const issuerPub = await keyRegistry.getPublicKey(token.issuer)
320
- if (!issuerPub) {
321
- return { ok: false, reason: `no-public-key-for-issuer:${token.issuer}` }
322
- }
323
-
324
- // Verify signature
325
- const { signature, ...unsigned } = token
326
- const payload = canonicalizeCapabilityToken(unsigned)
327
- const valid = await verifyCanonicalPayloadEd25519(payload, signature, issuerPub)
328
- if (!valid) {
329
- return { ok: false, reason: `invalid-signature-at-index:${i}` }
330
- }
331
- }
332
-
333
- const leaf = chain[chain.length - 1]
334
-
335
- // Check required scope
336
- if (opts?.requiredPathGlob !== undefined) {
337
- if (!tokenMatchesScope(leaf, opts.requiredPathGlob, opts.requiredMethod)) {
338
- return { ok: false, reason: 'leaf-token-does-not-cover-required-scope' }
339
- }
340
- }
341
-
342
- return { ok: true, token: leaf, chain }
343
- }
344
-
345
- // ---------------------------------------------------------------------------
346
- // Scope matching
347
- // ---------------------------------------------------------------------------
348
-
349
- export function tokenMatchesScope(token: CapabilityToken, pathGlob: string, method?: string): boolean {
350
- for (const scope of token.scopes) {
351
- // Check path: the scope's pathGlob must match the requested path
352
- if (!matchGlob(scope.pathGlob, pathGlob)) {
353
- continue
354
- }
355
-
356
- // Check method: if scope has no methods restriction, any method is allowed
357
- if (scope.methods === undefined) {
358
- return true
359
- }
360
- if (method === undefined) {
361
- return true
362
- }
363
- if (scope.methods.includes(method.toUpperCase())) {
364
- return true
365
- }
366
- }
367
- return false
368
- }
369
-
370
- // ---------------------------------------------------------------------------
371
- // Serialization
372
- // ---------------------------------------------------------------------------
373
-
374
- export function serializeCapabilityChain(chain: CapabilityToken[]): string {
375
- return JSON.stringify(chain)
376
- }
377
-
378
- export function parseCapabilityChain(serialized: string): CapabilityToken[] {
379
- return JSON.parse(serialized) as CapabilityToken[]
380
- }
@@ -1,263 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { encodeCbor, decodeCbor, CborEncoder, CborDecoder } from './cborCodec'
3
- import { encodeEnvelopeCbor, decodeEnvelopeCbor } from './envelopeCbor'
4
- import type { ProtocolEnvelope } from './protocol'
5
-
6
- // Helper: compare Uint8Arrays
7
- function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
8
- if (a.length !== b.length) return false
9
- for (let i = 0; i < a.length; i++) {
10
- if (a[i] !== b[i]) return false
11
- }
12
- return true
13
- }
14
-
15
- describe('CborCodec - round-trip', () => {
16
- it('null round-trips', () => {
17
- const encoded = encodeCbor(null)
18
- expect(decodeCbor(encoded)).toBe(null)
19
- })
20
-
21
- it('boolean false round-trips', () => {
22
- const encoded = encodeCbor(false)
23
- expect(decodeCbor(encoded)).toBe(false)
24
- })
25
-
26
- it('boolean true round-trips', () => {
27
- const encoded = encodeCbor(true)
28
- expect(decodeCbor(encoded)).toBe(true)
29
- })
30
-
31
- it('integer 0 round-trips', () => {
32
- const encoded = encodeCbor(0)
33
- expect(decodeCbor(encoded)).toBe(0)
34
- })
35
-
36
- it('integer 42 round-trips', () => {
37
- const encoded = encodeCbor(42)
38
- expect(decodeCbor(encoded)).toBe(42)
39
- })
40
-
41
- it('negative integer round-trips', () => {
42
- const encoded = encodeCbor(-100)
43
- expect(decodeCbor(encoded)).toBe(-100)
44
- })
45
-
46
- it('float round-trips', () => {
47
- const encoded = encodeCbor(3.14)
48
- const decoded = decodeCbor(encoded)
49
- expect(typeof decoded).toBe('number')
50
- expect(decoded as number).toBeCloseTo(3.14, 10)
51
- })
52
-
53
- it('string round-trips', () => {
54
- const encoded = encodeCbor('hello world')
55
- expect(decodeCbor(encoded)).toBe('hello world')
56
- })
57
-
58
- it('empty string round-trips', () => {
59
- const encoded = encodeCbor('')
60
- expect(decodeCbor(encoded)).toBe('')
61
- })
62
-
63
- it('array round-trips', () => {
64
- const arr = [1, 'two', true, null, 3.14]
65
- const encoded = encodeCbor(arr)
66
- const decoded = decodeCbor(encoded)
67
- expect(decoded).toEqual(arr)
68
- })
69
-
70
- it('nested object round-trips', () => {
71
- const obj = { a: 1, b: 'hello', c: [1, 2, 3], d: { e: true } }
72
- const encoded = encodeCbor(obj)
73
- const decoded = decodeCbor(encoded)
74
- expect(decoded).toEqual(obj)
75
- })
76
-
77
- it('Uint8Array round-trips', () => {
78
- const bytes = new Uint8Array([0x01, 0x02, 0x03, 0xff])
79
- const encoded = encodeCbor(bytes)
80
- const decoded = decodeCbor(encoded)
81
- expect(decoded).toBeInstanceOf(Uint8Array)
82
- expect(bytesEqual(decoded as Uint8Array, bytes)).toBe(true)
83
- })
84
- })
85
-
86
- describe('CborCodec - deterministic encoding', () => {
87
- it('encoding same object twice gives identical bytes', () => {
88
- const obj = { z: 3, a: 1, m: 2, nested: { x: 'hello' } }
89
- const enc1 = encodeCbor(obj)
90
- const enc2 = encodeCbor(obj)
91
- expect(bytesEqual(enc1, enc2)).toBe(true)
92
- })
93
-
94
- it('encoding independently with two encoder instances gives identical bytes', () => {
95
- const obj = { foo: 'bar', baz: 42 }
96
- const enc1 = new CborEncoder().encode(obj)
97
- const enc2 = new CborEncoder().encode(obj)
98
- expect(bytesEqual(enc1, enc2)).toBe(true)
99
- })
100
- })
101
-
102
- describe('CborCodec - key ordering', () => {
103
- it('map keys {z, a, m} are encoded in a < m < z byte order', () => {
104
- const obj = { z: 3, a: 1, m: 2 }
105
- const encoded = encodeCbor(obj)
106
- const decoded = decodeCbor(encoded) as Record<string, unknown>
107
- // Verify all keys present and correct values
108
- expect(decoded['a']).toBe(1)
109
- expect(decoded['m']).toBe(2)
110
- expect(decoded['z']).toBe(3)
111
-
112
- // Verify byte order: find the offset of each key char in encoded bytes
113
- // Key 'a' (0x61) must appear before 'm' (0x6d) which must appear before 'z' (0x7a)
114
- const textA = 0x61 // 'a'
115
- const textM = 0x6d // 'm'
116
- const textZ = 0x7a // 'z'
117
-
118
- let posA = -1, posM = -1, posZ = -1
119
- for (let i = 0; i < encoded.length; i++) {
120
- if (encoded[i] === textA && posA === -1) posA = i
121
- if (encoded[i] === textM && posM === -1) posM = i
122
- if (encoded[i] === textZ && posZ === -1) posZ = i
123
- }
124
- expect(posA).toBeGreaterThanOrEqual(0)
125
- expect(posM).toBeGreaterThan(posA)
126
- expect(posZ).toBeGreaterThan(posM)
127
- })
128
- })
129
-
130
- describe('CborCodec - integer encoding sizes', () => {
131
- it('0 encodes to 1 byte', () => {
132
- const encoded = encodeCbor(0)
133
- expect(encoded.length).toBe(1)
134
- expect(encoded[0]).toBe(0x00)
135
- })
136
-
137
- it('23 encodes to 1 byte', () => {
138
- const encoded = encodeCbor(23)
139
- expect(encoded.length).toBe(1)
140
- expect(encoded[0]).toBe(0x17) // 0<<5 | 23
141
- })
142
-
143
- it('24 encodes to 2 bytes', () => {
144
- const encoded = encodeCbor(24)
145
- expect(encoded.length).toBe(2)
146
- expect(encoded[0]).toBe(0x18) // 0<<5 | 24 (1-byte additional)
147
- expect(encoded[1]).toBe(24)
148
- })
149
-
150
- it('255 encodes to 2 bytes', () => {
151
- const encoded = encodeCbor(255)
152
- expect(encoded.length).toBe(2)
153
- expect(encoded[0]).toBe(0x18)
154
- expect(encoded[1]).toBe(255)
155
- })
156
-
157
- it('256 encodes to 3 bytes', () => {
158
- const encoded = encodeCbor(256)
159
- expect(encoded.length).toBe(3)
160
- expect(encoded[0]).toBe(0x19) // 0<<5 | 25 (2-byte additional)
161
- expect(encoded[1]).toBe(0x01)
162
- expect(encoded[2]).toBe(0x00)
163
- })
164
- })
165
-
166
- describe('envelopeCbor - round-trip', () => {
167
- const baseEnvelope: ProtocolEnvelope = {
168
- header: {
169
- version: '7h3/0.1',
170
- messageId: 'msg-1234567890-abc12345',
171
- timestampMs: 1700000000000,
172
- ttlMs: 60000,
173
- sender: 'agent:alice@example.com',
174
- nonce: 'abcdefghij',
175
- },
176
- body: {
177
- intent: 'TASK',
178
- content: 'Hello, agent!',
179
- },
180
- }
181
-
182
- it('round-trip preserves all required fields', () => {
183
- const encoded = encodeEnvelopeCbor(baseEnvelope)
184
- const decoded = decodeEnvelopeCbor(encoded)
185
-
186
- expect(decoded.header.version).toBe(baseEnvelope.header.version)
187
- expect(decoded.header.messageId).toBe(baseEnvelope.header.messageId)
188
- expect(decoded.header.timestampMs).toBe(baseEnvelope.header.timestampMs)
189
- expect(decoded.header.ttlMs).toBe(baseEnvelope.header.ttlMs)
190
- expect(decoded.header.sender).toBe(baseEnvelope.header.sender)
191
- expect(decoded.header.nonce).toBe(baseEnvelope.header.nonce)
192
- expect(decoded.body.intent).toBe(baseEnvelope.body.intent)
193
- expect(decoded.body.content).toBe(baseEnvelope.body.content)
194
- })
195
-
196
- it('round-trip preserves optional fields: recipient, capability, correlationId', () => {
197
- const full: ProtocolEnvelope = {
198
- ...baseEnvelope,
199
- header: { ...baseEnvelope.header, recipient: 'agent:bob@example.com' },
200
- body: { ...baseEnvelope.body, capability: 'file.read', correlationId: 'corr-xyz' },
201
- signature: {
202
- alg: 'HS256',
203
- keyId: 'my-key-id',
204
- value: 'base64urlencodedvalue',
205
- },
206
- }
207
-
208
- const encoded = encodeEnvelopeCbor(full)
209
- const decoded = decodeEnvelopeCbor(encoded)
210
-
211
- expect(decoded.header.recipient).toBe('agent:bob@example.com')
212
- expect(decoded.body.capability).toBe('file.read')
213
- expect(decoded.body.correlationId).toBe('corr-xyz')
214
- expect(decoded.signature?.alg).toBe('HS256')
215
- expect(decoded.signature?.keyId).toBe('my-key-id')
216
- expect(decoded.signature?.value).toBe('base64urlencodedvalue')
217
- })
218
-
219
- it('CBOR envelope is smaller than JSON equivalent (size comparison)', () => {
220
- const full: ProtocolEnvelope = {
221
- ...baseEnvelope,
222
- header: { ...baseEnvelope.header, recipient: 'agent:bob@example.com' },
223
- body: { ...baseEnvelope.body, capability: 'file.read', correlationId: 'corr-xyz' },
224
- signature: {
225
- alg: 'HS256',
226
- keyId: 'my-key-id',
227
- value: 'base64urlencodedvalue',
228
- },
229
- }
230
-
231
- const cborBytes = encodeEnvelopeCbor(full)
232
- const jsonBytes = new TextEncoder().encode(JSON.stringify(full))
233
-
234
- console.log(`CBOR size: ${cborBytes.length} bytes, JSON size: ${jsonBytes.length} bytes, ratio: ${(cborBytes.length / jsonBytes.length).toFixed(2)}`)
235
- expect(cborBytes.length).toBeLessThan(jsonBytes.length)
236
- })
237
-
238
- it('optional fields absent in input are absent in output', () => {
239
- // No recipient, capability, correlationId, signature
240
- const minimal: ProtocolEnvelope = {
241
- header: {
242
- version: '7h3/0.1',
243
- messageId: 'msg-min',
244
- timestampMs: 1700000000000,
245
- ttlMs: 30000,
246
- sender: 'agent:sender',
247
- nonce: 'nonce123',
248
- },
249
- body: {
250
- intent: 'PING',
251
- content: 'ping',
252
- },
253
- }
254
-
255
- const encoded = encodeEnvelopeCbor(minimal)
256
- const decoded = decodeEnvelopeCbor(encoded)
257
-
258
- expect(decoded.header.recipient).toBeUndefined()
259
- expect(decoded.body.capability).toBeUndefined()
260
- expect(decoded.body.correlationId).toBeUndefined()
261
- expect(decoded.signature).toBeUndefined()
262
- })
263
- })