@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,270 @@
1
+ /**
2
+ * Directory frames (spec §7, "Directory frames"; design §3.2): the frames a hosted relay, one that runs the
3
+ * key directory, speaks on the same WebSocket as the relay binding. They follow the frame rules of §7: one
4
+ * JSON object per text frame, discriminated by `t`; requests carry `req`, replies echo it, and a refusal is
5
+ * the relay binding's `nack {id: req, code, message}` (with `retryAfterMs` for `rate_limited`).
6
+ *
7
+ * Connection, hosted:
8
+ * relay → challenge {proto, relay, nonce} (unchanged)
9
+ * client → [pre-auth, optional] dir.bind {req, entry} first binding of this device → dir.ok {req, person, seq, sth}
10
+ * client → [pre-auth, optional] dir.sth, dir.heads, dir.device what a device about to bind reads; heads only, no log
11
+ * client → [pre-auth, optional] dir.lookup {.., proof} a device reads its own person's log (its key proven)
12
+ * client → hello {realm, person, device, ts, sig} sig = Ed25519(device, DOMAIN.relayAuth + JCS(hostedRelayAuthPayload))
13
+ * relay → welcome {sth} or close 4001 (`unknown_device`) when the device is not current
14
+ *
15
+ * After welcome, besides send / deliver / presence / recv.ack / ping of §7:
16
+ * dir.append {req, entry} → dir.ok {req, seq, sth}
17
+ * dir.keys {req, stmt} → dir.ok {req, sth}
18
+ * dir.sth {req, size?, from?, epoch?} → dir.sthIs {req, sth, head, consistency?, epoch?, rotation?}
19
+ * dir.lookup {req, persons, size?} → dir.persons {req, sth, head, persons}
20
+ * dir.heads {req, persons, size?} → dir.headsAre {req, sth, head, persons}
21
+ * dir.resolve {req, handle | email} → dir.resolved {req, persons}
22
+ * dir.search {req, query, limit} → dir.resolved {req, persons}
23
+ * dir.watch {req, persons} → dir.ok {req}; then dir.changed {persons, sth} pushes
24
+ * presence.watch {req, persons} → dir.ok {req}
25
+ * dir.erase {req, entries, token} → dir.ok {req}
26
+ * wake.arm {req, url, ticket, agents, awaiting} → dir.ok {req} a Jelly host's device, before it sleeps (§7, "Wake tickets")
27
+ * wake.disarm {req} → dir.ok {req}
28
+ * relay → dir.unbound {entry, sth} this device's own unbind, just before close 4003
29
+ *
30
+ * The directory's own structures (HostedPersonId, LogEntry, KeysStatement, STH, PersonView, ...) are in
31
+ * `schema/directory.ts` and `schema/common.ts`, with their verifiers in `packages/core/src/directory/` (spec §15).
32
+ * The frames take a hosted person (`HostedPersonId`) only.
33
+ */
34
+ import { z } from "../../vendor/zod/index.js";
35
+ import { Base64Url, Hash, HostedPersonId, LIMITS, Millis, RELAY_PROTOCOL, SessionName, Ulid } from "./common.js";
36
+ import { EpochProof } from "./directory.js";
37
+ import { RealmId } from "./realm.js";
38
+ import { DeviceId, KeyRotation, KeysStatement, LogEntry, PersonHead, PersonView, STH } from "./directory.js";
39
+ import { Jws } from "./jelly.js";
40
+ const Req = z.string().min(1).max(64);
41
+ const Size = z.number().int().positive();
42
+ /** A hosted handle (design §6.1): the normalised local part of a verified email. */
43
+ export const Handle = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/, "lowercase handle");
44
+ /** An exact email address, lower-cased (design §6.1): the domain is ASCII, as Jelly ID's verified emails are. */
45
+ export const Email = z
46
+ .string()
47
+ .max(254)
48
+ .regex(/^[^\s@A-Z]{1,64}@(?:[a-z0-9-]+\.)+[a-z0-9-]+$/, "lower-case email address");
49
+ /** An org (design §2.3): `domain:<d>` until Jelly contract v1.2, then the organisation id. Absent for an individual. */
50
+ export const OrgId = z.string().min(1).max(200);
51
+ /** The value a device signs (with `DOMAIN.relayAuth`) in the hosted `hello` frame. */
52
+ export function hostedRelayAuthPayload(args) {
53
+ return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, person: args.person, device: args.device, nonce: args.nonce, ts: args.ts };
54
+ }
55
+ /** The value a device signs (with `DOMAIN.dirRead`) in a `dir.lookup` before `hello`: its key, on this connection. */
56
+ export function dirReadPayload(args) {
57
+ return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, device: args.device, nonce: args.nonce };
58
+ }
59
+ /** A device's proof before `hello` (`dirReadPayload`): `sig` by `device` over this connection's challenge. */
60
+ export const DirReadProof = z.strictObject({ device: DeviceId, sig: Base64Url });
61
+ // ---- client → relay ----
62
+ /** Pre-auth: the first binding of this device, a `bind` entry with a `jelly` authority (design §2.2). */
63
+ export const DirBindFrame = z.object({ t: z.literal("dir.bind"), req: Req, entry: LogEntry });
64
+ /** The hosted `hello`: authenticates a device of a person in a directory realm. A roster relay refuses it. */
65
+ export const HostedHelloFrame = z.object({
66
+ t: z.literal("hello"),
67
+ realm: RealmId,
68
+ person: HostedPersonId,
69
+ device: DeviceId,
70
+ ts: Millis,
71
+ sig: Base64Url,
72
+ });
73
+ /** An `attest`, an `unbind`, or the `bind` of a QR approval (design §2.1, §2.5), appended to the sender's person's log. */
74
+ export const DirAppendFrame = z.object({ t: z.literal("dir.append"), req: Req, entry: LogEntry });
75
+ /** The sending device's own latest encryption-key statement (design §2.6). */
76
+ export const DirKeysFrame = z.object({ t: z.literal("dir.keys"), req: Req, stmt: KeysStatement });
77
+ /**
78
+ * The latest STH, or the STH at `size`. `from`: also a consistency proof from that size. `epoch`: also that
79
+ * epoch-log leaf with its inclusion proof (an entry's first epoch, design §2.2 step 7).
80
+ */
81
+ export const DirSthFrame = z.object({
82
+ t: z.literal("dir.sth"),
83
+ req: Req,
84
+ size: Size.optional(),
85
+ from: Size.optional(),
86
+ epoch: z.number().int().nonnegative().optional(),
87
+ });
88
+ /**
89
+ * Persons' views at the directory's latest STH, which is at least `size`. `knownSeq`: send only the log after it.
90
+ * Before `hello`, only with `proof` (a device's key on this connection), and only for persons whose log names that
91
+ * device, current or not: a device reads its own person's log; anyone else reads heads (`dir.heads`).
92
+ */
93
+ export const DirLookupFrame = z.object({
94
+ t: z.literal("dir.lookup"),
95
+ req: Req,
96
+ persons: z
97
+ .array(z.object({ person: HostedPersonId, knownSeq: z.number().int().nonnegative().optional() }))
98
+ .min(1)
99
+ .max(LIMITS.dirLookupPersons),
100
+ size: Size.optional(),
101
+ proof: DirReadProof.optional(),
102
+ });
103
+ /** Persons' heads (`PersonHead`) at the STH of `size`, or the latest; before or after `hello`. */
104
+ export const DirHeadsFrame = z.object({
105
+ t: z.literal("dir.heads"),
106
+ req: Req,
107
+ persons: z.array(HostedPersonId).min(1).max(LIMITS.dirLookupPersons),
108
+ size: Size.optional(),
109
+ });
110
+ /**
111
+ * Before `hello` only: the person whose log binds `device` as a current device, as `dir.headsAre` with that one head at
112
+ * the latest STH, or none. A device approved by link learns its person this way (design §2.5), then reads that
113
+ * person's log with `dir.lookup` and its proof.
114
+ */
115
+ export const DirDeviceFrame = z.object({
116
+ t: z.literal("dir.device"),
117
+ req: Req,
118
+ device: DeviceId,
119
+ });
120
+ /** By handle (same org only) or by exact email: exactly one of the two. */
121
+ export const DirResolveFrame = z
122
+ .object({ t: z.literal("dir.resolve"), req: Req, handle: Handle.optional(), email: Email.optional() })
123
+ .refine((frame) => (frame.handle === undefined) !== (frame.email === undefined), { message: "exactly one of handle and email" });
124
+ /** A same-org prefix search on handle, name and email local part. */
125
+ export const DirSearchFrame = z.object({
126
+ t: z.literal("dir.search"),
127
+ req: Req,
128
+ query: z.string().min(1).max(64),
129
+ limit: z.number().int().min(1).max(LIMITS.dirSearchResults),
130
+ });
131
+ /** Sets the persons this connection watches for log and key changes; it replaces the previous set, and `[]` clears it. */
132
+ export const DirWatchFrame = z.object({
133
+ t: z.literal("dir.watch"),
134
+ req: Req,
135
+ persons: z.array(HostedPersonId).max(LIMITS.watchPersons),
136
+ });
137
+ /** Sets the persons whose presence this connection watches (design §8.2); it replaces the previous set. */
138
+ export const PresenceWatchFrame = z.object({
139
+ t: z.literal("presence.watch"),
140
+ req: Req,
141
+ persons: z.array(HostedPersonId).max(LIMITS.watchPersons),
142
+ });
143
+ /** The person erases their account from Messaging (design §3.7): an `unbind` of every current device, and a fresh `cnf` token for this device. */
144
+ export const DirEraseFrame = z.object({
145
+ t: z.literal("dir.erase"),
146
+ req: Req,
147
+ entries: z.array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
148
+ token: Jws,
149
+ });
150
+ /** Most agents a wake ticket's filter names, and most awaited task requests (spec §7, "Wake tickets"). */
151
+ export const WAKE_AGENTS = 64;
152
+ export const WAKE_AWAITING = 500;
153
+ /** Most bytes of a wake ticket, and its longest life: Jelly's `jelly-wake+jwt` lives at most seven days. */
154
+ export const WAKE_TICKET_BYTES = 4096;
155
+ export const WAKE_TICKET_MAX_S = 7 * 24 * 60 * 60;
156
+ /** A wake URL: `https://<issuer host>/hosts/<hst>/wake` (or `http://` to a loopback issuer, for tests). */
157
+ export const WakeUrl = z.string().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts\/hst_[A-Za-z0-9._:-]{1,123}\/wake$/, "a Jelly ID wake URL");
158
+ /**
159
+ * A Jelly host's device arms a wake ticket before the host sleeps (spec §7, "Wake tickets"). `url` is Jelly ID's wake
160
+ * endpoint for this device's host; `ticket` the host's single-use `jelly-wake+jwt`, which the relay never verifies
161
+ * (Jelly ID does) and reads only for its `exp`; `agents` the local sessions whose own person's mail may wake the host;
162
+ * `awaiting` the request envelope ids of the tasks those agents wait on, whose updates (`replyTo`) may wake it. A new
163
+ * arm replaces the device's previous one.
164
+ */
165
+ export const WakeArmFrame = z.object({
166
+ t: z.literal("wake.arm"),
167
+ req: Req,
168
+ url: WakeUrl,
169
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
170
+ agents: z.array(SessionName).max(WAKE_AGENTS),
171
+ awaiting: z.array(Ulid).max(WAKE_AWAITING),
172
+ });
173
+ /** The relay forgets this device's wake ticket. */
174
+ export const WakeDisarmFrame = z.object({ t: z.literal("wake.disarm"), req: Req });
175
+ /** Every frame of a directory client that the relay binding (§7) does not already define. */
176
+ export const DirectoryClientFrame = z.discriminatedUnion("t", [
177
+ DirBindFrame,
178
+ HostedHelloFrame,
179
+ DirAppendFrame,
180
+ DirKeysFrame,
181
+ DirSthFrame,
182
+ DirLookupFrame,
183
+ DirDeviceFrame,
184
+ DirHeadsFrame,
185
+ DirResolveFrame,
186
+ DirSearchFrame,
187
+ DirWatchFrame,
188
+ PresenceWatchFrame,
189
+ DirEraseFrame,
190
+ WakeArmFrame,
191
+ WakeDisarmFrame,
192
+ ]);
193
+ // ---- relay → client ----
194
+ /** The answer to a hosted `hello`: the directory's latest STH. */
195
+ export const HostedWelcomeFrame = z.object({ t: z.literal("welcome"), sth: STH });
196
+ /**
197
+ * Success. `dir.bind` gets `person`, `seq` and `sth`; `dir.append` gets `seq` and `sth`; `dir.keys` gets `sth`;
198
+ * `dir.watch`, `presence.watch` and `dir.erase` get none of them.
199
+ */
200
+ export const DirOkFrame = z.object({
201
+ t: z.literal("dir.ok"),
202
+ req: Req,
203
+ person: HostedPersonId.optional(),
204
+ seq: z.number().int().nonnegative().optional(),
205
+ sth: STH.optional(),
206
+ });
207
+ /**
208
+ * An STH; `head` is its last epoch-log leaf (index `size − 1`, whose `mapRoot` lookups at this STH prove
209
+ * against) with its inclusion proof. `consistency` answers `from`, `epoch` answers `epoch`, and `rotation`
210
+ * comes with any STH signed by a new epoch key (design §3.8).
211
+ */
212
+ export const DirSthIsFrame = z.object({
213
+ t: z.literal("dir.sthIs"),
214
+ req: Req,
215
+ sth: STH,
216
+ head: EpochProof,
217
+ consistency: z.array(Hash).max(64).optional(),
218
+ epoch: EpochProof.optional(),
219
+ rotation: KeyRotation.optional(),
220
+ });
221
+ /** One view per person asked, at `sth`. Fewer when the frame would pass `LIMITS.frameBytes`: ask again for the rest. */
222
+ export const DirPersonsFrame = z.object({
223
+ t: z.literal("dir.persons"),
224
+ req: Req,
225
+ sth: STH,
226
+ /** The STH's last epoch leaf (index `size − 1`) with its inclusion proof: what `verifyPersonView` checks the map against. */
227
+ head: EpochProof,
228
+ persons: z.array(PersonView).max(LIMITS.dirLookupPersons),
229
+ });
230
+ /** One head per person asked, at `sth`; `head` as in `dir.persons`. */
231
+ export const DirHeadsAreFrame = z.object({
232
+ t: z.literal("dir.headsAre"),
233
+ req: Req,
234
+ sth: STH,
235
+ head: EpochProof,
236
+ persons: z.array(PersonHead).max(LIMITS.dirLookupPersons),
237
+ });
238
+ /** A person a handle, an email or a search matched, as the directory reads them from the person's latest verified token. */
239
+ export const ResolvedPerson = z.object({
240
+ person: HostedPersonId,
241
+ handle: Handle,
242
+ name: z.string().max(200).optional(),
243
+ email: Email.optional(),
244
+ org: OrgId.optional(),
245
+ });
246
+ /** An unknown email and an undiscoverable one get the same empty answer. */
247
+ export const DirResolvedFrame = z.object({
248
+ t: z.literal("dir.resolved"),
249
+ req: Req,
250
+ persons: z.array(ResolvedPerson).max(LIMITS.dirSearchResults),
251
+ });
252
+ /** A push: the logs or keys of these watched persons changed as of `sth`. */
253
+ export const DirChangedFrame = z.object({
254
+ t: z.literal("dir.changed"),
255
+ persons: z.array(HostedPersonId).min(1).max(LIMITS.watchPersons),
256
+ sth: STH,
257
+ });
258
+ /** A push to a device just removed: its own signed `unbind` entry, before the relay closes with 4003. */
259
+ export const DirUnboundFrame = z.object({ t: z.literal("dir.unbound"), entry: LogEntry, sth: STH });
260
+ /** Every frame a directory relay sends that the relay binding (§7) does not already define. */
261
+ export const DirectoryRelayFrame = z.discriminatedUnion("t", [
262
+ HostedWelcomeFrame,
263
+ DirOkFrame,
264
+ DirSthIsFrame,
265
+ DirPersonsFrame,
266
+ DirHeadsAreFrame,
267
+ DirResolvedFrame,
268
+ DirChangedFrame,
269
+ DirUnboundFrame,
270
+ ]);
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Hosted realms: the key directory's signed and hashed objects (spec §15). Every object here is hashed or
3
+ * signed as JCS, so each is strict: an unknown member is refused rather than carried unsigned.
4
+ */
5
+ import { z } from "../../vendor/zod/index.js";
6
+ import { Base64Url, EncKey, Hash, HostedPersonId, Key32, LIMITS, MachineId, MachineLabel, Millis, PROTOCOL } from "./common.js";
7
+ import { Jws } from "./jelly.js";
8
+ import { RealmId } from "./realm.js";
9
+ /** A map proof (§15.5): a 256-bit bitmap of the non-empty siblings, and those siblings, bottom up. */
10
+ export const MapProof = z.strictObject({
11
+ bitmap: Base64Url.length(43),
12
+ siblings: z.array(Hash).max(256),
13
+ });
14
+ /** What a live person's map leaf commits to (§15.5): their log head, and the hashes of their current keys statements. */
15
+ export const MapValue = z.strictObject({
16
+ log: z.strictObject({ seq: z.number().int().nonnegative(), hash: Hash }),
17
+ keys: z.array(Hash).max(LIMITS.devicesPerPerson),
18
+ });
19
+ /** A device's id: its Ed25519 public key, as a machine's (§3). */
20
+ export const DeviceId = MachineId.describe("Ed25519 public key of a device, base64url");
21
+ /** What kind of device it is; informational. */
22
+ export const DeviceKind = z.enum(["jelly-host", "laptop", "server", "other"]);
23
+ /** Why a device was unbound (§15.3). */
24
+ export const UnbindReason = z.enum(["self", "removed", "lost", "replaced", "account_deleted"]);
25
+ /** An Ed25519 signature: 64 bytes, base64url. */
26
+ const Signature = z.string().regex(/^[A-Za-z0-9_-]{86}$/, "64 bytes, base64url without padding");
27
+ /**
28
+ * A new device's request to be approved by one of its person's devices (§15.3): shown as
29
+ * `messaging-link.<base64url(JCS(request))>`. `sig` is the new device's, over JCS(request without sig).
30
+ */
31
+ export const LinkRequest = z.strictObject({
32
+ v: z.literal(PROTOCOL),
33
+ realm: RealmId,
34
+ device: DeviceId,
35
+ enc: EncKey,
36
+ label: MachineLabel,
37
+ kind: DeviceKind,
38
+ nonce: z.string().regex(/^[A-Za-z0-9_-]{22}$/, "16 bytes, base64url"),
39
+ ts: Millis,
40
+ sig: Signature,
41
+ });
42
+ /** Who vouches for a device-log entry (§15.3). */
43
+ export const Authority = z.discriminatedUnion("kind", [
44
+ /** Jelly ID, by a resource token bound (`cnf`) to the signing device. */
45
+ z.strictObject({ kind: z.literal("jelly"), token: Jws }),
46
+ /** One of the person's Jelly-vouched devices, approving the link request of a new one; the entry's `by`. */
47
+ z.strictObject({ kind: z.literal("device"), approver: DeviceId, link: LinkRequest }),
48
+ /** The directory, by its epoch key: only the unbinds of an erased account. */
49
+ z.strictObject({ kind: z.literal("service") }),
50
+ /** The device itself: only its own unbind. */
51
+ z.strictObject({ kind: z.literal("self") }),
52
+ ]);
53
+ /**
54
+ * One entry of a hosted person's device log (§15.3), hash-chained by `prev` and signed by `by` over
55
+ * JCS(entry without sig) with `DOMAIN.deviceLog`. `bind` names the device's `label` and `kind`; `attest` may
56
+ * change them; `unbind` gives a `reason` and nothing else.
57
+ */
58
+ export const LogEntry = z
59
+ .strictObject({
60
+ v: z.literal(PROTOCOL),
61
+ realm: RealmId,
62
+ person: HostedPersonId,
63
+ seq: z.number().int().nonnegative(),
64
+ prev: Hash.nullable(),
65
+ ts: Millis,
66
+ op: z.enum(["bind", "attest", "unbind"]),
67
+ device: DeviceId,
68
+ label: MachineLabel.optional(),
69
+ kind: DeviceKind.optional(),
70
+ reason: UnbindReason.optional(),
71
+ authority: Authority,
72
+ by: Key32,
73
+ sig: Signature,
74
+ })
75
+ .refine((entry) => (entry.seq === 0) === (entry.prev === null), { message: "prev is null exactly at seq 0", path: ["prev"] })
76
+ .refine((entry) => entry.op !== "bind" || (entry.label !== undefined && entry.kind !== undefined), { message: "a bind names label and kind", path: ["label"] })
77
+ .refine((entry) => (entry.op === "unbind") === (entry.reason !== undefined), { message: "an unbind, and only an unbind, gives a reason", path: ["reason"] })
78
+ .refine((entry) => entry.op !== "unbind" || (entry.label === undefined && entry.kind === undefined), { message: "an unbind names no label or kind", path: ["label"] });
79
+ /**
80
+ * A device's self-signed encryption key (§15.4): the directory keeps the latest per device (`seq` strictly
81
+ * increases). `sig` is the device's, over JCS(statement without sig) with `DOMAIN.deviceKeys`.
82
+ */
83
+ export const KeysStatement = z.strictObject({
84
+ v: z.literal(PROTOCOL),
85
+ realm: RealmId,
86
+ person: HostedPersonId,
87
+ device: DeviceId,
88
+ enc: EncKey,
89
+ seq: z.number().int().nonnegative(),
90
+ ts: Millis,
91
+ sig: Signature,
92
+ });
93
+ /**
94
+ * One leaf of the epoch log (§15.6): the map root of epoch `epoch` and of the epoch before it (`E[256]`, the
95
+ * empty map's root, before epoch 0). The leaf's data is UTF-8(JCS(leaf)).
96
+ */
97
+ export const EpochLeaf = z.strictObject({
98
+ epoch: z.number().int().nonnegative(),
99
+ ts: Millis,
100
+ mapRoot: Hash,
101
+ prevMapRoot: Hash,
102
+ });
103
+ /** An epoch-log leaf and its RFC 9162 audit path in the tree of the STH it is sent with (§15.6). */
104
+ export const EpochProof = z.strictObject({
105
+ index: z.number().int().nonnegative(),
106
+ leaf: EpochLeaf,
107
+ inclusion: z.array(Hash).max(64),
108
+ });
109
+ /**
110
+ * A signed tree head of the epoch log (§15.6): `size` leaves with root `root`, at `ts`. `sig` is the directory's
111
+ * epoch key's, over JCS(head without sig) with `DOMAIN.epoch`.
112
+ */
113
+ export const STH = z.strictObject({
114
+ v: z.literal(PROTOCOL),
115
+ realm: RealmId,
116
+ size: z.number().int().positive(),
117
+ root: Hash,
118
+ ts: Millis,
119
+ sig: Signature,
120
+ });
121
+ /**
122
+ * The directory's epoch key moving from `old` to `new` (§15.6): STHs of `fromSize` leaves and more are signed by
123
+ * `new`. Each key signs JCS(rotation without its two signatures) with `DOMAIN.dirKey`.
124
+ */
125
+ export const KeyRotation = z.strictObject({
126
+ v: z.literal(PROTOCOL),
127
+ realm: RealmId,
128
+ old: Key32,
129
+ new: Key32,
130
+ fromSize: z.number().int().positive(),
131
+ sigOld: Signature,
132
+ sigNew: Signature,
133
+ });
134
+ /**
135
+ * Evidence that log entry `seq` was in the directory by an epoch (§15.7): that epoch's leaf in the view's STH,
136
+ * and the person's map value there, which commits to a log head at or after `seq`.
137
+ */
138
+ export const EntryEpoch = z.strictObject({
139
+ seq: z.number().int().nonnegative(),
140
+ epoch: EpochProof,
141
+ value: MapValue,
142
+ mapProof: MapProof,
143
+ });
144
+ /**
145
+ * One person's head as the directory serves it at an STH (§7, "Directory frames"; §15.7): the map proof of their value
146
+ * at the STH's last epoch, and that value's preimage (`value`: their log head and the hashes of their current devices'
147
+ * keys statements), or none for a person the map does not hold, or `tombstone: true` for an erased one. It holds no
148
+ * log entry, so no Jelly token and nothing of a profile: what a device about to bind needs, and what anyone may read.
149
+ */
150
+ export const PersonHead = z.strictObject({
151
+ person: HostedPersonId,
152
+ mapProof: MapProof,
153
+ value: MapValue.optional(),
154
+ tombstone: z.literal(true).optional(),
155
+ });
156
+ /**
157
+ * One person as the directory serves them at an STH (§15.7): their log after the asker's `knownSeq` (all of it
158
+ * without one), the keys statements of their current devices, the map proof of their value at the STH's last
159
+ * epoch, and for every entry of `log` with a `jelly` authority, an `EntryEpoch` (its first epoch, from the
160
+ * directory's records). An erased person is `tombstone: true` with an empty log and no keys.
161
+ */
162
+ export const PersonView = z.strictObject({
163
+ person: HostedPersonId,
164
+ log: z.array(LogEntry).max(1024),
165
+ keys: z.array(KeysStatement).max(LIMITS.devicesPerPerson),
166
+ mapProof: MapProof,
167
+ epochs: z.array(EntryEpoch).max(1024).optional(),
168
+ tombstone: z.literal(true).optional(),
169
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The Messaging envelope: the end-to-end unit between machines. The sender signs it;
3
+ * every body except presence is sealed to the recipient machine's X25519 key.
4
+ * The relay can read the header (routing) but not the body.
5
+ */
6
+ import { z } from "../../vendor/zod/index.js";
7
+ import { Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "./a2a.js";
8
+ import { AgentKind, Base64Url, ChannelName, DeliveryTier, MachineId, MachineLabel, Millis, PersonId, PROTOCOL, SessionAddress, SessionName, SessionState, Ulid, LIMITS, } from "./common.js";
9
+ import { CardData, CardGetData, GrantSync } from "./consent.js";
10
+ import { DelegationChain } from "./delegation.js";
11
+ import { ErrorCode } from "./errors.js";
12
+ import { Anchor, RealmId } from "./realm.js";
13
+ export const EnvelopeType = z.enum(["message", "task.status", "task.artifact", "task.cancel", "receipt", "presence"]);
14
+ export const SEAL_ALG = "x25519-hkdf-sha256-a256gcm";
15
+ /**
16
+ * What the envelope is to its recipient's person (spec §5, "Class"), in the header so the signature and the AAD
17
+ * cover it: `self` between two devices of one person (`from.person` = `to.person`), `contact` between two persons,
18
+ * and `request` for a message request (spec §6.9, "Message requests"): a person's first line to a person of another
19
+ * org, which a directory relay paces by this header alone. Every sealed envelope carries it; presence does not.
20
+ */
21
+ export const EnvelopeClass = z.enum(["self", "contact", "request"]);
22
+ /**
23
+ * One end of an envelope (spec §5, "Ends"): a person, one of their devices, and optionally an agent (a session) on
24
+ * that device. With no `agent`, the end is the person (or, for receipts and task control, the device acting for them).
25
+ */
26
+ export const Party = z.object({
27
+ person: PersonId,
28
+ device: MachineId,
29
+ agent: SessionName.optional(),
30
+ });
31
+ /** Sender: the signing device, its person, and the agent that wrote the envelope, if an agent did. */
32
+ export const Sender = Party;
33
+ /** Recipient: one device of a person, and optionally an agent on it; presence alone goes to the whole team. */
34
+ export const Recipient = z.union([
35
+ Party,
36
+ z.object({ team: z.literal(true) }), // presence broadcast
37
+ ]);
38
+ /**
39
+ * Wire form. `sig` = Ed25519(from.device, DOMAIN.envelope + JCS(envelope without `sig`)).
40
+ * Sealed: enc = SEAL_ALG, epk + nonce present, body = base64url(ciphertext||tag),
41
+ * AAD = UTF-8(JCS(header)), header = envelope without body and sig.
42
+ * Plain (presence only): enc = "none", body = payload object, no epk/nonce.
43
+ * `realm` is the sender's realm, and `anchor` its view of the realm when it sealed the envelope (spec §5,
44
+ * "Realm and anchor"): the signature covers both, and so does the AAD of a sealed envelope.
45
+ */
46
+ export const Envelope = z.object({
47
+ v: z.literal(PROTOCOL),
48
+ id: Ulid,
49
+ realm: RealmId,
50
+ type: EnvelopeType,
51
+ from: Sender,
52
+ to: Recipient,
53
+ ts: Millis,
54
+ exp: Millis,
55
+ anchor: Anchor,
56
+ class: EnvelopeClass.optional(),
57
+ replyTo: Ulid.optional(),
58
+ enc: z.enum([SEAL_ALG, "none"]),
59
+ epk: Base64Url.optional(),
60
+ nonce: Base64Url.optional(),
61
+ body: z.unknown(),
62
+ sig: Base64Url,
63
+ });
64
+ // ---- Decrypted body payloads, by envelope type ----
65
+ /** A channel's persons: roster person names, or hosted `p_…` ids in a directory realm (spec §6.8). */
66
+ const ChannelPersons = z.array(PersonId).min(1).max(LIMITS.channelPersons);
67
+ /**
68
+ * A channel change or post (spec §6.8), on a `kind: "message"` envelope sealed to a device with no recipient agent.
69
+ * `create`: the full channel record with the person who created it, to every member device and to each later
70
+ * invitee. `invite`: persons added, to the existing members. `leave`: the sender left. `remove`: persons removed by
71
+ * the creator or an admin, to every member device. `post`: a line in the channel. `name` is always the channel's own
72
+ * name. A DM line has no `conv` (spec §5, "Meaning").
73
+ */
74
+ export const ChannelConv = z.discriminatedUnion("op", [
75
+ z.object({ kind: z.literal("channel"), op: z.literal("create"), id: Ulid, name: ChannelName, persons: ChannelPersons, creator: PersonId }),
76
+ z.object({ kind: z.literal("channel"), op: z.literal("invite"), id: Ulid, name: ChannelName, persons: ChannelPersons }),
77
+ z.object({ kind: z.literal("channel"), op: z.literal("leave"), id: Ulid, name: ChannelName }),
78
+ z.object({ kind: z.literal("channel"), op: z.literal("remove"), id: Ulid, name: ChannelName, persons: ChannelPersons }),
79
+ z.object({ kind: z.literal("channel"), op: z.literal("post"), id: Ulid, name: ChannelName }),
80
+ ]);
81
+ /**
82
+ * Sync between two devices of one person (spec §6.9, "Sync"): `grant` carries grants the person set or re-signed,
83
+ * `snapshot` every current grant, for a device of the person that is new to the sender. The message carries exactly
84
+ * one data part, a `GrantSync`.
85
+ */
86
+ export const SelfConv = z.object({ kind: z.literal("self"), what: z.enum(["grant", "snapshot"]) });
87
+ /**
88
+ * A message request (spec §6.9, "Message requests"), on an envelope of class `request` from a person to a person:
89
+ * its message carries exactly one text part of at most `LIMITS.requestChars`.
90
+ */
91
+ export const RequestConv = z.object({ kind: z.literal("request") });
92
+ /**
93
+ * A contact card between two persons (spec §6.9, "Contact cards"): `card` carries one data part `{card}`, the signed
94
+ * card; `get` asks for the receiver's card and carries one data part `{get: true}`.
95
+ */
96
+ export const CardConv = z.object({ kind: z.literal("card"), what: z.enum(["card", "get"]) });
97
+ /** The conversation a `message` belongs to when its ends alone do not say (spec §5, "Meaning"). */
98
+ export const Conv = z.discriminatedUnion("kind", [ChannelConv, SelfConv, RequestConv, CardConv]);
99
+ /**
100
+ * type "message". `kind: "task"` asks the recipient to run `message` as a new A2A task;
101
+ * the sender assigns `message.taskId` (a ULID) and `message.contextId`.
102
+ * Replies to a task are task.status/task.artifact envelopes, not messages.
103
+ */
104
+ export const MessagePayload = z
105
+ .object({
106
+ kind: z.enum(["message", "task"]),
107
+ message: Message,
108
+ /** Sender's hint that the recipient may wake an idle session. Recipient policy decides. */
109
+ wake: z.boolean().optional(),
110
+ /** Required on `kind: "task"`, absent otherwise: the signed chain from the root requester to this hop (spec §6.7). Receivers run `verifyChain`. */
111
+ delegation: DelegationChain.optional(),
112
+ /**
113
+ * The conversation, when the ends do not say it (spec §5, "Meaning"): a channel change or post (§6.8).
114
+ * `message.messageId` is a ULID shared by every copy; `message.contextId`, when set, is the ULID `messageId` of
115
+ * the thread's first line.
116
+ */
117
+ conv: Conv.optional(),
118
+ })
119
+ .refine((p) => (p.kind === "task" ? p.message.taskId !== undefined : true), { message: "a task message needs message.taskId" })
120
+ .refine((p) => (p.kind === "task") === (p.delegation !== undefined), { message: "a task request carries a delegation chain, and only a task request" })
121
+ .refine((p) => p.conv?.kind !== "self" || (p.message.parts.length === 1 && GrantSync.safeParse(p.message.parts[0].data).success), {
122
+ message: "a self sync message carries exactly one data part with its grants",
123
+ })
124
+ .refine((p) => p.conv?.kind !== "request" || requestMessage(p.message), {
125
+ message: `a message request carries one text part of 1 to ${LIMITS.requestChars} characters, and nothing else`,
126
+ })
127
+ .refine((p) => p.conv?.kind !== "card" || cardMessage(p.message, p.conv.what), {
128
+ message: "a card message carries one data part, the card or the request for one, and nothing else",
129
+ })
130
+ .refine((p) => p.message.taskId === undefined || Ulid.safeParse(p.message.taskId).success, {
131
+ message: "Messaging task ids are ULIDs",
132
+ });
133
+ /** The members a message request or card message may have: no metadata, extensions or other text beside its one part. */
134
+ const BARE_MESSAGE = new Set(["messageId", "role", "parts"]);
135
+ const bare = (message, part) => Object.keys(message).every((key) => BARE_MESSAGE.has(key)) && message.parts.length === 1 && Object.keys(message.parts[0]).join() === part;
136
+ /** Spec §6.9, "Message requests": exactly one text part of 1 to `LIMITS.requestChars` UTF-16 code units, and nothing else. */
137
+ function requestMessage(message) {
138
+ const text = message.parts[0]?.text;
139
+ return bare(message, "text") && typeof text === "string" && text.length >= 1 && text.length <= LIMITS.requestChars;
140
+ }
141
+ /** Spec §6.9, "Contact cards": exactly one data part, `{card}` or `{get: true}`, and nothing else. */
142
+ function cardMessage(message, what) {
143
+ return bare(message, "data") && (what === "card" ? CardData : CardGetData).safeParse(message.parts[0].data).success;
144
+ }
145
+ /** type "task.status": executor → requester. */
146
+ export const TaskStatusPayload = TaskStatusUpdateEvent;
147
+ /** type "task.artifact": executor → requester. */
148
+ export const TaskArtifactPayload = TaskArtifactUpdateEvent;
149
+ /** type "task.cancel": requester → executor. The executor daemon moves the task to CANCELED if not terminal. */
150
+ export const TaskCancelPayload = z.object({
151
+ taskId: Ulid,
152
+ reason: z.string().max(1000).optional(),
153
+ });
154
+ /** type "receipt": recipient → sender, signed proof of a delivery stage for envelope `ref`. */
155
+ export const ReceiptPayload = z.object({
156
+ ref: Ulid,
157
+ stage: z.enum(["delivered", "held", "handed_to_agent", "read", "rejected"]),
158
+ code: ErrorCode.optional(),
159
+ note: z.string().max(500).optional(),
160
+ });
161
+ export const SessionInfo = z.object({
162
+ session: SessionName,
163
+ agent: AgentKind,
164
+ repo: z.string().max(200).optional(),
165
+ branch: z.string().max(200).optional(),
166
+ state: SessionState,
167
+ doing: z.string().max(200).optional(),
168
+ tier: DeliveryTier,
169
+ since: Millis,
170
+ /** No client holds the session: it is kept (spec §8.2) and its mail waits for it. */
171
+ kept: z.literal(true).optional(),
172
+ /** This machine would act on a sender's wake hint for the session now (spec §6.5, §8.3, §8.6). */
173
+ wake: z.literal(true).optional(),
174
+ });
175
+ /** type "presence": plain (not sealed), to {team:true}. Latest `seq` per machine wins. */
176
+ export const PresencePayload = z.object({
177
+ seq: z.number().int().nonnegative(),
178
+ label: MachineLabel,
179
+ sessions: z.array(SessionInfo).max(LIMITS.presenceSessions),
180
+ /** Optional protocol features this machine supports, e.g. "tasks". Unknown features are ignored. */
181
+ features: z.array(z.string().max(64)).max(32).optional(),
182
+ /**
183
+ * Under a Jelly host: no window of this device's person is visible on it (`person.attached`, spec §6.5, §8.11). Its
184
+ * agents still run; the person is away from this device. Absent: the person may be at it.
185
+ */
186
+ away: z.literal(true).optional(),
187
+ });
188
+ export const PAYLOAD_BY_TYPE = {
189
+ message: MessagePayload,
190
+ "task.status": TaskStatusPayload,
191
+ "task.artifact": TaskArtifactPayload,
192
+ "task.cancel": TaskCancelPayload,
193
+ receipt: ReceiptPayload,
194
+ presence: PresencePayload,
195
+ };
196
+ /**
197
+ * Provenance the receiving daemon attaches at `Message.metadata[URI.provenance]` before
198
+ * handing a message to any client. Clients show it; peers can never set it (the daemon overwrites it).
199
+ */
200
+ export const Provenance = z.object({
201
+ envelopeId: Ulid,
202
+ from: SessionAddress.describe("sender address person/agent@label"),
203
+ person: PersonId,
204
+ /** The sending agent; absent when the person wrote it. */
205
+ agent: SessionName.optional(),
206
+ /** The sending device. */
207
+ device: MachineId,
208
+ label: MachineLabel,
209
+ verified: z.literal("signature"),
210
+ receivedAt: Millis,
211
+ });