@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,197 @@
1
+ /**
2
+ * Verifying a person (spec §15.7): a client checks what the directory serves of one person against the STH it
3
+ * adopted: the log extends what it holds, every entry passes the authority rules, every Jelly binding first
4
+ * appeared in an epoch before its token expired, the keys statements are the current devices', and the map at
5
+ * the STH's last epoch commits to exactly that log head and those keys.
6
+ */
7
+ import { asMessagingError, parseWith } from "../check.js";
8
+ import { PersonHead, PersonView } from "../schema/directory.js";
9
+ import { MessagingError } from "../schema/errors.js";
10
+ import { DeviceLog, logEntryHash } from "./device-log.js";
11
+ import { verifyEpochProof } from "./epoch.js";
12
+ import { keysStatementHash, verifyKeysStatement } from "./keys-statement.js";
13
+ import { mapValue, TOMBSTONE, verifyMapProof } from "./smt.js";
14
+ /** `directory_inconsistent`, with the evidence a client keeps and shows in its alarm. Its message never holds values. */
15
+ export class DirectoryInconsistency extends MessagingError {
16
+ evidence;
17
+ constructor(message, evidence) {
18
+ super("directory_inconsistent", message);
19
+ this.name = "DirectoryInconsistency";
20
+ this.evidence = evidence;
21
+ }
22
+ }
23
+ const fail = (what) => new MessagingError("directory_inconsistent", what);
24
+ /**
25
+ * Verifies one person's view and returns their new state (spec §15.7). Errors: `invalid_request` (shapes),
26
+ * `binding_invalid` (an entry, a keys statement, or a binding that first appeared in an epoch after its token's
27
+ * life), the log's own codes, and `directory_inconsistent` (a `DirectoryInconsistency` with evidence when the
28
+ * directory contradicts what the client holds; a plain one for a proof that fails).
29
+ */
30
+ export function verifyPersonView(view, context) {
31
+ try {
32
+ const parsed = parseWith(PersonView, view, "person view");
33
+ const { sth, known } = context;
34
+ if (known && known.person !== parsed.person)
35
+ throw new MessagingError("invalid_request", "the known state is another person's");
36
+ const head = verifyEpochProof(sth, context.head);
37
+ if (head.index !== sth.size - 1)
38
+ throw fail("the head epoch is not the tree head's last");
39
+ const mapRoot = head.leaf.mapRoot;
40
+ const logContext = { ...context, person: parsed.person };
41
+ if (parsed.tombstone) {
42
+ if (parsed.log.length > 0 || parsed.keys.length > 0 || (parsed.epochs?.length ?? 0) > 0)
43
+ throw new MessagingError("invalid_request", "an erased person has no log or keys");
44
+ if (!verifyMapProof(mapRoot, parsed.person, TOMBSTONE, parsed.mapProof))
45
+ throw new DirectoryInconsistency("the map does not hold the tombstone", { kind: "map", sth, head });
46
+ return { person: parsed.person, log: known?.log ?? new DeviceLog(context.realm, parsed.person), keys: new Map(), erased: true };
47
+ }
48
+ // 1. The log extends what the client holds; an overlap must be the same entries.
49
+ const log = known ? known.log.clone() : new DeviceLog(context.realm, parsed.person);
50
+ const held = log.length;
51
+ let start = held;
52
+ if (parsed.log.length > 0) {
53
+ const first = parsed.log[0].seq;
54
+ if (first > held)
55
+ throw fail("the log skips entries after those held");
56
+ for (const entry of parsed.log.slice(0, Math.max(0, held - first))) {
57
+ const ours = log.entries()[entry.seq];
58
+ if (logEntryHash(ours) !== logEntryHash(entry))
59
+ throw new DirectoryInconsistency("the log differs from the one held", { kind: "fork", seq: entry.seq, ours, theirs: entry });
60
+ }
61
+ start = Math.max(held, first);
62
+ for (const entry of parsed.log.slice(start - first)) {
63
+ try {
64
+ log.append(entry, logContext);
65
+ }
66
+ catch (error) {
67
+ if (error instanceof MessagingError && error.code === "log_conflict" && held > 0 && entry.seq === held) {
68
+ throw new DirectoryInconsistency("the log forks from the one held", { kind: "fork", seq: held - 1, ours: log.entries()[held - 1], theirs: entry });
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+ }
74
+ // 2. Every new Jelly binding first appeared in an epoch before its token expired (freshness at the append).
75
+ const witnesses = new Map((parsed.epochs ?? []).map((witness) => [witness.seq, witness]));
76
+ for (let seq = start; seq < log.length; seq++) {
77
+ const deadline = log.bindingDeadline(seq);
78
+ if (deadline === undefined)
79
+ continue;
80
+ const witness = witnesses.get(seq);
81
+ if (!witness)
82
+ throw new MessagingError("binding_invalid", "a Jelly binding comes without the epoch it first appeared in");
83
+ checkWitness(witness, seq, deadline, log, parsed.person, sth);
84
+ }
85
+ // 3. The keys statements: one per current device, each signed by it, none older than one held.
86
+ const keys = new Map();
87
+ for (const raw of parsed.keys) {
88
+ const statement = verifyKeysStatement(raw, { realm: context.realm, person: parsed.person });
89
+ if (!log.device(statement.device)?.current)
90
+ throw new MessagingError("binding_invalid", "a keys statement is for a device that is not current");
91
+ if (keys.has(statement.device))
92
+ throw new MessagingError("invalid_request", "two keys statements for one device");
93
+ const ours = known?.keys.get(statement.device);
94
+ if (ours && statement.seq < ours.seq)
95
+ throw new DirectoryInconsistency("a keys statement is older than the one held", { kind: "rollback", what: "keys", ours: ours.seq, theirs: statement.seq });
96
+ if (ours && statement.seq === ours.seq && keysStatementHash(statement) !== keysStatementHash(ours))
97
+ throw fail("two keys statements share a device and seq");
98
+ keys.set(statement.device, statement);
99
+ }
100
+ for (const [device, ours] of known?.keys ?? []) {
101
+ if (log.device(device)?.current && !keys.has(device))
102
+ throw new DirectoryInconsistency("a held keys statement is withheld", { kind: "rollback", what: "keys", ours: ours.seq, theirs: -1 });
103
+ }
104
+ // 4. The map at the tree head commits to exactly this head and these keys.
105
+ const logHead = log.head();
106
+ if (!logHead) {
107
+ if (!verifyMapProof(mapRoot, parsed.person, undefined, parsed.mapProof))
108
+ throw new DirectoryInconsistency("the map holds a person served as absent", { kind: "map", sth, head });
109
+ }
110
+ else {
111
+ const value = mapValue({ log: logHead, keys: [...keys.values()].sort((a, b) => (a.device < b.device ? -1 : 1)).map(keysStatementHash) });
112
+ if (!verifyMapProof(mapRoot, parsed.person, value, parsed.mapProof))
113
+ throw new DirectoryInconsistency("the map does not commit to the log and keys served", { kind: "map", sth, head });
114
+ }
115
+ return { person: parsed.person, log, keys, erased: false };
116
+ }
117
+ catch (error) {
118
+ throw asMessagingError(error, "person view");
119
+ }
120
+ }
121
+ /**
122
+ * Two states of one person, each verified (`verifyPersonView`) at its own tree head of one adopted epoch log, `earlier`
123
+ * at a size at most `later`'s (spec §15.9, "One tree head per view"). The later must extend the earlier: the same
124
+ * entries at every seq the earlier holds, and, for each device still current, a keys statement at least as new.
125
+ * A client that verified two views of a person concurrently checks them against each other with this instead of
126
+ * one against the other's tree head. An erased state on either side is not compared (an erase drops the log).
127
+ * Throws a `DirectoryInconsistency` with the evidence.
128
+ */
129
+ export function checkPersonSuccession(earlier, later) {
130
+ if (earlier.person !== later.person)
131
+ throw new MessagingError("invalid_request", "the two states are of two persons");
132
+ if (earlier.erased || later.erased)
133
+ return;
134
+ const ours = earlier.log.entries();
135
+ const theirs = later.log.entries();
136
+ if (theirs.length < ours.length)
137
+ throw new DirectoryInconsistency("a later tree head serves a shorter log", { kind: "rollback", what: "log", ours: ours.length, theirs: theirs.length });
138
+ for (let seq = 0; seq < ours.length; seq++) {
139
+ if (logEntryHash(ours[seq]) !== logEntryHash(theirs[seq]))
140
+ throw new DirectoryInconsistency("the log differs from the one held", { kind: "fork", seq, ours: ours[seq], theirs: theirs[seq] });
141
+ }
142
+ for (const [device, statement] of earlier.keys) {
143
+ if (!later.log.device(device)?.current)
144
+ continue;
145
+ const next = later.keys.get(device);
146
+ if (!next)
147
+ throw new DirectoryInconsistency("a held keys statement is withheld", { kind: "rollback", what: "keys", ours: statement.seq, theirs: -1 });
148
+ if (next.seq < statement.seq)
149
+ throw new DirectoryInconsistency("a keys statement is older than the one held", { kind: "rollback", what: "keys", ours: statement.seq, theirs: next.seq });
150
+ if (next.seq === statement.seq && keysStatementHash(next) !== keysStatementHash(statement))
151
+ throw fail("two keys statements share a device and seq");
152
+ }
153
+ }
154
+ /** Entry `seq` was in the map by an epoch whose `ts` is at most `deadline` (spec §15.7). Throws. */
155
+ function checkWitness(witness, seq, deadline, log, person, sth) {
156
+ const epoch = verifyEpochProof(sth, witness.epoch);
157
+ const committed = witness.value.log;
158
+ if (committed.seq < seq || log.hashAt(committed.seq) !== committed.hash)
159
+ throw fail("an epoch witness commits to another log");
160
+ if (!verifyMapProof(epoch.leaf.mapRoot, person, mapValue(witness.value), witness.mapProof))
161
+ throw fail("an epoch witness's map proof does not verify");
162
+ if (epoch.leaf.ts > deadline)
163
+ throw new MessagingError("binding_invalid", "a Jelly binding first appeared after its token's life");
164
+ }
165
+ /**
166
+ * Verifies one person's head (spec §7, `dir.heads`) against the STH a client adopted: the map at its last epoch holds
167
+ * exactly that value (`mapValue` of the preimage), the tombstone, or nothing. It proves the log head a device binding
168
+ * to the person extends (`seq + 1`, `prev` = `hash`), and nothing about the entries before it.
169
+ */
170
+ export function verifyPersonHead(view, context) {
171
+ try {
172
+ const parsed = parseWith(PersonHead, view, "person head");
173
+ const { sth } = context;
174
+ const head = verifyEpochProof(sth, context.head);
175
+ if (head.index !== sth.size - 1)
176
+ throw fail("the head epoch is not the tree head's last");
177
+ const mapRoot = head.leaf.mapRoot;
178
+ if (parsed.tombstone) {
179
+ if (parsed.value)
180
+ throw new MessagingError("invalid_request", "an erased person has no head");
181
+ if (!verifyMapProof(mapRoot, parsed.person, TOMBSTONE, parsed.mapProof))
182
+ throw new DirectoryInconsistency("the map does not hold the tombstone", { kind: "map", sth, head });
183
+ return { person: parsed.person, state: "erased" };
184
+ }
185
+ if (!parsed.value) {
186
+ if (!verifyMapProof(mapRoot, parsed.person, undefined, parsed.mapProof))
187
+ throw new DirectoryInconsistency("the map holds a person served as absent", { kind: "map", sth, head });
188
+ return { person: parsed.person, state: "absent" };
189
+ }
190
+ if (!verifyMapProof(mapRoot, parsed.person, mapValue(parsed.value), parsed.mapProof))
191
+ throw new DirectoryInconsistency("the map does not commit to the head served", { kind: "map", sth, head });
192
+ return { person: parsed.person, state: "live", log: parsed.value.log, keys: parsed.value.keys };
193
+ }
194
+ catch (error) {
195
+ throw asMessagingError(error, "person head");
196
+ }
197
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The hosted `PersonId` (spec §15.1): a Jelly account `(iss, sub)` hashed into `p_` and 26 base32 characters
3
+ * (130 bits). A roster person name cannot collide with it: `_` is not allowed in a person name.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { utf8 } from "../b64u.js";
7
+ import { parseWith } from "../check.js";
8
+ import { canonicalize } from "../jcs.js";
9
+ import { DOMAIN } from "../schema/common.js";
10
+ import { JellySubject } from "../schema/jelly.js";
11
+ import { z } from "../../vendor/zod/index.js";
12
+ const BASE32 = "abcdefghijklmnopqrstuvwxyz234567";
13
+ /** RFC 4648 §6 base32 in lower case, without padding. */
14
+ export function base32Lower(bytes) {
15
+ let out = "";
16
+ let buffer = 0;
17
+ let bits = 0;
18
+ for (const byte of bytes) {
19
+ buffer = (buffer << 8) | byte;
20
+ bits += 8;
21
+ while (bits >= 5) {
22
+ out += BASE32[(buffer >>> (bits - 5)) & 31];
23
+ bits -= 5;
24
+ }
25
+ buffer &= (1 << bits) - 1;
26
+ }
27
+ if (bits > 0)
28
+ out += BASE32[(buffer << (5 - bits)) & 31];
29
+ return out;
30
+ }
31
+ const Account = z.object({ iss: z.string().min(1).max(2048), sub: JellySubject });
32
+ /**
33
+ * `"p_" + base32lower(SHA-256(UTF-8("messaging/1/person\n") ‖ UTF-8(JCS({iss, sub}))))[0..26]`. `iss` is the
34
+ * issuer exactly as the token states it; `sub` must be a Jelly account id (`acc_…`). Throws `invalid_request`.
35
+ */
36
+ export function personId(iss, sub) {
37
+ const account = parseWith(Account, { iss, sub }, "Jelly account");
38
+ const digest = createHash("sha256").update(utf8(DOMAIN.person)).update(utf8(canonicalize(account))).digest();
39
+ return `p_${base32Lower(digest).slice(0, 26)}`;
40
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A directory realm (spec §15.8): what a daemon home pins of its key directory, and the realm id that follows
3
+ * from it. The id commits to the directory's origin, the Jelly issuer that vouches for its persons and the epoch
4
+ * key that signs its tree heads, so two directories never share a realm and a client can tell which one an
5
+ * envelope belongs to.
6
+ */
7
+ import { z } from "../../vendor/zod/index.js";
8
+ import { parseWith } from "../check.js";
9
+ import { sha256 } from "../hash.js";
10
+ import { canonicalize } from "../jcs.js";
11
+ import { assertEd25519PublicKey } from "../keys.js";
12
+ import { Key32, RelayUrl } from "../schema/common.js";
13
+ import { MessagingError } from "../schema/errors.js";
14
+ /** The Jelly ID issuer, as its tokens write `iss`: an `https://` URL, or `http://` to a loopback host (tests). */
15
+ const IssuerUrl = z.string().max(2048).refine(isIssuerUrl, "an https:// issuer, or http:// on a loopback host");
16
+ /** What a daemon's `config.json` pins of a directory realm (design §1.1): its relay, issuer, resource and epoch key. */
17
+ export const DirectoryRealmConfig = z.object({
18
+ kind: z.literal("directory"),
19
+ url: RelayUrl,
20
+ issuer: IssuerUrl,
21
+ /** The `aud` of the realm's Jelly tokens. */
22
+ resource: z.string().min(1).max(2048),
23
+ /** The directory's epoch key, Ed25519 `x`: the only key that may sign the first STH this home adopts. */
24
+ key: Key32,
25
+ /**
26
+ * Until Jelly contract v1.2: the domains whose verified emails make a person a member of org `domain:<d>`
27
+ * (design §2.3). A person whose email domain is not listed is an individual.
28
+ */
29
+ orgDomains: z.array(z.string().regex(/^(?:[a-z0-9-]+\.)+[a-z0-9-]+$/).max(253)).max(1000).optional(),
30
+ });
31
+ /**
32
+ * `base64url(SHA-256(JCS({kind: "directory", origin, issuer, key})))`, where `origin` is the WHATWG origin of the
33
+ * relay URL. Throws `invalid_request` for a config that does not parse or a key that is not a usable Ed25519 key.
34
+ */
35
+ export function directoryRealmId(config) {
36
+ const parsed = parseWith(DirectoryRealmConfig.pick({ url: true, issuer: true, key: true }), { url: config.url, issuer: config.issuer, key: config.key }, "directory realm");
37
+ assertEd25519PublicKey(parsed.key, "directory epoch key");
38
+ const origin = new URL(parsed.url).origin;
39
+ if (origin === "null")
40
+ throw new MessagingError("invalid_request", "directory realm url has no origin");
41
+ return sha256(canonicalize({ kind: "directory", origin, issuer: parsed.issuer, key: parsed.key }));
42
+ }
43
+ function loopback(host) {
44
+ return host === "localhost" || host === "[::1]" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
45
+ }
46
+ function parse(text) {
47
+ try {
48
+ return new URL(text);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ function isIssuerUrl(text) {
55
+ const url = parse(text);
56
+ if (!url || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "")
57
+ return false;
58
+ return url.protocol === "https:" || (url.protocol === "http:" && loopback(url.hostname));
59
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * The directory's map (spec §15.5): a sparse Merkle tree of depth 256 from `SHA-256(UTF-8(PersonId))` to a
3
+ * 32-byte value, with fixed empty-subtree hashes and compact proofs (a 256-bit bitmap of the non-empty
4
+ * siblings and those siblings, bottom up). Every key's path has all 256 levels: there is no shortcut leaf in
5
+ * the hashes, so a proof is checked by 256 hashes. In memory the tree is a compressed trie that keeps hashes
6
+ * only where paths branch, so memory grows with the persons, not with 256 nodes each.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { b64u, utf8 } from "../b64u.js";
10
+ import { parseWith } from "../check.js";
11
+ import { canonicalize } from "../jcs.js";
12
+ import { DOMAIN } from "../schema/common.js";
13
+ import { MapValue } from "../schema/directory.js";
14
+ import { MessagingError } from "../schema/errors.js";
15
+ export const MAP_DEPTH = 256;
16
+ function digest(...parts) {
17
+ const hash = createHash("sha256");
18
+ for (const part of parts)
19
+ hash.update(part);
20
+ return new Uint8Array(hash.digest());
21
+ }
22
+ /** E[0] = H(0x02); E[h+1] = H(0x01 ‖ E[h] ‖ E[h]). */
23
+ const EMPTY = [digest(Uint8Array.of(0x02))];
24
+ for (let h = 0; h < MAP_DEPTH; h++)
25
+ EMPTY.push(digest(Uint8Array.of(0x01), EMPTY[h], EMPTY[h]));
26
+ /** The hash of an empty subtree of height `h` (0 ≤ h ≤ 256), base64url. */
27
+ export function emptyMapHash(height) {
28
+ const hash = EMPTY[height];
29
+ if (!hash)
30
+ throw new MessagingError("invalid_request", "map height out of range");
31
+ return b64u.encode(hash);
32
+ }
33
+ /** The map key of a person: `SHA-256(UTF-8(person))`, base64url. */
34
+ export function mapKey(person) {
35
+ return b64u.encode(digest(utf8(person)));
36
+ }
37
+ /**
38
+ * The value a live person's leaf holds: `SHA-256(JCS({log: {seq, hash}, keys}))`, where `log` is the head of
39
+ * their device log and `keys` the hashes of the current devices' keys statements, ordered by device.
40
+ */
41
+ export function mapValue(value) {
42
+ const parsed = parseWith(MapValue, value, "map value");
43
+ return b64u.encode(digest(utf8(canonicalize(parsed))));
44
+ }
45
+ /** The value an erased person's leaf holds: `SHA-256(UTF-8("messaging/1/tombstone"))`. */
46
+ export const TOMBSTONE = b64u.encode(digest(utf8(DOMAIN.tombstone)));
47
+ /** Bit `index` of a key, from the most significant bit of its first byte. */
48
+ function bit(key, index) {
49
+ return (key[index >> 3] >> (7 - (index & 7))) & 1;
50
+ }
51
+ function leafHash(key, value) {
52
+ return digest(Uint8Array.of(0x00), key, value);
53
+ }
54
+ function node(left, right) {
55
+ return digest(Uint8Array.of(0x01), left, right);
56
+ }
57
+ function decode32(hash, what) {
58
+ let bytes;
59
+ try {
60
+ bytes = b64u.decode(hash);
61
+ }
62
+ catch {
63
+ throw new MessagingError("invalid_request", `${what} must be 32 bytes of base64url`);
64
+ }
65
+ if (bytes.length !== 32)
66
+ throw new MessagingError("invalid_request", `${what} must be 32 bytes of base64url`);
67
+ return bytes;
68
+ }
69
+ function bottomOf(n) {
70
+ return n.kind === "leaf" ? MAP_DEPTH : n.bottom;
71
+ }
72
+ /** The subtree hash of `n` at depth `depth` (top ≤ depth ≤ bottom): its bottom hash, hashed up past empty siblings. */
73
+ function hashAt(n, depth) {
74
+ let hash = n.kind === "leaf" ? leafHash(n.key, n.value) : node(n.children[0].hash, n.children[1].hash);
75
+ for (let d = bottomOf(n); d > depth; d--) {
76
+ const empty = EMPTY[MAP_DEPTH - d];
77
+ hash = bit(n.key, d - 1) === 0 ? node(hash, empty) : node(empty, hash);
78
+ }
79
+ return hash;
80
+ }
81
+ function leaf(top, key, value) {
82
+ const n = { kind: "leaf", top, key, value, hash: EMPTY[0] };
83
+ n.hash = hashAt(n, top);
84
+ return n;
85
+ }
86
+ function branch(top, bottom, children) {
87
+ const n = { kind: "branch", top, bottom, key: children[0].key, children, hash: EMPTY[0] };
88
+ n.hash = hashAt(n, top);
89
+ return n;
90
+ }
91
+ /** The first bit in [from, to) where `a` and `b` differ, or `to`. */
92
+ function firstDifference(a, b, from, to) {
93
+ for (let i = from; i < to; i++)
94
+ if (bit(a, i) !== bit(b, i))
95
+ return i;
96
+ return to;
97
+ }
98
+ /** The trie over sorted leaves [from, to), hanging at depth `top`. */
99
+ function build(leaves, top, from, to) {
100
+ if (to - from === 1)
101
+ return leaf(top, leaves[from].key, leaves[from].value);
102
+ // Sorted keys share every bit before the first difference of the first and the last.
103
+ const bottom = firstDifference(leaves[from].key, leaves[to - 1].key, top, MAP_DEPTH);
104
+ let mid = from;
105
+ while (bit(leaves[mid].key, bottom) === 0)
106
+ mid++;
107
+ return branch(top, bottom, [build(leaves, bottom + 1, from, mid), build(leaves, bottom + 1, mid, to)]);
108
+ }
109
+ /**
110
+ * The map, in memory. `set` costs about 256 hashes plus the branches above the person; `prove` reads siblings
111
+ * from the trie. A person never leaves the map: an erased person's value becomes `TOMBSTONE`.
112
+ */
113
+ export class SparseMerkleTree {
114
+ #root;
115
+ #values = new Map();
116
+ #frozen = false;
117
+ /**
118
+ * A map of these persons and values, built at once: each leaf's path is hashed once. This is the way to load
119
+ * a large map (about 2.4 million hashes for 10,000 persons). A person given twice keeps the last value.
120
+ */
121
+ static from(entries) {
122
+ const tree = new SparseMerkleTree();
123
+ const leaves = new Map();
124
+ for (const [person, value] of entries) {
125
+ const key = digest(utf8(person));
126
+ const id = b64u.encode(key);
127
+ leaves.set(id, { key, value: decode32(value, "map value") });
128
+ tree.#values.set(id, value);
129
+ }
130
+ const sorted = [...leaves.values()].sort((a, b) => Buffer.compare(a.key, b.key));
131
+ tree.#root = sorted.length === 0 ? undefined : build(sorted, 0, 0, sorted.length);
132
+ return tree;
133
+ }
134
+ get size() {
135
+ return this.#values.size;
136
+ }
137
+ root() {
138
+ return b64u.encode(this.#root?.hash ?? EMPTY[MAP_DEPTH]);
139
+ }
140
+ get(person) {
141
+ if (this.#frozen)
142
+ throw new MessagingError("internal", "a map snapshot answers root and prove only");
143
+ return this.#values.get(mapKey(person));
144
+ }
145
+ /** Sets a person's value (from `mapValue`, or `TOMBSTONE`). */
146
+ set(person, value) {
147
+ if (this.#frozen)
148
+ throw new MessagingError("internal", "a map snapshot does not change");
149
+ const key = digest(utf8(person));
150
+ const bytes = decode32(value, "map value");
151
+ this.#values.set(b64u.encode(key), value);
152
+ this.#root = this.#root ? insert(this.#root, key, bytes) : leaf(0, key, bytes);
153
+ }
154
+ /**
155
+ * The map as it is now, for `root` and `prove` only: a later `set` on this tree does not change it. Cheap: the trie
156
+ * is persistent, so the snapshot shares every node.
157
+ */
158
+ snapshot() {
159
+ const frozen = new SparseMerkleTree();
160
+ frozen.#root = this.#root;
161
+ frozen.#frozen = true;
162
+ return frozen;
163
+ }
164
+ /**
165
+ * A tree that starts as this one and changes on its own: a `set` on either leaves the other as it was. The trie is
166
+ * shared (it is persistent); only the person index is copied. A directory prepares an epoch on a copy and adopts it
167
+ * once the epoch is stored.
168
+ */
169
+ copy() {
170
+ if (this.#frozen)
171
+ throw new MessagingError("internal", "a map snapshot answers root and prove only");
172
+ const copy = new SparseMerkleTree();
173
+ copy.#root = this.#root;
174
+ copy.#values = new Map(this.#values);
175
+ return copy;
176
+ }
177
+ /** The proof for `person`, present or absent, against `root()`. */
178
+ prove(person) {
179
+ const key = digest(utf8(person));
180
+ const found = [];
181
+ let current = this.#root;
182
+ while (current) {
183
+ const bottom = bottomOf(current);
184
+ const differs = firstDifference(key, current.key, current.top, bottom);
185
+ if (differs < bottom) {
186
+ // Absent: all of `current` below the divergence is the one non-empty sibling there.
187
+ found.push([MAP_DEPTH - differs - 1, hashAt(current, differs + 1)]);
188
+ break;
189
+ }
190
+ if (current.kind === "leaf")
191
+ break;
192
+ const side = bit(key, current.bottom);
193
+ found.push([MAP_DEPTH - current.bottom - 1, current.children[1 - side].hash]);
194
+ current = current.children[side];
195
+ }
196
+ found.sort((a, b) => a[0] - b[0]);
197
+ const bitmap = new Uint8Array(32);
198
+ for (const [height] of found)
199
+ bitmap[height >> 3] |= 0x80 >> (height & 7);
200
+ return { bitmap: b64u.encode(bitmap), siblings: found.map(([, hash]) => b64u.encode(hash)) };
201
+ }
202
+ }
203
+ /**
204
+ * `n` with `key` set to `value`. Persistent: it copies every node on the path and changes none, so a tree's earlier
205
+ * root still proves the map as it was (`SparseMerkleTree.snapshot`).
206
+ */
207
+ function insert(n, key, value) {
208
+ const bottom = bottomOf(n);
209
+ const differs = firstDifference(key, n.key, n.top, bottom);
210
+ if (differs < bottom) {
211
+ // Split: a branch at the first differing depth, over the old subtree (now hanging lower) and a new leaf.
212
+ const lowered = { ...n, top: differs + 1 };
213
+ lowered.hash = hashAt(lowered, lowered.top);
214
+ const fresh = leaf(differs + 1, key, value);
215
+ return branch(n.top, differs, bit(key, differs) === 0 ? [fresh, lowered] : [lowered, fresh]);
216
+ }
217
+ let copy;
218
+ if (n.kind === "leaf") {
219
+ copy = { ...n, value };
220
+ }
221
+ else {
222
+ const side = bit(key, n.bottom);
223
+ const children = [n.children[0], n.children[1]];
224
+ children[side] = insert(n.children[side], key, value);
225
+ copy = { ...n, children };
226
+ }
227
+ copy.hash = hashAt(copy, copy.top);
228
+ return copy;
229
+ }
230
+ /**
231
+ * The root that `proof` gives for `person` holding `value` (undefined: no leaf, an absent person). Throws
232
+ * `invalid_request` for a malformed proof: a bitmap that is not 32 bytes, a sibling count that differs from
233
+ * the bitmap's, or a sibling equal to the empty hash of its height (a proof names only non-empty siblings).
234
+ */
235
+ export function mapRootFromProof(person, value, proof) {
236
+ const key = digest(utf8(person));
237
+ const bitmap = decode32(proof.bitmap, "map proof bitmap");
238
+ if (!Array.isArray(proof.siblings))
239
+ throw new MessagingError("invalid_request", "map proof siblings must be an array");
240
+ let next = 0;
241
+ let hash = value === undefined ? EMPTY[0] : leafHash(key, decode32(value, "map value"));
242
+ for (let height = 0; height < MAP_DEPTH; height++) {
243
+ let sibling = EMPTY[height];
244
+ if ((bitmap[height >> 3] >> (7 - (height & 7))) & 1) {
245
+ const given = proof.siblings[next++];
246
+ if (given === undefined)
247
+ throw new MessagingError("invalid_request", "map proof has fewer siblings than its bitmap");
248
+ sibling = decode32(given, "map proof sibling");
249
+ if (Buffer.from(sibling).equals(EMPTY[height]))
250
+ throw new MessagingError("invalid_request", "map proof names an empty sibling");
251
+ }
252
+ hash = bit(key, MAP_DEPTH - 1 - height) === 0 ? node(hash, sibling) : node(sibling, hash);
253
+ }
254
+ if (next !== proof.siblings.length)
255
+ throw new MessagingError("invalid_request", "map proof has more siblings than its bitmap");
256
+ return b64u.encode(hash);
257
+ }
258
+ /** True when `proof` shows `person` holding `value` (undefined: absent) in the map with root `root`. Never throws. */
259
+ export function verifyMapProof(root, person, value, proof) {
260
+ try {
261
+ return mapRootFromProof(person, value, proof) === root;
262
+ }
263
+ catch {
264
+ return false;
265
+ }
266
+ }