@joe-broadhead/messaging 0.0.0-stage → 0.7.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 (257) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +12 -0
  4. package/README.md +36 -2
  5. package/dist/bin/messaging-mcp.js +25887 -0
  6. package/dist/bin/messaging-relay.js +19014 -0
  7. package/dist/bin/messaging.js +14732 -0
  8. package/dist/bin/messagingd.js +28682 -0
  9. package/dist/lib/core-testing.js +1 -0
  10. package/dist/lib/core.js +1 -0
  11. package/dist/lib/ipc-testing.js +1 -0
  12. package/dist/lib/ipc.js +1 -0
  13. package/dist/lib/modules/core/address.js +42 -0
  14. package/dist/lib/modules/core/agent/operations.js +595 -0
  15. package/dist/lib/modules/core/agent/present.js +244 -0
  16. package/dist/lib/modules/core/agent/text.js +79 -0
  17. package/dist/lib/modules/core/agent-tools.js +458 -0
  18. package/dist/lib/modules/core/b64u.js +20 -0
  19. package/dist/lib/modules/core/cards.js +55 -0
  20. package/dist/lib/modules/core/check.js +20 -0
  21. package/dist/lib/modules/core/consent.js +199 -0
  22. package/dist/lib/modules/core/contact-card.js +74 -0
  23. package/dist/lib/modules/core/delegation.js +174 -0
  24. package/dist/lib/modules/core/directory/device-log.js +258 -0
  25. package/dist/lib/modules/core/directory/epoch.js +103 -0
  26. package/dist/lib/modules/core/directory/handle.js +29 -0
  27. package/dist/lib/modules/core/directory/jelly-device.js +204 -0
  28. package/dist/lib/modules/core/directory/jelly-issuer.js +12 -0
  29. package/dist/lib/modules/core/directory/jelly-token.js +305 -0
  30. package/dist/lib/modules/core/directory/keys-statement.js +43 -0
  31. package/dist/lib/modules/core/directory/link.js +81 -0
  32. package/dist/lib/modules/core/directory/merkle-log.js +250 -0
  33. package/dist/lib/modules/core/directory/person-view.js +197 -0
  34. package/dist/lib/modules/core/directory/person.js +40 -0
  35. package/dist/lib/modules/core/directory/realm.js +59 -0
  36. package/dist/lib/modules/core/directory/smt.js +266 -0
  37. package/dist/lib/modules/core/envelope.js +320 -0
  38. package/dist/lib/modules/core/hash.js +5 -0
  39. package/dist/lib/modules/core/head.js +27 -0
  40. package/dist/lib/modules/core/identity.js +71 -0
  41. package/dist/lib/modules/core/index.js +44 -0
  42. package/dist/lib/modules/core/jcs.js +96 -0
  43. package/dist/lib/modules/core/jelly-host-double.js +35 -0
  44. package/dist/lib/modules/core/jelly-host.js +211 -0
  45. package/dist/lib/modules/core/keys.js +79 -0
  46. package/dist/lib/modules/core/peer-text.js +563 -0
  47. package/dist/lib/modules/core/realm.js +1 -0
  48. package/dist/lib/modules/core/roster.js +454 -0
  49. package/dist/lib/modules/core/schema/a2a.js +85 -0
  50. package/dist/lib/modules/core/schema/common.js +254 -0
  51. package/dist/lib/modules/core/schema/consent.js +120 -0
  52. package/dist/lib/modules/core/schema/delegation.js +58 -0
  53. package/dist/lib/modules/core/schema/directory-frames.js +270 -0
  54. package/dist/lib/modules/core/schema/directory.js +169 -0
  55. package/dist/lib/modules/core/schema/envelope.js +211 -0
  56. package/dist/lib/modules/core/schema/errors.js +65 -0
  57. package/dist/lib/modules/core/schema/index.js +15 -0
  58. package/dist/lib/modules/core/schema/ipc/consent.js +181 -0
  59. package/dist/lib/modules/core/schema/ipc/device.js +276 -0
  60. package/dist/lib/modules/core/schema/ipc/jelly.js +189 -0
  61. package/dist/lib/modules/core/schema/ipc/shapes.js +185 -0
  62. package/dist/lib/modules/core/schema/ipc/surfaces.js +223 -0
  63. package/dist/lib/modules/core/schema/ipc.js +451 -0
  64. package/dist/lib/modules/core/schema/jelly.js +122 -0
  65. package/dist/lib/modules/core/schema/realm.js +27 -0
  66. package/dist/lib/modules/core/schema/relay.js +112 -0
  67. package/dist/lib/modules/core/schema/roster.js +126 -0
  68. package/dist/lib/modules/core/sign.js +43 -0
  69. package/dist/lib/modules/core/task.js +37 -0
  70. package/dist/lib/modules/core/testing.js +20 -0
  71. package/dist/lib/modules/core/ulid.js +36 -0
  72. package/dist/lib/modules/core/version.js +6 -0
  73. package/dist/lib/modules/core/wire.js +255 -0
  74. package/dist/lib/modules/ipc/client.js +289 -0
  75. package/dist/lib/modules/ipc/framing.js +33 -0
  76. package/dist/lib/modules/ipc/index.js +6 -0
  77. package/dist/lib/modules/ipc/paths.js +102 -0
  78. package/dist/lib/modules/ipc/pipes.js +65 -0
  79. package/dist/lib/modules/ipc/rpc.js +8 -0
  80. package/dist/lib/modules/ipc/server.js +317 -0
  81. package/dist/lib/modules/ipc/testing.js +145 -0
  82. package/dist/lib/modules/ipc/trust.js +166 -0
  83. package/dist/lib/modules/ipc/windows-acl.js +275 -0
  84. package/dist/lib/modules/vendor/zod/index.js +7 -0
  85. package/dist/lib/modules/vendor/zod/v4/classic/checks.js +1 -0
  86. package/dist/lib/modules/vendor/zod/v4/classic/coerce.js +17 -0
  87. package/dist/lib/modules/vendor/zod/v4/classic/compat.js +31 -0
  88. package/dist/lib/modules/vendor/zod/v4/classic/deep-partial.js +13 -0
  89. package/dist/lib/modules/vendor/zod/v4/classic/errors.js +57 -0
  90. package/dist/lib/modules/vendor/zod/v4/classic/external.js +17 -0
  91. package/dist/lib/modules/vendor/zod/v4/classic/from-json-schema.js +881 -0
  92. package/dist/lib/modules/vendor/zod/v4/classic/in-out.js +36 -0
  93. package/dist/lib/modules/vendor/zod/v4/classic/iso.js +15 -0
  94. package/dist/lib/modules/vendor/zod/v4/classic/parse.js +16 -0
  95. package/dist/lib/modules/vendor/zod/v4/classic/schemas.js +1535 -0
  96. package/dist/lib/modules/vendor/zod/v4/core/api.js +1107 -0
  97. package/dist/lib/modules/vendor/zod/v4/core/checks.js +516 -0
  98. package/dist/lib/modules/vendor/zod/v4/core/compile.js +1866 -0
  99. package/dist/lib/modules/vendor/zod/v4/core/core.js +141 -0
  100. package/dist/lib/modules/vendor/zod/v4/core/doc.js +38 -0
  101. package/dist/lib/modules/vendor/zod/v4/core/errors.js +267 -0
  102. package/dist/lib/modules/vendor/zod/v4/core/index.js +19 -0
  103. package/dist/lib/modules/vendor/zod/v4/core/json-schema-generator.js +99 -0
  104. package/dist/lib/modules/vendor/zod/v4/core/json-schema-processors.js +843 -0
  105. package/dist/lib/modules/vendor/zod/v4/core/json-schema.js +1 -0
  106. package/dist/lib/modules/vendor/zod/v4/core/memoizer.js +311 -0
  107. package/dist/lib/modules/vendor/zod/v4/core/parse.js +175 -0
  108. package/dist/lib/modules/vendor/zod/v4/core/regexes.js +156 -0
  109. package/dist/lib/modules/vendor/zod/v4/core/registries.js +51 -0
  110. package/dist/lib/modules/vendor/zod/v4/core/schemas.js +2636 -0
  111. package/dist/lib/modules/vendor/zod/v4/core/to-json-schema.js +651 -0
  112. package/dist/lib/modules/vendor/zod/v4/core/util.js +873 -0
  113. package/dist/lib/modules/vendor/zod/v4/core/versions.js +5 -0
  114. package/dist/lib/modules/vendor/zod/v4/core/visit.js +169 -0
  115. package/dist/lib/modules/vendor/zod/v4/locales/ar.js +111 -0
  116. package/dist/lib/modules/vendor/zod/v4/locales/az.js +110 -0
  117. package/dist/lib/modules/vendor/zod/v4/locales/be.js +168 -0
  118. package/dist/lib/modules/vendor/zod/v4/locales/bg.js +125 -0
  119. package/dist/lib/modules/vendor/zod/v4/locales/bn.js +113 -0
  120. package/dist/lib/modules/vendor/zod/v4/locales/ca.js +112 -0
  121. package/dist/lib/modules/vendor/zod/v4/locales/ckb.js +133 -0
  122. package/dist/lib/modules/vendor/zod/v4/locales/cs.js +116 -0
  123. package/dist/lib/modules/vendor/zod/v4/locales/da.js +120 -0
  124. package/dist/lib/modules/vendor/zod/v4/locales/de.js +113 -0
  125. package/dist/lib/modules/vendor/zod/v4/locales/el.js +112 -0
  126. package/dist/lib/modules/vendor/zod/v4/locales/en.js +125 -0
  127. package/dist/lib/modules/vendor/zod/v4/locales/eo.js +114 -0
  128. package/dist/lib/modules/vendor/zod/v4/locales/es.js +136 -0
  129. package/dist/lib/modules/vendor/zod/v4/locales/fa.js +119 -0
  130. package/dist/lib/modules/vendor/zod/v4/locales/fi.js +117 -0
  131. package/dist/lib/modules/vendor/zod/v4/locales/fr-CA.js +112 -0
  132. package/dist/lib/modules/vendor/zod/v4/locales/fr.js +129 -0
  133. package/dist/lib/modules/vendor/zod/v4/locales/gu.js +113 -0
  134. package/dist/lib/modules/vendor/zod/v4/locales/he.js +221 -0
  135. package/dist/lib/modules/vendor/zod/v4/locales/hi.js +111 -0
  136. package/dist/lib/modules/vendor/zod/v4/locales/hr.js +126 -0
  137. package/dist/lib/modules/vendor/zod/v4/locales/hu.js +113 -0
  138. package/dist/lib/modules/vendor/zod/v4/locales/hy.js +158 -0
  139. package/dist/lib/modules/vendor/zod/v4/locales/id.js +111 -0
  140. package/dist/lib/modules/vendor/zod/v4/locales/index.js +63 -0
  141. package/dist/lib/modules/vendor/zod/v4/locales/is.js +114 -0
  142. package/dist/lib/modules/vendor/zod/v4/locales/it.js +113 -0
  143. package/dist/lib/modules/vendor/zod/v4/locales/ja.js +112 -0
  144. package/dist/lib/modules/vendor/zod/v4/locales/ka.js +117 -0
  145. package/dist/lib/modules/vendor/zod/v4/locales/kh.js +5 -0
  146. package/dist/lib/modules/vendor/zod/v4/locales/km.js +115 -0
  147. package/dist/lib/modules/vendor/zod/v4/locales/kn.js +116 -0
  148. package/dist/lib/modules/vendor/zod/v4/locales/ko.js +116 -0
  149. package/dist/lib/modules/vendor/zod/v4/locales/lt.js +207 -0
  150. package/dist/lib/modules/vendor/zod/v4/locales/mk.js +114 -0
  151. package/dist/lib/modules/vendor/zod/v4/locales/ms.js +112 -0
  152. package/dist/lib/modules/vendor/zod/v4/locales/ne.js +111 -0
  153. package/dist/lib/modules/vendor/zod/v4/locales/nl.js +115 -0
  154. package/dist/lib/modules/vendor/zod/v4/locales/nn.js +113 -0
  155. package/dist/lib/modules/vendor/zod/v4/locales/no.js +113 -0
  156. package/dist/lib/modules/vendor/zod/v4/locales/ota.js +114 -0
  157. package/dist/lib/modules/vendor/zod/v4/locales/pl.js +114 -0
  158. package/dist/lib/modules/vendor/zod/v4/locales/ps.js +119 -0
  159. package/dist/lib/modules/vendor/zod/v4/locales/pt-BR.js +143 -0
  160. package/dist/lib/modules/vendor/zod/v4/locales/pt.js +142 -0
  161. package/dist/lib/modules/vendor/zod/v4/locales/ro.js +122 -0
  162. package/dist/lib/modules/vendor/zod/v4/locales/ru.js +168 -0
  163. package/dist/lib/modules/vendor/zod/v4/locales/sk.js +116 -0
  164. package/dist/lib/modules/vendor/zod/v4/locales/sl.js +114 -0
  165. package/dist/lib/modules/vendor/zod/v4/locales/sv.js +115 -0
  166. package/dist/lib/modules/vendor/zod/v4/locales/ta.js +115 -0
  167. package/dist/lib/modules/vendor/zod/v4/locales/tg.js +117 -0
  168. package/dist/lib/modules/vendor/zod/v4/locales/th.js +115 -0
  169. package/dist/lib/modules/vendor/zod/v4/locales/tk.js +107 -0
  170. package/dist/lib/modules/vendor/zod/v4/locales/tr.js +110 -0
  171. package/dist/lib/modules/vendor/zod/v4/locales/ua.js +5 -0
  172. package/dist/lib/modules/vendor/zod/v4/locales/uk.js +113 -0
  173. package/dist/lib/modules/vendor/zod/v4/locales/ur.js +115 -0
  174. package/dist/lib/modules/vendor/zod/v4/locales/uz.js +113 -0
  175. package/dist/lib/modules/vendor/zod/v4/locales/vi.js +113 -0
  176. package/dist/lib/modules/vendor/zod/v4/locales/yo.js +112 -0
  177. package/dist/lib/modules/vendor/zod/v4/locales/zh-CN.js +114 -0
  178. package/dist/lib/modules/vendor/zod/v4/locales/zh-TW.js +112 -0
  179. package/dist/lib/testing-jelly-id.js +1219 -0
  180. package/dist/lib/zod.js +1 -0
  181. package/dist/spec/messaging-v1.md +3426 -0
  182. package/dist/third-party-notices.md +239 -0
  183. package/dist/third-party.json +88 -0
  184. package/dist/types/core/address.d.ts +16 -0
  185. package/dist/types/core/agent/operations.d.ts +129 -0
  186. package/dist/types/core/agent/present.d.ts +57 -0
  187. package/dist/types/core/agent/text.d.ts +62 -0
  188. package/dist/types/core/agent-tools.d.ts +948 -0
  189. package/dist/types/core/b64u.d.ts +6 -0
  190. package/dist/types/core/cards.d.ts +21 -0
  191. package/dist/types/core/check.d.ts +10 -0
  192. package/dist/types/core/consent.d.ts +132 -0
  193. package/dist/types/core/contact-card.d.ts +34 -0
  194. package/dist/types/core/delegation.d.ts +71 -0
  195. package/dist/types/core/directory/device-log.d.ts +119 -0
  196. package/dist/types/core/directory/epoch.d.ts +53 -0
  197. package/dist/types/core/directory/handle.d.ts +11 -0
  198. package/dist/types/core/directory/jelly-device.d.ts +88 -0
  199. package/dist/types/core/directory/jelly-issuer.d.ts +8 -0
  200. package/dist/types/core/directory/jelly-token.d.ts +116 -0
  201. package/dist/types/core/directory/keys-statement.d.ts +22 -0
  202. package/dist/types/core/directory/link.d.ts +36 -0
  203. package/dist/types/core/directory/merkle-log.d.ts +40 -0
  204. package/dist/types/core/directory/person-view.d.ts +90 -0
  205. package/dist/types/core/directory/person.d.ts +8 -0
  206. package/dist/types/core/directory/realm.d.ts +23 -0
  207. package/dist/types/core/directory/smt.d.ts +52 -0
  208. package/dist/types/core/envelope.d.ts +68 -0
  209. package/dist/types/core/hash.d.ts +2 -0
  210. package/dist/types/core/head.d.ts +34 -0
  211. package/dist/types/core/identity.d.ts +35 -0
  212. package/dist/types/core/index.d.ts +38 -0
  213. package/dist/types/core/jcs.d.ts +12 -0
  214. package/dist/types/core/jelly-host-double.d.ts +21 -0
  215. package/dist/types/core/jelly-host.d.ts +86 -0
  216. package/dist/types/core/keys.d.ts +16 -0
  217. package/dist/types/core/peer-text.d.ts +87 -0
  218. package/dist/types/core/realm.d.ts +36 -0
  219. package/dist/types/core/roster.d.ts +146 -0
  220. package/dist/types/core/schema/a2a.d.ts +251 -0
  221. package/dist/types/core/schema/common.d.ts +272 -0
  222. package/dist/types/core/schema/consent.d.ts +140 -0
  223. package/dist/types/core/schema/delegation.d.ts +58 -0
  224. package/dist/types/core/schema/directory-frames.d.ts +1301 -0
  225. package/dist/types/core/schema/directory.d.ts +375 -0
  226. package/dist/types/core/schema/envelope.d.ts +750 -0
  227. package/dist/types/core/schema/errors.d.ts +104 -0
  228. package/dist/types/core/schema/index.d.ts +15 -0
  229. package/dist/types/core/schema/ipc/consent.d.ts +555 -0
  230. package/dist/types/core/schema/ipc/device.d.ts +729 -0
  231. package/dist/types/core/schema/ipc/jelly.d.ts +501 -0
  232. package/dist/types/core/schema/ipc/shapes.d.ts +411 -0
  233. package/dist/types/core/schema/ipc/surfaces.d.ts +674 -0
  234. package/dist/types/core/schema/ipc.d.ts +3131 -0
  235. package/dist/types/core/schema/jelly.d.ts +160 -0
  236. package/dist/types/core/schema/realm.d.ts +38 -0
  237. package/dist/types/core/schema/relay.d.ts +1122 -0
  238. package/dist/types/core/schema/roster.d.ts +347 -0
  239. package/dist/types/core/sign.d.ts +13 -0
  240. package/dist/types/core/task.d.ts +6 -0
  241. package/dist/types/core/testing.d.ts +18 -0
  242. package/dist/types/core/ulid.d.ts +5 -0
  243. package/dist/types/core/version.d.ts +6 -0
  244. package/dist/types/core/wire.d.ts +14 -0
  245. package/dist/types/ipc/client.d.ts +85 -0
  246. package/dist/types/ipc/framing.d.ts +12 -0
  247. package/dist/types/ipc/index.d.ts +6 -0
  248. package/dist/types/ipc/paths.d.ts +39 -0
  249. package/dist/types/ipc/pipes.d.ts +16 -0
  250. package/dist/types/ipc/rpc.d.ts +13 -0
  251. package/dist/types/ipc/server.d.ts +94 -0
  252. package/dist/types/ipc/testing.d.ts +89 -0
  253. package/dist/types/ipc/trust.d.ts +49 -0
  254. package/dist/types/ipc/windows-acl.d.ts +56 -0
  255. package/dist/types/testing/jelly-id.d.ts +518 -0
  256. package/dist/types/zod.d.ts +1 -0
  257. package/package.json +77 -4
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Jelly ID resource tokens as a hosted realm verifies them (Jelly's contract v1.1 §4, §5, §11; spec §15.2):
3
+ * decoding, the verification steps against a key the caller has found, the key's validity window from the key
4
+ * history, the `cnf` binding, and the key archive → JWKS → key history lookup with the fetches injected (core
5
+ * does no I/O). Also the reading of Jelly's refusals when a host mints a bound token (§3, §6).
6
+ */
7
+ import { verify as ed25519Verify } from "node:crypto";
8
+ import { b64u, utf8 } from "../b64u.js";
9
+ import { assertEd25519PublicKey, publicKeyFromRaw } from "../keys.js";
10
+ import { parseWire } from "../wire.js";
11
+ import { MessagingError } from "../schema/errors.js";
12
+ import { ArchivedJellyKey, JELLY_DEVICE_SOURCE, JellyClaims, JellyHistoryKey, JellyJwk, JellyJwks, JellyKeyHistory, JellyTokenHeader, Jws, } from "../schema/jelly.js";
13
+ /** Contract v1.1 §5 step 5: a token lives at most 900 s, and its `iat` may be at most 60 s ahead of now. */
14
+ export const JELLY_MAX_LIFETIME_S = 900;
15
+ export const JELLY_IAT_LEEWAY_S = 60;
16
+ /** Contract v1.1 §5 step 2: Jelly's key set is believed for 24 h; the key history for 5 min. */
17
+ export const JELLY_JWKS_TTL_MS = 24 * 60 * 60 * 1000;
18
+ export const JELLY_HISTORY_TTL_MS = 5 * 60 * 1000;
19
+ /** An archived key's revocation is rechecked against the key history when older than this (spec §15.2). */
20
+ export const JELLY_REVOCATION_CHECK_MS = 24 * 60 * 60 * 1000;
21
+ /** An unknown `kid` refetches a key set at most this often, so a stream of unknown `kid`s cannot loop. */
22
+ export const JELLY_REFETCH_MIN_MS = 60 * 1000;
23
+ /** The largest key set or key history read; a larger answer counts as unavailable. */
24
+ const KEY_SET_BYTES = 1_048_576;
25
+ const invalid = (what) => new MessagingError("binding_invalid", what);
26
+ function segment(text, what) {
27
+ let bytes;
28
+ try {
29
+ bytes = b64u.decode(text);
30
+ }
31
+ catch {
32
+ throw invalid(`Jelly token ${what} is not base64url`);
33
+ }
34
+ try {
35
+ return parseWire(new TextDecoder("utf-8", { fatal: true }).decode(bytes), { maxBytes: 8192 });
36
+ }
37
+ catch {
38
+ throw invalid(`Jelly token ${what} is not JSON`);
39
+ }
40
+ }
41
+ /**
42
+ * Decodes a compact resource token: three base64url segments, a header with `alg: EdDSA`, `typ:
43
+ * jelly-resource+jwt` and a printable `kid`, and claims of the contract's shape (contract v1.1 §4, §5 step 1;
44
+ * `cnf`, when present, exactly `{jwk: {kty, crv, x}}`). Verifies nothing. Throws `binding_invalid`.
45
+ */
46
+ export function decodeJellyToken(token) {
47
+ if (!Jws.safeParse(token).success)
48
+ throw invalid("Jelly token is not a compact JWS");
49
+ const [header, payload, signature] = token.split(".");
50
+ const parsedHeader = JellyTokenHeader.safeParse(segment(header, "header"));
51
+ if (!parsedHeader.success)
52
+ throw invalid("Jelly token header is not EdDSA jelly-resource+jwt with a key id");
53
+ const claims = JellyClaims.safeParse(segment(payload, "claims"));
54
+ if (!claims.success) {
55
+ const at = claims.error.issues[0]?.path.map(String).join(".");
56
+ throw invalid(`Jelly token claims are malformed${at ? ` at ${at}` : ""}`);
57
+ }
58
+ let sig;
59
+ try {
60
+ sig = b64u.decode(signature);
61
+ }
62
+ catch {
63
+ throw invalid("Jelly token signature is not base64url");
64
+ }
65
+ if (sig.length !== 64)
66
+ throw invalid("Jelly token signature is not 64 bytes");
67
+ return { header: parsedHeader.data, claims: claims.data, signingInput: utf8(`${header}.${payload}`), signature: sig };
68
+ }
69
+ /**
70
+ * True when a key may have signed a token issued at `iat` (seconds): always for a key seen only in the key set;
71
+ * for a key the history describes, `iat ∈ [nbf, retired_at)` (an open end while `retired_at` is null) and
72
+ * before any `revoked_at` (contract v1.1 §5 step 2, §11).
73
+ */
74
+ export function jellyKeyCovers(key, iat) {
75
+ if (key.nbf !== undefined && iat < key.nbf)
76
+ return false;
77
+ if (key.retiredAt !== undefined && key.retiredAt !== null && iat >= key.retiredAt)
78
+ return false;
79
+ if (key.revokedAt !== undefined && key.revokedAt !== null && iat >= key.revokedAt)
80
+ return false;
81
+ return true;
82
+ }
83
+ /** True when `x` is a key Jelly ID would bind (contract v1.1 §3): a canonical Ed25519 point not of small order. */
84
+ export function isBindableKey(x) {
85
+ try {
86
+ assertEd25519PublicKey(x, "cnf key");
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /** The `cnf_jwk` a host asks Jelly ID to bind for `device` (contract v1.1 §3, §6). Throws `invalid_request` for an unusable key. */
94
+ export function cnfJwk(device) {
95
+ assertEd25519PublicKey(device, "device key");
96
+ return { kty: "OKP", crv: "Ed25519", x: device };
97
+ }
98
+ /**
99
+ * Verifies a resource token (contract v1.1 §5; spec §15.2 steps 1–6): the header, the key and its window, the
100
+ * signature, `iss` and `aud`, the lifetime, `sub`, how it was minted (`hst`, or `src: "device"` with no `hst`), and
101
+ * `cnf`. Returns the decoded token. Throws
102
+ * `binding_invalid`; never reports a missing key as anything else (finding it is `JellyKeyring`'s).
103
+ */
104
+ export function verifyJellyToken(token, options) {
105
+ const decoded = decodeJellyToken(token);
106
+ const { header, claims } = decoded;
107
+ const key = options.key(header.kid);
108
+ if (!key)
109
+ throw invalid("Jelly token names a key no source knows");
110
+ if (!jellyKeyCovers(key, claims.iat))
111
+ throw invalid("Jelly token was issued outside its key's validity");
112
+ let ok = false;
113
+ try {
114
+ ok = ed25519Verify(null, decoded.signingInput, publicKeyFromRaw("ed25519", key.x), decoded.signature);
115
+ }
116
+ catch {
117
+ ok = false;
118
+ }
119
+ if (!ok)
120
+ throw invalid("Jelly token signature does not verify");
121
+ if (claims.iss !== options.issuer)
122
+ throw invalid("Jelly token issuer is not the realm's");
123
+ if (claims.aud !== options.audience)
124
+ throw invalid("Jelly token audience is not the realm's resource");
125
+ if (claims.exp <= claims.iat || claims.exp - claims.iat > JELLY_MAX_LIFETIME_S)
126
+ throw invalid("Jelly token lifetime is not in (0, 900] seconds");
127
+ if (options.now !== undefined) {
128
+ const now = Math.floor(options.now / 1000);
129
+ if (claims.exp <= now)
130
+ throw invalid("Jelly token has expired");
131
+ if (claims.iat > now + JELLY_IAT_LEEWAY_S)
132
+ throw invalid("Jelly token is issued in the future");
133
+ }
134
+ // How it was minted (contract v1.1 §3, v1.2 §12.4): by a host (`hst`), or by a device that signed in
135
+ // through the browser (`src: "device"`, bound, no `hst`). Both, neither, or another `src` is not a token we know.
136
+ if (claims.src !== undefined) {
137
+ if (claims.src !== JELLY_DEVICE_SOURCE)
138
+ throw invalid("Jelly token was minted by a source Messaging does not know");
139
+ if (claims.hst !== undefined)
140
+ throw invalid("Jelly token names both a host and a device sign-in");
141
+ if (claims.cnf === undefined)
142
+ throw invalid("a Jelly token from a device sign-in is not bound to a key");
143
+ }
144
+ else if (claims.hst === undefined) {
145
+ throw invalid("Jelly token names neither a host nor a device sign-in");
146
+ }
147
+ if (claims.cnf !== undefined && !isBindableKey(claims.cnf.jwk.x))
148
+ throw invalid("Jelly token is bound to an unusable key");
149
+ if (options.cnf !== undefined) {
150
+ if (claims.cnf === undefined)
151
+ throw invalid("Jelly token is not bound to a key");
152
+ if (claims.cnf.jwk.x !== options.cnf)
153
+ throw invalid("Jelly token is bound to another key");
154
+ }
155
+ return decoded;
156
+ }
157
+ /**
158
+ * Finds the keys of resource tokens, in the order of spec §15.2: the client's own archive of every key it has
159
+ * verified, then Jelly ID's key set (cached 24 h; an unknown `kid` refetches it at most once a minute), then the
160
+ * key history (cached 5 min). A key found in the key set or the history joins the archive, with the history's
161
+ * window. An archived key whose revocation was last checked over 24 h ago is checked again, and keeps its archived
162
+ * window if the history cannot be fetched. Persist `archive()`; pass it back to the constructor.
163
+ */
164
+ export class JellyKeyring {
165
+ #sources;
166
+ #archive = new Map();
167
+ #jwks;
168
+ #jwksTried = Number.NEGATIVE_INFINITY;
169
+ #history;
170
+ #historyTried = Number.NEGATIVE_INFINITY;
171
+ constructor(sources, archive = []) {
172
+ this.#sources = sources;
173
+ for (const key of archive) {
174
+ const parsed = ArchivedJellyKey.safeParse(key);
175
+ if (!parsed.success)
176
+ throw new MessagingError("invalid_request", "invalid archived Jelly key");
177
+ this.#archive.set(parsed.data.kid, parsed.data);
178
+ }
179
+ }
180
+ /** Every key this client has verified, to persist. */
181
+ archive() {
182
+ return [...this.#archive.values()].map((key) => ({ ...key }));
183
+ }
184
+ /** The archived key with this `kid`, without fetching. */
185
+ archived(kid) {
186
+ return this.#archive.get(kid);
187
+ }
188
+ /**
189
+ * Finds the keys of these tokens (a token that does not decode is skipped: verifying it fails anyway) and
190
+ * returns the synchronous lookup `verifyJellyToken` takes. Throws `jelly_unavailable` when a `kid` is in no
191
+ * source and a source it needed could not be fetched: never `binding_invalid` for an unreachable Jelly ID.
192
+ */
193
+ async prepare(tokens, now) {
194
+ const kids = new Set();
195
+ for (const token of tokens) {
196
+ try {
197
+ kids.add(decodeJellyToken(token).header.kid);
198
+ }
199
+ catch {
200
+ // Undecodable: verifyJellyToken refuses it.
201
+ }
202
+ }
203
+ for (const kid of kids)
204
+ await this.resolve(kid, now);
205
+ return (kid) => this.#archive.get(kid);
206
+ }
207
+ /** Finds one key (see the class). Returns undefined when every source answered and none knows it. */
208
+ async resolve(kid, now) {
209
+ const archived = this.#archive.get(kid);
210
+ if (archived) {
211
+ if (archived.historyAt === undefined || now - archived.historyAt > JELLY_REVOCATION_CHECK_MS) {
212
+ try {
213
+ await this.#loadHistory(now);
214
+ }
215
+ catch {
216
+ // Keep the archived window; the next use checks again.
217
+ }
218
+ }
219
+ return this.#archive.get(kid);
220
+ }
221
+ let unavailable = false;
222
+ try {
223
+ const fresh = this.#jwks !== undefined && now - this.#jwks.at <= JELLY_JWKS_TTL_MS;
224
+ if (!fresh || (!this.#jwks.keys.has(kid) && now - this.#jwksTried >= JELLY_REFETCH_MIN_MS))
225
+ await this.#loadJwks(now);
226
+ }
227
+ catch {
228
+ unavailable = true;
229
+ }
230
+ const fromSet = this.#jwks?.keys.get(kid);
231
+ try {
232
+ const fresh = this.#history !== undefined && now - this.#history.at <= JELLY_HISTORY_TTL_MS;
233
+ if (!fresh || (!this.#history.keys.has(kid) && now - this.#historyTried >= JELLY_REFETCH_MIN_MS))
234
+ await this.#loadHistory(now);
235
+ }
236
+ catch {
237
+ if (!fromSet)
238
+ unavailable = true;
239
+ }
240
+ const fromHistory = this.#history?.keys.get(kid);
241
+ if (fromSet) {
242
+ // The key set chooses the key (contract v1.1 §5 step 2); the history's window applies only to the same key.
243
+ this.#archive.set(kid, { kid, x: fromSet.x, ...(fromHistory?.x === fromSet.x ? this.#window(fromHistory, now) : {}) });
244
+ }
245
+ else if (fromHistory) {
246
+ this.#archive.set(kid, { kid, x: fromHistory.x, ...this.#window(fromHistory, now) });
247
+ }
248
+ const found = this.#archive.get(kid);
249
+ if (!found && unavailable)
250
+ throw new MessagingError("jelly_unavailable", "Jelly ID's keys could not be fetched");
251
+ return found;
252
+ }
253
+ #window(key, now) {
254
+ return { nbf: key.nbf, retiredAt: key.retired_at, revokedAt: key.revoked_at ?? null, historyAt: now };
255
+ }
256
+ async #loadJwks(now) {
257
+ this.#jwksTried = now;
258
+ const set = JellyJwks.parse(parseWire(await this.#sources.fetchJwks(), { maxBytes: KEY_SET_BYTES }));
259
+ const keys = new Map();
260
+ for (const entry of set.keys) {
261
+ const key = JellyJwk.safeParse(entry);
262
+ if (key.success && isBindableKey(key.data.x))
263
+ keys.set(key.data.kid, key.data);
264
+ }
265
+ this.#jwks = { at: now, keys };
266
+ }
267
+ async #loadHistory(now) {
268
+ this.#historyTried = now;
269
+ const history = JellyKeyHistory.parse(parseWire(await this.#sources.fetchHistory(), { maxBytes: KEY_SET_BYTES }));
270
+ const keys = new Map();
271
+ for (const entry of history.keys) {
272
+ const key = JellyHistoryKey.safeParse(entry);
273
+ if (key.success && isBindableKey(key.data.x))
274
+ keys.set(key.data.kid, key.data);
275
+ }
276
+ this.#history = { at: now, keys };
277
+ // Every archived key the history names takes its current window; a key under another `x` is not the same key.
278
+ for (const [kid, archived] of this.#archive) {
279
+ const seen = keys.get(kid);
280
+ if (seen && seen.x === archived.x)
281
+ this.#archive.set(kid, { kid, x: archived.x, ...this.#window(seen, now) });
282
+ else if (!seen)
283
+ this.#archive.set(kid, { ...archived, historyAt: now });
284
+ }
285
+ }
286
+ }
287
+ /**
288
+ * Reads a refusal from Jelly ID's mint route (`{error, detail?}`) or the engine accessor (an `Error` with
289
+ * `code` and `detail`). Branches on `detail`: a plain `invalid_target` still means no such resource.
290
+ */
291
+ export function jellyMintRefusal(code, detail) {
292
+ switch (code) {
293
+ case "invalid_target":
294
+ return detail === "invalid_cnf_jwk" ? "unbindable_key" : "no_resource";
295
+ case "access_denied":
296
+ return "denied";
297
+ case "not_enrolled":
298
+ case "invalid_client":
299
+ return "not_enrolled";
300
+ case "invalid_request":
301
+ return "bad_request";
302
+ default:
303
+ return "retry";
304
+ }
305
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Keys statements (spec §15.4): a hosted device's self-signed encryption key. They are not log entries, so a
3
+ * rotation does not grow the log; the map commits to each current device's latest statement.
4
+ */
5
+ import { asMessagingError, parseWith } from "../check.js";
6
+ import { canonicalize } from "../jcs.js";
7
+ import { sha256 } from "../hash.js";
8
+ import { sign, verify } from "../sign.js";
9
+ import { DOMAIN, LIMITS, PROTOCOL } from "../schema/common.js";
10
+ import { KeysStatement } from "../schema/directory.js";
11
+ import { MessagingError } from "../schema/errors.js";
12
+ /** `SHA-256(JCS(statement))`, the signature included: what the map commits to. */
13
+ export function keysStatementHash(statement) {
14
+ return sha256(canonicalize(statement));
15
+ }
16
+ /** This device's statement of its current encryption key. Throws `invalid_request`. */
17
+ export function createKeysStatement(identity, args) {
18
+ const unsigned = { v: PROTOCOL, realm: args.realm, person: args.person, device: identity.machine, enc: identity.enc, seq: args.seq, ts: args.ts };
19
+ return parseWith(KeysStatement, { ...unsigned, sig: sign(identity, DOMAIN.deviceKeys, unsigned) }, "keys statement");
20
+ }
21
+ /**
22
+ * Checks a keys statement: the schema and `LIMITS.keysStatementBytes` (`invalid_request`, `too_large`), then
23
+ * the realm, the person, and the device's signature (`binding_invalid`). Whether the device is current, and
24
+ * whether `seq` is above the last one seen, are the caller's (`verifyPersonView` checks both).
25
+ */
26
+ export function verifyKeysStatement(statement, context) {
27
+ try {
28
+ const parsed = parseWith(KeysStatement, statement, "keys statement");
29
+ if (Buffer.byteLength(canonicalize(parsed), "utf8") > LIMITS.keysStatementBytes)
30
+ throw new MessagingError("too_large", "keys statement is over its size limit");
31
+ if (parsed.realm !== context.realm)
32
+ throw new MessagingError("binding_invalid", "keys statement is for another realm");
33
+ if (parsed.person !== context.person)
34
+ throw new MessagingError("binding_invalid", "keys statement is for another person");
35
+ const { sig, ...unsigned } = parsed;
36
+ if (!verify(parsed.device, DOMAIN.deviceKeys, unsigned, sig))
37
+ throw new MessagingError("binding_invalid", "keys statement signature does not verify");
38
+ return parsed;
39
+ }
40
+ catch (error) {
41
+ throw asMessagingError(error, "keys statement");
42
+ }
43
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Approving a new device by link (spec §15.3): the new device shows a signed link request as text (and a QR
3
+ * code) with a six-digit short authentication string; one of its person's Jelly-vouched devices checks both and
4
+ * appends the `bind`.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { b64u, utf8 } from "../b64u.js";
8
+ import { asMessagingError, parseWith } from "../check.js";
9
+ import { canonicalize } from "../jcs.js";
10
+ import { sign, verify } from "../sign.js";
11
+ import { parseWire } from "../wire.js";
12
+ import { DOMAIN, LIMITS, PROTOCOL } from "../schema/common.js";
13
+ import { LinkRequest } from "../schema/directory.js";
14
+ import { MessagingError } from "../schema/errors.js";
15
+ /** The prefix of a link request's text form. A link is not a secret, but it should not travel through chat. */
16
+ export const LINK_PREFIX = "messaging-link.";
17
+ /** The largest link text read: far above a request's size. */
18
+ const LINK_TEXT_BYTES = 2048;
19
+ /** The new device's signed link request. Throws `invalid_request`. */
20
+ export function createLinkRequest(identity, args) {
21
+ if (args.nonce.length !== 16)
22
+ throw new MessagingError("invalid_request", "link nonce must be 16 bytes");
23
+ const unsigned = { v: PROTOCOL, realm: args.realm, device: identity.machine, enc: identity.enc, label: args.label, kind: args.kind, nonce: b64u.encode(args.nonce), ts: args.ts };
24
+ return parseWith(LinkRequest, { ...unsigned, sig: sign(identity, DOMAIN.deviceLink, unsigned) }, "link request");
25
+ }
26
+ /** `messaging-link.` and the base64url of the request's JCS. */
27
+ export function encodeLink(link) {
28
+ return LINK_PREFIX + b64u.encode(utf8(canonicalize(parseWith(LinkRequest, link, "link request"))));
29
+ }
30
+ /** Reads a link's text (surrounding whitespace ignored), with `parseWire`. Verifies nothing. Throws `invalid_request`. */
31
+ export function decodeLink(text) {
32
+ const trimmed = typeof text === "string" ? text.trim() : "";
33
+ if (!trimmed.startsWith(LINK_PREFIX) || trimmed.length > LINK_TEXT_BYTES)
34
+ throw new MessagingError("invalid_request", "not a messaging-link text");
35
+ let json;
36
+ try {
37
+ json = new TextDecoder("utf-8", { fatal: true }).decode(b64u.decode(trimmed.slice(LINK_PREFIX.length)));
38
+ }
39
+ catch {
40
+ throw new MessagingError("invalid_request", "link text is not base64url of UTF-8");
41
+ }
42
+ return parseWith(LinkRequest, parseWire(json, { maxBytes: LINK_TEXT_BYTES }), "link request");
43
+ }
44
+ /** True when the request's signature is its device's. Never throws. */
45
+ export function verifyLinkSignature(link) {
46
+ try {
47
+ const { sig, ...unsigned } = link;
48
+ return verify(link.device, DOMAIN.deviceLink, unsigned, sig);
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /**
55
+ * What an approving device checks before it shows the request (§15.3): the schema (`invalid_request`), the
56
+ * signature and the realm (`binding_invalid`), and `ts` no older than `LIMITS.linkTtlMs` and no later than the
57
+ * clock skew ahead of `now` (`expired`).
58
+ */
59
+ export function verifyLinkRequest(link, context) {
60
+ try {
61
+ const parsed = parseWith(LinkRequest, link, "link request");
62
+ if (!verifyLinkSignature(parsed))
63
+ throw new MessagingError("binding_invalid", "link request signature does not verify");
64
+ if (parsed.realm !== context.realm)
65
+ throw new MessagingError("binding_invalid", "link request is for another realm");
66
+ if (parsed.ts > context.now + LIMITS.clockSkewMs || context.now - parsed.ts > LIMITS.linkTtlMs)
67
+ throw new MessagingError("expired", "link request is outside its ten minutes");
68
+ return parsed;
69
+ }
70
+ catch (error) {
71
+ throw asMessagingError(error, "link request");
72
+ }
73
+ }
74
+ /**
75
+ * The six-digit short authentication string both devices show: the first four bytes of SHA-256(JCS(request)),
76
+ * big-endian, modulo 1,000,000, zero-padded. The person compares the two before approving.
77
+ */
78
+ export function linkSas(link) {
79
+ const digest = createHash("sha256").update(utf8(canonicalize(parseWith(LinkRequest, link, "link request")))).digest();
80
+ return String(digest.readUInt32BE(0) % 1_000_000).padStart(6, "0");
81
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * An RFC 9162 (Certificate Transparency 2.0) Merkle tree over SHA-256: the hashes, the tree head, and audit
3
+ * paths and consistency proofs, made and verified (spec §15.6). The epoch log of a hosted realm is one.
4
+ * Hashes cross the API as base64url (`Hash`); a malformed one makes a verification false, never a throw.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { b64u } from "../b64u.js";
8
+ import { MessagingError } from "../schema/errors.js";
9
+ const LEAF = Uint8Array.of(0x00);
10
+ const NODE = Uint8Array.of(0x01);
11
+ function digest(...parts) {
12
+ const hash = createHash("sha256");
13
+ for (const part of parts)
14
+ hash.update(part);
15
+ return new Uint8Array(hash.digest());
16
+ }
17
+ /** `SHA-256(0x00 ‖ data)`, the hash of a leaf. */
18
+ export function merkleLeafHash(data) {
19
+ return b64u.encode(digest(LEAF, data));
20
+ }
21
+ function node(left, right) {
22
+ return digest(NODE, left, right);
23
+ }
24
+ /** The largest power of two below `n` (n ≥ 2). */
25
+ function split(n) {
26
+ let k = 1;
27
+ while (k * 2 < n)
28
+ k *= 2;
29
+ return k;
30
+ }
31
+ function decode(hash) {
32
+ try {
33
+ const bytes = b64u.decode(hash);
34
+ return bytes.length === 32 ? bytes : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ function equal(a, b) {
41
+ if (a.length !== b.length)
42
+ return false;
43
+ let diff = 0;
44
+ for (let i = 0; i < a.length; i++)
45
+ diff |= a[i] ^ b[i];
46
+ return diff === 0;
47
+ }
48
+ /** True for 1, 2, 4, …: exact for every safe integer (no 32-bit bitwise operators). */
49
+ function isPowerOfTwo(n) {
50
+ while (n > 1 && n % 2 === 0)
51
+ n /= 2;
52
+ return n === 1;
53
+ }
54
+ function isSize(n) {
55
+ return Number.isSafeInteger(n) && n >= 0;
56
+ }
57
+ /**
58
+ * The leaf hashes of a log, appended in order. It answers the tree head at any size it holds, and the audit
59
+ * path and consistency proof between sizes (RFC 9162 §2.1.1-§2.1.4). Subtree roots are cached, so a proof
60
+ * costs O(log² n) hashes after the first.
61
+ */
62
+ export class MerkleLog {
63
+ #leaves = [];
64
+ #cache = new Map();
65
+ /** A log of these leaf hashes (from `merkleLeafHash`), in order. */
66
+ static fromLeafHashes(hashes) {
67
+ const log = new MerkleLog();
68
+ for (const hash of hashes)
69
+ log.appendLeafHash(hash);
70
+ return log;
71
+ }
72
+ get size() {
73
+ return this.#leaves.length;
74
+ }
75
+ /** Appends the leaf whose data is `data`; returns its index. */
76
+ append(data) {
77
+ return this.appendLeafHash(merkleLeafHash(data));
78
+ }
79
+ /** Appends a leaf by its hash; returns its index. */
80
+ appendLeafHash(hash) {
81
+ const bytes = decode(hash);
82
+ if (!bytes)
83
+ throw new MessagingError("invalid_request", "leaf hash must be 32 bytes of base64url");
84
+ this.#leaves.push(bytes);
85
+ return this.#leaves.length - 1;
86
+ }
87
+ /** The root the log would have with the leaf `hash` appended; the log itself does not change. */
88
+ rootWith(hash) {
89
+ const bytes = decode(hash);
90
+ if (!bytes)
91
+ throw new MessagingError("invalid_request", "leaf hash must be 32 bytes of base64url");
92
+ const size = this.#leaves.length;
93
+ this.#leaves.push(bytes);
94
+ try {
95
+ return b64u.encode(this.#mth(0, size + 1));
96
+ }
97
+ finally {
98
+ this.#leaves.pop();
99
+ // The complete subtrees that end at the new leaf are not this log's.
100
+ for (let n = 1; n <= size + 1; n *= 2)
101
+ this.#cache.delete(`${size + 1 - n}:${n}`);
102
+ }
103
+ }
104
+ leafHash(index) {
105
+ this.#size(index + 1);
106
+ return b64u.encode(this.#leaves[index]);
107
+ }
108
+ /** MTH of the first `size` leaves (default: all). The empty tree's root is SHA-256 of nothing. */
109
+ root(size = this.size) {
110
+ this.#size(size);
111
+ return b64u.encode(size === 0 ? digest() : this.#mth(0, size));
112
+ }
113
+ /** The audit path of leaf `index` in the tree of the first `size` leaves, bottom up (RFC 9162 §2.1.3.1). */
114
+ inclusionProof(index, size = this.size) {
115
+ this.#size(size);
116
+ if (!isSize(index) || index >= size)
117
+ throw new MessagingError("invalid_request", "leaf index is beyond the tree size");
118
+ return this.#path(index, 0, size).map((hash) => b64u.encode(hash));
119
+ }
120
+ /** The consistency proof from the tree of `from` leaves to the tree of `size` leaves (RFC 9162 §2.1.4.1). */
121
+ consistencyProof(from, size = this.size) {
122
+ this.#size(size);
123
+ if (!isSize(from) || from > size)
124
+ throw new MessagingError("invalid_request", "consistency proof from a size beyond the tree");
125
+ if (from === 0 || from === size)
126
+ return [];
127
+ return this.#subproof(from, 0, size, true).map((hash) => b64u.encode(hash));
128
+ }
129
+ #size(size) {
130
+ if (!isSize(size) || size > this.#leaves.length)
131
+ throw new MessagingError("invalid_request", "tree size is beyond the log");
132
+ }
133
+ /** MTH(D[start:start+n]), n ≥ 1. Complete subtrees (n a power of two) are cached: they never change. */
134
+ #mth(start, n) {
135
+ if (n === 1)
136
+ return this.#leaves[start];
137
+ const complete = isPowerOfTwo(n);
138
+ const key = `${start}:${n}`;
139
+ if (complete) {
140
+ const cached = this.#cache.get(key);
141
+ if (cached)
142
+ return cached;
143
+ }
144
+ const k = split(n);
145
+ const hash = node(this.#mth(start, k), this.#mth(start + k, n - k));
146
+ if (complete)
147
+ this.#cache.set(key, hash);
148
+ return hash;
149
+ }
150
+ /** PATH(m, D[start:start+n]). */
151
+ #path(m, start, n) {
152
+ if (n === 1)
153
+ return [];
154
+ const k = split(n);
155
+ return m < k ? [...this.#path(m, start, k), this.#mth(start + k, n - k)] : [...this.#path(m - k, start + k, n - k), this.#mth(start, k)];
156
+ }
157
+ /** SUBPROOF(m, D[start:start+n], b). */
158
+ #subproof(m, start, n, whole) {
159
+ if (m === n)
160
+ return whole ? [] : [this.#mth(start, n)];
161
+ const k = split(n);
162
+ return m <= k
163
+ ? [...this.#subproof(m, start, k, whole), this.#mth(start + k, n - k)]
164
+ : [...this.#subproof(m - k, start + k, n - k, false), this.#mth(start, k)];
165
+ }
166
+ }
167
+ /**
168
+ * True when `proof` is the audit path of the leaf with hash `leafHash` at `index` in a tree of `size` leaves
169
+ * whose root is `root` (RFC 9162 §2.1.3.2). Never throws.
170
+ */
171
+ export function verifyInclusion(index, size, leafHash, proof, root) {
172
+ if (!isSize(index) || !isSize(size) || index >= size || !Array.isArray(proof))
173
+ return false;
174
+ const leaf = decode(leafHash);
175
+ const expected = decode(root);
176
+ const path = proof.map(decode);
177
+ if (!leaf || !expected || path.some((hash) => !hash))
178
+ return false;
179
+ let fn = index;
180
+ let sn = size - 1;
181
+ let r = leaf;
182
+ for (const p of path) {
183
+ if (sn === 0)
184
+ return false;
185
+ if (fn % 2 === 1 || fn === sn) {
186
+ r = node(p, r);
187
+ if (fn % 2 === 0) {
188
+ while (fn % 2 === 0 && fn !== 0) {
189
+ fn = Math.floor(fn / 2);
190
+ sn = Math.floor(sn / 2);
191
+ }
192
+ }
193
+ }
194
+ else {
195
+ r = node(r, p);
196
+ }
197
+ fn = Math.floor(fn / 2);
198
+ sn = Math.floor(sn / 2);
199
+ }
200
+ return sn === 0 && equal(r, expected);
201
+ }
202
+ /**
203
+ * True when `proof` shows that the tree of `size1` leaves with root `root1` is a prefix of the tree of `size2`
204
+ * leaves with root `root2` (RFC 9162 §2.1.4.2). Equal sizes need an empty proof and equal roots; a proof from
205
+ * the empty tree proves nothing and is false. Never throws.
206
+ */
207
+ export function verifyConsistency(size1, size2, proof, root1, root2) {
208
+ if (!isSize(size1) || !isSize(size2) || size1 === 0 || size2 < size1 || !Array.isArray(proof))
209
+ return false;
210
+ const first = decode(root1);
211
+ const second = decode(root2);
212
+ const path = proof.map(decode);
213
+ if (!first || !second || path.some((hash) => !hash))
214
+ return false;
215
+ if (size1 === size2)
216
+ return path.length === 0 && equal(first, second);
217
+ const c = [...path];
218
+ if (c.length === 0)
219
+ return false;
220
+ if (isPowerOfTwo(size1))
221
+ c.unshift(first);
222
+ let fn = size1 - 1;
223
+ let sn = size2 - 1;
224
+ while (fn % 2 === 1) {
225
+ fn = Math.floor(fn / 2);
226
+ sn = Math.floor(sn / 2);
227
+ }
228
+ let fr = c[0];
229
+ let sr = c[0];
230
+ for (const p of c.slice(1)) {
231
+ if (sn === 0)
232
+ return false;
233
+ if (fn % 2 === 1 || fn === sn) {
234
+ fr = node(p, fr);
235
+ sr = node(p, sr);
236
+ if (fn % 2 === 0) {
237
+ while (fn % 2 === 0 && fn !== 0) {
238
+ fn = Math.floor(fn / 2);
239
+ sn = Math.floor(sn / 2);
240
+ }
241
+ }
242
+ }
243
+ else {
244
+ sr = node(sr, p);
245
+ }
246
+ fn = Math.floor(fn / 2);
247
+ sn = Math.floor(sn / 2);
248
+ }
249
+ return sn === 0 && equal(fr, first) && equal(sr, second);
250
+ }