@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,454 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { b64u, utf8 } from "./b64u.js";
3
+ import { asMessagingError, parseWith } from "./check.js";
4
+ import { sha256 } from "./hash.js";
5
+ import { canonicalize } from "./jcs.js";
6
+ import { assertEd25519PublicKey, privateKeyFromRaw, rawPublicKey } from "./keys.js";
7
+ import { DOMAIN, LIMITS, PROTOCOL, RELAY_PROTOCOL, } from "./schema/common.js";
8
+ import { MessagingError } from "./schema/errors.js";
9
+ import { GenesisEntry, InviteTicket, RosterEntry, TICKET_PREFIX, } from "./schema/roster.js";
10
+ import { signWith, sign, verify } from "./sign.js";
11
+ import { ulid } from "./ulid.js";
12
+ import { parseWire } from "./wire.js";
13
+ /** SHA-256 of the JCS form of the full entry, including `sig`. The genesis hash is the team id. */
14
+ export function entryHash(entry) {
15
+ return sha256(canonicalize(entry));
16
+ }
17
+ /**
18
+ * A verified roster. Every entry passes schema, chain, signature and authority checks (the rules
19
+ * in `schema/roster.ts`) before it is appended; a failed `apply` leaves the roster unchanged.
20
+ * Entries are stored as frozen copies of the received JSON, so hashes cover unknown fields too.
21
+ * Members are stored as fresh `{machine, enc, person, role, label}` objects, never as received.
22
+ * Every method throws only MessagingError.
23
+ */
24
+ export class Roster {
25
+ team;
26
+ name;
27
+ #entries = [];
28
+ #members = new Map();
29
+ #removed = new Set();
30
+ #invites = new Map();
31
+ #head;
32
+ constructor(genesis, hash) {
33
+ const { body } = genesis;
34
+ this.team = hash;
35
+ this.name = body.name;
36
+ this.#entries.push(genesis);
37
+ this.#members.set(body.founder.machine, memberOf(body.founder));
38
+ this.#head = { seq: 0, hash };
39
+ }
40
+ /** Verifies the whole chain from genesis. Throws the first failing entry's MessagingError. */
41
+ static fromEntries(entries) {
42
+ try {
43
+ return Roster.#replay(entries);
44
+ }
45
+ catch (error) {
46
+ throw asMessagingError(error, "roster");
47
+ }
48
+ }
49
+ static #replay(entries) {
50
+ if (!Array.isArray(entries) || entries.length === 0)
51
+ throw new MessagingError("invalid_request", "a roster needs a genesis entry");
52
+ const { entry, hash } = admit(entries[0]);
53
+ if (entry.kind !== "genesis")
54
+ throw new MessagingError("roster_conflict", "the first roster entry must be genesis");
55
+ if (entry.seq !== 0)
56
+ throw new MessagingError("roster_conflict", "genesis must have seq 0");
57
+ if (!verifyEntry(entry))
58
+ throw new MessagingError("bad_signature", "genesis signature is invalid");
59
+ const { founder } = entry.body;
60
+ if (entry.by !== founder.machine)
61
+ throw new MessagingError("forbidden", "genesis must be signed by the founder");
62
+ if (founder.role !== "admin")
63
+ throw new MessagingError("forbidden", "the founder must be an admin");
64
+ const roster = new Roster(entry, hash);
65
+ for (const next of entries.slice(1))
66
+ roster.#append(next, {});
67
+ return roster;
68
+ }
69
+ /**
70
+ * Validates one entry against the head and appends it. Checks run in order: schema, I-JSON and keys
71
+ * (`invalid_request`), chain (`roster_conflict` for seq/prev; `invalid_request` for team and ts),
72
+ * freshness (`expired`), signature (`bad_signature`), then authority (`forbidden`) and invites
73
+ * (`invite_invalid`). Pass `now` when accepting a live append: `ts` must then be within `skewMs`
74
+ * (default `LIMITS.clockSkewMs`) of `now`, so a joiner cannot backdate a redeem past its invite's expiry.
75
+ */
76
+ apply(raw, options = {}) {
77
+ try {
78
+ this.#append(raw, options);
79
+ }
80
+ catch (error) {
81
+ throw asMessagingError(error, "roster entry");
82
+ }
83
+ }
84
+ /**
85
+ * Checks `raw` as `apply` would have when the head was at `seq − 1`, without appending it: schema, chain
86
+ * (its `prev` must be the hash of this roster's entry at `seq − 1`), team, signature and authority, all
87
+ * against the state at that point. Returns the entry's hash. An entry that passes and whose hash differs
88
+ * from `entries()[seq]` proves a fork: two valid entries at one seq. `seq` more than one past the head is
89
+ * `roster_conflict`; a seq-0 entry passes only if it is this team's genesis. Other errors are as for
90
+ * `apply`. Checking an entry below the head replays the chain up to it.
91
+ */
92
+ checkAt(raw) {
93
+ try {
94
+ const { entry, hash } = admit(raw);
95
+ if (entry.seq === 0) {
96
+ Roster.#replay([raw]);
97
+ if (hash !== this.team)
98
+ throw new MessagingError("invalid_request", "genesis is for another team");
99
+ return hash;
100
+ }
101
+ if (entry.seq > this.#head.seq + 1)
102
+ throw new MessagingError("roster_conflict", `entry is past head ${this.#head.seq} + 1`);
103
+ const before = entry.seq === this.#head.seq + 1 ? this : Roster.#replay(this.#entries.slice(0, entry.seq));
104
+ before.#check(entry, {});
105
+ return hash;
106
+ }
107
+ catch (error) {
108
+ throw asMessagingError(error, "roster entry");
109
+ }
110
+ }
111
+ #append(raw, options) {
112
+ const { now, skewMs } = options;
113
+ if (now !== undefined && !Number.isSafeInteger(now))
114
+ throw new MessagingError("invalid_request", "now must be an integer");
115
+ if (skewMs !== undefined && (!Number.isSafeInteger(skewMs) || skewMs < 0)) {
116
+ throw new MessagingError("invalid_request", "skewMs must be a non-negative integer");
117
+ }
118
+ const { entry, hash } = admit(raw);
119
+ const commit = this.#check(entry, options);
120
+ commit();
121
+ this.#entries.push(entry);
122
+ this.#head = { seq: entry.seq, hash };
123
+ }
124
+ /** Every check of `apply` after admission; returns the state change to make. Changes nothing itself. */
125
+ #check(entry, options) {
126
+ if (entry.kind === "genesis" || entry.seq !== this.#head.seq + 1 || entry.prev !== this.#head.hash) {
127
+ throw new MessagingError("roster_conflict", `entry does not extend head ${this.#head.seq}`);
128
+ }
129
+ if (entry.team !== this.team)
130
+ throw new MessagingError("invalid_request", "entry is for another team");
131
+ const last = this.#entries[this.#entries.length - 1];
132
+ if (entry.ts < last.ts - LIMITS.clockSkewMs)
133
+ throw new MessagingError("invalid_request", "entry ts goes backwards");
134
+ if (options.now !== undefined && Math.abs(entry.ts - options.now) > (options.skewMs ?? LIMITS.clockSkewMs)) {
135
+ throw new MessagingError("expired", "entry ts is outside the clock skew window");
136
+ }
137
+ if (!verifyEntry(entry))
138
+ throw new MessagingError("bad_signature", "roster entry signature is invalid");
139
+ return this.#authorize(entry);
140
+ }
141
+ head() {
142
+ return { ...this.#head };
143
+ }
144
+ entries() {
145
+ return [...this.#entries];
146
+ }
147
+ member(machine) {
148
+ const member = this.#members.get(machine);
149
+ return member && { ...member };
150
+ }
151
+ // ---- the realm view (spec §5): a roster realm is the team, its anchor the head ----
152
+ /** The realm's id: the team id. */
153
+ get realm() {
154
+ return this.team;
155
+ }
156
+ /** A current member's machine; the same as `member`. */
157
+ device(machine) {
158
+ return this.member(machine);
159
+ }
160
+ /** `{kind: "roster"}` and the head. */
161
+ anchor() {
162
+ return { kind: "roster", ...this.#head };
163
+ }
164
+ /** `ahead` past the head; `consistent` for an entry of this chain; `diverged` for another hash at a seq it has. */
165
+ relate(anchor) {
166
+ // A roster realm has no epochs: `verifyEnvelope` refuses another kind's anchor first.
167
+ if (anchor.kind !== "roster")
168
+ throw new MessagingError("invalid_request", "a roster realm relates roster anchors only");
169
+ const own = this.#head;
170
+ if (anchor.seq > own.seq)
171
+ return { relation: "ahead" };
172
+ const ours = anchor.seq === own.seq ? own.hash : entryHash(this.#entries[anchor.seq]);
173
+ return ours === anchor.hash ? { relation: "consistent" } : { relation: "diverged", ours };
174
+ }
175
+ members() {
176
+ return [...this.#members.values()].map((m) => ({ ...m }));
177
+ }
178
+ isAdmin(machine) {
179
+ return this.#members.get(machine)?.role === "admin";
180
+ }
181
+ /** The invite and its state at `now` (an open invite is `expired` once `now >= expires`). */
182
+ invite(inviteId, now = Date.now()) {
183
+ const record = this.#invites.get(inviteId);
184
+ if (!record)
185
+ return undefined;
186
+ const state = record.state === "open" && now >= record.entry.body.expires ? "expired" : record.state;
187
+ return { entry: record.entry, state };
188
+ }
189
+ wasRemoved(machine) {
190
+ return this.#removed.has(machine);
191
+ }
192
+ /** Checks authority for `entry` without changing state; returns the state change to apply. */
193
+ #authorize(entry) {
194
+ switch (entry.kind) {
195
+ case "member.add": {
196
+ this.#requireAdmin(entry.by);
197
+ this.#requireNew(entry.body);
198
+ return () => this.#members.set(entry.body.machine, memberOf(entry.body));
199
+ }
200
+ case "member.remove": {
201
+ const target = this.#requireMember(entry.body.machine);
202
+ if (entry.by !== target.machine)
203
+ this.#requireAdmin(entry.by);
204
+ this.#requireNotLastAdmin(target);
205
+ return () => {
206
+ this.#members.delete(target.machine);
207
+ this.#removed.add(target.machine);
208
+ };
209
+ }
210
+ case "member.role": {
211
+ this.#requireAdmin(entry.by);
212
+ const target = this.#requireMember(entry.body.machine);
213
+ if (entry.body.role !== "admin")
214
+ this.#requireNotLastAdmin(target);
215
+ return () => this.#members.set(target.machine, { ...target, role: entry.body.role });
216
+ }
217
+ case "member.rekey": {
218
+ // A removed machine is not a current member, so it can never be rekeyed (or rekey itself).
219
+ const target = this.#requireMember(entry.body.machine);
220
+ if (entry.by !== target.machine)
221
+ this.#requireAdmin(entry.by);
222
+ return () => this.#members.set(target.machine, { ...target, enc: entry.body.enc });
223
+ }
224
+ case "invite.create": {
225
+ this.#requireAdmin(entry.by);
226
+ if (this.#invites.has(entry.body.inviteId))
227
+ throw new MessagingError("invite_invalid", "invite id already exists");
228
+ if (entry.body.expires - entry.ts > LIMITS.ttlMs)
229
+ throw new MessagingError("invite_invalid", "invite lifetime exceeds LIMITS.ttlMs");
230
+ return () => this.#invites.set(entry.body.inviteId, { entry, state: "open" });
231
+ }
232
+ case "invite.redeem": {
233
+ const { inviteId, proof, member } = entry.body;
234
+ if (entry.by !== member.machine)
235
+ throw new MessagingError("forbidden", "a redeem must be signed by the joining machine");
236
+ const record = this.#invites.get(inviteId);
237
+ if (!record)
238
+ throw new MessagingError("invite_invalid", "unknown invite");
239
+ if (record.state !== "open")
240
+ throw new MessagingError("invite_invalid", `invite is ${record.state}`);
241
+ const invite = record.entry.body;
242
+ if (entry.ts >= invite.expires)
243
+ throw new MessagingError("invite_invalid", "invite has expired");
244
+ if (!verify(invite.invitePub, DOMAIN.invite, redeemClaim(this.team, inviteId, member), proof)) {
245
+ throw new MessagingError("invite_invalid", "invite proof does not verify for this member");
246
+ }
247
+ if (member.person !== invite.person || member.role !== "member") {
248
+ throw new MessagingError("invite_invalid", "member does not match the invite");
249
+ }
250
+ this.#requireNew(member);
251
+ return () => {
252
+ record.state = "redeemed";
253
+ this.#members.set(member.machine, memberOf(member));
254
+ };
255
+ }
256
+ case "invite.revoke": {
257
+ this.#requireAdmin(entry.by);
258
+ const record = this.#invites.get(entry.body.inviteId);
259
+ if (!record)
260
+ throw new MessagingError("invite_invalid", "unknown invite");
261
+ if (record.state !== "open")
262
+ throw new MessagingError("invite_invalid", `invite is ${record.state}`);
263
+ return () => {
264
+ record.state = "revoked";
265
+ };
266
+ }
267
+ }
268
+ }
269
+ #requireAdmin(machine) {
270
+ if (!this.isAdmin(machine))
271
+ throw new MessagingError("forbidden", "only a current admin may do this");
272
+ }
273
+ #requireMember(machine) {
274
+ const member = this.#members.get(machine);
275
+ if (!member)
276
+ throw new MessagingError("forbidden", "target is not a current member");
277
+ return member;
278
+ }
279
+ /**
280
+ * A machine joins once: never while it is a member, and never again after removal (so its person never
281
+ * changes). Its (person, label) must not be used by a current member, so `person/session@label` names one machine.
282
+ */
283
+ #requireNew(member) {
284
+ if (this.#members.has(member.machine))
285
+ throw new MessagingError("forbidden", "machine is already a member");
286
+ if (this.#removed.has(member.machine))
287
+ throw new MessagingError("forbidden", "a removed machine can never rejoin");
288
+ for (const current of this.#members.values()) {
289
+ if (current.person === member.person && current.label === member.label) {
290
+ throw new MessagingError("forbidden", "a current member already uses this person and label");
291
+ }
292
+ }
293
+ }
294
+ #requireNotLastAdmin(target) {
295
+ if (target.role !== "admin")
296
+ return;
297
+ const admins = [...this.#members.values()].filter((m) => m.role === "admin").length;
298
+ if (admins <= 1)
299
+ throw new MessagingError("forbidden", "the last admin cannot be removed or demoted");
300
+ }
301
+ }
302
+ /**
303
+ * Checks that a raw entry is I-JSON within `LIMITS.jsonDepth`, matches the schema and admits only usable
304
+ * keys, then returns a frozen JSON copy with its hash. Canonicalizing first bounds every later step.
305
+ */
306
+ function admit(raw) {
307
+ const json = canonicalize(raw);
308
+ parseWith(RosterEntry, raw, "roster entry");
309
+ const entry = deepFreeze(JSON.parse(json));
310
+ for (const [key, what] of admittedKeys(entry))
311
+ assertEd25519PublicKey(key, what);
312
+ return { entry, hash: sha256(json) };
313
+ }
314
+ /** Ed25519 keys an entry brings into the roster: they must be canonical and not of small order (spec §3). */
315
+ function admittedKeys(entry) {
316
+ switch (entry.kind) {
317
+ case "genesis":
318
+ return [[entry.body.founder.machine, "founder machine"]];
319
+ case "member.add":
320
+ return [[entry.body.machine, "member machine"]];
321
+ case "invite.redeem":
322
+ return [[entry.body.member.machine, "member machine"]];
323
+ case "invite.create":
324
+ return [[entry.body.invitePub, "invitePub"]];
325
+ default:
326
+ return [];
327
+ }
328
+ }
329
+ function verifyEntry(entry) {
330
+ const { sig, ...unsigned } = entry;
331
+ return verify(entry.by, DOMAIN.roster, unsigned, sig);
332
+ }
333
+ /** Copies exactly the member fields, so signer-chosen extra keys (even an own `__proto__`) never reach roster state. */
334
+ function memberOf(member) {
335
+ return { machine: member.machine, enc: member.enc, person: member.person, role: member.role, label: member.label };
336
+ }
337
+ /** The value an invite seed signs (with `DOMAIN.invite`) for a redeem. `member` is the raw member object of the entry. */
338
+ function redeemClaim(team, inviteId, member) {
339
+ return { team, inviteId, member };
340
+ }
341
+ /** Freezes a JSON value and everything in it, iteratively. */
342
+ function deepFreeze(value) {
343
+ const pending = [value];
344
+ while (pending.length > 0) {
345
+ const next = pending.pop();
346
+ if (typeof next !== "object" || next === null || Object.isFrozen(next))
347
+ continue;
348
+ Object.freeze(next);
349
+ for (const child of Object.values(next))
350
+ pending.push(child);
351
+ }
352
+ return value;
353
+ }
354
+ /** The invite seed of a ticket as an Ed25519 private key. */
355
+ function seedKey(secret) {
356
+ return privateKeyFromRaw("ed25519", b64u.decode(secret));
357
+ }
358
+ export function createGenesis(args) {
359
+ const { identity } = args;
360
+ const founder = { machine: identity.machine, enc: identity.enc, person: args.person, role: "admin", label: args.label };
361
+ const unsigned = {
362
+ v: PROTOCOL,
363
+ seq: 0,
364
+ ts: args.now ?? Date.now(),
365
+ by: identity.machine,
366
+ prev: null,
367
+ kind: "genesis",
368
+ body: { name: args.name, founder },
369
+ };
370
+ parseWith(GenesisEntry, { ...unsigned, sig: "" }, "genesis");
371
+ return { ...unsigned, sig: sign(identity, DOMAIN.roster, unsigned) };
372
+ }
373
+ /** Signs a new entry that extends the roster head. Authority is checked when the entry is applied. */
374
+ export function createEntry(roster, identity, kind, body, now = Date.now()) {
375
+ const head = roster.head();
376
+ const unsigned = { v: PROTOCOL, seq: head.seq + 1, ts: now, by: identity.machine, team: roster.team, prev: head.hash, kind, body };
377
+ const parsed = parseWith(RosterEntry, { ...unsigned, sig: "" }, `${kind} entry`);
378
+ const { sig: _sig, ...canonical } = parsed;
379
+ return { ...canonical, sig: sign(identity, DOMAIN.roster, canonical) };
380
+ }
381
+ /**
382
+ * An `invite.create` entry and its ticket. The secret is a fresh 32-byte Ed25519 seed; only its public key
383
+ * (`invitePub`) goes in the roster. `ttlMs` is at most `LIMITS.ttlMs`.
384
+ */
385
+ export function createInvite(roster, identity, args) {
386
+ if (!Number.isSafeInteger(args.ttlMs) || args.ttlMs <= 0 || args.ttlMs > LIMITS.ttlMs) {
387
+ throw new MessagingError("invalid_request", "ttlMs must be a positive integer up to LIMITS.ttlMs");
388
+ }
389
+ const now = args.now ?? Date.now();
390
+ const secret = b64u.encode(randomBytes(32));
391
+ const inviteId = ulid(now);
392
+ const expires = now + args.ttlMs;
393
+ const ticket = encodeTicket({
394
+ v: PROTOCOL,
395
+ team: roster.team,
396
+ name: roster.name,
397
+ relay: args.relay,
398
+ inviteId,
399
+ secret,
400
+ person: args.person,
401
+ expires,
402
+ });
403
+ const entry = createEntry(roster, identity, "invite.create", {
404
+ inviteId,
405
+ invitePub: rawPublicKey(seedKey(secret)),
406
+ person: args.person,
407
+ expires,
408
+ }, now);
409
+ return { entry, ticket, secret };
410
+ }
411
+ /** `messaging1.` + base64url(UTF-8(JCS(ticket))). */
412
+ export function encodeTicket(ticket) {
413
+ return TICKET_PREFIX + b64u.encode(utf8(canonicalize(parseWith(InviteTicket, ticket, "invite ticket"))));
414
+ }
415
+ /** Parses a ticket with `parseWire`. Errors never quote the ticket and chain no `cause`. */
416
+ export function decodeTicket(text) {
417
+ if (typeof text !== "string" || !text.startsWith(TICKET_PREFIX))
418
+ throw new MessagingError("invalid_request", "not a messaging1 invite ticket");
419
+ let json;
420
+ try {
421
+ json = new TextDecoder("utf-8", { fatal: true }).decode(b64u.decode(text.slice(TICKET_PREFIX.length)));
422
+ }
423
+ catch {
424
+ throw new MessagingError("invalid_request", "invite ticket is not encoded correctly");
425
+ }
426
+ return parseWith(InviteTicket, parseWire(json), "invite ticket");
427
+ }
428
+ /**
429
+ * The `invite.redeem` entry for a ticket, signed by the joining identity. Its `proof` is signed with the
430
+ * ticket's seed over {team, inviteId, member}, so the redeem holds only for this member; the seed is not in it.
431
+ */
432
+ export function createRedeem(roster, ticket, identity, args) {
433
+ if (ticket.team !== roster.team)
434
+ throw new MessagingError("invite_invalid", "ticket is for another team");
435
+ const member = { machine: identity.machine, enc: identity.enc, person: ticket.person, role: "member", label: args.label };
436
+ const proof = signWith(seedKey(ticket.secret), DOMAIN.invite, redeemClaim(roster.team, ticket.inviteId, member));
437
+ return createEntry(roster, identity, "invite.redeem", { inviteId: ticket.inviteId, proof, member }, args.now);
438
+ }
439
+ /** The value a machine signs (with `DOMAIN.relayAuth`) in the relay `hello` frame. */
440
+ export function relayAuthPayload(args) {
441
+ return { proto: RELAY_PROTOCOL, relay: args.relay, team: args.team, machine: args.machine, nonce: args.nonce, ts: args.ts };
442
+ }
443
+ /**
444
+ * The value an invite seed signs (with `DOMAIN.inviteJoin`) in a pre-auth `join` frame: the relay id and
445
+ * nonce from the connection's challenge, and the invite. The relay verifies it under the open invite's `invitePub`.
446
+ */
447
+ export function inviteJoinPayload(args) {
448
+ return { relay: args.relay, team: args.team, inviteId: args.inviteId, nonce: args.nonce };
449
+ }
450
+ /** The `join` frame `proof` for `ticket` on a connection whose challenge carried `relay` and `nonce`. It proves the seed without revealing it. */
451
+ export function inviteJoinProof(ticket, challenge) {
452
+ const payload = inviteJoinPayload({ relay: challenge.relay, team: ticket.team, inviteId: ticket.inviteId, nonce: challenge.nonce });
453
+ return signWith(seedKey(ticket.secret), DOMAIN.inviteJoin, payload);
454
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A2A v1 JSON shapes (proto3 JSON mapping of `lf.a2a.v1`, specification/a2a.proto).
3
+ * Field names are lowerCamelCase; enums are their proto names; bytes are base64.
4
+ * Objects strip unknown keys on parse, so newer peers' extra fields are tolerated.
5
+ * Signatures are always verified over the raw received JSON, before parsing.
6
+ */
7
+ import { z } from "../../vendor/zod/index.js";
8
+ const Struct = z.record(z.string(), z.unknown());
9
+ export const TaskState = z.enum([
10
+ "TASK_STATE_SUBMITTED",
11
+ "TASK_STATE_WORKING",
12
+ "TASK_STATE_COMPLETED",
13
+ "TASK_STATE_FAILED",
14
+ "TASK_STATE_CANCELED",
15
+ "TASK_STATE_INPUT_REQUIRED",
16
+ "TASK_STATE_REJECTED",
17
+ "TASK_STATE_AUTH_REQUIRED",
18
+ ]);
19
+ export const TERMINAL_STATES = [
20
+ "TASK_STATE_COMPLETED",
21
+ "TASK_STATE_FAILED",
22
+ "TASK_STATE_CANCELED",
23
+ "TASK_STATE_REJECTED",
24
+ ];
25
+ export const INTERRUPTED_STATES = ["TASK_STATE_INPUT_REQUIRED", "TASK_STATE_AUTH_REQUIRED"];
26
+ export const Role = z.enum(["ROLE_USER", "ROLE_AGENT"]);
27
+ /** Exactly one of text | raw | url | data. `data` may be any JSON value, including null. */
28
+ export const Part = z
29
+ .object({
30
+ text: z.string().optional(),
31
+ raw: z.string().optional().describe("base64 bytes"),
32
+ url: z.string().optional(),
33
+ data: z.unknown().optional(),
34
+ metadata: Struct.optional(),
35
+ filename: z.string().max(255).optional(),
36
+ mediaType: z.string().max(255).optional(),
37
+ })
38
+ .refine((p) => [p.text, p.raw, p.url].filter((v) => v !== undefined).length + ("data" in p ? 1 : 0) === 1, {
39
+ message: "a part has exactly one of text, raw, url or data",
40
+ });
41
+ export const Message = z.object({
42
+ messageId: z.string().min(1).max(128),
43
+ contextId: z.string().max(128).optional(),
44
+ taskId: z.string().max(128).optional(),
45
+ role: Role,
46
+ parts: z.array(Part).min(1),
47
+ metadata: Struct.optional(),
48
+ extensions: z.array(z.string()).optional(),
49
+ referenceTaskIds: z.array(z.string()).optional(),
50
+ });
51
+ export const TaskStatus = z.object({
52
+ state: TaskState,
53
+ message: Message.optional(),
54
+ timestamp: z.string().optional().describe("ISO 8601"),
55
+ });
56
+ export const Artifact = z.object({
57
+ artifactId: z.string().min(1).max(128),
58
+ name: z.string().max(200).optional(),
59
+ description: z.string().max(2000).optional(),
60
+ parts: z.array(Part).min(1),
61
+ metadata: Struct.optional(),
62
+ extensions: z.array(z.string()).optional(),
63
+ });
64
+ export const Task = z.object({
65
+ id: z.string().min(1).max(128),
66
+ contextId: z.string().max(128).optional(),
67
+ status: TaskStatus,
68
+ artifacts: z.array(Artifact).optional(),
69
+ history: z.array(Message).optional(),
70
+ metadata: Struct.optional(),
71
+ });
72
+ export const TaskStatusUpdateEvent = z.object({
73
+ taskId: z.string().min(1),
74
+ contextId: z.string(),
75
+ status: TaskStatus,
76
+ metadata: Struct.optional(),
77
+ });
78
+ export const TaskArtifactUpdateEvent = z.object({
79
+ taskId: z.string().min(1),
80
+ contextId: z.string(),
81
+ artifact: Artifact,
82
+ append: z.boolean().optional(),
83
+ lastChunk: z.boolean().optional(),
84
+ metadata: Struct.optional(),
85
+ });