@meshline/sdk 0.0.0-stage → 0.1.0-alpha.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 (338) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -2
  3. package/dist/channels/repository.d.ts +73 -0
  4. package/dist/channels/repository.d.ts.map +1 -0
  5. package/dist/channels/repository.js +367 -0
  6. package/dist/channels/repository.js.map +1 -0
  7. package/dist/client/account-operations.d.ts +41 -0
  8. package/dist/client/account-operations.d.ts.map +1 -0
  9. package/dist/client/account-operations.js +304 -0
  10. package/dist/client/account-operations.js.map +1 -0
  11. package/dist/client/conversations.d.ts +36 -0
  12. package/dist/client/conversations.d.ts.map +1 -0
  13. package/dist/client/conversations.js +144 -0
  14. package/dist/client/conversations.js.map +1 -0
  15. package/dist/client.d.ts +53 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +162 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/components/account.d.ts +38 -0
  20. package/dist/components/account.d.ts.map +1 -0
  21. package/dist/components/account.js +243 -0
  22. package/dist/components/account.js.map +1 -0
  23. package/dist/components/channel.d.ts +78 -0
  24. package/dist/components/channel.d.ts.map +1 -0
  25. package/dist/components/channel.js +475 -0
  26. package/dist/components/channel.js.map +1 -0
  27. package/dist/components/component.d.ts +49 -0
  28. package/dist/components/component.d.ts.map +1 -0
  29. package/dist/components/component.js +194 -0
  30. package/dist/components/component.js.map +1 -0
  31. package/dist/components/device.d.ts +60 -0
  32. package/dist/components/device.d.ts.map +1 -0
  33. package/dist/components/device.js +648 -0
  34. package/dist/components/device.js.map +1 -0
  35. package/dist/components/group.d.ts +141 -0
  36. package/dist/components/group.d.ts.map +1 -0
  37. package/dist/components/group.js +933 -0
  38. package/dist/components/group.js.map +1 -0
  39. package/dist/components/message.d.ts +63 -0
  40. package/dist/components/message.d.ts.map +1 -0
  41. package/dist/components/message.js +439 -0
  42. package/dist/components/message.js.map +1 -0
  43. package/dist/components/profile.d.ts +37 -0
  44. package/dist/components/profile.d.ts.map +1 -0
  45. package/dist/components/profile.js +232 -0
  46. package/dist/components/profile.js.map +1 -0
  47. package/dist/crypto/groups.d.ts +56 -0
  48. package/dist/crypto/groups.d.ts.map +1 -0
  49. package/dist/crypto/groups.js +163 -0
  50. package/dist/crypto/groups.js.map +1 -0
  51. package/dist/crypto/messages.d.ts +52 -0
  52. package/dist/crypto/messages.d.ts.map +1 -0
  53. package/dist/crypto/messages.js +180 -0
  54. package/dist/crypto/messages.js.map +1 -0
  55. package/dist/crypto/primitives.d.ts +30 -0
  56. package/dist/crypto/primitives.d.ts.map +1 -0
  57. package/dist/crypto/primitives.js +102 -0
  58. package/dist/crypto/primitives.js.map +1 -0
  59. package/dist/errors.d.ts +11 -0
  60. package/dist/errors.d.ts.map +1 -0
  61. package/dist/errors.js +14 -0
  62. package/dist/errors.js.map +1 -0
  63. package/dist/groups/account-sync.d.ts +28 -0
  64. package/dist/groups/account-sync.d.ts.map +1 -0
  65. package/dist/groups/account-sync.js +235 -0
  66. package/dist/groups/account-sync.js.map +1 -0
  67. package/dist/groups/messages.d.ts +39 -0
  68. package/dist/groups/messages.d.ts.map +1 -0
  69. package/dist/groups/messages.js +141 -0
  70. package/dist/groups/messages.js.map +1 -0
  71. package/dist/groups/operations.d.ts +57 -0
  72. package/dist/groups/operations.d.ts.map +1 -0
  73. package/dist/groups/operations.js +225 -0
  74. package/dist/groups/operations.js.map +1 -0
  75. package/dist/groups/repository.d.ts +56 -0
  76. package/dist/groups/repository.d.ts.map +1 -0
  77. package/dist/groups/repository.js +254 -0
  78. package/dist/groups/repository.js.map +1 -0
  79. package/dist/groups/secrets.d.ts +67 -0
  80. package/dist/groups/secrets.d.ts.map +1 -0
  81. package/dist/groups/secrets.js +452 -0
  82. package/dist/groups/secrets.js.map +1 -0
  83. package/dist/groups/state.d.ts +39 -0
  84. package/dist/groups/state.d.ts.map +1 -0
  85. package/dist/groups/state.js +201 -0
  86. package/dist/groups/state.js.map +1 -0
  87. package/dist/identity/identifiers.d.ts +9 -0
  88. package/dist/identity/identifiers.d.ts.map +1 -0
  89. package/dist/identity/identifiers.js +37 -0
  90. package/dist/identity/identifiers.js.map +1 -0
  91. package/dist/identity/neo.d.ts +11 -0
  92. package/dist/identity/neo.d.ts.map +1 -0
  93. package/dist/identity/neo.js +77 -0
  94. package/dist/identity/neo.js.map +1 -0
  95. package/dist/index.d.ts +53 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +46 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/interactions.d.ts +31 -0
  100. package/dist/interactions.d.ts.map +1 -0
  101. package/dist/interactions.js +2 -0
  102. package/dist/interactions.js.map +1 -0
  103. package/dist/messages/account-sender.d.ts +9 -0
  104. package/dist/messages/account-sender.d.ts.map +1 -0
  105. package/dist/messages/account-sender.js +46 -0
  106. package/dist/messages/account-sender.js.map +1 -0
  107. package/dist/messages/contacts.d.ts +103 -0
  108. package/dist/messages/contacts.d.ts.map +1 -0
  109. package/dist/messages/contacts.js +525 -0
  110. package/dist/messages/contacts.js.map +1 -0
  111. package/dist/messages/outbox.d.ts +66 -0
  112. package/dist/messages/outbox.d.ts.map +1 -0
  113. package/dist/messages/outbox.js +167 -0
  114. package/dist/messages/outbox.js.map +1 -0
  115. package/dist/messages/receiver.d.ts +35 -0
  116. package/dist/messages/receiver.d.ts.map +1 -0
  117. package/dist/messages/receiver.js +113 -0
  118. package/dist/messages/receiver.js.map +1 -0
  119. package/dist/messages/repository.d.ts +89 -0
  120. package/dist/messages/repository.d.ts.map +1 -0
  121. package/dist/messages/repository.js +295 -0
  122. package/dist/messages/repository.js.map +1 -0
  123. package/dist/models/authentication.d.ts +37 -0
  124. package/dist/models/authentication.d.ts.map +1 -0
  125. package/dist/models/authentication.js +53 -0
  126. package/dist/models/authentication.js.map +1 -0
  127. package/dist/models/channels.d.ts +141 -0
  128. package/dist/models/channels.d.ts.map +1 -0
  129. package/dist/models/channels.js +258 -0
  130. package/dist/models/channels.js.map +1 -0
  131. package/dist/models/contacts.d.ts +68 -0
  132. package/dist/models/contacts.d.ts.map +1 -0
  133. package/dist/models/contacts.js +186 -0
  134. package/dist/models/contacts.js.map +1 -0
  135. package/dist/models/content.d.ts +37 -0
  136. package/dist/models/content.d.ts.map +1 -0
  137. package/dist/models/content.js +137 -0
  138. package/dist/models/content.js.map +1 -0
  139. package/dist/models/device-state.d.ts +8 -0
  140. package/dist/models/device-state.d.ts.map +1 -0
  141. package/dist/models/device-state.js +22 -0
  142. package/dist/models/device-state.js.map +1 -0
  143. package/dist/models/group-admission.d.ts +111 -0
  144. package/dist/models/group-admission.d.ts.map +1 -0
  145. package/dist/models/group-admission.js +153 -0
  146. package/dist/models/group-admission.js.map +1 -0
  147. package/dist/models/group-keys.d.ts +30 -0
  148. package/dist/models/group-keys.d.ts.map +1 -0
  149. package/dist/models/group-keys.js +52 -0
  150. package/dist/models/group-keys.js.map +1 -0
  151. package/dist/models/group-management.d.ts +190 -0
  152. package/dist/models/group-management.d.ts.map +1 -0
  153. package/dist/models/group-management.js +263 -0
  154. package/dist/models/group-management.js.map +1 -0
  155. package/dist/models/groups.d.ts +81 -0
  156. package/dist/models/groups.d.ts.map +1 -0
  157. package/dist/models/groups.js +127 -0
  158. package/dist/models/groups.js.map +1 -0
  159. package/dist/models/identity.d.ts +47 -0
  160. package/dist/models/identity.d.ts.map +1 -0
  161. package/dist/models/identity.js +136 -0
  162. package/dist/models/identity.js.map +1 -0
  163. package/dist/models/messages.d.ts +104 -0
  164. package/dist/models/messages.d.ts.map +1 -0
  165. package/dist/models/messages.js +168 -0
  166. package/dist/models/messages.js.map +1 -0
  167. package/dist/models/profile.d.ts +24 -0
  168. package/dist/models/profile.d.ts.map +1 -0
  169. package/dist/models/profile.js +47 -0
  170. package/dist/models/profile.js.map +1 -0
  171. package/dist/models/relay.d.ts +39 -0
  172. package/dist/models/relay.d.ts.map +1 -0
  173. package/dist/models/relay.js +160 -0
  174. package/dist/models/relay.js.map +1 -0
  175. package/dist/protocol/codec.d.ts +36 -0
  176. package/dist/protocol/codec.d.ts.map +1 -0
  177. package/dist/protocol/codec.js +144 -0
  178. package/dist/protocol/codec.js.map +1 -0
  179. package/dist/protocol/context.d.ts +14 -0
  180. package/dist/protocol/context.d.ts.map +1 -0
  181. package/dist/protocol/context.js +43 -0
  182. package/dist/protocol/context.js.map +1 -0
  183. package/dist/protocol/encoding.d.ts +15 -0
  184. package/dist/protocol/encoding.d.ts.map +1 -0
  185. package/dist/protocol/encoding.js +133 -0
  186. package/dist/protocol/encoding.js.map +1 -0
  187. package/dist/protocol/json.d.ts +16 -0
  188. package/dist/protocol/json.d.ts.map +1 -0
  189. package/dist/protocol/json.js +172 -0
  190. package/dist/protocol/json.js.map +1 -0
  191. package/dist/protocol/validation.d.ts +4 -0
  192. package/dist/protocol/validation.d.ts.map +1 -0
  193. package/dist/protocol/validation.js +18 -0
  194. package/dist/protocol/validation.js.map +1 -0
  195. package/dist/runtime/abort.d.ts +6 -0
  196. package/dist/runtime/abort.d.ts.map +1 -0
  197. package/dist/runtime/abort.js +32 -0
  198. package/dist/runtime/abort.js.map +1 -0
  199. package/dist/runtime/async-gate.d.ts +6 -0
  200. package/dist/runtime/async-gate.d.ts.map +1 -0
  201. package/dist/runtime/async-gate.js +15 -0
  202. package/dist/runtime/async-gate.js.map +1 -0
  203. package/dist/runtime/async-pulse.d.ts +7 -0
  204. package/dist/runtime/async-pulse.d.ts.map +1 -0
  205. package/dist/runtime/async-pulse.js +23 -0
  206. package/dist/runtime/async-pulse.js.map +1 -0
  207. package/dist/runtime/clock.d.ts +16 -0
  208. package/dist/runtime/clock.d.ts.map +1 -0
  209. package/dist/runtime/clock.js +62 -0
  210. package/dist/runtime/clock.js.map +1 -0
  211. package/dist/runtime/events.d.ts +11 -0
  212. package/dist/runtime/events.d.ts.map +1 -0
  213. package/dist/runtime/events.js +45 -0
  214. package/dist/runtime/events.js.map +1 -0
  215. package/dist/runtime/url.d.ts +3 -0
  216. package/dist/runtime/url.d.ts.map +1 -0
  217. package/dist/runtime/url.js +23 -0
  218. package/dist/runtime/url.js.map +1 -0
  219. package/dist/storage/signed-request.d.ts +14 -0
  220. package/dist/storage/signed-request.d.ts.map +1 -0
  221. package/dist/storage/signed-request.js +26 -0
  222. package/dist/storage/signed-request.js.map +1 -0
  223. package/dist/storage/store.d.ts +58 -0
  224. package/dist/storage/store.d.ts.map +1 -0
  225. package/dist/storage/store.js +52 -0
  226. package/dist/storage/store.js.map +1 -0
  227. package/dist/storage/transaction.d.ts +7 -0
  228. package/dist/storage/transaction.d.ts.map +1 -0
  229. package/dist/storage/transaction.js +22 -0
  230. package/dist/storage/transaction.js.map +1 -0
  231. package/dist/transport/client.d.ts +61 -0
  232. package/dist/transport/client.d.ts.map +1 -0
  233. package/dist/transport/client.js +392 -0
  234. package/dist/transport/client.js.map +1 -0
  235. package/dist/transport/discovery.d.ts +19 -0
  236. package/dist/transport/discovery.d.ts.map +1 -0
  237. package/dist/transport/discovery.js +75 -0
  238. package/dist/transport/discovery.js.map +1 -0
  239. package/dist/transport/endpoint.d.ts +6 -0
  240. package/dist/transport/endpoint.d.ts.map +1 -0
  241. package/dist/transport/endpoint.js +35 -0
  242. package/dist/transport/endpoint.js.map +1 -0
  243. package/dist/transport/http.d.ts +52 -0
  244. package/dist/transport/http.d.ts.map +1 -0
  245. package/dist/transport/http.js +128 -0
  246. package/dist/transport/http.js.map +1 -0
  247. package/dist/transport/pool.d.ts +31 -0
  248. package/dist/transport/pool.d.ts.map +1 -0
  249. package/dist/transport/pool.js +137 -0
  250. package/dist/transport/pool.js.map +1 -0
  251. package/dist/transport/relay-error.d.ts +23 -0
  252. package/dist/transport/relay-error.d.ts.map +1 -0
  253. package/dist/transport/relay-error.js +47 -0
  254. package/dist/transport/relay-error.js.map +1 -0
  255. package/dist/transport/rpc.d.ts +31 -0
  256. package/dist/transport/rpc.d.ts.map +1 -0
  257. package/dist/transport/rpc.js +56 -0
  258. package/dist/transport/rpc.js.map +1 -0
  259. package/dist/transport/session.d.ts +49 -0
  260. package/dist/transport/session.d.ts.map +1 -0
  261. package/dist/transport/session.js +169 -0
  262. package/dist/transport/session.js.map +1 -0
  263. package/dist/transport/socket.d.ts +55 -0
  264. package/dist/transport/socket.d.ts.map +1 -0
  265. package/dist/transport/socket.js +266 -0
  266. package/dist/transport/socket.js.map +1 -0
  267. package/dist/transport/subscription.d.ts +36 -0
  268. package/dist/transport/subscription.d.ts.map +1 -0
  269. package/dist/transport/subscription.js +169 -0
  270. package/dist/transport/subscription.js.map +1 -0
  271. package/package.json +35 -4
  272. package/src/channels/repository.ts +244 -0
  273. package/src/client/account-operations.ts +183 -0
  274. package/src/client/conversations.ts +95 -0
  275. package/src/client.ts +108 -0
  276. package/src/components/account.ts +206 -0
  277. package/src/components/channel.ts +253 -0
  278. package/src/components/component.ts +151 -0
  279. package/src/components/device.ts +455 -0
  280. package/src/components/group.ts +546 -0
  281. package/src/components/message.ts +253 -0
  282. package/src/components/profile.ts +179 -0
  283. package/src/crypto/groups.ts +107 -0
  284. package/src/crypto/messages.ts +129 -0
  285. package/src/crypto/primitives.ts +103 -0
  286. package/src/errors.ts +13 -0
  287. package/src/groups/account-sync.ts +144 -0
  288. package/src/groups/messages.ts +89 -0
  289. package/src/groups/operations.ts +145 -0
  290. package/src/groups/repository.ts +161 -0
  291. package/src/groups/secrets.ts +256 -0
  292. package/src/groups/state.ts +138 -0
  293. package/src/identity/identifiers.ts +41 -0
  294. package/src/identity/neo.ts +72 -0
  295. package/src/index.ts +52 -0
  296. package/src/interactions.ts +35 -0
  297. package/src/messages/account-sender.ts +33 -0
  298. package/src/messages/contacts.ts +379 -0
  299. package/src/messages/outbox.ts +152 -0
  300. package/src/messages/receiver.ts +86 -0
  301. package/src/messages/repository.ts +230 -0
  302. package/src/models/authentication.ts +63 -0
  303. package/src/models/channels.ts +155 -0
  304. package/src/models/contacts.ts +141 -0
  305. package/src/models/content.ts +105 -0
  306. package/src/models/device-state.ts +18 -0
  307. package/src/models/group-admission.ts +110 -0
  308. package/src/models/group-keys.ts +34 -0
  309. package/src/models/group-management.ts +182 -0
  310. package/src/models/groups.ts +100 -0
  311. package/src/models/identity.ts +164 -0
  312. package/src/models/messages.ts +139 -0
  313. package/src/models/profile.ts +46 -0
  314. package/src/models/relay.ts +151 -0
  315. package/src/protocol/codec.ts +144 -0
  316. package/src/protocol/context.ts +39 -0
  317. package/src/protocol/encoding.ts +106 -0
  318. package/src/protocol/json.ts +140 -0
  319. package/src/protocol/validation.ts +13 -0
  320. package/src/runtime/abort.ts +27 -0
  321. package/src/runtime/async-gate.ts +16 -0
  322. package/src/runtime/async-pulse.ts +20 -0
  323. package/src/runtime/clock.ts +63 -0
  324. package/src/runtime/events.ts +31 -0
  325. package/src/runtime/url.ts +17 -0
  326. package/src/storage/signed-request.ts +26 -0
  327. package/src/storage/store.ts +90 -0
  328. package/src/storage/transaction.ts +17 -0
  329. package/src/transport/client.ts +328 -0
  330. package/src/transport/discovery.ts +64 -0
  331. package/src/transport/endpoint.ts +32 -0
  332. package/src/transport/http.ts +129 -0
  333. package/src/transport/pool.ts +115 -0
  334. package/src/transport/relay-error.ts +46 -0
  335. package/src/transport/rpc.ts +56 -0
  336. package/src/transport/session.ts +154 -0
  337. package/src/transport/socket.ts +248 -0
  338. package/src/transport/subscription.ts +117 -0
