@joe-broadhead/messaging 0.0.0-stage → 0.7.1

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 (273) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +12 -0
  4. package/README.md +36 -2
  5. package/dist/bin/messaging-mcp.js +26524 -0
  6. package/dist/bin/messaging-relay.js +19768 -0
  7. package/dist/bin/messaging.js +15465 -0
  8. package/dist/bin/messagingd.js +29449 -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 +477 -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/conversation/context.js +80 -0
  24. package/dist/lib/modules/core/conversation/event.js +100 -0
  25. package/dist/lib/modules/core/conversation/ids.js +38 -0
  26. package/dist/lib/modules/core/conversation/link.js +35 -0
  27. package/dist/lib/modules/core/conversation/log.js +766 -0
  28. package/dist/lib/modules/core/conversation/ratchet.js +243 -0
  29. package/dist/lib/modules/core/conversation/seal.js +198 -0
  30. package/dist/lib/modules/core/delegation.js +174 -0
  31. package/dist/lib/modules/core/directory/device-log.js +258 -0
  32. package/dist/lib/modules/core/directory/epoch.js +103 -0
  33. package/dist/lib/modules/core/directory/handle.js +29 -0
  34. package/dist/lib/modules/core/directory/jelly-device.js +204 -0
  35. package/dist/lib/modules/core/directory/jelly-issuer.js +12 -0
  36. package/dist/lib/modules/core/directory/jelly-token.js +305 -0
  37. package/dist/lib/modules/core/directory/keys-statement.js +43 -0
  38. package/dist/lib/modules/core/directory/link.js +81 -0
  39. package/dist/lib/modules/core/directory/merkle-log.js +250 -0
  40. package/dist/lib/modules/core/directory/person-view.js +197 -0
  41. package/dist/lib/modules/core/directory/person.js +40 -0
  42. package/dist/lib/modules/core/directory/realm.js +59 -0
  43. package/dist/lib/modules/core/directory/smt.js +266 -0
  44. package/dist/lib/modules/core/envelope.js +320 -0
  45. package/dist/lib/modules/core/hash.js +5 -0
  46. package/dist/lib/modules/core/head.js +27 -0
  47. package/dist/lib/modules/core/identity.js +71 -0
  48. package/dist/lib/modules/core/index.js +53 -0
  49. package/dist/lib/modules/core/jcs.js +96 -0
  50. package/dist/lib/modules/core/jelly-host-double.js +35 -0
  51. package/dist/lib/modules/core/jelly-host.js +211 -0
  52. package/dist/lib/modules/core/keys.js +79 -0
  53. package/dist/lib/modules/core/peer-text.js +563 -0
  54. package/dist/lib/modules/core/realm.js +1 -0
  55. package/dist/lib/modules/core/roster.js +454 -0
  56. package/dist/lib/modules/core/schema/a2a.js +85 -0
  57. package/dist/lib/modules/core/schema/common.js +310 -0
  58. package/dist/lib/modules/core/schema/consent.js +120 -0
  59. package/dist/lib/modules/core/schema/conversation.js +292 -0
  60. package/dist/lib/modules/core/schema/delegation.js +58 -0
  61. package/dist/lib/modules/core/schema/directory-frames.js +270 -0
  62. package/dist/lib/modules/core/schema/directory.js +169 -0
  63. package/dist/lib/modules/core/schema/envelope.js +211 -0
  64. package/dist/lib/modules/core/schema/errors.js +104 -0
  65. package/dist/lib/modules/core/schema/index.js +16 -0
  66. package/dist/lib/modules/core/schema/ipc/consent.js +181 -0
  67. package/dist/lib/modules/core/schema/ipc/device.js +276 -0
  68. package/dist/lib/modules/core/schema/ipc/jelly.js +189 -0
  69. package/dist/lib/modules/core/schema/ipc/shapes.js +185 -0
  70. package/dist/lib/modules/core/schema/ipc/surfaces.js +223 -0
  71. package/dist/lib/modules/core/schema/ipc.js +451 -0
  72. package/dist/lib/modules/core/schema/jelly.js +122 -0
  73. package/dist/lib/modules/core/schema/realm.js +27 -0
  74. package/dist/lib/modules/core/schema/relay.js +112 -0
  75. package/dist/lib/modules/core/schema/roster.js +126 -0
  76. package/dist/lib/modules/core/sign.js +43 -0
  77. package/dist/lib/modules/core/task.js +37 -0
  78. package/dist/lib/modules/core/testing.js +21 -0
  79. package/dist/lib/modules/core/ulid.js +36 -0
  80. package/dist/lib/modules/core/version.js +6 -0
  81. package/dist/lib/modules/core/wire.js +255 -0
  82. package/dist/lib/modules/ipc/client.js +289 -0
  83. package/dist/lib/modules/ipc/framing.js +33 -0
  84. package/dist/lib/modules/ipc/index.js +6 -0
  85. package/dist/lib/modules/ipc/paths.js +102 -0
  86. package/dist/lib/modules/ipc/pipes.js +65 -0
  87. package/dist/lib/modules/ipc/rpc.js +8 -0
  88. package/dist/lib/modules/ipc/server.js +317 -0
  89. package/dist/lib/modules/ipc/testing.js +145 -0
  90. package/dist/lib/modules/ipc/trust.js +166 -0
  91. package/dist/lib/modules/ipc/windows-acl.js +275 -0
  92. package/dist/lib/modules/vendor/zod/index.js +7 -0
  93. package/dist/lib/modules/vendor/zod/v4/classic/checks.js +1 -0
  94. package/dist/lib/modules/vendor/zod/v4/classic/coerce.js +17 -0
  95. package/dist/lib/modules/vendor/zod/v4/classic/compat.js +31 -0
  96. package/dist/lib/modules/vendor/zod/v4/classic/deep-partial.js +13 -0
  97. package/dist/lib/modules/vendor/zod/v4/classic/errors.js +57 -0
  98. package/dist/lib/modules/vendor/zod/v4/classic/external.js +17 -0
  99. package/dist/lib/modules/vendor/zod/v4/classic/from-json-schema.js +881 -0
  100. package/dist/lib/modules/vendor/zod/v4/classic/in-out.js +36 -0
  101. package/dist/lib/modules/vendor/zod/v4/classic/iso.js +15 -0
  102. package/dist/lib/modules/vendor/zod/v4/classic/parse.js +16 -0
  103. package/dist/lib/modules/vendor/zod/v4/classic/schemas.js +1535 -0
  104. package/dist/lib/modules/vendor/zod/v4/core/api.js +1107 -0
  105. package/dist/lib/modules/vendor/zod/v4/core/checks.js +516 -0
  106. package/dist/lib/modules/vendor/zod/v4/core/compile.js +1866 -0
  107. package/dist/lib/modules/vendor/zod/v4/core/core.js +141 -0
  108. package/dist/lib/modules/vendor/zod/v4/core/doc.js +38 -0
  109. package/dist/lib/modules/vendor/zod/v4/core/errors.js +267 -0
  110. package/dist/lib/modules/vendor/zod/v4/core/index.js +19 -0
  111. package/dist/lib/modules/vendor/zod/v4/core/json-schema-generator.js +99 -0
  112. package/dist/lib/modules/vendor/zod/v4/core/json-schema-processors.js +843 -0
  113. package/dist/lib/modules/vendor/zod/v4/core/json-schema.js +1 -0
  114. package/dist/lib/modules/vendor/zod/v4/core/memoizer.js +311 -0
  115. package/dist/lib/modules/vendor/zod/v4/core/parse.js +175 -0
  116. package/dist/lib/modules/vendor/zod/v4/core/regexes.js +156 -0
  117. package/dist/lib/modules/vendor/zod/v4/core/registries.js +51 -0
  118. package/dist/lib/modules/vendor/zod/v4/core/schemas.js +2636 -0
  119. package/dist/lib/modules/vendor/zod/v4/core/to-json-schema.js +651 -0
  120. package/dist/lib/modules/vendor/zod/v4/core/util.js +873 -0
  121. package/dist/lib/modules/vendor/zod/v4/core/versions.js +5 -0
  122. package/dist/lib/modules/vendor/zod/v4/core/visit.js +169 -0
  123. package/dist/lib/modules/vendor/zod/v4/locales/ar.js +111 -0
  124. package/dist/lib/modules/vendor/zod/v4/locales/az.js +110 -0
  125. package/dist/lib/modules/vendor/zod/v4/locales/be.js +168 -0
  126. package/dist/lib/modules/vendor/zod/v4/locales/bg.js +125 -0
  127. package/dist/lib/modules/vendor/zod/v4/locales/bn.js +113 -0
  128. package/dist/lib/modules/vendor/zod/v4/locales/ca.js +112 -0
  129. package/dist/lib/modules/vendor/zod/v4/locales/ckb.js +133 -0
  130. package/dist/lib/modules/vendor/zod/v4/locales/cs.js +116 -0
  131. package/dist/lib/modules/vendor/zod/v4/locales/da.js +120 -0
  132. package/dist/lib/modules/vendor/zod/v4/locales/de.js +113 -0
  133. package/dist/lib/modules/vendor/zod/v4/locales/el.js +112 -0
  134. package/dist/lib/modules/vendor/zod/v4/locales/en.js +125 -0
  135. package/dist/lib/modules/vendor/zod/v4/locales/eo.js +114 -0
  136. package/dist/lib/modules/vendor/zod/v4/locales/es.js +136 -0
  137. package/dist/lib/modules/vendor/zod/v4/locales/fa.js +119 -0
  138. package/dist/lib/modules/vendor/zod/v4/locales/fi.js +117 -0
  139. package/dist/lib/modules/vendor/zod/v4/locales/fr-CA.js +112 -0
  140. package/dist/lib/modules/vendor/zod/v4/locales/fr.js +129 -0
  141. package/dist/lib/modules/vendor/zod/v4/locales/gu.js +113 -0
  142. package/dist/lib/modules/vendor/zod/v4/locales/he.js +221 -0
  143. package/dist/lib/modules/vendor/zod/v4/locales/hi.js +111 -0
  144. package/dist/lib/modules/vendor/zod/v4/locales/hr.js +126 -0
  145. package/dist/lib/modules/vendor/zod/v4/locales/hu.js +113 -0
  146. package/dist/lib/modules/vendor/zod/v4/locales/hy.js +158 -0
  147. package/dist/lib/modules/vendor/zod/v4/locales/id.js +111 -0
  148. package/dist/lib/modules/vendor/zod/v4/locales/index.js +63 -0
  149. package/dist/lib/modules/vendor/zod/v4/locales/is.js +114 -0
  150. package/dist/lib/modules/vendor/zod/v4/locales/it.js +113 -0
  151. package/dist/lib/modules/vendor/zod/v4/locales/ja.js +112 -0
  152. package/dist/lib/modules/vendor/zod/v4/locales/ka.js +117 -0
  153. package/dist/lib/modules/vendor/zod/v4/locales/kh.js +5 -0
  154. package/dist/lib/modules/vendor/zod/v4/locales/km.js +115 -0
  155. package/dist/lib/modules/vendor/zod/v4/locales/kn.js +116 -0
  156. package/dist/lib/modules/vendor/zod/v4/locales/ko.js +116 -0
  157. package/dist/lib/modules/vendor/zod/v4/locales/lt.js +207 -0
  158. package/dist/lib/modules/vendor/zod/v4/locales/mk.js +114 -0
  159. package/dist/lib/modules/vendor/zod/v4/locales/ms.js +112 -0
  160. package/dist/lib/modules/vendor/zod/v4/locales/ne.js +111 -0
  161. package/dist/lib/modules/vendor/zod/v4/locales/nl.js +115 -0
  162. package/dist/lib/modules/vendor/zod/v4/locales/nn.js +113 -0
  163. package/dist/lib/modules/vendor/zod/v4/locales/no.js +113 -0
  164. package/dist/lib/modules/vendor/zod/v4/locales/ota.js +114 -0
  165. package/dist/lib/modules/vendor/zod/v4/locales/pl.js +114 -0
  166. package/dist/lib/modules/vendor/zod/v4/locales/ps.js +119 -0
  167. package/dist/lib/modules/vendor/zod/v4/locales/pt-BR.js +143 -0
  168. package/dist/lib/modules/vendor/zod/v4/locales/pt.js +142 -0
  169. package/dist/lib/modules/vendor/zod/v4/locales/ro.js +122 -0
  170. package/dist/lib/modules/vendor/zod/v4/locales/ru.js +168 -0
  171. package/dist/lib/modules/vendor/zod/v4/locales/sk.js +116 -0
  172. package/dist/lib/modules/vendor/zod/v4/locales/sl.js +114 -0
  173. package/dist/lib/modules/vendor/zod/v4/locales/sv.js +115 -0
  174. package/dist/lib/modules/vendor/zod/v4/locales/ta.js +115 -0
  175. package/dist/lib/modules/vendor/zod/v4/locales/tg.js +117 -0
  176. package/dist/lib/modules/vendor/zod/v4/locales/th.js +115 -0
  177. package/dist/lib/modules/vendor/zod/v4/locales/tk.js +107 -0
  178. package/dist/lib/modules/vendor/zod/v4/locales/tr.js +110 -0
  179. package/dist/lib/modules/vendor/zod/v4/locales/ua.js +5 -0
  180. package/dist/lib/modules/vendor/zod/v4/locales/uk.js +113 -0
  181. package/dist/lib/modules/vendor/zod/v4/locales/ur.js +115 -0
  182. package/dist/lib/modules/vendor/zod/v4/locales/uz.js +113 -0
  183. package/dist/lib/modules/vendor/zod/v4/locales/vi.js +113 -0
  184. package/dist/lib/modules/vendor/zod/v4/locales/yo.js +112 -0
  185. package/dist/lib/modules/vendor/zod/v4/locales/zh-CN.js +114 -0
  186. package/dist/lib/modules/vendor/zod/v4/locales/zh-TW.js +112 -0
  187. package/dist/lib/testing-jelly-id.js +1219 -0
  188. package/dist/lib/zod.js +1 -0
  189. package/dist/spec/messaging-v1.md +3744 -0
  190. package/dist/third-party-notices.md +239 -0
  191. package/dist/third-party.json +88 -0
  192. package/dist/types/core/address.d.ts +16 -0
  193. package/dist/types/core/agent/operations.d.ts +129 -0
  194. package/dist/types/core/agent/present.d.ts +57 -0
  195. package/dist/types/core/agent/text.d.ts +62 -0
  196. package/dist/types/core/agent-tools.d.ts +966 -0
  197. package/dist/types/core/b64u.d.ts +6 -0
  198. package/dist/types/core/cards.d.ts +21 -0
  199. package/dist/types/core/check.d.ts +10 -0
  200. package/dist/types/core/consent.d.ts +132 -0
  201. package/dist/types/core/contact-card.d.ts +34 -0
  202. package/dist/types/core/conversation/context.d.ts +56 -0
  203. package/dist/types/core/conversation/event.d.ts +36 -0
  204. package/dist/types/core/conversation/ids.d.ts +18 -0
  205. package/dist/types/core/conversation/link.d.ts +16 -0
  206. package/dist/types/core/conversation/log.d.ts +142 -0
  207. package/dist/types/core/conversation/ratchet.d.ts +95 -0
  208. package/dist/types/core/conversation/seal.d.ts +47 -0
  209. package/dist/types/core/delegation.d.ts +71 -0
  210. package/dist/types/core/directory/device-log.d.ts +119 -0
  211. package/dist/types/core/directory/epoch.d.ts +53 -0
  212. package/dist/types/core/directory/handle.d.ts +11 -0
  213. package/dist/types/core/directory/jelly-device.d.ts +88 -0
  214. package/dist/types/core/directory/jelly-issuer.d.ts +8 -0
  215. package/dist/types/core/directory/jelly-token.d.ts +116 -0
  216. package/dist/types/core/directory/keys-statement.d.ts +22 -0
  217. package/dist/types/core/directory/link.d.ts +36 -0
  218. package/dist/types/core/directory/merkle-log.d.ts +40 -0
  219. package/dist/types/core/directory/person-view.d.ts +90 -0
  220. package/dist/types/core/directory/person.d.ts +8 -0
  221. package/dist/types/core/directory/realm.d.ts +23 -0
  222. package/dist/types/core/directory/smt.d.ts +52 -0
  223. package/dist/types/core/envelope.d.ts +68 -0
  224. package/dist/types/core/hash.d.ts +2 -0
  225. package/dist/types/core/head.d.ts +34 -0
  226. package/dist/types/core/identity.d.ts +35 -0
  227. package/dist/types/core/index.d.ts +45 -0
  228. package/dist/types/core/jcs.d.ts +12 -0
  229. package/dist/types/core/jelly-host-double.d.ts +21 -0
  230. package/dist/types/core/jelly-host.d.ts +86 -0
  231. package/dist/types/core/keys.d.ts +16 -0
  232. package/dist/types/core/peer-text.d.ts +87 -0
  233. package/dist/types/core/realm.d.ts +36 -0
  234. package/dist/types/core/roster.d.ts +146 -0
  235. package/dist/types/core/schema/a2a.d.ts +251 -0
  236. package/dist/types/core/schema/common.d.ts +328 -0
  237. package/dist/types/core/schema/consent.d.ts +140 -0
  238. package/dist/types/core/schema/conversation.d.ts +1657 -0
  239. package/dist/types/core/schema/delegation.d.ts +58 -0
  240. package/dist/types/core/schema/directory-frames.d.ts +1301 -0
  241. package/dist/types/core/schema/directory.d.ts +375 -0
  242. package/dist/types/core/schema/envelope.d.ts +756 -0
  243. package/dist/types/core/schema/errors.d.ts +142 -0
  244. package/dist/types/core/schema/index.d.ts +16 -0
  245. package/dist/types/core/schema/ipc/consent.d.ts +555 -0
  246. package/dist/types/core/schema/ipc/device.d.ts +735 -0
  247. package/dist/types/core/schema/ipc/jelly.d.ts +501 -0
  248. package/dist/types/core/schema/ipc/shapes.d.ts +414 -0
  249. package/dist/types/core/schema/ipc/surfaces.d.ts +680 -0
  250. package/dist/types/core/schema/ipc.d.ts +3146 -0
  251. package/dist/types/core/schema/jelly.d.ts +160 -0
  252. package/dist/types/core/schema/realm.d.ts +38 -0
  253. package/dist/types/core/schema/relay.d.ts +1134 -0
  254. package/dist/types/core/schema/roster.d.ts +347 -0
  255. package/dist/types/core/sign.d.ts +13 -0
  256. package/dist/types/core/task.d.ts +6 -0
  257. package/dist/types/core/testing.d.ts +19 -0
  258. package/dist/types/core/ulid.d.ts +5 -0
  259. package/dist/types/core/version.d.ts +6 -0
  260. package/dist/types/core/wire.d.ts +14 -0
  261. package/dist/types/ipc/client.d.ts +85 -0
  262. package/dist/types/ipc/framing.d.ts +12 -0
  263. package/dist/types/ipc/index.d.ts +6 -0
  264. package/dist/types/ipc/paths.d.ts +39 -0
  265. package/dist/types/ipc/pipes.d.ts +16 -0
  266. package/dist/types/ipc/rpc.d.ts +13 -0
  267. package/dist/types/ipc/server.d.ts +94 -0
  268. package/dist/types/ipc/testing.d.ts +89 -0
  269. package/dist/types/ipc/trust.d.ts +49 -0
  270. package/dist/types/ipc/windows-acl.d.ts +56 -0
  271. package/dist/types/testing/jelly-id.d.ts +518 -0
  272. package/dist/types/zod.d.ts +1 -0
  273. 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,80 @@
