@suveren/gateway 0.7.4 → 0.8.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 (264) hide show
  1. package/content/integrations/mollie.json +103 -28
  2. package/dist/control-plane/index.mjs +248 -58
  3. package/dist/mcp-server/http.mjs +577 -124
  4. package/dist/ui/assets/index-BFnSVa9N.js +110 -0
  5. package/dist/ui/assets/{index-CkWaRck1.css → index-BYVb_0oG.css} +1 -1
  6. package/dist/ui/index.html +2 -2
  7. package/node_modules/@hap/core/dist/index.d.mts +285 -13
  8. package/node_modules/@hap/core/dist/index.d.ts +285 -13
  9. package/node_modules/@hap/core/dist/index.js +281 -13
  10. package/node_modules/@hap/core/dist/index.mjs +266 -12
  11. package/node_modules/@hap/core/package.json +4 -2
  12. package/node_modules/@hap/core/src/did-key.ts +126 -0
  13. package/node_modules/@hap/core/src/frame.ts +115 -14
  14. package/node_modules/@hap/core/src/gatekeeper.ts +26 -0
  15. package/node_modules/@hap/core/src/identity.ts +21 -5
  16. package/node_modules/@hap/core/src/index.ts +3 -0
  17. package/node_modules/@hap/core/src/mandate.ts +207 -0
  18. package/node_modules/@hap/core/src/receipt.ts +74 -0
  19. package/node_modules/@hap/core/src/types.ts +87 -5
  20. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/accept.js +54 -0
  21. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/charset.js +10 -34
  22. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/encoding.js +9 -30
  23. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/language.js +15 -36
  24. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/mediaType.js +16 -134
  25. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/LICENSE +22 -0
  26. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/README.md +71 -0
  27. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.d.ts +46 -0
  28. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js +176 -0
  29. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js.map +1 -0
  30. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/package.json +52 -0
  31. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/package.json +16 -10
  32. package/node_modules/@types/node/README.md +1 -1
  33. package/node_modules/@types/node/buffer.buffer.d.ts +10 -5
  34. package/node_modules/@types/node/crypto.d.ts +2 -4
  35. package/node_modules/@types/node/dgram.d.ts +119 -4
  36. package/node_modules/@types/node/ffi.d.ts +1 -1
  37. package/node_modules/@types/node/fs/promises.d.ts +61 -65
  38. package/node_modules/@types/node/fs.d.ts +86 -76
  39. package/node_modules/@types/node/http.d.ts +17 -1
  40. package/node_modules/@types/node/http2.d.ts +12 -4
  41. package/node_modules/@types/node/index.d.ts +1 -0
  42. package/node_modules/@types/node/inspector.d.ts +2 -1
  43. package/node_modules/@types/node/net.d.ts +108 -16
  44. package/node_modules/@types/node/package.json +2 -2
  45. package/node_modules/@types/node/process.d.ts +52 -0
  46. package/node_modules/@types/node/quic.d.ts +246 -38
  47. package/node_modules/@types/node/sqlite.d.ts +6 -2
  48. package/node_modules/@types/node/stream/iter.d.ts +4 -4
  49. package/node_modules/@types/node/test.d.ts +36 -0
  50. package/node_modules/@types/node/tls.d.ts +24 -0
  51. package/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +5 -0
  52. package/node_modules/@types/node/ts5.6/index.d.ts +1 -0
  53. package/node_modules/@types/node/ts5.7/index.d.ts +1 -0
  54. package/node_modules/@types/node/tty.d.ts +4 -3
  55. package/node_modules/@types/node/vfs.d.ts +210 -0
  56. package/node_modules/@types/node/vm.d.ts +1 -1
  57. package/node_modules/express-rate-limit/dist/index.cjs +6 -3
  58. package/node_modules/express-rate-limit/dist/index.d.cts +11 -0
  59. package/node_modules/express-rate-limit/dist/index.d.mts +11 -0
  60. package/node_modules/express-rate-limit/dist/index.d.ts +11 -0
  61. package/node_modules/express-rate-limit/dist/index.mjs +6 -3
  62. package/node_modules/express-rate-limit/{license.md → license} +7 -6
  63. package/node_modules/express-rate-limit/package.json +5 -5
  64. package/node_modules/express-rate-limit/readme.md +1 -0
  65. package/node_modules/fast-uri/index.js +215 -47
  66. package/node_modules/fast-uri/lib/schemes.js +9 -4
  67. package/node_modules/fast-uri/lib/utils.js +396 -92
  68. package/node_modules/fast-uri/package.json +1 -1
  69. package/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
  70. package/node_modules/fast-uri/test/equal.test.js +31 -3
  71. package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
  72. package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  73. package/node_modules/fast-uri/test/ipv6-validation.test.js +124 -0
  74. package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  75. package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  76. package/node_modules/fast-uri/test/parse.test.js +7 -3
  77. package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
  78. package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  79. package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
  80. package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  81. package/node_modules/fast-uri/test/security.test.js +75 -3
  82. package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  83. package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  84. package/node_modules/hono/dist/cjs/client/client.js +25 -11
  85. package/node_modules/hono/dist/cjs/client/utils.js +4 -1
  86. package/node_modules/hono/dist/cjs/context.js +4 -0
  87. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
  88. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +1 -1
  89. package/node_modules/hono/dist/cjs/helper/ssg/utils.js +30 -10
  90. package/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
  91. package/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
  92. package/node_modules/hono/dist/cjs/middleware/cors/index.js +1 -1
  93. package/node_modules/hono/dist/cjs/middleware/csrf/index.js +1 -1
  94. package/node_modules/hono/dist/cjs/middleware/etag/digest.js +1 -1
  95. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -3
  96. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +3 -1
  97. package/node_modules/hono/dist/cjs/request.js +8 -4
  98. package/node_modules/hono/dist/cjs/router/linear-router/router.js +7 -2
  99. package/node_modules/hono/dist/cjs/router/pattern-router/router.js +3 -9
  100. package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +10 -3
  101. package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +47 -83
  102. package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +2 -1
  103. package/node_modules/hono/dist/cjs/router/trie-router/node.js +46 -69
  104. package/node_modules/hono/dist/cjs/router/trie-router/router.js +3 -11
  105. package/node_modules/hono/dist/cjs/router/utils.js +27 -0
  106. package/node_modules/hono/dist/cjs/utils/body.js +15 -3
  107. package/node_modules/hono/dist/cjs/utils/cookie.js +1 -1
  108. package/node_modules/hono/dist/cjs/utils/ipaddr.js +5 -3
  109. package/node_modules/hono/dist/cjs/utils/stream.js +7 -1
  110. package/node_modules/hono/dist/cjs/utils/url.js +9 -1
  111. package/node_modules/hono/dist/client/client.js +25 -11
  112. package/node_modules/hono/dist/client/utils.js +4 -1
  113. package/node_modules/hono/dist/context.js +4 -0
  114. package/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
  115. package/node_modules/hono/dist/helper/ssg/ssg.js +1 -1
  116. package/node_modules/hono/dist/helper/ssg/utils.js +30 -10
  117. package/node_modules/hono/dist/jsx/dom/render.js +2 -0
  118. package/node_modules/hono/dist/middleware/cache/index.js +1 -1
  119. package/node_modules/hono/dist/middleware/cors/index.js +1 -1
  120. package/node_modules/hono/dist/middleware/csrf/index.js +1 -1
  121. package/node_modules/hono/dist/middleware/etag/digest.js +1 -1
  122. package/node_modules/hono/dist/middleware/etag/index.js +3 -3
  123. package/node_modules/hono/dist/middleware/pretty-json/index.js +3 -1
  124. package/node_modules/hono/dist/request.js +8 -4
  125. package/node_modules/hono/dist/router/linear-router/router.js +7 -2
  126. package/node_modules/hono/dist/router/pattern-router/router.js +3 -9
  127. package/node_modules/hono/dist/router/reg-exp-router/node.js +6 -2
  128. package/node_modules/hono/dist/router/reg-exp-router/router.js +53 -84
  129. package/node_modules/hono/dist/router/reg-exp-router/trie.js +2 -1
  130. package/node_modules/hono/dist/router/trie-router/node.js +46 -69
  131. package/node_modules/hono/dist/router/trie-router/router.js +3 -11
  132. package/node_modules/hono/dist/router/utils.js +5 -0
  133. package/node_modules/hono/dist/types/context.d.ts +4 -0
  134. package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +3 -0
  135. package/node_modules/hono/dist/types/router/trie-router/node.d.ts +1 -2
  136. package/node_modules/hono/dist/types/router/trie-router/router.d.ts +0 -1
  137. package/node_modules/hono/dist/types/router/utils.d.ts +1 -0
  138. package/node_modules/hono/dist/types/utils/url.d.ts +4 -0
  139. package/node_modules/hono/dist/utils/body.js +15 -3
  140. package/node_modules/hono/dist/utils/cookie.js +1 -1
  141. package/node_modules/hono/dist/utils/ipaddr.js +5 -3
  142. package/node_modules/hono/dist/utils/stream.js +7 -1
  143. package/node_modules/hono/dist/utils/url.js +9 -1
  144. package/node_modules/hono/package.json +1 -1
  145. package/node_modules/ip-address/README.md +92 -82
  146. package/node_modules/ip-address/dist/common.d.ts +20 -0
  147. package/node_modules/ip-address/dist/common.js +36 -0
  148. package/node_modules/ip-address/dist/common.js.map +1 -1
  149. package/node_modules/ip-address/dist/ipv4.d.ts +50 -0
  150. package/node_modules/ip-address/dist/ipv4.js +75 -0
  151. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  152. package/node_modules/ip-address/dist/ipv6.d.ts +79 -10
  153. package/node_modules/ip-address/dist/ipv6.js +120 -30
  154. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  155. package/node_modules/ip-address/dist/v4/constants.d.ts +12 -0
  156. package/node_modules/ip-address/dist/v4/constants.js +40 -1
  157. package/node_modules/ip-address/dist/v4/constants.js.map +1 -1
  158. package/node_modules/ip-address/dist/v6/constants.d.ts +14 -0
  159. package/node_modules/ip-address/dist/v6/constants.js +47 -1
  160. package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
  161. package/node_modules/ip-address/package.json +1 -1
  162. package/node_modules/is-plain-object/README.md +39 -95
  163. package/node_modules/is-plain-object/dist/is-plain-object.js +0 -2
  164. package/node_modules/is-plain-object/is-plain-object.d.ts +6 -1
  165. package/node_modules/is-plain-object/package.json +8 -34
  166. package/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts +7 -16
  167. package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +10 -20
  168. package/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts +7 -16
  169. package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +16 -38
  170. package/node_modules/jose/dist/types/jwe/general/decrypt.d.ts +7 -16
  171. package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +19 -38
  172. package/node_modules/jose/dist/types/jwk/embedded.d.ts +3 -8
  173. package/node_modules/jose/dist/types/jwk/thumbprint.d.ts +5 -5
  174. package/node_modules/jose/dist/types/jwks/local.d.ts +6 -10
  175. package/node_modules/jose/dist/types/jwks/remote.d.ts +17 -28
  176. package/node_modules/jose/dist/types/jws/compact/sign.d.ts +5 -10
  177. package/node_modules/jose/dist/types/jws/compact/verify.d.ts +9 -18
  178. package/node_modules/jose/dist/types/jws/flattened/sign.d.ts +7 -16
  179. package/node_modules/jose/dist/types/jws/flattened/verify.d.ts +9 -18
  180. package/node_modules/jose/dist/types/jws/general/sign.d.ts +11 -21
  181. package/node_modules/jose/dist/types/jws/general/verify.d.ts +12 -21
  182. package/node_modules/jose/dist/types/jwt/decrypt.d.ts +10 -20
  183. package/node_modules/jose/dist/types/jwt/encrypt.d.ts +17 -29
  184. package/node_modules/jose/dist/types/jwt/sign.d.ts +12 -19
  185. package/node_modules/jose/dist/types/jwt/unsecured.d.ts +17 -28
  186. package/node_modules/jose/dist/types/jwt/verify.d.ts +9 -19
  187. package/node_modules/jose/dist/types/key/export.d.ts +4 -4
  188. package/node_modules/jose/dist/types/key/generate_key_pair.d.ts +6 -8
  189. package/node_modules/jose/dist/types/key/generate_secret.d.ts +4 -11
  190. package/node_modules/jose/dist/types/key/import.d.ts +10 -17
  191. package/node_modules/jose/dist/types/types.d.ts +48 -86
  192. package/node_modules/jose/dist/types/util/base64url.d.ts +2 -2
  193. package/node_modules/jose/dist/types/util/decode_jwt.d.ts +3 -10
  194. package/node_modules/jose/dist/types/util/decode_protected_header.d.ts +4 -4
  195. package/node_modules/jose/dist/types/util/errors.d.ts +26 -53
  196. package/node_modules/jose/dist/webapi/index.js +70 -32
  197. package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +7 -8
  198. package/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +42 -26
  199. package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +14 -10
  200. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +53 -71
  201. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +38 -49
  202. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +121 -158
  203. package/node_modules/jose/dist/webapi/jwk/embedded.js +30 -18
  204. package/node_modules/jose/dist/webapi/jwk/thumbprint.js +54 -64
  205. package/node_modules/jose/dist/webapi/jwks/local.js +49 -74
  206. package/node_modules/jose/dist/webapi/jwks/remote.js +91 -160
  207. package/node_modules/jose/dist/webapi/jws/compact/sign.js +21 -20
  208. package/node_modules/jose/dist/webapi/jws/compact/verify.js +7 -8
  209. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +29 -30
  210. package/node_modules/jose/dist/webapi/jws/flattened/verify.js +21 -23
  211. package/node_modules/jose/dist/webapi/jws/general/sign.js +61 -70
  212. package/node_modules/jose/dist/webapi/jws/general/verify.js +47 -41
  213. package/node_modules/jose/dist/webapi/jwt/decrypt.js +13 -22
  214. package/node_modules/jose/dist/webapi/jwt/encrypt.js +58 -100
  215. package/node_modules/jose/dist/webapi/jwt/sign.js +18 -50
  216. package/node_modules/jose/dist/webapi/jwt/unsecured.js +34 -63
  217. package/node_modules/jose/dist/webapi/jwt/verify.js +12 -14
  218. package/node_modules/jose/dist/webapi/key/export.js +32 -37
  219. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +40 -44
  220. package/node_modules/jose/dist/webapi/key/generate_secret.js +35 -39
  221. package/node_modules/jose/dist/webapi/key/import.js +52 -52
  222. package/node_modules/jose/dist/webapi/lib/asn1.js +130 -164
  223. package/node_modules/jose/dist/webapi/lib/base64.js +18 -20
  224. package/node_modules/jose/dist/webapi/lib/buffer_utils.js +34 -38
  225. package/node_modules/jose/dist/webapi/lib/content_encryption.js +79 -123
  226. package/node_modules/jose/dist/webapi/lib/crypto_key.js +24 -25
  227. package/node_modules/jose/dist/webapi/lib/deflate.js +46 -40
  228. package/node_modules/jose/dist/webapi/lib/helpers.js +40 -37
  229. package/node_modules/jose/dist/webapi/lib/invalid_key_input.js +10 -24
  230. package/node_modules/jose/dist/webapi/lib/is_key_like.js +17 -15
  231. package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +106 -93
  232. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +210 -176
  233. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +105 -107
  234. package/node_modules/jose/dist/webapi/lib/jwk_metadata.js +19 -0
  235. package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +9 -16
  236. package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +62 -61
  237. package/node_modules/jose/dist/webapi/lib/jws_sign.js +58 -72
  238. package/node_modules/jose/dist/webapi/lib/jws_verify.js +90 -103
  239. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +134 -185
  240. package/node_modules/jose/dist/webapi/lib/key.js +84 -121
  241. package/node_modules/jose/dist/webapi/lib/key_algorithm.js +13 -8
  242. package/node_modules/jose/dist/webapi/lib/key_descriptor.js +8 -6
  243. package/node_modules/jose/dist/webapi/lib/key_management.js +200 -225
  244. package/node_modules/jose/dist/webapi/lib/key_options.js +8 -0
  245. package/node_modules/jose/dist/webapi/lib/options.js +57 -44
  246. package/node_modules/jose/dist/webapi/lib/signing.js +18 -22
  247. package/node_modules/jose/dist/webapi/lib/type_checks.js +28 -33
  248. package/node_modules/jose/dist/webapi/util/base64url.js +26 -35
  249. package/node_modules/jose/dist/webapi/util/decode_jwt.js +32 -31
  250. package/node_modules/jose/dist/webapi/util/decode_protected_header.js +18 -23
  251. package/node_modules/jose/dist/webapi/util/errors.js +94 -84
  252. package/node_modules/jose/package.json +17 -81
  253. package/package.json +2 -2
  254. package/profiles/calendar/0.5.profile.json +190 -0
  255. package/profiles/charge/0.5.profile.json +210 -0
  256. package/profiles/customers/0.7.profile.json +157 -0
  257. package/profiles/deploy/0.9.profile.json +176 -0
  258. package/profiles/email/0.6.profile.json +208 -0
  259. package/profiles/index.json +9 -1
  260. package/profiles/publish/0.5.profile.json +186 -0
  261. package/profiles/purchase/0.5.profile.json +231 -0
  262. package/profiles/records/0.5.profile.json +117 -0
  263. package/dist/ui/assets/index-0Q2eNytX.js +0 -110
  264. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/HISTORY.md +0 -114