@@ -0,0 +1,141 @@
1
+ import { requireLength, verifyDevice } from '../crypto/primitives.js';
2
+ import { ProtocolError } from '../errors.js';
3
+ import { validateIdentifier } from '../identity/identifiers.js';
4
+ import { validateAccountId } from '../identity/neo.js';
5
+ import { array, boolean, bytes, defineCodec, dictionary, enumeration, integer, text, type ExtensibleModel, type ValueCodec } from '../protocol/codec.js';
6
+ import { signingInput, type NetworkContext } from '../protocol/context.js';
7
+ import { encodeUtf8, equalBytes } from '../protocol/encoding.js';
8
+ import { requireObject, requireSafeInteger } from '../protocol/json.js';
9
+ import { validateCompleteObject } from '../protocol/validation.js';
10
+ import { containsNonWhitespace } from './content.js';
11
+ import { accountDeviceStateCodec, authorizedDevice, certificateId, deviceCertificateCodec, validateCertificate, validateDeviceState, type AccountDeviceState, type DeviceCertificate } from './identity.js';
12
+
13
+ export interface ContactGrant extends ExtensibleModel { readonly grantor: string; readonly grantee: string; readonly expiresAt?: number; readonly signatures: Readonly<Record<string, Uint8Array>> }
14
+ export const contactGrantCodec = defineCodec<ContactGrant>({ grantor: { wire: 'grantor', codec: text }, grantee: { wire: 'grantee', codec: text },
15
+ expiresAt: { wire: 'expires_at', codec: integer, optional: true }, signatures: { wire: 'signatures', codec: dictionary(bytes) } }, 'meshline.contact.grant');
16
+ export const contactGrantInput = (value: ContactGrant, context: NetworkContext): Uint8Array => signingInput(contactGrantCodec.encode(value), context, ['signatures']);
17
+ export function validateContactGrant(value: ContactGrant, now: number): void {
18
+ validateCompleteObject(contactGrantCodec.encode(value));
19
+ validateAccountId(value.grantor); validateAccountId(value.grantee); requireSafeInteger(now, 0);
20
+ if (value.expiresAt !== undefined) { requireSafeInteger(value.expiresAt, 0); if (value.expiresAt <= now) throw new ProtocolError('expired_grant', 'Contact grant has expired.'); }
21
+ const signatures = Object.entries(value.signatures);
22
+ if (!signatures.length) throw new ProtocolError('invalid_grant', 'A contact grant requires at least one signature.');
23
+ for (const [id, signature] of signatures) { validateIdentifier('device', id); requireLength(signature, 64, 'Grant signature'); }
24
+ contactGrantCodec.encode(value);
25
+ }
26
+ export function verifyContactGrant(value: ContactGrant, state: AccountDeviceState, context: NetworkContext, now: number): void {
27
+ if (Object.keys(filterContactGrantSignatures(value, state, context, now).signatures).length === 0)
28
+ throw new ProtocolError('unauthorized_grant', 'Contact grant has no valid signature from a currently authorized device.');
29
+ }
30
+ /** Returns a copy containing only signatures whose device identity is currently authorized by the verified state. */
31
+ export function filterContactGrantSignatures(value: ContactGrant, state: AccountDeviceState, context: NetworkContext, now: number): ContactGrant {
32
+ validateContactGrant(value, now); validateDeviceState(state, context);
33
+ if (value.grantor !== state.account) throw new ProtocolError('invalid_identity', 'Contact grant state belongs to another grantor.');
34
+ const input = contactGrantInput(value, context);
35
+ const signatures: Record<string, Uint8Array> = Object.create(null) as Record<string, Uint8Array>;
36
+ for (const certificate of state.certificates) {
37
+ const id = certificateId(certificate, context); const signature = value.signatures[id];
38
+ if (certificate.notBefore <= now && now < certificate.expiresAt && signature !== undefined && verifyDevice(input, signature, certificate.signingPublicKey)) signatures[id] = signature.slice();
39
+ }
40
+ return contactGrantCodec.decode(contactGrantCodec.encode({ ...value, signatures }));
41
+ }
42
+ export interface ContactGrantSelection { readonly action: 'replace' | 'keep' | 'merge' | 'out_of_scope' | 'invalid'; readonly grant: ContactGrant | undefined; readonly error?: unknown }
43
+ /** Saved grants are scoped to one network/account direction. Equal-expiry bodies never replace differing signed extensions. */
44
+ export function selectContactGrant(current: ContactGrant | undefined, candidate: ContactGrant, state: AccountDeviceState,
45
+ context: NetworkContext, now: number, candidateContext = context): ContactGrantSelection {
46
+ const previous = current && contactGrantCodec.decode(contactGrantCodec.encode(current));
47
+ if (candidateContext.toString() !== context.toString() || previous && (candidate.grantor !== previous.grantor || candidate.grantee !== previous.grantee))
48
+ return { action: 'out_of_scope', grant: previous };
49
+ let incoming: ContactGrant;
50
+ try {
51
+ incoming = filterContactGrantSignatures(candidate, state, context, now);
52
+ if (Object.keys(incoming.signatures).length === 0) throw new ProtocolError('unauthorized_grant', 'Candidate has no currently authorized signature.');
53
+ } catch (error) { if (!(error instanceof ProtocolError)) throw error; return { action: 'invalid', grant: previous, error }; }
54
+ if (!previous) return { action: 'replace', grant: incoming };
55
+ if (!equalBytes(contactGrantInput(previous, context), contactGrantInput(incoming, context)))
56
+ return (incoming.expiresAt ?? Infinity) > (previous.expiresAt ?? Infinity) ? { action: 'replace', grant: incoming } : { action: 'keep', grant: previous };
57
+ const saved = filterContactGrantSignatures(previous, state, context, now);
58
+ return { action: 'merge', grant: { ...incoming, signatures: { ...incoming.signatures, ...saved.signatures } } };
59
+ }
60
+ export interface ContactInvite extends ExtensibleModel { readonly inviter: string; readonly expiresAt: number; readonly signerDeviceId: string; readonly deviceSignature: Uint8Array }
61
+ export const contactInviteCodec = defineCodec<ContactInvite>({ inviter: { wire: 'inviter', codec: text }, expiresAt: { wire: 'expires_at', codec: integer },
62
+ signerDeviceId: { wire: 'signer_device_id', codec: text }, deviceSignature: { wire: 'device_signature', codec: bytes } }, 'meshline.contact.invite');
63
+ export const contactInviteInput = (value: ContactInvite, context: NetworkContext): Uint8Array => signingInput(contactInviteCodec.encode(value), context, ['device_signature']);
64
+ export function validateContactInvite(value: ContactInvite, now: number): void {
65
+ validateCompleteObject(contactInviteCodec.encode(value));
66
+ validateAccountId(value.inviter); requireSafeInteger(now, 0); requireSafeInteger(value.expiresAt, 0);
67
+ validateIdentifier('device', value.signerDeviceId); requireLength(value.deviceSignature, 64, 'Invitation signature');
68
+ if (value.expiresAt <= now) throw new ProtocolError('expired_invite', 'Contact invitation has expired.'); contactInviteCodec.encode(value);
69
+ }
70
+ export function verifyContactInvite(value: ContactInvite, state: AccountDeviceState, context: NetworkContext, now: number): void {
71
+ validateContactInvite(value, now); validateDeviceState(state, context);
72
+ if (value.inviter !== state.account) throw new ProtocolError('invalid_identity', 'Invitation belongs to another account.');
73
+ const certificate = authorizedDevice(state, value.signerDeviceId, context, now);
74
+ if (!verifyDevice(contactInviteInput(value, context), value.deviceSignature, certificate.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid contact invitation signature.');
75
+ }
76
+ export type ContactAuthorization = ContactGrant | ContactInvite;
77
+ export const contactAuthorizationCodec: ValueCodec<ContactAuthorization> = {
78
+ decode(value) {
79
+ const type = requireObject(value)['$type'];
80
+ if (type === 'meshline.contact.grant') return contactGrantCodec.decode(value);
81
+ if (type === 'meshline.contact.invite') return contactInviteCodec.decode(value);
82
+ throw new ProtocolError('invalid_type', 'Expected a contact grant or invitation.');
83
+ },
84
+ encode(value) { return 'grantor' in value ? contactGrantCodec.encode(value) : contactInviteCodec.encode(value); },
85
+ };
86
+ export interface SignedDeviceStateQuery extends ExtensibleModel {
87
+ readonly account: string; readonly authorization: ContactAuthorization; readonly signerCertificate: DeviceCertificate; readonly createdAt: number; readonly deviceSignature: Uint8Array;
88
+ }
89
+ export const signedDeviceStateQueryCodec = defineCodec<SignedDeviceStateQuery>({ account: { wire: 'account', codec: text }, authorization: { wire: 'authorization', codec: contactAuthorizationCodec },
90
+ signerCertificate: { wire: 'signer_certificate', codec: deviceCertificateCodec }, createdAt: { wire: 'created_at', codec: integer }, deviceSignature: { wire: 'device_signature', codec: bytes } }, 'meshline.device.state.resolve');
91
+ export const deviceStateQueryInput = (value: SignedDeviceStateQuery, context: NetworkContext): Uint8Array => signingInput(signedDeviceStateQueryCodec.encode(value), context, ['device_signature']);
92
+ export function validateDeviceStateQuery(value: SignedDeviceStateQuery, context: NetworkContext, now: number): void {
93
+ validateCompleteObject(signedDeviceStateQueryCodec.encode(value));
94
+ validateAccountId(value.account); validateCertificate(value.signerCertificate, context); requireSafeInteger(value.createdAt, 0);
95
+ if (value.account === value.signerCertificate.account) throw new ProtocolError('invalid_authorization', 'Signed device queries cannot target the caller account.');
96
+ if ('grantor' in value.authorization) {
97
+ validateContactGrant(value.authorization, now);
98
+ if (value.authorization.grantor !== value.account || value.authorization.grantee !== value.signerCertificate.account) throw new ProtocolError('invalid_authorization', 'Contact grant does not authorize this query.');
99
+ } else { validateContactInvite(value.authorization, now); if (value.authorization.inviter !== value.account) throw new ProtocolError('invalid_authorization', 'Invitation is for another account.'); }
100
+ if (!verifyDevice(deviceStateQueryInput(value, context), value.deviceSignature, value.signerCertificate.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid signed device query.');
101
+ }
102
+ export interface ContactConsent extends ExtensibleModel { readonly deviceState: AccountDeviceState; readonly grant: ContactGrant; readonly note?: string }
103
+ export const contactConsentCodec = defineCodec<ContactConsent>({ deviceState: { wire: 'device_state', codec: accountDeviceStateCodec }, grant: { wire: 'grant', codec: contactGrantCodec }, note: { wire: 'note', codec: text, optional: true } }, 'meshline.contact.consent');
104
+ export function validateContactConsent(value: ContactConsent, context: NetworkContext, now: number): void {
105
+ validateCompleteObject(contactConsentCodec.encode(value));
106
+ if (value.note !== undefined && (!containsNonWhitespace(value.note) || encodeUtf8(value.note).length > 1024)) throw new ProtocolError('invalid_note', 'Consent note must contain non-whitespace text and fit 1024 UTF-8 bytes.');
107
+ verifyContactGrant(value.grant, value.deviceState, context, now); contactConsentCodec.encode(value);
108
+ }
109
+ export interface ContactRecord extends ExtensibleModel {
110
+ readonly account: string; readonly alias?: string; readonly status: 'active' | 'deleted';
111
+ readonly grantFromContact?: ContactGrant; readonly grantToContact?: ContactGrant; readonly updatedAt: number;
112
+ }
113
+ export const contactRecordCodec = defineCodec<ContactRecord>({ account: { wire: 'account', codec: text }, alias: { wire: 'alias', codec: text, optional: true }, status: { wire: 'status', codec: enumeration('active', 'deleted') },
114
+ grantFromContact: { wire: 'grant_from_contact', codec: contactGrantCodec, optional: true }, grantToContact: { wire: 'grant_to_contact', codec: contactGrantCodec, optional: true }, updatedAt: { wire: 'updated_at', codec: integer } });
115
+ export function validateContactRecord(value: ContactRecord, now: number): void {
116
+ validateCompleteObject(contactRecordCodec.encode(value));
117
+ validateAccountId(value.account); requireSafeInteger(value.updatedAt, 0); requireSafeInteger(now, 0);
118
+ if (value.updatedAt > now + 300) throw new ProtocolError('invalid_time', 'Contact update is more than five minutes in the future.');
119
+ if (value.alias !== undefined && value.alias !== '' && (!containsNonWhitespace(value.alias) || encodeUtf8(value.alias).length > 256)) throw new ProtocolError('invalid_alias', 'Nonempty contact alias must contain non-whitespace text and fit 256 UTF-8 bytes.');
120
+ if (value.status === 'deleted' && (value.grantFromContact || value.grantToContact)) throw new ProtocolError('invalid_contact', 'Deleted contact records cannot contain grants.');
121
+ contactRecordCodec.encode(value);
122
+ }
123
+ export interface AccountContactSync extends ExtensibleModel { readonly records?: readonly ContactRecord[]; readonly requestSnapshot?: boolean }
124
+ export const accountContactSyncCodec = defineCodec<AccountContactSync>({ records: { wire: 'records', codec: array(contactRecordCodec), optional: true }, requestSnapshot: { wire: 'request_snapshot', codec: boolean, optional: true } }, 'meshline.account.contacts.sync');
125
+ export function validateAccountContactSync(value: AccountContactSync, now: number, ownerAccount?: string): void {
126
+ validateCompleteObject(accountContactSyncCodec.encode(value));
127
+ const accounts = new Set<string>(); let owner = ownerAccount;
128
+ for (const record of value.records ?? []) {
129
+ validateContactRecord(record, now);
130
+ if (accounts.has(record.account)) throw new ProtocolError('duplicate_contact', 'A contact sync batch cannot contain duplicate accounts.'); accounts.add(record.account);
131
+ if (record.grantFromContact) {
132
+ const grant = record.grantFromContact; validateContactGrant(grant, now);
133
+ if (grant.grantor !== record.account || owner !== undefined && grant.grantee !== owner) throw new ProtocolError('invalid_identity', 'Incoming contact grant has inconsistent accounts.'); owner = grant.grantee;
134
+ }
135
+ if (record.grantToContact) {
136
+ const grant = record.grantToContact; validateContactGrant(grant, now);
137
+ if (grant.grantee !== record.account || owner !== undefined && grant.grantor !== owner) throw new ProtocolError('invalid_identity', 'Outgoing contact grant has inconsistent accounts.'); owner = grant.grantor;
138
+ }
139
+ }
140
+ if (owner !== undefined && accounts.has(owner)) throw new ProtocolError('invalid_identity', 'A contact sync batch cannot contain its owner.'); accountContactSyncCodec.encode(value);
141
+ }
@@ -0,0 +1,105 @@
1
+ import { decryptAes, encryptAes, requireLength, sha256 } from '../crypto/primitives.js';
2
+ import { ProtocolError } from '../errors.js';
3
+ import { bytes, defineCodec, integer, text, type ExtensibleModel } from '../protocol/codec.js';
4
+ import { decodeBase64Url, encodeUtf8, equalBytes } from '../protocol/encoding.js';
5
+ import { canonicalBytes, requireSafeInteger } from '../protocol/json.js';
6
+ import { validateCompleteObject } from '../protocol/validation.js';
7
+ import { parseAbsoluteUrl } from '../runtime/url.js';
8
+
9
+ /** Protocol Unicode 17 White_Space, deliberately independent of JavaScript trim(). */
10
+ export function containsNonWhitespace(value: string): boolean { return /[^\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/u.test(value); }
11
+ const token = "[!#$%&'*+.^_`|~0-9A-Za-z-]+";
12
+ const quoted = '"(?:[\\t\\x20\\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t\\x20-\\x7E\\x80-\\xFF])*"';
13
+ const mediaStart = new RegExp(`^${token}/${token}`);
14
+ const mediaParameter = new RegExp(`[ \\t]*;[ \\t]*(?:(${token})=(${token}|${quoted}))?`, 'y');
15
+ /** RFC 9110 grammar; repeated charset parameters must all specify UTF-8 for bodies. */
16
+ export function validateMediaType(value: string, requireUtf8 = false): void {
17
+ if (typeof value !== 'string') throw new ProtocolError('invalid_media_type', 'Expected a media type.');
18
+ const start = mediaStart.exec(value);
19
+ if (!start) throw new ProtocolError('invalid_media_type', 'Invalid media type.');
20
+ let offset = start[0].length;
21
+ while (offset < value.length) {
22
+ mediaParameter.lastIndex = offset;
23
+ const parameter = mediaParameter.exec(value);
24
+ if (!parameter) throw new ProtocolError('invalid_media_type', 'Invalid media type parameter.');
25
+ if (requireUtf8 && parameter[1]?.toLowerCase() === 'charset') {
26
+ const raw = parameter[2]!;
27
+ const charset = raw.startsWith('"') ? raw.slice(1, -1).replace(/\\([\s\S])/g, '$1') : raw;
28
+ if (charset.toLowerCase() !== 'utf-8') throw new ProtocolError('invalid_charset', 'Message body charset must be UTF-8.');
29
+ }
30
+ offset = mediaParameter.lastIndex;
31
+ }
32
+ }
33
+ export interface ContentEncryption extends ExtensibleModel { readonly alg: string; readonly key: Uint8Array; readonly nonce: Uint8Array }
34
+ export const contentEncryptionCodec = defineCodec<ContentEncryption>({ alg: { wire: 'alg', codec: text }, key: { wire: 'key', codec: bytes }, nonce: { wire: 'nonce', codec: bytes } });
35
+ export function validateContentEncryption(value: ContentEncryption): void {
36
+ validateCompleteObject(contentEncryptionCodec.encode(value));
37
+ if (value.alg !== 'AES-256-GCM') throw new ProtocolError('unsupported_algorithm', 'Content encryption must use AES-256-GCM.');
38
+ requireLength(value.key, 32, 'Content key'); requireLength(value.nonce, 12, 'Content nonce');
39
+ }
40
+ export interface ContentReference extends ExtensibleModel {
41
+ readonly uri: string; readonly hash: string; readonly contentType: string; readonly size: number; readonly encryption?: ContentEncryption;
42
+ }
43
+ export const contentReferenceCodec = defineCodec<ContentReference>({ uri: { wire: 'uri', codec: text }, hash: { wire: 'hash', codec: text },
44
+ contentType: { wire: 'content_type', codec: text }, size: { wire: 'size', codec: integer }, encryption: { wire: 'encryption', codec: contentEncryptionCodec, optional: true } });
45
+ export function contentHashBytes(hash: string): Uint8Array {
46
+ if (typeof hash !== 'string' || !hash.startsWith('sha256:')) throw new ProtocolError('invalid_hash', 'Expected a sha256 content hash.');
47
+ const bytes = decodeBase64Url(hash.slice(7)); requireLength(bytes, 32, 'Content hash'); return bytes;
48
+ }
49
+ export function validateContentReference(value: ContentReference): void {
50
+ validateCompleteObject(contentReferenceCodec.encode(value));
51
+ encodeUtf8(value.uri);
52
+ if (!/^https:\/\//i.test(value.uri) || /[\s\u0085\x00-\x1f\x7f-\x9f\\]/u.test(value.uri) || /%(?![0-9a-fA-F]{2})/.test(value.uri))
53
+ throw new ProtocolError('invalid_uri', 'Content requires a well-formed absolute HTTPS URI.');
54
+ try { if (!parseAbsoluteUrl(value.uri).hostname) throw new Error('Missing host'); }
55
+ catch (cause) { throw new ProtocolError('invalid_uri', 'Content requires a valid HTTPS host.', { cause }); }
56
+ contentHashBytes(value.hash); requireSafeInteger(value.size, 0); validateMediaType(value.contentType);
57
+ if (value.encryption) validateContentEncryption(value.encryption);
58
+ contentReferenceCodec.encode(value);
59
+ }
60
+ export function contentHashUri(value: ContentReference): string { contentHashBytes(value.hash); return `ni:///sha-256;${value.hash.slice(7)}`; }
61
+ export function validateAttachments(attachments: readonly ContentReference[]): void {
62
+ const hashes = new Set<string>();
63
+ for (const reference of attachments) {
64
+ validateContentReference(reference);
65
+ if (hashes.has(reference.hash)) throw new ProtocolError('duplicate_attachment', 'Attachments cannot contain duplicate plaintext hashes.');
66
+ hashes.add(reference.hash);
67
+ }
68
+ }
69
+ export function resolveContentReference(uri: string, attachments: readonly ContentReference[]): ContentReference | undefined {
70
+ validateAttachments(attachments);
71
+ if (!/^ni:\/\/\//i.test(uri) || /[/?#]/.test(uri.slice(6))) return undefined;
72
+ const separator = uri.indexOf(';', 6); if (separator < 0) return undefined;
73
+ try {
74
+ if (decodeURIComponent(uri.slice(6, separator)) !== 'sha-256') return undefined;
75
+ const hash = `sha256:${decodeURIComponent(uri.slice(separator + 1))}`; contentHashBytes(hash);
76
+ return attachments.find(value => value.hash === hash);
77
+ } catch { return undefined; } // Malformed user-facing ni links are simply unresolved.
78
+ }
79
+ export function verifyContent(value: ContentReference, plaintext: Uint8Array): void {
80
+ if (plaintext.length !== value.size || !equalBytes(sha256(plaintext), contentHashBytes(value.hash)))
81
+ throw new ProtocolError('invalid_content', 'Plaintext does not match the content size and hash.');
82
+ }
83
+ export function contentEncryptionAad(value: ContentReference): Uint8Array {
84
+ if (!value.encryption) throw new ProtocolError('encryption_required', 'Content reference has no encryption parameters.');
85
+ validateContentReference(value);
86
+ return canonicalBytes({ $type: 'meshline.content.reference.aad', alg: value.encryption.alg, hash: value.hash, content_type: value.contentType, size: value.size });
87
+ }
88
+ /** Low-level helper; the caller supplies a fresh independent key and nonce for every attachment. */
89
+ export function encryptContent(value: ContentReference, plaintext: Uint8Array): Uint8Array {
90
+ const aad = contentEncryptionAad(value); verifyContent(value, plaintext);
91
+ return encryptAes(value.encryption!.key, value.encryption!.nonce, plaintext, aad);
92
+ }
93
+ export function decryptContent(value: ContentReference, ciphertext: Uint8Array): Uint8Array {
94
+ const aad = contentEncryptionAad(value);
95
+ const plaintext = decryptAes(value.encryption!.key, value.encryption!.nonce, ciphertext, aad);
96
+ try { verifyContent(value, plaintext); return plaintext; } catch (error) { plaintext.fill(0); throw error; }
97
+ }
98
+ export interface MessageBody extends ExtensibleModel { readonly contentType: string; readonly text: string }
99
+ export const messageBodyCodec = defineCodec<MessageBody>({ contentType: { wire: 'content_type', codec: text }, text: { wire: 'text', codec: text } });
100
+ export function validateMessageBody(value: MessageBody): void {
101
+ validateCompleteObject(messageBodyCodec.encode(value));
102
+ validateMediaType(value.contentType, true); encodeUtf8(value.text);
103
+ if (!containsNonWhitespace(value.text)) throw new ProtocolError('invalid_body', 'Message body must contain non-whitespace text.');
104
+ messageBodyCodec.encode(value);
105
+ }
@@ -0,0 +1,18 @@
1
+ import { ProtocolError } from '../errors.js';
2
+ import { defineCodec, enumeration, integer, type ExtensibleModel } from '../protocol/codec.js';
3
+ import { requireSafeInteger } from '../protocol/json.js';
4
+ import { validateCompleteObject } from '../protocol/validation.js';
5
+
6
+ export interface DeviceStatePublishResponse extends ExtensibleModel { readonly status: 'accepted' | 'staged'; readonly stagedUntil?: number }
7
+ export const deviceStatePublishResponseCodec = defineCodec<DeviceStatePublishResponse>({
8
+ status: { wire: 'status', codec: enumeration('accepted', 'staged') }, stagedUntil: { wire: 'staged_until', codec: integer, optional: true },
9
+ });
10
+ export function validateDeviceStatePublishResponse(value: DeviceStatePublishResponse, now: number): void {
11
+ validateCompleteObject(deviceStatePublishResponseCodec.encode(value));
12
+ if (value.status === 'accepted') {
13
+ if (value.stagedUntil !== undefined) throw new ProtocolError('invalid_response', 'An accepted device state must omit staged_until.');
14
+ } else if (value.status === 'staged') {
15
+ requireSafeInteger(value.stagedUntil, 1);
16
+ if (value.stagedUntil <= now) throw new ProtocolError('invalid_response', 'The staged device state has already expired.');
17
+ } else throw new ProtocolError('invalid_response', 'Unknown device state publication status.');
18
+ }
@@ -0,0 +1,110 @@
1
+ import { ProtocolError } from '../errors.js';
2
+ import { validateIdentifier } from '../identity/identifiers.js';
3
+ import { validateAccountId } from '../identity/neo.js';
4
+ import { array, defineCodec, dictionary, integer, text, type ExtensibleModel } from '../protocol/codec.js';
5
+ import type { NetworkContext } from '../protocol/context.js';
6
+ import { requireSafeInteger } from '../protocol/json.js';
7
+ import { validateCompleteObject } from '../protocol/validation.js';
8
+ import { contentHashBytes } from './content.js';
9
+ import { groupApplicationApprovalCodec, groupApplicationCodec, groupInviteCodec, groupMemberRecoveryApprovalCodec, groupMemberRecoveryRequestCodec, validateGroupManagementPayload, verifyGroupApplication, verifyGroupInvite, verifyGroupMemberRecoveryRequest,
10
+ type GroupApplication, type GroupApplicationApproval, type GroupInvite, type GroupMemberRecoveryApproval, type GroupMemberRecoveryRequest } from './group-management.js';
11
+ import { groupSecretBoxCodec, validateGroupSecretBox, type GroupSecretBox } from './groups.js';
12
+ import { certificateId, deviceCertificateCodec, validateCertificate, type DeviceCertificate } from './identity.js';
13
+
14
+ const groupId = { wire: 'group_id', codec: text } as const;
15
+ const signerCertificate = { wire: 'signer_certificate', codec: deviceCertificateCodec } as const;
16
+ const acceptedAt = { wire: 'accepted_at', codec: integer } as const;
17
+ const next = { wire: 'next', codec: text, optional: true } as const;
18
+ const certificates = { wire: 'certificates', codec: array(deviceCertificateCodec) } as const;
19
+ function cursor(value: string): void { if (!/^[A-Za-z0-9._~-]+$/.test(value) || /[^A-Za-z0-9._~-]/.test(value)) throw new ProtocolError('invalid_cursor', 'Group list cursor must contain nonempty ASCII URL-safe text.'); }
20
+ export interface GroupListQuery extends ExtensibleModel { readonly groupId: string; readonly cursor?: string; readonly limit?: number }
21
+ export const groupListQueryCodec = defineCodec<GroupListQuery>({ groupId, cursor: { wire: 'cursor', codec: text, optional: true }, limit: { wire: 'limit', codec: integer, optional: true } });
22
+ export function validateGroupListQuery(value: GroupListQuery): void { validateCompleteObject(groupListQueryCodec.encode(value)); validateIdentifier('group', value.groupId); if (value.cursor !== undefined) cursor(value.cursor); if (value.limit !== undefined) requireSafeInteger(value.limit, 1); }
23
+ function page(count: number, nextCursor: string | undefined, query: GroupListQuery): void {
24
+ validateGroupListQuery(query); if (nextCursor !== undefined) cursor(nextCursor);
25
+ if (nextCursor !== undefined && (!count || nextCursor === query.cursor) || query.limit !== undefined && count > query.limit) throw new ProtocolError('invalid_pagination', 'Group list is empty with a continuation, repeats its cursor, or exceeds the requested limit.');
26
+ }
27
+ export interface GroupInviteQuery extends ExtensibleModel { readonly groupId: string; readonly inviteId: string }
28
+ export const groupInviteQueryCodec = defineCodec<GroupInviteQuery>({ groupId, inviteId: { wire: 'invite_id', codec: text } });
29
+ export function validateGroupInviteQuery(value: GroupInviteQuery): void { validateCompleteObject(groupInviteQueryCodec.encode(value)); validateIdentifier('group', value.groupId); validateIdentifier('invite', value.inviteId); }
30
+ export interface GroupResolveQuery extends ExtensibleModel { readonly groupId: string; readonly inviteId?: string }
31
+ export const groupResolveQueryCodec = defineCodec<GroupResolveQuery>({ groupId, inviteId: { wire: 'invite_id', codec: text, optional: true } });
32
+ export function validateGroupResolveQuery(value: GroupResolveQuery): void { validateCompleteObject(groupResolveQueryCodec.encode(value)); validateIdentifier('group', value.groupId); if (value.inviteId !== undefined) validateIdentifier('invite', value.inviteId); }
33
+ export interface GroupAccountsRequest extends ExtensibleModel { readonly groupId: string; readonly accounts: readonly string[] }
34
+ export const groupAccountsRequestCodec = defineCodec<GroupAccountsRequest>({ groupId, accounts: { wire: 'accounts', codec: array(text) } });
35
+ export function validateGroupAccountsRequest(value: GroupAccountsRequest): void {
36
+ validateCompleteObject(groupAccountsRequestCodec.encode(value)); validateIdentifier('group', value.groupId);
37
+ if (!value.accounts.length || new Set(value.accounts).size !== value.accounts.length) throw new ProtocolError('invalid_accounts', 'Group account list must be nonempty and unique.'); for (const account of value.accounts) validateAccountId(account);
38
+ }
39
+ export interface GroupInviteResolveResult extends ExtensibleModel { readonly invite: GroupInvite; readonly signerCertificate: DeviceCertificate; readonly uses: number }
40
+ export const groupInviteResolveResultCodec = defineCodec<GroupInviteResolveResult>({ invite: { wire: 'invite', codec: groupInviteCodec }, signerCertificate, uses: { wire: 'uses', codec: integer } });
41
+ export function validateGroupInviteResolveResult(value: GroupInviteResolveResult, query: GroupInviteQuery, context: NetworkContext, now: number): void {
42
+ validateCompleteObject(groupInviteResolveResultCodec.encode(value)); validateGroupInviteQuery(query); verifyGroupInvite(value.invite, value.signerCertificate, context, now); requireSafeInteger(value.uses, 0);
43
+ if (value.invite.groupId !== query.groupId || value.invite.inviteId !== query.inviteId) throw new ProtocolError('invalid_binding', 'Relay returned another group invitation.');
44
+ if (value.invite.invitee !== undefined && value.uses > 1 || value.invite.maxUses !== undefined && value.uses > value.invite.maxUses) throw new ProtocolError('invalid_uses', 'Invitation use count exceeds its limit.');
45
+ }
46
+ export interface GroupInviteEntry extends ExtensibleModel { readonly invite: GroupInvite; readonly signerDeviceId: string; readonly uses: number }
47
+ export const groupInviteEntryCodec = defineCodec<GroupInviteEntry>({ invite: { wire: 'invite', codec: groupInviteCodec }, signerDeviceId: { wire: 'signer_device_id', codec: text }, uses: { wire: 'uses', codec: integer } });
48
+ export interface GroupInvitePage extends ExtensibleModel { readonly invites: readonly GroupInviteEntry[]; readonly certificates: readonly DeviceCertificate[]; readonly next?: string }
49
+ export const groupInvitePageCodec = defineCodec<GroupInvitePage>({ invites: { wire: 'invites', codec: array(groupInviteEntryCodec) }, certificates, next });
50
+ export function validateGroupInvitePage(value: GroupInvitePage, query: GroupListQuery, context: NetworkContext, now: number): void {
51
+ validateCompleteObject(groupInvitePageCodec.encode(value)); page(value.invites.length, value.next, query);
52
+ const signers = new Map<string, DeviceCertificate>(); for (const certificate of value.certificates) { validateCertificate(certificate, context); const id = certificateId(certificate, context); if (signers.has(id)) throw new ProtocolError('duplicate_certificate', 'Invitation page repeats a signing device.'); signers.set(id, certificate); }
53
+ const ids = new Set<string>(); for (const entry of value.invites) {
54
+ validateIdentifier('device', entry.signerDeviceId); const signer = signers.get(entry.signerDeviceId); if (!signer) throw new ProtocolError('missing_certificate', 'Invitation signing certificate is missing.');
55
+ if (ids.has(entry.invite.inviteId)) throw new ProtocolError('duplicate_invite', 'Invitation page repeats an invitation.'); ids.add(entry.invite.inviteId);
56
+ validateGroupInviteResolveResult({ invite: entry.invite, signerCertificate: signer, uses: entry.uses }, { groupId: query.groupId, inviteId: entry.invite.inviteId }, context, now);
57
+ }
58
+ }
59
+ export interface GroupApplicationEntry extends ExtensibleModel { readonly application: GroupApplication; readonly signerCertificate: DeviceCertificate; readonly acceptedAt: number }
60
+ export const groupApplicationEntryCodec = defineCodec<GroupApplicationEntry>({ application: { wire: 'application', codec: groupApplicationCodec }, signerCertificate, acceptedAt });
61
+ export function validateGroupApplicationEntry(value: GroupApplicationEntry, expectedGroupId: string, context: NetworkContext): void {
62
+ validateCompleteObject(groupApplicationEntryCodec.encode(value)); validateIdentifier('group', expectedGroupId); requireSafeInteger(value.acceptedAt, 0); verifyGroupApplication(value.application, value.signerCertificate, context);
63
+ if (value.application.groupId !== expectedGroupId) throw new ProtocolError('invalid_binding', 'Application belongs to another group.');
64
+ }
65
+ export interface GroupApplicationPage extends ExtensibleModel { readonly applications: readonly GroupApplicationEntry[]; readonly next?: string }
66
+ export const groupApplicationPageCodec = defineCodec<GroupApplicationPage>({ applications: { wire: 'applications', codec: array(groupApplicationEntryCodec) }, next });
67
+ export function validateGroupApplicationPage(value: GroupApplicationPage, query: GroupListQuery, context: NetworkContext): void {
68
+ validateCompleteObject(groupApplicationPageCodec.encode(value)); page(value.applications.length, value.next, query); const accounts = new Set<string>();
69
+ for (const entry of value.applications) { validateGroupApplicationEntry(entry, query.groupId, context); if (accounts.has(entry.application.account)) throw new ProtocolError('duplicate_account', 'Application page repeats an account.'); accounts.add(entry.application.account); }
70
+ }
71
+ export interface GroupRecoverySubmitResult extends ExtensibleModel { readonly acceptedAt: number; readonly expiresAt: number }
72
+ const recoveryInterval = { acceptedAt, expiresAt: { wire: 'expires_at', codec: integer } } as const;
73
+ export const groupRecoverySubmitResultCodec = defineCodec<GroupRecoverySubmitResult>(recoveryInterval);
74
+ export function validateGroupRecoverySubmitResult(value: GroupRecoverySubmitResult): void {
75
+ validateCompleteObject(groupRecoverySubmitResultCodec.encode(value)); requireSafeInteger(value.acceptedAt, 0); requireSafeInteger(value.expiresAt, 0);
76
+ if (value.expiresAt <= value.acceptedAt) throw new ProtocolError('invalid_expiry', 'Recovery acceptance interval must have a later expiry.');
77
+ }
78
+ export interface GroupRecoveryEntry extends ExtensibleModel, GroupRecoverySubmitResult { readonly request: GroupMemberRecoveryRequest; readonly signerCertificate: DeviceCertificate }
79
+ export const groupRecoveryEntryCodec = defineCodec<GroupRecoveryEntry>({ request: { wire: 'request', codec: groupMemberRecoveryRequestCodec }, signerCertificate, ...recoveryInterval });
80
+ export function validateGroupRecoveryEntry(value: GroupRecoveryEntry, expectedGroupId: string, context: NetworkContext): void {
81
+ validateCompleteObject(groupRecoveryEntryCodec.encode(value)); validateIdentifier('group', expectedGroupId); validateGroupRecoverySubmitResult({ acceptedAt: value.acceptedAt, expiresAt: value.expiresAt }); verifyGroupMemberRecoveryRequest(value.request, value.signerCertificate, context);
82
+ if (value.request.groupId !== expectedGroupId) throw new ProtocolError('invalid_binding', 'Recovery request belongs to another group.');
83
+ }
84
+ export interface GroupRecoveryPage extends ExtensibleModel { readonly requests: readonly GroupRecoveryEntry[]; readonly next?: string }
85
+ export const groupRecoveryPageCodec = defineCodec<GroupRecoveryPage>({ requests: { wire: 'requests', codec: array(groupRecoveryEntryCodec) }, next });
86
+ export function validateGroupRecoveryPage(value: GroupRecoveryPage, query: GroupListQuery, context: NetworkContext): void {
87
+ validateCompleteObject(groupRecoveryPageCodec.encode(value)); page(value.requests.length, value.next, query); const accounts = new Set<string>();
88
+ for (const entry of value.requests) { validateGroupRecoveryEntry(entry, query.groupId, context); if (accounts.has(entry.request.account)) throw new ProtocolError('duplicate_account', 'Recovery page repeats an account.'); accounts.add(entry.request.account); }
89
+ }
90
+ export interface GroupApplicationApproveRequest extends ExtensibleModel { readonly approval: GroupApplicationApproval; readonly clientSecretCommitment: string; readonly clientSecretBoxes: Readonly<Record<string, GroupSecretBox>> }
91
+ export interface GroupRecoveryApproveRequest extends ExtensibleModel { readonly approval: GroupMemberRecoveryApproval; readonly clientSecretCommitment: string; readonly clientSecretBoxes: Readonly<Record<string, GroupSecretBox>> }
92
+ const wrapping = { clientSecretCommitment: { wire: 'client_secret_commitment', codec: text }, clientSecretBoxes: { wire: 'client_secret_boxes', codec: dictionary(groupSecretBoxCodec) } } as const;
93
+ export const groupApplicationApproveRequestCodec = defineCodec<GroupApplicationApproveRequest>({ approval: { wire: 'approval', codec: groupApplicationApprovalCodec }, ...wrapping });
94
+ export const groupRecoveryApproveRequestCodec = defineCodec<GroupRecoveryApproveRequest>({ approval: { wire: 'approval', codec: groupMemberRecoveryApprovalCodec }, ...wrapping });
95
+ function boxes(value: GroupApplicationApproveRequest | GroupRecoveryApproveRequest): void {
96
+ contentHashBytes(value.clientSecretCommitment);
97
+ if (Object.keys(value.clientSecretBoxes).length !== value.approval.members.length) throw new ProtocolError('invalid_recipients', 'Client secret boxes must match exactly the approved accounts.');
98
+ for (const member of value.approval.members) { if (!Object.hasOwn(value.clientSecretBoxes, member.account)) throw new ProtocolError('invalid_recipients', 'An approved account is missing its client secret box.'); validateGroupSecretBox(value.clientSecretBoxes[member.account]!); }
99
+ }
100
+ export function validateGroupApplicationApproveRequest(value: GroupApplicationApproveRequest): void { validateCompleteObject(groupApplicationApproveRequestCodec.encode(value)); validateGroupManagementPayload({ kind: 'applicationApproval', value: value.approval }); boxes(value); }
101
+ export function validateGroupRecoveryApproveRequest(value: GroupRecoveryApproveRequest): void { validateCompleteObject(groupRecoveryApproveRequestCodec.encode(value)); validateGroupManagementPayload({ kind: 'memberRecoveryApproval', value: value.approval }); boxes(value); }
102
+ export interface GroupSubscriptionRequest extends ExtensibleModel { readonly groupIds: readonly string[] }
103
+ export const groupSubscriptionRequestCodec = defineCodec<GroupSubscriptionRequest>({ groupIds: { wire: 'group_ids', codec: array(text) } });
104
+ export function validateGroupSubscriptionRequest(value: GroupSubscriptionRequest): void { validateCompleteObject(groupSubscriptionRequestCodec.encode(value)); if (new Set(value.groupIds).size !== value.groupIds.length) throw new ProtocolError('duplicate_group', 'Group subscription repeats a group.'); for (const id of value.groupIds) validateIdentifier('group', id); }
105
+ export interface GroupChangedNotification extends ExtensibleModel { readonly groupId: string }
106
+ export const groupChangedNotificationCodec = defineCodec<GroupChangedNotification>({ groupId });
107
+ export function validateGroupChangedNotification(value: GroupChangedNotification): void { validateCompleteObject(groupChangedNotificationCodec.encode(value)); validateIdentifier('group', value.groupId); }
108
+ export interface GroupTimelineChangedNotification extends GroupChangedNotification { readonly head: number }
109
+ export const groupTimelineChangedNotificationCodec = defineCodec<GroupTimelineChangedNotification>({ groupId, head: { wire: 'head', codec: integer } });
110
+ export function validateGroupTimelineChangedNotification(value: GroupTimelineChangedNotification): void { validateCompleteObject(groupTimelineChangedNotificationCodec.encode(value)); validateIdentifier('group', value.groupId); requireSafeInteger(value.head, 0); }
@@ -0,0 +1,34 @@
1
+ import { ProtocolError } from '../errors.js';
2
+ import { validateIdentifier } from '../identity/identifiers.js';
3
+ import { validateAccountId } from '../identity/neo.js';
4
+ import { array, boolean, defineCodec, dictionary, integer, text, type ExtensibleModel } from '../protocol/codec.js';
5
+ import { requireSafeInteger } from '../protocol/json.js';
6
+ import { validateCompleteObject } from '../protocol/validation.js';
7
+ import { contentHashBytes } from './content.js';
8
+ import { groupSecretBoxCodec, validateGroupSecretBox, type GroupSecretBox } from './groups.js';
9
+
10
+ export interface GroupKeyEntry extends ExtensibleModel { readonly epoch: number; readonly clientSecretBox?: GroupSecretBox; readonly relaySecretBox: GroupSecretBox }
11
+ export const groupKeyEntryCodec = defineCodec<GroupKeyEntry>({ epoch: { wire: 'epoch', codec: integer }, clientSecretBox: { wire: 'client_secret_box', codec: groupSecretBoxCodec, optional: true }, relaySecretBox: { wire: 'relay_secret_box', codec: groupSecretBoxCodec } });
12
+ export function validateGroupKeyEntry(value: GroupKeyEntry): void { validateCompleteObject(groupKeyEntryCodec.encode(value)); requireSafeInteger(value.epoch, 0); if (value.clientSecretBox) validateGroupSecretBox(value.clientSecretBox); validateGroupSecretBox(value.relaySecretBox); }
13
+ export interface GroupKeyPage extends ExtensibleModel { readonly keys: readonly GroupKeyEntry[]; readonly hasMore: boolean }
14
+ export const groupKeyPageCodec = defineCodec<GroupKeyPage>({ keys: { wire: 'keys', codec: array(groupKeyEntryCodec) }, hasMore: { wire: 'has_more', codec: boolean } });
15
+ export function validateGroupKeyPage(value: GroupKeyPage, after = -1): void {
16
+ validateCompleteObject(groupKeyPageCodec.encode(value)); requireSafeInteger(after, -1);
17
+ if (value.hasMore && !value.keys.length) throw new ProtocolError('invalid_pagination', 'An empty group key page cannot have more entries.');
18
+ if (value.keys.length && !value.keys[0]!.clientSecretBox) throw new ProtocolError('missing_secret_box', 'The first group key entry in each page requires a client secret box.');
19
+ for (const entry of value.keys) { validateGroupKeyEntry(entry); if (entry.epoch <= after) throw new ProtocolError('invalid_epoch', 'Group key epochs must increase after the requested cursor.'); after = entry.epoch; }
20
+ }
21
+ export interface GroupRotationPrepareRequest extends ExtensibleModel { readonly groupId: string; readonly baseCommitment: string; readonly clientSecretCommitment: string; readonly clientSecretBoxes: Readonly<Record<string, GroupSecretBox>> }
22
+ export const groupRotationPrepareRequestCodec = defineCodec<GroupRotationPrepareRequest>({ groupId: { wire: 'group_id', codec: text }, baseCommitment: { wire: 'base_commitment', codec: text }, clientSecretCommitment: { wire: 'client_secret_commitment', codec: text }, clientSecretBoxes: { wire: 'client_secret_boxes', codec: dictionary(groupSecretBoxCodec) } });
23
+ export function validateGroupRotationPrepareRequest(value: GroupRotationPrepareRequest): void {
24
+ validateCompleteObject(groupRotationPrepareRequestCodec.encode(value)); validateIdentifier('group', value.groupId); contentHashBytes(value.baseCommitment); contentHashBytes(value.clientSecretCommitment);
25
+ if (value.baseCommitment === value.clientSecretCommitment) throw new ProtocolError('unchanged_secret', 'Prepared client secret commitment must change.');
26
+ const boxes = Object.entries(value.clientSecretBoxes); if (!boxes.length) throw new ProtocolError('invalid_size', 'Rotation preparation requires at least one client secret box.');
27
+ for (const [account, box] of boxes) { validateAccountId(account); validateGroupSecretBox(box); }
28
+ }
29
+ export interface GroupRotationPrepareResult extends ExtensibleModel { readonly prepared: number; readonly expiresAt: number }
30
+ export const groupRotationPrepareResultCodec = defineCodec<GroupRotationPrepareResult>({ prepared: { wire: 'prepared', codec: integer }, expiresAt: { wire: 'expires_at', codec: integer } });
31
+ export function validateGroupRotationPrepareResult(value: GroupRotationPrepareResult, now: number): void {
32
+ validateCompleteObject(groupRotationPrepareResultCodec.encode(value)); requireSafeInteger(value.prepared, 0); requireSafeInteger(value.expiresAt, 0); requireSafeInteger(now, 0);
33
+ if (value.expiresAt <= now) throw new ProtocolError('expired_preparation', 'Group rotation preparation has expired.');
34
+ }