1
+ /**
2
+ * History as context (spec §16.8): what one agent of a person may mine of one conversation, from the person's rules,
3
+ * the defaults, and the conversation's policy. The stricter of the person's rule and the policy wins. Pure.
4
+ */
5
+ import { parseWith } from "../check.js";
6
+ import { ContextRules, } from "../schema/conversation.js";
7
+ /** The defaults that apply where a person has no rule (decided 2026-11-08). `lookbackDays` absent: the retention. */
8
+ export const CONTEXT_DEFAULTS = Object.freeze({
9
+ own: Object.freeze({ access: "allow" }),
10
+ "channel.public": Object.freeze({ access: "allow", lookbackDays: 90 }),
11
+ "channel.private": Object.freeze({ access: "ask", lookbackDays: 30 }),
12
+ group: Object.freeze({ access: "ask", lookbackDays: 30 }),
13
+ dm: Object.freeze({ access: "ask", lookbackDays: 14 }),
14
+ });
15
+ /**
16
+ * The scope a conversation falls in for `person`: `own` when its participant persons are `person` alone, else its
17
+ * kind (a channel by its visibility). A `self` conversation has none: no agent mines it.
18
+ */
19
+ export function contextKindOf(person, conv) {
20
+ if (conv.kind === "self")
21
+ return undefined;
22
+ if (conv.persons.length > 0 && conv.persons.every((p) => p === person))
23
+ return "own";
24
+ if (conv.kind === "channel")
25
+ return conv.visibility === "public" ? "channel.public" : "channel.private";
26
+ return conv.kind;
27
+ }
28
+ /**
29
+ * The person's rule for (agent, conversation), most specific first: a `cid` scope over a `kind` scope, then a named
30
+ * agent over `*`. Undefined when no rule matches; the defaults then apply.
31
+ */
32
+ export function matchContextRule(rules, agent, cid, kind) {
33
+ const rank = (r) => {
34
+ const scope = "cid" in r.scope ? (r.scope.cid === cid ? 2 : 0) : r.scope.kind === kind ? 1 : 0;
35
+ if (scope === 0)
36
+ return 0;
37
+ const who = r.agent === agent ? 2 : r.agent === "*" ? 1 : 0;
38
+ return who === 0 ? 0 : scope * 2 + who;
39
+ };
40
+ let best;
41
+ let bestRank = 0;
42
+ for (const rule of rules) {
43
+ const r = rank(rule);
44
+ if (r > bestRank) {
45
+ best = rule;
46
+ bestRank = r;
47
+ }
48
+ }
49
+ return best;
50
+ }
51
+ /**
52
+ * The effective rule for agent `agent` of `person` reading `conv` as context (not by its seat):
53
+ * - the conversation's policy `seats` or `none` forbids it, whatever the person's rule says;
54
+ * - else the person's rule, or the default: `off` forbids, `ask` holds, `allow` serves;
55
+ * - the lookback is the smallest of the rule's (or the default's) `lookbackDays`, the conversation's `retainDays`,
56
+ * and the person's local retention for it.
57
+ * Throws `invalid_request` for a rule set that fails `ContextRules`.
58
+ */
59
+ export function resolveContext(args) {
60
+ const rules = parseWith(ContextRules, args.rules, "context rules");
61
+ const kind = contextKindOf(args.person, args.conv);
62
+ if (kind === undefined)
63
+ return { access: "forbidden", source: "self" };
64
+ if (args.conv.agentContext !== "members")
65
+ return { access: "forbidden", source: "policy" };
66
+ const rule = matchContextRule(rules, args.agent, args.conv.cid, kind);
67
+ const chosen = rule ?? CONTEXT_DEFAULTS[kind];
68
+ if (chosen.access === "off")
69
+ return { access: "forbidden", source: "rule" };
70
+ const bounds = [chosen.lookbackDays, args.conv.retainDays, args.localRetentionDays].filter((n) => n !== undefined);
71
+ return { access: chosen.access, lookbackDays: Math.min(...bounds), source: rule ? "rule" : "default" };
72
+ }
73
+ /**
74
+ * What a seated agent reads by its seat (spec §16.8): everything from its seat's `seq` on, unless the policy is
75
+ * `none`, when it reads only the events addressed to it and the threads they are in. The person's rules do not
76
+ * apply to seat reads.
77
+ */
78
+ export function seatReads(agentContext) {
79
+ return agentContext === "none" ? "items" : "conversation";
80
+ }
@@ -0,0 +1,100 @@
1
+ /** Conversation events: admission, signing and the log's hash chain (spec §16.2, §16.4). */
2
+ import { createHash } from "node:crypto";
3
+ import { b64u, utf8 } from "../b64u.js";
4
+ import { asMessagingError, parseWith } from "../check.js";
5
+ import { canonicalize } from "../jcs.js";
6
+ import { assertEd25519PublicKey } from "../keys.js";
7
+ import { DOMAIN, LIMITS } from "../schema/common.js";
8
+ import { ConvEvent as ConvEventSchema, ConvId as ConvIdSchema } from "../schema/conversation.js";
9
+ import { MessagingError } from "../schema/errors.js";
10
+ import { sign, verify } from "../sign.js";
11
+ /** h(0) of `cid`'s log: SHA-256(DOMAIN.convGenesis ‖ JCS(cid)), base64url. */
12
+ export function convGenesisHash(cid) {
13
+ const id = parseWith(ConvIdSchema, cid, "conversation id");
14
+ return createHash("sha256").update(utf8(DOMAIN.convGenesis)).update(utf8(canonicalize(id))).digest("base64url");
15
+ }
16
+ /** SHA-256(JCS(event)), the raw digest, as the chain links it. Throws `invalid_request` for non-I-JSON. */
17
+ export function convEventDigest(event) {
18
+ return createHash("sha256").update(utf8(canonicalize(event))).digest();
19
+ }
20
+ /** h(seq) = SHA-256(h(seq − 1) ‖ SHA-256(JCS(event))), over the raw 32-byte digests, base64url. */
21
+ export function convEntryHash(prev, event) {
22
+ return convLinkHash(prev, convEventDigest(event));
23
+ }
24
+ /** `convEntryHash` from the event's digest: what a member checks for an entry whose content has expired. */
25
+ export function convLinkHash(prev, digest) {
26
+ const previous = b64u.decode(prev);
27
+ if (previous.length !== 32 || digest.length !== 32)
28
+ throw new MessagingError("invalid_request", "a chain link joins two 32-byte digests");
29
+ return createHash("sha256").update(previous).update(digest).digest("base64url");
30
+ }
31
+ /** The header of an event: everything but `body` and `sig`. JCS of it is the AAD of a sealed body. */
32
+ export function convEventHeader(event) {
33
+ const { body: _body, sig: _sig, ...header } = event;
34
+ return header;
35
+ }
36
+ /**
37
+ * Checks that `raw` is I-JSON within `LIMITS.jsonDepth` and at most `LIMITS.convEventBytes` (`too_large`), that it
38
+ * matches `ConvEvent`, and that `by.device` is a usable Ed25519 key. Returns a frozen JSON copy of the raw value, so
39
+ * the signature and the hash cover exactly what was received. The signature is not checked.
40
+ */
41
+ export function admitConvEvent(raw) {
42
+ const json = canonicalize(raw);
43
+ if (Buffer.byteLength(json, "utf8") > LIMITS.convEventBytes)
44
+ throw new MessagingError("too_large", "conversation event is over LIMITS.convEventBytes");
45
+ parseWith(ConvEventSchema, raw, "conversation event");
46
+ const event = deepFreeze(JSON.parse(json));
47
+ assertEd25519PublicKey(event.by.device, "event device");
48
+ return event;
49
+ }
50
+ /**
51
+ * Signs an event (spec §16.4): `sig = Ed25519(by.device, DOMAIN.convEvent ‖ JCS(event without sig))`. `by.device`
52
+ * must be `identity`'s machine. Refuses an event that fails the schema (`invalid_request`) or is over
53
+ * `LIMITS.convEventBytes` once signed (`too_large`). Authority is checked when the event is applied.
54
+ */
55
+ export function signConvEvent(identity, unsigned) {
56
+ try {
57
+ const value = JSON.parse(canonicalize(unsigned));
58
+ if (value.by?.device !== identity.machine)
59
+ throw new MessagingError("invalid_request", "by.device must be the signing machine");
60
+ const signed = { ...value, sig: sign(identity, DOMAIN.convEvent, value) };
61
+ admitConvEvent(signed);
62
+ return signed;
63
+ }
64
+ catch (error) {
65
+ throw asMessagingError(error, "conversation event");
66
+ }
67
+ }
68
+ /** True only for a valid signature by `event.by.device` over the event without `sig`. Never throws. */
69
+ export function convEventSigned(event) {
70
+ const { sig, ...unsigned } = event;
71
+ return verify(event.by.device, DOMAIN.convEvent, unsigned, sig);
72
+ }
73
+ /**
74
+ * Admits `raw` (`admitConvEvent`) and checks its signature over the raw value (`bad_signature`). Membership and
75
+ * authority are `ConvLog.apply`'s. Throws only MessagingError.
76
+ */
77
+ export function verifyConvEvent(raw) {
78
+ try {
79
+ const event = admitConvEvent(raw);
80
+ if (!convEventSigned(event))
81
+ throw new MessagingError("bad_signature", "conversation event signature is invalid");
82
+ return event;
83
+ }
84
+ catch (error) {
85
+ throw asMessagingError(error, "conversation event");
86
+ }
87
+ }
88
+ /** Freezes a JSON value and everything in it, iteratively. */
89
+ export function deepFreeze(value) {
90
+ const pending = [value];
91
+ while (pending.length > 0) {
92
+ const next = pending.pop();
93
+ if (typeof next !== "object" || next === null || Object.isFrozen(next))
94
+ continue;
95
+ Object.freeze(next);
96
+ for (const child of Object.values(next))
97
+ pending.push(child);
98
+ }
99
+ return value;
100
+ }
@@ -0,0 +1,38 @@
1
+ /** Conversation and participant identifiers (spec §16.1). */
2
+ import { createHash } from "node:crypto";
3
+ import { utf8 } from "../b64u.js";
4
+ import { parseWith } from "../check.js";
5
+ import { base32Lower } from "../directory/person.js";
6
+ import { canonicalize } from "../jcs.js";
7
+ import { DOMAIN, PersonId } from "../schema/common.js";
8
+ import { ParticipantId } from "../schema/conversation.js";
9
+ function derived(prefix, domain, value) {
10
+ const digest = createHash("sha256").update(utf8(domain)).update(utf8(canonicalize(value))).digest();
11
+ return prefix + base32Lower(digest).slice(0, 26);
12
+ }
13
+ /**
14
+ * The id of the dm between `a` and `b`: `"d_" + base32lower(SHA-256(DOMAIN.dm ‖ JCS([a, b] sorted)))[0..26]`. Both
15
+ * ends derive the same id, so a dm has no create race. `a` may equal `b`: a person's note to themselves.
16
+ */
17
+ export function dmConvId(a, b) {
18
+ const pair = [parseWith(ParticipantId, a, "participant"), parseWith(ParticipantId, b, "participant")].sort();
19
+ return derived("d_", DOMAIN.dm, pair);
20
+ }
21
+ /** The id of `person`'s own `self` conversation: `"s_" + base32lower(SHA-256(DOMAIN.self ‖ JCS(person)))[0..26]`. */
22
+ export function selfConvId(person) {
23
+ return derived("s_", DOMAIN.self, parseWith(PersonId, person, "person"));
24
+ }
25
+ /** The person a participant is, or belongs to. */
26
+ export function participantPerson(p) {
27
+ const slash = p.indexOf("/");
28
+ return slash < 0 ? p : p.slice(0, slash);
29
+ }
30
+ /** The agent's session name, for an agent participant. */
31
+ export function participantAgent(p) {
32
+ const slash = p.indexOf("/");
33
+ return slash < 0 ? undefined : p.slice(slash + 1);
34
+ }
35
+ /** The participant an event's `by` names: `person`, or `person/agent`. */
36
+ export function participantOf(by) {
37
+ return by.agent === undefined ? by.person : `${by.person}/${by.agent}`;
38
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Linked channels (spec §16.10), reserved: the checks a person's own devices make on `via` lines against the link
3
+ * records of the person's `self` conversation. No daemon uses them yet. Pure.
4
+ */
5
+ import { parseWith } from "../check.js";
6
+ import { LinkRecord as LinkRecordSchema } from "../schema/conversation.js";
7
+ import { MessagingError } from "../schema/errors.js";
8
+ /** The links in force: for each link id the record written last (records are in `seq` order), unless revoked. */
9
+ export function currentLinks(records) {
10
+ const last = new Map();
11
+ for (const raw of records) {
12
+ const record = parseWith(LinkRecordSchema, raw, "link record");
13
+ last.set(record.id, record);
14
+ }
15
+ return [...last.values()].filter((r) => r.revokedAt === undefined);
16
+ }
17
+ /**
18
+ * Checks a line's `via` against the author's links, as the author's own devices can (other members cannot see the
19
+ * person's links): only a person writes from a linked channel, never an agent, and only through a link of that
20
+ * channel that the authoring device hosts and that is in force. Throws `forbidden`.
21
+ */
22
+ export function checkVia(body, by, links) {
23
+ if (body.t !== "line" || body.via === undefined)
24
+ return;
25
+ if (by.agent !== undefined)
26
+ throw new MessagingError("forbidden", "only a person writes from a linked channel");
27
+ const channel = body.via.channel;
28
+ if (!currentLinks(links).some((l) => l.channel === channel && l.host === by.device)) {
29
+ throw new MessagingError("forbidden", "no link of this channel is in force on the authoring device");
30
+ }
31
+ }
32
+ /** The reader id of a link's channel provider, for the audience guard: `ext:<channel>:<link id>`. */
33
+ export function linkReader(link) {
34
+ return `ext:${link.channel}:${link.id}`;
35
+ }