@@ -151,9 +151,9 @@ function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
151
151
  }
152
152
 
153
153
  // src/identity.ts
154
- function validateSubject(subject) {
154
+ function validateSubject(subject, opts) {
155
155
  const errors = [];
156
- const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
156
+ const { did, assurance, method, trust_root, verifier, disclose } = subject;
157
157
  if (!did) errors.push("subject.did is required");
158
158
  if (assurance === "low") {
159
159
  if (method !== "self_declared") errors.push(`low assurance requires method "self_declared", got "${method}"`);
@@ -166,7 +166,10 @@ function validateSubject(subject) {
166
166
  } else if (method === "eudi") {
167
167
  if (trust_root !== "external") errors.push('eudi requires trust_root "external"');
168
168
  if (!verifier) errors.push("eudi requires a verifier");
169
- if (!owner_signature) errors.push("eudi requires an owner_signature");
169
+ if (opts?.ownerMandates !== void 0) {
170
+ const entry = opts.ownerMandates.find((m) => m.did === did && m.binding === "eudi");
171
+ if (!entry) errors.push('eudi requires an owner_mandates entry with binding "eudi" for this DID');
172
+ }
170
173
  } else {
171
174
  errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
172
175
  }
@@ -284,15 +287,51 @@ function validateContextParams(params, profile) {
284
287
  }
285
288
  return { valid: errors.length === 0, errors };
286
289
  }
290
+ var CanonicalValueError = class extends Error {
291
+ code;
292
+ field;
293
+ constructor(code, field, message) {
294
+ super(message);
295
+ this.name = "CanonicalValueError";
296
+ this.code = code;
297
+ this.field = field;
298
+ }
299
+ };
300
+ function percentEncodeCanonicalValue(raw) {
301
+ const bytes = new TextEncoder().encode(raw);
302
+ let out = "";
303
+ for (const b of bytes) {
304
+ if (b === 61 || b === 37 || b < 32 || b > 126) {
305
+ out += "%" + b.toString(16).toUpperCase().padStart(2, "0");
306
+ } else {
307
+ out += String.fromCharCode(b);
308
+ }
309
+ }
310
+ return out;
311
+ }
312
+ function canonicalRecords(params, keyOrder, code) {
313
+ const lines = [];
314
+ for (const key of keyOrder) {
315
+ const value = params[key];
316
+ if (value === void 0 || value === null) continue;
317
+ const raw = String(value);
318
+ if (raw.includes("\n") || raw.includes("\r")) {
319
+ throw new CanonicalValueError(
320
+ code,
321
+ key,
322
+ `Value for "${key}" contains a raw newline or carriage return. Refusing: a hash over stripped or normalized input would not represent what was authorized.`
323
+ );
324
+ }
325
+ lines.push(`${key}=${percentEncodeCanonicalValue(raw)}`);
326
+ }
327
+ return lines.join("\n");
328
+ }
287
329
  function canonicalBounds(params, profile) {
288
330
  const validation = validateBoundsParams(params, profile);
289
331
  if (!validation.valid) {
290
332
  throw new Error(`Invalid bounds parameters: ${validation.errors.join("; ")}`);
291
333
  }
292
- const lines = profile.boundsSchema.keyOrder.map(
293
- (key) => `${key}=${String(params[key])}`
294
- );
295
- return lines.join("\n");
334
+ return canonicalRecords(params, profile.boundsSchema.keyOrder, "BOUNDS_INVALID_VALUE");
296
335
  }
297
336
  function canonicalContext(params, profile) {
298
337
  if (!profile.contextSchema || Object.keys(profile.contextSchema.fields).length === 0) {
@@ -302,10 +341,7 @@ function canonicalContext(params, profile) {
302
341
  if (!validation.valid) {
303
342
  throw new Error(`Invalid context parameters: ${validation.errors.join("; ")}`);
304
343
  }
305
- const lines = profile.contextSchema.keyOrder.map(
306
- (key) => `${key}=${String(params[key])}`
307
- );
308
- return lines.join("\n");
344
+ return canonicalRecords(params, profile.contextSchema.keyOrder, "CONTEXT_INVALID_VALUE");
309
345
  }
310
346
  function computeBoundsHash(params, profile) {
311
347
  const canonical = canonicalBounds(params, profile);
@@ -435,7 +471,17 @@ async function verify(request, publicKeyHex, now = Math.floor(Date.now() / 1e3),
435
471
  return verifyV3(request, profile, publicKeyHex, now, executionLog, errors);
436
472
  }
437
473
  }
474
+ function noAttestationSupplied(request) {
475
+ return !Array.isArray(request.attestations) || request.attestations.length === 0;
476
+ }
477
+ function noAttestationResult() {
478
+ return {
479
+ approved: false,
480
+ errors: [{ code: "MALFORMED_ATTESTATION", message: "No attestation supplied \u2014 nothing to verify" }]
481
+ };
482
+ }
438
483
  async function verifyV3(request, profile, publicKeyHex, now, executionLog, errors) {
484
+ if (noAttestationSupplied(request)) return noAttestationResult();
439
485
  let expectedFrameHash;
440
486
  try {
441
487
  expectedFrameHash = computeFrameHash(request.frame, profile);
@@ -496,6 +542,7 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
496
542
  return { approved: true };
497
543
  }
498
544
  async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
545
+ if (noAttestationSupplied(request)) return noAttestationResult();
499
546
  const errors = [];
500
547
  const bounds = request.frame;
501
548
  const context = request.context;
@@ -869,10 +916,207 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
869
916
  }
870
917
  return errors;
871
918
  }
919
+
920
+ // src/did-key.ts
921
+ var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
922
+ var B58_MAP = {};
923
+ for (let i = 0; i < B58_ALPHABET.length; i++) B58_MAP[B58_ALPHABET[i]] = i;
924
+ var ED25519_MULTICODEC = Uint8Array.from([237, 1]);
925
+ var ED25519_KEY_BYTES = 32;
926
+ function base58btcEncode(bytes) {
927
+ let zeros = 0;
928
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
929
+ const digits = [];
930
+ for (let i = zeros; i < bytes.length; i++) {
931
+ let carry = bytes[i];
932
+ for (let j = 0; j < digits.length; j++) {
933
+ carry += digits[j] * 256;
934
+ digits[j] = carry % 58;
935
+ carry = carry / 58 | 0;
936
+ }
937
+ while (carry > 0) {
938
+ digits.push(carry % 58);
939
+ carry = carry / 58 | 0;
940
+ }
941
+ }
942
+ let out = "1".repeat(zeros);
943
+ for (let i = digits.length - 1; i >= 0; i--) out += B58_ALPHABET[digits[i]];
944
+ return out;
945
+ }
946
+ function base58btcDecode(s) {
947
+ let zeros = 0;
948
+ while (zeros < s.length && s[zeros] === "1") zeros++;
949
+ const bytes = [];
950
+ for (let i = zeros; i < s.length; i++) {
951
+ const val = B58_MAP[s[i]];
952
+ if (val === void 0) throw new Error(`invalid base58 character "${s[i]}"`);
953
+ let carry = val;
954
+ for (let j = 0; j < bytes.length; j++) {
955
+ carry += bytes[j] * 58;
956
+ bytes[j] = carry & 255;
957
+ carry >>= 8;
958
+ }
959
+ while (carry > 0) {
960
+ bytes.push(carry & 255);
961
+ carry >>= 8;
962
+ }
963
+ }
964
+ const out = new Uint8Array(zeros + bytes.length);
965
+ for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[bytes.length - 1 - i];
966
+ return out;
967
+ }
968
+ function encodeDidKey(publicKey) {
969
+ if (publicKey.length !== ED25519_KEY_BYTES) {
970
+ throw new Error(`INVALID_KEY: expected ${ED25519_KEY_BYTES} bytes, got ${publicKey.length}`);
971
+ }
972
+ const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKey.length);
973
+ prefixed.set(ED25519_MULTICODEC, 0);
974
+ prefixed.set(publicKey, ED25519_MULTICODEC.length);
975
+ return `did:key:z${base58btcEncode(prefixed)}`;
976
+ }
977
+ function decodeDidKey(did) {
978
+ if (!did.startsWith("did:key:z")) {
979
+ throw new Error(`NOT_KEY_BEARING: ${JSON.stringify(did)} is not a base58btc did:key`);
980
+ }
981
+ let decoded;
982
+ try {
983
+ decoded = base58btcDecode(did.slice("did:key:z".length));
984
+ } catch (err) {
985
+ throw new Error(`NOT_KEY_BEARING: ${err.message}`);
986
+ }
987
+ if (decoded.length !== ED25519_MULTICODEC.length + ED25519_KEY_BYTES || decoded[0] !== ED25519_MULTICODEC[0] || decoded[1] !== ED25519_MULTICODEC[1]) {
988
+ throw new Error("NOT_KEY_BEARING: not an ed25519-pub multicodec payload");
989
+ }
990
+ return decoded.slice(ED25519_MULTICODEC.length);
991
+ }
992
+ function isKeyBearingDid(did) {
993
+ try {
994
+ decodeDidKey(did);
995
+ return true;
996
+ } catch {
997
+ return false;
998
+ }
999
+ }
1000
+
1001
+ // src/mandate.ts
1002
+ import * as ed2 from "@noble/ed25519";
1003
+ var MandateError = class extends Error {
1004
+ constructor(code, message) {
1005
+ super(`${code}: ${message}`);
1006
+ this.code = code;
1007
+ this.name = "MandateError";
1008
+ }
1009
+ };
1010
+ function buildMandateProjection(payload, entry) {
1011
+ const { profile_id, bounds_hash, context_hash, execution_context_hash, gate_content_hashes, commitment_mode, expires_at } = payload;
1012
+ if (!bounds_hash || !context_hash || !commitment_mode) {
1013
+ throw new MandateError("MALFORMED_ATTESTATION", "mandate projection requires bounds_hash, context_hash and commitment_mode");
1014
+ }
1015
+ const projection = {
1016
+ typ: "HAP-mandate",
1017
+ version: "0.6",
1018
+ profile_id,
1019
+ owner_did: entry.did,
1020
+ bounds_hash,
1021
+ context_hash,
1022
+ execution_context_hash,
1023
+ gate_content_hashes,
1024
+ commitment_mode,
1025
+ expires_at,
1026
+ nonce: entry.nonce
1027
+ };
1028
+ if (payload.intent_disclosure_hash !== void 0) {
1029
+ projection.intent_disclosure_hash = payload.intent_disclosure_hash;
1030
+ }
1031
+ return projection;
1032
+ }
1033
+ function mandateSigningBytes(projection) {
1034
+ return new TextEncoder().encode(canonicalize(projection));
1035
+ }
1036
+ function approvalSigningBytes(approval) {
1037
+ return new TextEncoder().encode(canonicalize(approval));
1038
+ }
1039
+ function base64urlToBytes(s) {
1040
+ const base64 = s.replace(/-/g, "+").replace(/_/g, "/");
1041
+ const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
1042
+ return new Uint8Array(Buffer.from(base64 + padding, "base64"));
1043
+ }
1044
+ function bytesToBase64url(bytes) {
1045
+ return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1046
+ }
1047
+ async function signMandateProjection(projection, privateKey) {
1048
+ const sig = await ed2.signAsync(mandateSigningBytes(projection), privateKey);
1049
+ return bytesToBase64url(sig);
1050
+ }
1051
+ async function signApproval(approval, privateKey) {
1052
+ const sig = await ed2.signAsync(approvalSigningBytes(approval), privateKey);
1053
+ return bytesToBase64url(sig);
1054
+ }
1055
+ async function verifyOwnerMandate(attestation, entry) {
1056
+ const owners = attestation.payload.resolved_owners ?? [];
1057
+ if (!owners.includes(entry.did)) {
1058
+ throw new MandateError("OWNER_NOT_RESOLVED", `signing DID ${entry.did} is not in resolved_owners`);
1059
+ }
1060
+ if (entry.alg !== "EdDSA") {
1061
+ throw new MandateError("MANDATE_SIGNATURE_INVALID", `alg ${entry.alg} disagrees with the DID's key type (DID is authoritative)`);
1062
+ }
1063
+ let publicKey;
1064
+ try {
1065
+ publicKey = decodeDidKey(entry.did);
1066
+ } catch (err) {
1067
+ throw new MandateError("NOT_KEY_BEARING", err.message);
1068
+ }
1069
+ const projection = buildMandateProjection(attestation.payload, entry);
1070
+ const ok = await ed2.verifyAsync(base64urlToBytes(entry.signature), mandateSigningBytes(projection), publicKey);
1071
+ if (!ok) {
1072
+ throw new MandateError("MANDATE_SIGNATURE_INVALID", `owner mandate signature by ${entry.did} does not verify`);
1073
+ }
1074
+ }
1075
+ async function verifyOwnerMandates(attestation) {
1076
+ const entries = attestation.payload.owner_mandates ?? [];
1077
+ for (const entry of entries) {
1078
+ await verifyOwnerMandate(attestation, entry);
1079
+ }
1080
+ return entries;
1081
+ }
1082
+ async function verifyApproval(approval, signature, signerDid) {
1083
+ let publicKey;
1084
+ try {
1085
+ publicKey = decodeDidKey(signerDid);
1086
+ } catch (err) {
1087
+ throw new MandateError("NOT_KEY_BEARING", err.message);
1088
+ }
1089
+ const ok = await ed2.verifyAsync(base64urlToBytes(signature), approvalSigningBytes(approval), publicKey);
1090
+ if (!ok) {
1091
+ throw new MandateError("APPROVAL_SIGNATURE_INVALID", `approval signature by ${signerDid} does not verify`);
1092
+ }
1093
+ }
1094
+
1095
+ // src/receipt.ts
1096
+ import * as ed3 from "@noble/ed25519";
1097
+ async function verifyReceiptSignature(receipt, publicKeyHex) {
1098
+ const { signature, ...unsigned } = receipt;
1099
+ if (!signature) {
1100
+ throw new Error("INVALID_SIGNATURE: receipt carries no signature");
1101
+ }
1102
+ const bytes = new TextEncoder().encode(canonicalize(unsigned));
1103
+ const base64 = signature.replace(/-/g, "+").replace(/_/g, "/");
1104
+ const padding = base64.length % 4 === 0 ? "" : "=".repeat(4 - base64.length % 4);
1105
+ const sigBytes = new Uint8Array(Buffer.from(base64 + padding, "base64"));
1106
+ const publicKeyBytes = new Uint8Array(Buffer.from(publicKeyHex, "hex"));
1107
+ const ok = await ed3.verifyAsync(sigBytes, bytes, publicKeyBytes).catch(() => false);
1108
+ if (!ok) {
1109
+ throw new Error("INVALID_SIGNATURE: receipt signature verification failed");
1110
+ }
1111
+ }
872
1112
  export {
1113
+ CanonicalValueError,
873
1114
  ContentBindingError,
1115
+ MandateError,
1116
+ approvalSigningBytes,
874
1117
  attestationId,
875
1118
  bindingAppliesTo,
1119
+ buildMandateProjection,
876
1120
  canonicalBounds,
877
1121
  canonicalContext,
878
1122
  canonicalFrame,
@@ -889,26 +1133,36 @@ export {
889
1133
  computeIntentHash,
890
1134
  contentCanonicalBytes,
891
1135
  decodeAttestationBlob,
1136
+ decodeDidKey,
892
1137
  deriveIdentityLine,
893
1138
  encodeAttestationBlob,
1139
+ encodeDidKey,
894
1140
  frameHash,
895
1141
  getAllProfiles,
896
1142
  getProfile,
897
1143
  intentDisclosureCanonicalBytes,
898
1144
  isFieldBinding,
1145
+ isKeyBearingDid,
899
1146
  isV4Attestation,
900
1147
  listProfiles,
1148
+ mandateSigningBytes,
901
1149
  registerProfile,
902
1150
  selectBoundFields,
1151
+ signApproval,
1152
+ signMandateProjection,
903
1153
  validateBoundsParams,
904
1154
  validateContextParams,
905
1155
  validateFrameParams,
906
1156
  validateSubject,
907
1157
  verify,
1158
+ verifyApproval,
908
1159
  verifyAttestation,
909
1160
  verifyAttestationSignature,
910
1161
  verifyAttestationV4,
911
1162
  verifyBoundsHash,
912
1163
  verifyContextHash,
913
- verifyFrameHash
1164
+ verifyFrameHash,
1165
+ verifyOwnerMandate,
1166
+ verifyOwnerMandates,
1167
+ verifyReceiptSignature
914
1168
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.8.4",
3
+ "version": "0.10.0",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "scripts": {
25
25
  "build": "tsup src/index.ts --format cjs,esm --dts",
26
26
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
27
+ "typecheck": "tsc --noEmit",
27
28
  "test": "vitest run",
28
29
  "test:watch": "vitest",
29
30
  "prepublishOnly": "pnpm build",
@@ -39,5 +40,6 @@
39
40
  "tsup": "^8.0.0",
40
41
  "typescript": "^5.3.0",
41
42
  "vitest": "^1.0.0"
42
- }
43
+ },
44
+ "homepage": "https://humanagencyprotocol.org"
43
45
  }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * did:key — encode/decode for KEY-BEARING DIDs (HAP v0.6).
3
+ *
4
+ * A real `did:key` is self-certifying: the public key IS the identifier, so
5
+ * substituting the key produces a DIFFERENT DID and no key directory is ever
6
+ * consulted. That property is what the Owner Mandate design stands on
7
+ * (protocol.md → "Identity DIDs vs signing DIDs"): a signing DID MUST be
8
+ * key-bearing, and verification uses the key carried in the identifier —
9
+ * deliberately, there is no `public_key` field anywhere on the wire.
10
+ *
11
+ * Format (W3C did:key method, Ed25519):
12
+ * did:key:z<base58btc( 0xed 0x01 ‖ 32-byte-public-key )>
13
+ * `z` is the multibase prefix for base58btc; `0xed 0x01` is the multicodec
14
+ * varint for ed25519-pub. The result always starts `did:key:z6Mk`.
15
+ *
16
+ * Scope: Ed25519 only — the one signing curve this protocol version uses.
17
+ * Anything else (including the reference implementation's legacy decorative
18
+ * `did:key:<uuid-fragment>` identifiers) decodes as NOT key-bearing.
19
+ */
20
+
21
+ const B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
22
+ const B58_MAP: Record<string, number> = {};
23
+ for (let i = 0; i < B58_ALPHABET.length; i++) B58_MAP[B58_ALPHABET[i]] = i;
24
+
25
+ /** multicodec ed25519-pub (0xed) as unsigned varint, followed by key bytes. */
26
+ const ED25519_MULTICODEC = Uint8Array.from([0xed, 0x01]);
27
+ const ED25519_KEY_BYTES = 32;
28
+
29
+ function base58btcEncode(bytes: Uint8Array): string {
30
+ // Count leading zero bytes — they encode as leading '1's.
31
+ let zeros = 0;
32
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
33
+
34
+ const digits: number[] = [];
35
+ for (let i = zeros; i < bytes.length; i++) {
36
+ let carry = bytes[i];
37
+ for (let j = 0; j < digits.length; j++) {
38
+ carry += digits[j] * 256;
39
+ digits[j] = carry % 58;
40
+ carry = (carry / 58) | 0;
41
+ }
42
+ while (carry > 0) {
43
+ digits.push(carry % 58);
44
+ carry = (carry / 58) | 0;
45
+ }
46
+ }
47
+
48
+ let out = '1'.repeat(zeros);
49
+ for (let i = digits.length - 1; i >= 0; i--) out += B58_ALPHABET[digits[i]];
50
+ return out;
51
+ }
52
+
53
+ function base58btcDecode(s: string): Uint8Array {
54
+ let zeros = 0;
55
+ while (zeros < s.length && s[zeros] === '1') zeros++;
56
+
57
+ const bytes: number[] = [];
58
+ for (let i = zeros; i < s.length; i++) {
59
+ const val = B58_MAP[s[i]];
60
+ if (val === undefined) throw new Error(`invalid base58 character "${s[i]}"`);
61
+ let carry = val;
62
+ for (let j = 0; j < bytes.length; j++) {
63
+ carry += bytes[j] * 58;
64
+ bytes[j] = carry & 0xff;
65
+ carry >>= 8;
66
+ }
67
+ while (carry > 0) {
68
+ bytes.push(carry & 0xff);
69
+ carry >>= 8;
70
+ }
71
+ }
72
+
73
+ const out = new Uint8Array(zeros + bytes.length);
74
+ for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[bytes.length - 1 - i];
75
+ return out;
76
+ }
77
+
78
+ /** Encode a 32-byte Ed25519 public key as a key-bearing `did:key:z6Mk…`. */
79
+ export function encodeDidKey(publicKey: Uint8Array): string {
80
+ if (publicKey.length !== ED25519_KEY_BYTES) {
81
+ throw new Error(`INVALID_KEY: expected ${ED25519_KEY_BYTES} bytes, got ${publicKey.length}`);
82
+ }
83
+ const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKey.length);
84
+ prefixed.set(ED25519_MULTICODEC, 0);
85
+ prefixed.set(publicKey, ED25519_MULTICODEC.length);
86
+ return `did:key:z${base58btcEncode(prefixed)}`;
87
+ }
88
+
89
+ /**
90
+ * Decode a key-bearing `did:key` to its Ed25519 public key bytes.
91
+ *
92
+ * @throws Error prefixed `NOT_KEY_BEARING:` for anything that is not a valid
93
+ * base58btc-multibase Ed25519 did:key — including the legacy decorative
94
+ * `did:key:<uuid-fragment>` identifiers, other multibase prefixes, and other
95
+ * key types. The error name is the point: under the mandate design, a
96
+ * non-key-bearing signing DID must fail STRUCTURALLY, never fall back.
97
+ */
98
+ export function decodeDidKey(did: string): Uint8Array {
99
+ if (!did.startsWith('did:key:z')) {
100
+ throw new Error(`NOT_KEY_BEARING: ${JSON.stringify(did)} is not a base58btc did:key`);
101
+ }
102
+ let decoded: Uint8Array;
103
+ try {
104
+ decoded = base58btcDecode(did.slice('did:key:z'.length));
105
+ } catch (err) {
106
+ throw new Error(`NOT_KEY_BEARING: ${(err as Error).message}`);
107
+ }
108
+ if (
109
+ decoded.length !== ED25519_MULTICODEC.length + ED25519_KEY_BYTES ||
110
+ decoded[0] !== ED25519_MULTICODEC[0] ||
111
+ decoded[1] !== ED25519_MULTICODEC[1]
112
+ ) {
113
+ throw new Error('NOT_KEY_BEARING: not an ed25519-pub multicodec payload');
114
+ }
115
+ return decoded.slice(ED25519_MULTICODEC.length);
116
+ }
117
+
118
+ /** True iff the DID decodes as a key-bearing Ed25519 did:key. Never throws. */
119
+ export function isKeyBearingDid(did: string): boolean {
120
+ try {
121
+ decodeDidKey(did);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
@@ -2,8 +2,11 @@
2
2
  * Frame Canonicalization for Agent Profiles
3
3
  *
4
4
  * Agent profiles support mixed-type fields (strings and numbers).
5
- * Canonical form: all values are converted to strings via String(value).
6
- * Keys are ordered according to the profile's keyOrder.
5
+ * Canonical form: `key=value` records joined with LF, keys in the profile's
6
+ * keyOrder. Values are stringified with String(value) the shortest
7
+ * round-trippable form for numbers — then percent-encoded per protocol.md
8
+ * (`=`, `%`, and every byte outside printable ASCII); a value carrying a raw
9
+ * LF/CR is refused, never normalized. See `canonicalRecords` below.
7
10
  *
8
11
  * v0.3: frameSchema
9
12
  * v0.4: boundsSchema + contextSchema (separate hashes)
@@ -176,11 +179,115 @@ export function validateContextParams(
176
179
  return { valid: errors.length === 0, errors };
177
180
  }
178
181
 
182
+ // ─── Value Encoding (normative, v0.5+) ───────────────────────────────────────
183
+
184
+ /**
185
+ * Thrown when a value cannot be canonicalized at all — currently only for raw
186
+ * LF/CR inside a value. Carries the protocol error code so callers can map it
187
+ * straight onto the wire without string-matching a message.
188
+ *
189
+ * protocol.md → *Bounds & Scope Canonicalization* → Value encoding:
190
+ * "Values MUST NOT contain raw newline (\n) or carriage-return (\r)
191
+ * characters. Implementations MUST reject input containing them; silent
192
+ * stripping or normalization is a violation because it produces a hash that
193
+ * does not faithfully represent the input."
194
+ */
195
+ export class CanonicalValueError extends Error {
196
+ readonly code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE';
197
+ readonly field: string;
198
+
199
+ constructor(code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE', field: string, message: string) {
200
+ super(message);
201
+ this.name = 'CanonicalValueError';
202
+ this.code = code;
203
+ this.field = field;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Percent-encode a value per protocol.md → *Value encoding*.
209
+ *
210
+ * Encoded, over the value's UTF-8 bytes, as `%` + two UPPERCASE hex digits:
211
+ * - `=` (0x3D) — otherwise it would be read as the key/value separator
212
+ * - `%` (0x25) — so the encoding is self-inverse
213
+ * - every byte outside printable ASCII 0x20–0x7E
214
+ *
215
+ * LF and CR are deliberately NOT in this list: they are refused upstream, so
216
+ * encoding them is unreachable (v0.7 removed the spec's contradiction here).
217
+ *
218
+ * This runs at canonicalization time only. Stored values keep the human's
219
+ * original bytes.
220
+ */
221
+ function percentEncodeCanonicalValue(raw: string): string {
222
+ const bytes = new TextEncoder().encode(raw);
223
+ let out = '';
224
+ for (const b of bytes) {
225
+ if (b === 0x3d || b === 0x25 || b < 0x20 || b > 0x7e) {
226
+ out += '%' + b.toString(16).toUpperCase().padStart(2, '0');
227
+ } else {
228
+ out += String.fromCharCode(b);
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+
234
+ /**
235
+ * The one place `key=value` records are built for bounds and context.
236
+ *
237
+ * Rules applied here (all normative, protocol.md → *Bounds & Scope
238
+ * Canonicalization*):
239
+ * - keys in the schema's keyOrder, never alphabetical
240
+ * - a value carrying a raw LF/CR is REFUSED (never stripped or encoded)
241
+ * - `=`, `%`, and any byte outside 0x20–0x7E are percent-encoded (UPPERCASE)
242
+ * - numbers use JS `String()`, which is the shortest round-trippable form
243
+ * (`String(20.0)` === "20")
244
+ * - a key with no value is OMITTED entirely — it emits no record
245
+ *
246
+ * On the omission rule: required keys are guaranteed present by the caller's
247
+ * validation (`validateBoundsParams` / `validateContextParams` reject a missing
248
+ * required field), so "explicit inclusion of all required keys" still holds.
249
+ * What remains are *optional* keys the human never set. Rendering those as the
250
+ * literal string "undefined" — the pre-fix behaviour — hashed a JavaScript
251
+ * artifact that no other language would produce and that collides with a real
252
+ * value of "undefined". Omitting them (rather than emitting `key=`) also keeps
253
+ * "the human set no limit" distinct from "the human set an empty value", and
254
+ * matches this package's JSON canonicalization, which drops undefined-valued
255
+ * properties.
256
+ */
257
+ function canonicalRecords(
258
+ params: Record<string, string | number | undefined>,
259
+ keyOrder: string[],
260
+ code: 'BOUNDS_INVALID_VALUE' | 'CONTEXT_INVALID_VALUE',
261
+ ): string {
262
+ const lines: string[] = [];
263
+
264
+ for (const key of keyOrder) {
265
+ const value = params[key];
266
+ if (value === undefined || value === null) continue;
267
+
268
+ const raw = String(value);
269
+ if (raw.includes('\n') || raw.includes('\r')) {
270
+ throw new CanonicalValueError(
271
+ code,
272
+ key,
273
+ `Value for "${key}" contains a raw newline or carriage return. ` +
274
+ 'Refusing: a hash over stripped or normalized input would not represent what was authorized.',
275
+ );
276
+ }
277
+
278
+ lines.push(`${key}=${percentEncodeCanonicalValue(raw)}`);
279
+ }
280
+
281
+ return lines.join('\n');
282
+ }
283
+
179
284
  /**
180
285
  * Builds the canonical bounds string from parameters.
181
- * All values are converted to strings. Keys are ordered per profile's boundsSchema.keyOrder.
286
+ * Keys are ordered per profile's boundsSchema.keyOrder; values are encoded per
287
+ * protocol.md → *Value encoding* (see `canonicalRecords`).
182
288
  *
183
289
  * @throws Error if any field fails validation
290
+ * @throws CanonicalValueError (code BOUNDS_INVALID_VALUE) if a value carries a raw LF/CR
184
291
  */
185
292
  export function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile): string {
186
293
  const validation = validateBoundsParams(params, profile);
@@ -188,19 +295,17 @@ export function canonicalBounds(params: AgentBoundsParams, profile: AgentProfile
188
295
  throw new Error(`Invalid bounds parameters: ${validation.errors.join('; ')}`);
189
296
  }
190
297
 
191
- const lines = profile.boundsSchema!.keyOrder.map(
192
- (key) => `${key}=${String(params[key])}`
193
- );
194
-
195
- return lines.join('\n');
298
+ return canonicalRecords(params, profile.boundsSchema!.keyOrder, 'BOUNDS_INVALID_VALUE');
196
299
  }
197
300
 
198
301
  /**
199
302
  * Builds the canonical context string from parameters.
200
- * All values are converted to strings. Keys are ordered per profile's contextSchema.keyOrder.
303
+ * Keys are ordered per profile's contextSchema.keyOrder; values are encoded per
304
+ * protocol.md → *Value encoding* (see `canonicalRecords`).
201
305
  * For empty context (no contextSchema or no fields), returns "".
202
306
  *
203
307
  * @throws Error if any field fails validation
308
+ * @throws CanonicalValueError (code CONTEXT_INVALID_VALUE) if a value carries a raw LF/CR
204
309
  */
205
310
  export function canonicalContext(params: AgentContextParams, profile: AgentProfile): string {
206
311
  // No contextSchema or no fields → empty context
@@ -213,11 +318,7 @@ export function canonicalContext(params: AgentContextParams, profile: AgentProfi
213
318
  throw new Error(`Invalid context parameters: ${validation.errors.join('; ')}`);
214
319
  }
215
320
 
216
- const lines = profile.contextSchema.keyOrder.map(
217
- (key) => `${key}=${String(params[key])}`
218
- );
219
-
220
- return lines.join('\n');
321
+ return canonicalRecords(params, profile.contextSchema.keyOrder, 'CONTEXT_INVALID_VALUE');
221
322
  }
222
323
 
223
324
  /**