@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,199 @@
1
+ /**
2
+ * Consent (spec §6.9), pure: signing and verifying a person's grants, choosing between two copies of one grant,
3
+ * re-signing the grants of a removed device, and deciding what a receiving person's daemon does with an envelope
4
+ * from another person. The daemon supplies the facts (the relation, the grants whose signers are current, the
5
+ * delegation chain) and the clock.
6
+ */
7
+ import { asMessagingError, parseWith } from "./check.js";
8
+ import { canonicalize } from "./jcs.js";
9
+ import { utf8 } from "./b64u.js";
10
+ import { DOMAIN, LIMITS, PROTOCOL } from "./schema/common.js";
11
+ import { ConsentGrant, grantProblem, } from "./schema/consent.js";
12
+ import { MessagingError } from "./schema/errors.js";
13
+ import { sign, signingInput, verifyInput } from "./sign.js";
14
+ import { ulid } from "./ulid.js";
15
+ /** Signs a new grant with `identity`, a device of `args.person`. Throws `invalid_request` for a grant that breaks a rule. */
16
+ export function signConsentGrant(identity, args) {
17
+ const unsigned = {
18
+ v: PROTOCOL,
19
+ realm: args.realm,
20
+ id: args.id ?? ulid(args.now),
21
+ person: args.person,
22
+ by: identity.machine,
23
+ ts: args.now,
24
+ subject: args.subject,
25
+ ...defined(args.levels),
26
+ };
27
+ return finish(identity, unsigned);
28
+ }
29
+ function finish(identity, unsigned) {
30
+ const problem = grantProblem(unsigned);
31
+ if (problem)
32
+ throw new MessagingError("invalid_request", problem);
33
+ const grant = parseWith(ConsentGrant, { ...unsigned, sig: sign(identity, DOMAIN.grant, unsigned) }, "grant");
34
+ if (utf8(canonicalize(grant)).length > LIMITS.grantBytes)
35
+ throw new MessagingError("too_large", `a grant is at most ${LIMITS.grantBytes} bytes`);
36
+ return grant;
37
+ }
38
+ /**
39
+ * Checks a grant received from another device: the schema and its rules, the size, the realm, the person when
40
+ * given, and the signature by `by` over the raw value (so members a newer device adds stay covered). Whether `by`
41
+ * is a current device of `person` is the caller's: that changes over time (spec §6.9, "Signers"). Throws only
42
+ * MessagingError: `invalid_request`, `too_large` or `bad_signature`.
43
+ */
44
+ export function verifyConsentGrant(raw, ctx) {
45
+ try {
46
+ const grant = parseWith(ConsentGrant, raw, "grant");
47
+ // I-JSON within LIMITS.jsonDepth, then the size of the whole grant as received.
48
+ if (utf8(canonicalize(raw)).length > LIMITS.grantBytes)
49
+ throw new MessagingError("too_large", `a grant is at most ${LIMITS.grantBytes} bytes`);
50
+ const { sig, ...unsigned } = raw;
51
+ const input = signingInput(DOMAIN.grant, unsigned);
52
+ if (grant.realm !== ctx.realm)
53
+ throw new MessagingError("invalid_request", "the grant is for another realm");
54
+ if (ctx.person !== undefined && grant.person !== ctx.person)
55
+ throw new MessagingError("invalid_request", "the grant is another person's");
56
+ if (typeof sig !== "string" || !verifyInput(grant.by, input, sig))
57
+ throw new MessagingError("bad_signature", "the grant's signature is invalid");
58
+ return grant;
59
+ }
60
+ catch (error) {
61
+ throw asMessagingError(error, "grant");
62
+ }
63
+ }
64
+ /** The key one grant is stored under: its subject. One grant per subject. */
65
+ export function grantSubjectKey(subject) {
66
+ return "person" in subject ? `person:${subject.person}` : `channel:${subject.channel}`;
67
+ }
68
+ /**
69
+ * Of two copies of the grant for one subject, the one that counts (spec §6.9, "Merging"): the newer by `ts`, then
70
+ * by `id`. Copies that tie on both are one grant signed twice: a copy whose signer is current beats one whose
71
+ * signer is not; between two current signers (or two removed ones), the signer that sorts first wins, then the
72
+ * signature. The choice is commutative and idempotent, so devices that see the same copies in any order agree.
73
+ */
74
+ export function preferGrant(a, b, current) {
75
+ if (a.ts !== b.ts)
76
+ return a.ts > b.ts ? a : b;
77
+ if (a.id !== b.id)
78
+ return a.id > b.id ? a : b;
79
+ const ca = current(a.by);
80
+ const cb = current(b.by);
81
+ if (ca !== cb)
82
+ return ca ? a : b;
83
+ if (a.by !== b.by)
84
+ return a.by < b.by ? a : b;
85
+ return a.sig <= b.sig ? a : b;
86
+ }
87
+ /** Levels that narrow reach: blocks never lapse (spec §6.9, "Signers"). */
88
+ export function restrictingLevels(grant) {
89
+ return defined({
90
+ chat: grant.chat === "blocked" || grant.chat === "muted" ? grant.chat : undefined,
91
+ agentMail: grant.agentMail === "blocked" ? grant.agentMail : undefined,
92
+ ask: grant.ask === "no" ? grant.ask : undefined,
93
+ discoverable: grant.discoverable === false ? false : undefined,
94
+ });
95
+ }
96
+ /** Levels that widen reach: they lapse with their signer, into the person's review list. */
97
+ export function wideningLevels(grant) {
98
+ return defined({
99
+ chat: grant.chat === "open" ? grant.chat : undefined,
100
+ agentMail: grant.agentMail === "open" ? grant.agentMail : undefined,
101
+ ask: grant.ask === "open" || grant.ask === "request" ? grant.ask : undefined,
102
+ trustedUntil: grant.trustedUntil,
103
+ discoverable: grant.discoverable === true ? true : undefined,
104
+ });
105
+ }
106
+ /**
107
+ * Re-signs a grant whose signer is no longer current with `identity`, a current device of the same person
108
+ * (spec §6.9, "Signers"). `keep: "all"` (a replacement) keeps every level; `keep: "restricting"` (any other
109
+ * removal) keeps only the restricting ones, and returns undefined when there are none: the grant lapses whole.
110
+ * The copy keeps `id` and `ts`, so it never overrides a newer change, and records `resignedAt`.
111
+ */
112
+ export function resignGrant(identity, grant, opts) {
113
+ const levels = opts.keep === "all" ? levelsOf(grant) : restrictingLevels(grant);
114
+ if (opts.keep === "restricting" && Object.keys(levels).length === 0)
115
+ return undefined;
116
+ const { v, realm, id, person, ts, subject } = grant;
117
+ return finish(identity, { v, realm, id, person, by: identity.machine, ts, subject, ...levels, resignedAt: Math.max(opts.now, ts) });
118
+ }
119
+ export function levelsOf(grant) {
120
+ return defined({ chat: grant.chat, agentMail: grant.agentMail, ask: grant.ask, trustedUntil: grant.trustedUntil, discoverable: grant.discoverable });
121
+ }
122
+ const DEFAULTS = {
123
+ org: { chat: "open", agentMail: "open", ask: "request" },
124
+ contact: { chat: "open", agentMail: "blocked", ask: "no" },
125
+ stranger: { chat: "request_required", agentMail: "blocked", ask: "no" },
126
+ };
127
+ const ASK_RANK = { no: 0, request: 1, open: 2 };
128
+ /**
129
+ * Spec §6.9, "Effective policy": each level is the person grant's when it sets one; otherwise, for `ask` and
130
+ * `agentMail`, the most permissive among the grants of channels the sender is in; otherwise the relation's
131
+ * default. An expired `trustedUntil` reads as absent. The caller passes only grants whose signer is current.
132
+ */
133
+ export function effectivePolicy(relation, person, channels, now) {
134
+ const base = DEFAULTS[relation];
135
+ const channelAsk = channels.map((grant) => grant.ask).filter((level) => level !== undefined);
136
+ const channelMail = channels.map((grant) => grant.agentMail).filter((level) => level !== undefined);
137
+ const ask = person?.ask ?? (channelAsk.length > 0 ? channelAsk.reduce((a, b) => (ASK_RANK[b] > ASK_RANK[a] ? b : a)) : undefined);
138
+ const agentMail = person?.agentMail ?? (channelMail.length > 0 ? (channelMail.includes("open") ? "open" : "blocked") : undefined);
139
+ const trusted = person?.trustedUntil !== undefined && person.trustedUntil > now && person.ask === "open";
140
+ return {
141
+ chat: person?.chat ?? base.chat,
142
+ agentMail: agentMail ?? base.agentMail,
143
+ ask: ask ?? base.ask,
144
+ ...(trusted ? { trustedUntil: person.trustedUntil } : {}),
145
+ granted: { chat: person?.chat !== undefined, agentMail: agentMail !== undefined, ask: ask !== undefined },
146
+ };
147
+ }
148
+ /**
149
+ * Spec §6.9, "Enforcement": step 10 of the receiver order (§5), before the inbound policy of §8.3.
150
+ * - The person's own traffic is delivered as `self`.
151
+ * - Answers to the person's own traffic are delivered whatever the grants, so a change never strands a task.
152
+ * - A `blocked` chat level refuses every other class from the sender.
153
+ * - A `stranger` reaches the person only through a level a grant opened; otherwise `request_required`.
154
+ * - Lines follow `chat` (person) or `agentMail` (agent); `muted` delivers silently.
155
+ * - Asks follow `ask`: `open` delivers guarded, or trusted while the person grant trusts the sender and every person
156
+ * in the delegation chain is the sender or the receiver; `request` holds; `no` refuses, `request_required` when
157
+ * it is the default of a `contact` or a `stranger`.
158
+ * - A message request from a `stranger` is held for the person to accept; from anyone else it is a DM line.
159
+ */
160
+ export function decide(args) {
161
+ const { inbound } = args;
162
+ if (args.relation === "self" || args.sender === args.self)
163
+ return { action: "deliver", consent: "self" };
164
+ const policy = effectivePolicy(args.relation, args.grant, args.channels, args.now);
165
+ const trusted = (chain) => policy.trustedUntil !== undefined && chain.every((person) => person === args.sender || person === args.self);
166
+ if (inbound.kind === "answer")
167
+ return { action: "deliver", consent: trusted([args.sender]) ? "trusted" : "guarded" };
168
+ if (policy.chat === "blocked")
169
+ return { action: "refuse", code: "refused" };
170
+ const stranger = args.relation === "stranger";
171
+ switch (inbound.kind) {
172
+ case "op":
173
+ if (stranger)
174
+ return { action: "refuse", code: "request_required" };
175
+ return { action: "deliver", consent: "guarded" };
176
+ case "line":
177
+ if (inbound.agent) {
178
+ if (policy.agentMail === "open")
179
+ return { action: "deliver", consent: "guarded" };
180
+ return { action: "refuse", code: stranger && !policy.granted.agentMail ? "request_required" : "refused" };
181
+ }
182
+ if (policy.chat === "request_required")
183
+ return { action: "refuse", code: "request_required" };
184
+ return policy.chat === "muted" ? { action: "deliver", consent: "guarded", silent: true } : { action: "deliver", consent: "guarded" };
185
+ case "request":
186
+ if (stranger)
187
+ return { action: "hold" };
188
+ return policy.chat === "muted" ? { action: "deliver", consent: "guarded", silent: true } : { action: "deliver", consent: "guarded" };
189
+ case "ask":
190
+ if (policy.ask === "open")
191
+ return { action: "deliver", consent: trusted(inbound.chain) ? "trusted" : "guarded" };
192
+ if (policy.ask === "request")
193
+ return { action: "hold" };
194
+ return { action: "refuse", code: !policy.granted.ask && (stranger || args.relation === "contact") ? "request_required" : "refused" };
195
+ }
196
+ }
197
+ function defined(value) {
198
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
199
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Contact cards (spec §6.9, "Contact cards"), pure: what a person tells another person they may send them, signed by
3
+ * one of the person's devices. A card is advice for the sender's surfaces; the person's own daemon enforces the
4
+ * grants, which may be newer.
5
+ */
6
+ import { utf8 } from "./b64u.js";
7
+ import { asMessagingError, parseWith } from "./check.js";
8
+ import { effectivePolicy } from "./consent.js";
9
+ import { canonicalize } from "./jcs.js";
10
+ import { DOMAIN, LIMITS, PROTOCOL } from "./schema/common.js";
11
+ import { cardProblem, ContactCard } from "./schema/consent.js";
12
+ import { MessagingError } from "./schema/errors.js";
13
+ import { sign, signingInput, verifyInput } from "./sign.js";
14
+ /** Signs a card with `identity`, a device of `args.from`. Throws `invalid_request` or `too_large`. */
15
+ export function signContactCard(identity, args) {
16
+ const unsigned = {
17
+ v: PROTOCOL,
18
+ realm: args.realm,
19
+ from: args.from,
20
+ to: args.to,
21
+ by: identity.machine,
22
+ seq: args.seq,
23
+ ts: args.now,
24
+ ...(args.grantId !== undefined ? { grantId: args.grantId } : {}),
25
+ can: { chat: args.can.chat, agentMail: args.can.agentMail, ask: args.can.ask, ...(args.can.trustedUntil !== undefined ? { trustedUntil: args.can.trustedUntil } : {}) },
26
+ };
27
+ const problem = cardProblem(unsigned);
28
+ if (problem)
29
+ throw new MessagingError("invalid_request", problem);
30
+ const card = parseWith(ContactCard, { ...unsigned, sig: sign(identity, DOMAIN.card, unsigned) }, "card");
31
+ if (utf8(canonicalize(card)).length > LIMITS.cardBytes)
32
+ throw new MessagingError("too_large", `a card is at most ${LIMITS.cardBytes} bytes`);
33
+ return card;
34
+ }
35
+ /**
36
+ * Checks a card received from another person: the schema and its rules, the size, the realm, its two persons, and
37
+ * the signature by `by` over the raw value. Whether `by` is a current device of `from` is the caller's: a card
38
+ * whose signer was removed counts as absent. Throws only MessagingError: `invalid_request`, `too_large` or
39
+ * `bad_signature`.
40
+ */
41
+ export function verifyContactCard(raw, ctx) {
42
+ try {
43
+ const card = parseWith(ContactCard, raw, "card");
44
+ if (utf8(canonicalize(raw)).length > LIMITS.cardBytes)
45
+ throw new MessagingError("too_large", `a card is at most ${LIMITS.cardBytes} bytes`);
46
+ const { sig, ...unsigned } = raw;
47
+ const input = signingInput(DOMAIN.card, unsigned);
48
+ if (card.realm !== ctx.realm)
49
+ throw new MessagingError("invalid_request", "the card is for another realm");
50
+ if (card.from !== ctx.from)
51
+ throw new MessagingError("invalid_request", "the card is not its sender's");
52
+ if (card.to !== ctx.to)
53
+ throw new MessagingError("invalid_request", "the card is for another person");
54
+ if (typeof sig !== "string" || !verifyInput(card.by, input, sig))
55
+ throw new MessagingError("bad_signature", "the card's signature is invalid");
56
+ return card;
57
+ }
58
+ catch (error) {
59
+ throw asMessagingError(error, "card");
60
+ }
61
+ }
62
+ /**
63
+ * What a card says for a sender of `relation` with the person grant `grant` (spec §6.9, "Contact cards"): the
64
+ * person grant over the relation's defaults. Channel grants are not on a card: the sender knows its own channels.
65
+ */
66
+ export function cardCan(relation, grant, now) {
67
+ const policy = effectivePolicy(relation, grant, [], now);
68
+ return {
69
+ chat: policy.chat === "open" || policy.chat === "muted",
70
+ agentMail: policy.agentMail === "open",
71
+ ask: policy.ask,
72
+ ...(policy.trustedUntil !== undefined ? { trustedUntil: policy.trustedUntil } : {}),
73
+ };
74
+ }
@@ -0,0 +1,174 @@
1
+ import { utf8 } from "./b64u.js";
2
+ import { parseAddress } from "./address.js";
3
+ import { asMessagingError, parseWith } from "./check.js";
4
+ import { sha256 } from "./hash.js";
5
+ import { canonicalize } from "./jcs.js";
6
+ import { DOMAIN, LIMITS } from "./schema/common.js";
7
+ import { DelegationChain, DelegationGrant } from "./schema/delegation.js";
8
+ import { MessagingError } from "./schema/errors.js";
9
+ import { sign, signingInput, verifyInput } from "./sign.js";
10
+ /** SHA-256 of the JCS form of the full grant, including `sig`: the next grant's `parent`. */
11
+ export function grantHash(grant) {
12
+ return sha256(canonicalize(grant));
13
+ }
14
+ /**
15
+ * Signs one grant with the machine key (spec §6.7): a root, or the next hop after `parent`. The new grant
16
+ * may narrow the parent's scope and maxDepth, never widen them, and expires no later than the parent.
17
+ * It does not check the chain as a whole; run `verifyChain` on the extended chain for that.
18
+ * Throws only MessagingError: `invalid_request` for bad arguments, `forbidden` for a widening, `expired`
19
+ * for a parent that has expired.
20
+ */
21
+ export function createGrant(identity, args) {
22
+ try {
23
+ return grant(identity, args);
24
+ }
25
+ catch (error) {
26
+ throw asMessagingError(error, "delegation grant");
27
+ }
28
+ }
29
+ function grant(identity, args) {
30
+ const now = args.now ?? Date.now();
31
+ const ttl = args.ttlMs ?? LIMITS.ttlMs;
32
+ if (!Number.isSafeInteger(now))
33
+ throw new MessagingError("invalid_request", "now must be an integer");
34
+ if (!Number.isSafeInteger(ttl) || ttl <= 0 || ttl > LIMITS.ttlMs)
35
+ throw new MessagingError("invalid_request", "ttlMs out of range");
36
+ const raw = args.parent;
37
+ const parent = raw === undefined ? undefined : parseWith(DelegationGrant, raw, "parent grant");
38
+ if (parent !== undefined) {
39
+ if (parent.to !== args.from)
40
+ throw new MessagingError("invalid_request", "a grant extends the grant to its delegating session");
41
+ }
42
+ const maxDepth = args.maxDepth ?? parent?.maxDepth ?? LIMITS.delegationDepth;
43
+ const scope = args.scope ?? parent?.scope;
44
+ if (parent !== undefined) {
45
+ if (maxDepth > parent.maxDepth)
46
+ throw new MessagingError("forbidden", "maxDepth would widen the parent grant");
47
+ if (!narrows(scope, parent.scope))
48
+ throw new MessagingError("forbidden", "scope would widen the parent grant");
49
+ }
50
+ const exp = parent === undefined ? now + ttl : Math.min(now + ttl, parent.exp);
51
+ if (exp <= now)
52
+ throw new MessagingError("expired", "the parent grant has expired");
53
+ const unsigned = {
54
+ realm: args.realm,
55
+ task: args.task,
56
+ from: args.from,
57
+ to: args.to,
58
+ by: identity.machine,
59
+ ts: now,
60
+ exp,
61
+ maxDepth,
62
+ ...(scope !== undefined ? { scope: [...scope] } : {}),
63
+ ...(raw !== undefined ? { parent: grantHash(raw) } : {}),
64
+ };
65
+ parseWith(DelegationGrant.omit({ sig: true }), unsigned, "delegation grant");
66
+ return { ...unsigned, sig: sign(identity, DOMAIN.delegation, unsigned) };
67
+ }
68
+ /**
69
+ * Verifies a delegation chain for the hop it arrived on (spec §6.7), against the locally verified realm.
70
+ * Checks, in order, stopping at the first failure:
71
+ *
72
+ * 1. schema, I-JSON and 1…`LIMITS.delegationDepth` grants (`invalid_request`); JCS within
73
+ * `LIMITS.delegationBytes` (`too_large`);
74
+ * 2. for each grant, root first: this realm (`invalid_request`); the time window, as for envelopes
75
+ * (`expired`); `by` a current member whose person and label `from` names (`not_member`); the
76
+ * signature over the raw grant (`bad_signature`); linkage, no `parent` on the root and `from = previous.to`,
77
+ * `parent = grantHash(previous)` after it (`invalid_request`); narrowing of scope, maxDepth and exp
78
+ * (`forbidden`);
79
+ * 3. at most the smallest maxDepth grants (`forbidden`);
80
+ * 4. no session twice (`forbidden`);
81
+ * 5. the last grant is for this hop (`forbidden`).
82
+ *
83
+ * Throws only MessagingError, and never quotes the chain.
84
+ */
85
+ export function verifyChain(raw, ctx) {
86
+ try {
87
+ return check(raw, ctx);
88
+ }
89
+ catch (error) {
90
+ throw asMessagingError(error, "delegation chain");
91
+ }
92
+ }
93
+ function check(raw, ctx) {
94
+ const { realm, now, hop } = ctx;
95
+ if (!Number.isSafeInteger(now))
96
+ throw new MessagingError("invalid_request", "now must be an integer");
97
+ parseWith(DelegationChain, raw, "delegation chain");
98
+ // The raw value, so unknown fields stay covered; canonicalizing it also requires I-JSON.
99
+ const grants = raw;
100
+ if (utf8(canonicalize(grants)).length > LIMITS.delegationBytes)
101
+ throw new MessagingError("too_large", `a delegation chain is limited to ${LIMITS.delegationBytes} bytes`);
102
+ let maxDepth = LIMITS.delegationDepth;
103
+ let previous;
104
+ for (const grant of grants) {
105
+ const { sig, ...unsigned } = grant;
106
+ const input = signingInput(DOMAIN.delegation, unsigned);
107
+ if (grant.realm !== realm.realm)
108
+ throw new MessagingError("invalid_request", "a grant is for another realm");
109
+ if (grant.ts > grant.exp)
110
+ throw new MessagingError("expired", "a grant's ts is after its exp");
111
+ if (grant.ts > now + LIMITS.clockSkewMs)
112
+ throw new MessagingError("expired", "a grant's timestamp is in the future");
113
+ if (grant.exp <= now)
114
+ throw new MessagingError("expired", "a grant has expired");
115
+ if (grant.exp - grant.ts > LIMITS.ttlMs)
116
+ throw new MessagingError("expired", "a grant's lifetime exceeds the maximum");
117
+ const signer = realm.device(grant.by);
118
+ if (!signer)
119
+ throw new MessagingError("not_member", "a grant's signer is not a current member");
120
+ const from = parseAddress(grant.from);
121
+ if (signer.person !== from.person || signer.label !== from.label) {
122
+ throw new MessagingError("not_member", "a grant's from does not name its signer");
123
+ }
124
+ if (!verifyInput(grant.by, input, sig))
125
+ throw new MessagingError("bad_signature", "a grant signature is invalid");
126
+ if (previous === undefined) {
127
+ if (grant.parent !== undefined)
128
+ throw new MessagingError("invalid_request", "a root grant has no parent");
129
+ }
130
+ else {
131
+ if (grant.from !== previous.to)
132
+ throw new MessagingError("invalid_request", "a grant does not extend the previous grant's recipient");
133
+ if (grant.parent !== grantHash(previous))
134
+ throw new MessagingError("invalid_request", "a grant's parent is not the previous grant");
135
+ if (!narrows(grant.scope, previous.scope))
136
+ throw new MessagingError("forbidden", "a grant widens the previous grant's scope");
137
+ if (grant.maxDepth > previous.maxDepth)
138
+ throw new MessagingError("forbidden", "a grant raises the previous grant's maxDepth");
139
+ if (grant.exp > previous.exp)
140
+ throw new MessagingError("forbidden", "a grant outlives the previous grant");
141
+ }
142
+ maxDepth = Math.min(maxDepth, grant.maxDepth);
143
+ previous = grant;
144
+ }
145
+ if (grants.length > maxDepth)
146
+ throw new MessagingError("forbidden", "the chain is deeper than its maxDepth allows");
147
+ const root = grants[0];
148
+ const last = grants[grants.length - 1];
149
+ const addresses = [root.from, ...grants.map((grant) => grant.to)];
150
+ if (new Set(addresses).size !== addresses.length)
151
+ throw new MessagingError("forbidden", "a session appears twice in the chain");
152
+ if (last.by !== hop.by || last.from !== hop.from || last.to !== hop.to || last.task !== hop.task) {
153
+ throw new MessagingError("forbidden", "the chain does not authorize this request");
154
+ }
155
+ return {
156
+ grants: structuredClone(grants),
157
+ summary: {
158
+ addresses,
159
+ depth: grants.length,
160
+ maxDepth,
161
+ ...(last.scope !== undefined ? { scope: [...last.scope] } : {}),
162
+ exp: Math.min(...grants.map((grant) => grant.exp)),
163
+ },
164
+ };
165
+ }
166
+ /** Whether `child` names nothing `parent` does not. An absent scope is unrestricted. */
167
+ function narrows(child, parent) {
168
+ if (parent === undefined)
169
+ return true;
170
+ if (child === undefined)
171
+ return false;
172
+ const allowed = new Set(parent);
173
+ return child.every((label) => allowed.has(label));
174
+ }