@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,182 @@
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, enumeration, integer, text, type ExtensibleModel, type ProtocolCodec, type ValueCodec } from '../protocol/codec.js';
6
+ import { signingInput, type NetworkContext } from '../protocol/context.js';
7
+ import { encodeUtf8 } from '../protocol/encoding.js';
8
+ import { canonicalJson, parseJson, requireObject, requireSafeInteger, type JsonObject } from '../protocol/json.js';
9
+ import { validateCompleteObject } from '../protocol/validation.js';
10
+ import { containsNonWhitespace, contentHashBytes } from './content.js';
11
+ import { certificateId, deviceCertificateCodec, validateCertificate, type DeviceCertificate } from './identity.js';
12
+ import { groupMemberKeyCodec, groupSecretBoxCodec, validateGroupMemberKey, validateGroupSecretBox, type GroupMemberKey, type GroupSecretBox } from './groups.js';
13
+
14
+ export type GroupStatus = 'active' | 'closed';
15
+ export type GroupRole = 'owner' | 'administrator' | 'member';
16
+ export type GroupInvitePolicy = 'administrators' | 'members_targeted' | 'members_shareable';
17
+ const policy = enumeration('administrators', 'members_targeted', 'members_shareable');
18
+ const role = enumeration('owner', 'administrator', 'member');
19
+ function name(value: string): void { if (!containsNonWhitespace(value) || encodeUtf8(value).length > 256) throw new ProtocolError('invalid_name', 'Group name requires text and at most 256 UTF-8 bytes.'); }
20
+ function description(value: string | undefined): void { if (value !== undefined && value !== '' && (!containsNonWhitespace(value) || encodeUtf8(value).length > 4096)) throw new ProtocolError('invalid_description', 'Nonempty group description requires text and at most 4096 UTF-8 bytes.'); }
21
+ const signed = { groupId: { wire: 'group_id', codec: text }, deviceSignature: { wire: 'device_signature', codec: bytes } } as const;
22
+ const management = { ...signed, prevHash: { wire: 'prev_hash', codec: text } } as const;
23
+ export interface GroupCreate extends ExtensibleModel {
24
+ readonly groupId: string; readonly nonce: Uint8Array; readonly name: string; readonly description?: string; readonly memberCapacity: number;
25
+ readonly invitePolicy: GroupInvitePolicy; readonly owner: GroupMemberKey; readonly clientSecretCommitment: string; readonly deviceSignature: Uint8Array;
26
+ }
27
+ export const groupCreateCodec = defineCodec<GroupCreate>({ ...signed, nonce: { wire: 'nonce', codec: bytes }, name: { wire: 'name', codec: text }, description: { wire: 'description', codec: text, optional: true },
28
+ memberCapacity: { wire: 'member_capacity', codec: integer }, invitePolicy: { wire: 'invite_policy', codec: policy }, owner: { wire: 'owner', codec: groupMemberKeyCodec }, clientSecretCommitment: { wire: 'client_secret_commitment', codec: text } }, 'meshline.group.create');
29
+ export const groupCreateInput = (value: GroupCreate, context: NetworkContext): Uint8Array => signingInput(groupCreateCodec.encode(value), context, ['device_signature']);
30
+ export function validateGroupCreate(value: GroupCreate): void {
31
+ validateCompleteObject(groupCreateCodec.encode(value)); validateIdentifier('group', value.groupId); requireLength(value.nonce, 16, 'Group nonce'); name(value.name); description(value.description);
32
+ requireSafeInteger(value.memberCapacity, 1); validateGroupMemberKey(value.owner); contentHashBytes(value.clientSecretCommitment); requireLength(value.deviceSignature, 64, 'Group signature');
33
+ }
34
+ export interface GroupCreateRequest extends ExtensibleModel { readonly create: GroupCreate; readonly clientSecretBox: GroupSecretBox }
35
+ export const groupCreateRequestCodec = defineCodec<GroupCreateRequest>({ create: { wire: 'create', codec: groupCreateCodec }, clientSecretBox: { wire: 'client_secret_box', codec: groupSecretBoxCodec } });
36
+ export function validateGroupCreateRequest(value: GroupCreateRequest): void { validateCompleteObject(groupCreateRequestCodec.encode(value)); validateGroupCreate(value.create); validateGroupSecretBox(value.clientSecretBox); }
37
+ export interface GroupManagementOperation extends ExtensibleModel { readonly groupId: string; readonly prevHash: string; readonly deviceSignature: Uint8Array }
38
+ function validateOperation(value: GroupManagementOperation): void { validateIdentifier('group', value.groupId); contentHashBytes(value.prevHash); requireLength(value.deviceSignature, 64, 'Group management signature'); }
39
+ export interface GroupUpdate extends GroupManagementOperation { readonly name?: string; readonly description?: string | null; readonly memberCapacity?: number; readonly invitePolicy?: GroupInvitePolicy }
40
+ export const groupUpdateCodec = defineCodec<GroupUpdate>({ ...management, name: { wire: 'name', codec: text, optional: true }, description: { wire: 'description', codec: text, optional: true, nullable: true },
41
+ memberCapacity: { wire: 'member_capacity', codec: integer, optional: true }, invitePolicy: { wire: 'invite_policy', codec: policy, optional: true } }, 'meshline.group.update');
42
+ export function validateGroupUpdate(value: GroupUpdate): void {
43
+ const wire = groupUpdateCodec.encode(value); if (wire.description === null) delete wire.description; validateCompleteObject(wire); validateOperation(value);
44
+ if (value.name === undefined && value.description === undefined && value.memberCapacity === undefined && value.invitePolicy === undefined) throw new ProtocolError('empty_update', 'A group update requires a known modifiable field.');
45
+ if (value.name !== undefined) name(value.name); if (value.description !== null) description(value.description); if (value.memberCapacity !== undefined) requireSafeInteger(value.memberCapacity, 1);
46
+ }
47
+ export interface GroupApplicationApproval extends GroupManagementOperation { readonly members: readonly GroupMemberKey[] }
48
+ export const groupApplicationApprovalCodec = defineCodec<GroupApplicationApproval>({ ...management, members: { wire: 'members', codec: array(groupMemberKeyCodec) } }, 'meshline.group.application.approval');
49
+ export interface GroupMemberRecoveryApproval extends GroupManagementOperation { readonly members: readonly GroupMemberKey[] }
50
+ export const groupMemberRecoveryApprovalCodec = defineCodec<GroupMemberRecoveryApproval>({ ...management, members: { wire: 'members', codec: array(groupMemberKeyCodec) } }, 'meshline.group.member.recovery.approval');
51
+ function validateMembers(members: readonly GroupMemberKey[]): void {
52
+ if (!members.length) throw new ProtocolError('invalid_size', 'A member list cannot be empty.'); const seen = new Set<string>();
53
+ for (const member of members) { validateGroupMemberKey(member); if (seen.has(member.account)) throw new ProtocolError('duplicate_member', 'A member list repeats an account.'); seen.add(member.account); }
54
+ }
55
+ export interface GroupRoleUpdate extends GroupManagementOperation { readonly account: string; readonly role: 'administrator' | 'member' }
56
+ export const groupRoleUpdateCodec = defineCodec<GroupRoleUpdate>({ ...management, account: { wire: 'account', codec: text }, role: { wire: 'role', codec: enumeration('administrator', 'member') } }, 'meshline.group.role.update');
57
+ export interface GroupOwnerTransfer extends GroupManagementOperation { readonly newOwnerAccount: string }
58
+ export const groupOwnerTransferCodec = defineCodec<GroupOwnerTransfer>({ ...management, newOwnerAccount: { wire: 'new_owner_account', codec: text } }, 'meshline.group.owner.transfer');
59
+ export interface GroupMemberLeave extends GroupManagementOperation { readonly account: string }
60
+ export const groupMemberLeaveCodec = defineCodec<GroupMemberLeave>({ ...management, account: { wire: 'account', codec: text } }, 'meshline.group.member.leave');
61
+ export interface GroupMemberRemoval extends GroupManagementOperation { readonly accounts: readonly string[] }
62
+ export const groupMemberRemovalCodec = defineCodec<GroupMemberRemoval>({ ...management, accounts: { wire: 'accounts', codec: array(text) } }, 'meshline.group.member.removal');
63
+ export interface GroupMemberBan extends GroupManagementOperation { readonly accounts: readonly string[] }
64
+ export const groupMemberBanCodec = defineCodec<GroupMemberBan>({ ...management, accounts: { wire: 'accounts', codec: array(text) } }, 'meshline.group.member.ban');
65
+ export interface GroupMemberUnban extends GroupManagementOperation { readonly accounts: readonly string[] }
66
+ export const groupMemberUnbanCodec = defineCodec<GroupMemberUnban>({ ...management, accounts: { wire: 'accounts', codec: array(text) } }, 'meshline.group.member.unban');
67
+ function validateAccounts(accounts: readonly string[]): void {
68
+ if (!accounts.length) throw new ProtocolError('invalid_size', 'An account list cannot be empty.'); const seen = new Set<string>();
69
+ for (const account of accounts) { validateAccountId(account); if (seen.has(account)) throw new ProtocolError('duplicate_account', 'An account list repeats an account.'); seen.add(account); }
70
+ }
71
+ export interface GroupSecretRotation extends GroupManagementOperation { readonly clientSecretCommitment: string; readonly ownerEncryptionPublicKey?: Uint8Array }
72
+ export const groupSecretRotationCodec = defineCodec<GroupSecretRotation>({ ...management, clientSecretCommitment: { wire: 'client_secret_commitment', codec: text }, ownerEncryptionPublicKey: { wire: 'owner_encryption_public_key', codec: bytes, optional: true } }, 'meshline.group.secret.rotation');
73
+ export interface GroupClose extends GroupManagementOperation {}
74
+ export const groupCloseCodec = defineCodec<GroupClose>(management, 'meshline.group.close');
75
+ export interface GroupKeyRotated extends ExtensibleModel {}
76
+ export const groupKeyRotatedCodec = defineCodec<GroupKeyRotated>({}, 'meshline.group.key.rotated');
77
+
78
+ const managementCodecs = { create: groupCreateCodec, update: groupUpdateCodec, applicationApproval: groupApplicationApprovalCodec, roleUpdate: groupRoleUpdateCodec,
79
+ ownerTransfer: groupOwnerTransferCodec, memberLeave: groupMemberLeaveCodec, memberRemoval: groupMemberRemovalCodec, memberBan: groupMemberBanCodec,
80
+ memberUnban: groupMemberUnbanCodec, memberRecoveryApproval: groupMemberRecoveryApprovalCodec, secretRotation: groupSecretRotationCodec, close: groupCloseCodec } as const;
81
+ type ManagementModels = { [K in keyof typeof managementCodecs]: ReturnType<(typeof managementCodecs)[K]['decode']> };
82
+ export type GroupManagementPayload = { [K in keyof ManagementModels]: { readonly kind: K; readonly value: ManagementModels[K] } }[keyof ManagementModels];
83
+ const managementKinds: Readonly<Record<string, keyof ManagementModels>> = { 'meshline.group.create': 'create', 'meshline.group.update': 'update', 'meshline.group.application.approval': 'applicationApproval',
84
+ 'meshline.group.role.update': 'roleUpdate', 'meshline.group.owner.transfer': 'ownerTransfer', 'meshline.group.member.leave': 'memberLeave', 'meshline.group.member.removal': 'memberRemoval',
85
+ 'meshline.group.member.ban': 'memberBan', 'meshline.group.member.unban': 'memberUnban', 'meshline.group.member.recovery.approval': 'memberRecoveryApproval', 'meshline.group.secret.rotation': 'secretRotation', 'meshline.group.close': 'close' };
86
+ export const groupManagementPayloadCodec: ValueCodec<GroupManagementPayload> = {
87
+ decode(value) {
88
+ const wire = requireObject(value); const kind = typeof wire.$type === 'string' && Object.hasOwn(managementKinds, wire.$type) ? managementKinds[wire.$type] : undefined;
89
+ if (!kind) throw new ProtocolError('unsupported_group_operation', 'Unknown group administration type; state verification must pause.');
90
+ return { kind, value: managementCodecs[kind].decode(wire) } as GroupManagementPayload;
91
+ },
92
+ encode(value) {
93
+ const codec = managementCodecs[value.kind] as ProtocolCodec<GroupManagementPayload['value']> | undefined;
94
+ if (!codec) throw new ProtocolError('unsupported_group_operation', 'Unknown group administration kind.'); return codec.encode(value.value);
95
+ },
96
+ };
97
+ export const groupManagementInput = (value: GroupManagementPayload, context: NetworkContext): Uint8Array => signingInput(requireObject(groupManagementPayloadCodec.encode(value)), context, ['device_signature']);
98
+ export function validateGroupManagementPayload(value: GroupManagementPayload): void {
99
+ if (value.kind === 'create') { validateGroupCreate(value.value); return; }
100
+ if (value.kind === 'update') { validateGroupUpdate(value.value); return; }
101
+ validateCompleteObject(requireObject(groupManagementPayloadCodec.encode(value))); validateOperation(value.value);
102
+ switch (value.kind) {
103
+ case 'applicationApproval': case 'memberRecoveryApproval': validateMembers(value.value.members); break;
104
+ case 'memberRemoval': case 'memberBan': case 'memberUnban': validateAccounts(value.value.accounts); break;
105
+ case 'roleUpdate': case 'memberLeave': validateAccountId(value.value.account); break;
106
+ case 'ownerTransfer': validateAccountId(value.value.newOwnerAccount); break;
107
+ case 'secretRotation': contentHashBytes(value.value.clientSecretCommitment); if (value.value.ownerEncryptionPublicKey) requireLength(value.value.ownerEncryptionPublicKey, 32, 'Owner encryption public key'); break;
108
+ case 'close': break;
109
+ }
110
+ }
111
+ export function verifyGroupManagementPayload(value: GroupManagementPayload, certificate: DeviceCertificate, context: NetworkContext): void {
112
+ validateGroupManagementPayload(value); validateCertificate(certificate, context);
113
+ if (!verifyDevice(groupManagementInput(value, context), value.value.deviceSignature, certificate.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid group administration signature.');
114
+ }
115
+
116
+ export interface GroupInvite extends ExtensibleModel { readonly inviteId: string; readonly groupId: string; readonly inviter: string; readonly invitee?: string; readonly maxUses?: number; readonly createdAt: number; readonly expiresAt: number; readonly deviceSignature: Uint8Array }
117
+ export const groupInviteCodec = defineCodec<GroupInvite>({ ...signed, inviteId: { wire: 'invite_id', codec: text }, inviter: { wire: 'inviter', codec: text }, invitee: { wire: 'invitee', codec: text, optional: true }, maxUses: { wire: 'max_uses', codec: integer, optional: true },
118
+ createdAt: { wire: 'created_at', codec: integer }, expiresAt: { wire: 'expires_at', codec: integer } }, 'meshline.group.invite');
119
+ export const groupInviteInput = (value: GroupInvite, context: NetworkContext): Uint8Array => signingInput(groupInviteCodec.encode(value), context, ['device_signature']);
120
+ export function validateGroupInvite(value: GroupInvite, now: number): void {
121
+ validateCompleteObject(groupInviteCodec.encode(value)); validateIdentifier('invite', value.inviteId); validateIdentifier('group', value.groupId); validateAccountId(value.inviter);
122
+ if (value.invitee !== undefined) validateAccountId(value.invitee); if (value.maxUses !== undefined) requireSafeInteger(value.maxUses, 1);
123
+ if (value.invitee !== undefined && value.maxUses !== undefined) throw new ProtocolError('invalid_invite', 'Only shareable invitations may specify a use limit.');
124
+ requireSafeInteger(value.createdAt, 0); requireSafeInteger(value.expiresAt, 0); requireSafeInteger(now, 0); requireLength(value.deviceSignature, 64, 'Group invitation signature');
125
+ if (value.expiresAt <= value.createdAt || value.expiresAt <= now) throw new ProtocolError('expired_invite', 'Group invitation must expire after its creation and the current time.');
126
+ }
127
+ export function verifyGroupInvite(value: GroupInvite, certificate: DeviceCertificate, context: NetworkContext, now: number): void {
128
+ validateGroupInvite(value, now); validateCertificate(certificate, context);
129
+ if (certificate.account !== value.inviter) throw new ProtocolError('invalid_identity', 'Group invitation certificate belongs to another account.');
130
+ if (!verifyDevice(groupInviteInput(value, context), value.deviceSignature, certificate.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid group invitation signature.');
131
+ }
132
+ export interface GroupApplication extends ExtensibleModel { readonly groupId: string; readonly inviteId: string; readonly account: string; readonly memberEncryptionPublicKey: Uint8Array; readonly deviceSignature: Uint8Array }
133
+ const memberRequest = { ...signed, account: { wire: 'account', codec: text }, memberEncryptionPublicKey: { wire: 'member_encryption_public_key', codec: bytes } } as const;
134
+ export const groupApplicationCodec = defineCodec<GroupApplication>({ ...memberRequest, inviteId: { wire: 'invite_id', codec: text } }, 'meshline.group.application');
135
+ export const groupApplicationInput = (value: GroupApplication, context: NetworkContext): Uint8Array => signingInput(groupApplicationCodec.encode(value), context, ['device_signature']);
136
+ export function validateGroupApplication(value: GroupApplication): void { validateCompleteObject(groupApplicationCodec.encode(value)); validateIdentifier('group', value.groupId); validateIdentifier('invite', value.inviteId); validateAccountId(value.account); requireLength(value.memberEncryptionPublicKey, 32, 'Member encryption public key'); requireLength(value.deviceSignature, 64, 'Application signature'); }
137
+ export interface GroupMemberRecoveryRequest extends ExtensibleModel { readonly groupId: string; readonly account: string; readonly memberEncryptionPublicKey: Uint8Array; readonly deviceSignature: Uint8Array }
138
+ export const groupMemberRecoveryRequestCodec = defineCodec<GroupMemberRecoveryRequest>(memberRequest, 'meshline.group.member.recovery');
139
+ export const groupMemberRecoveryRequestInput = (value: GroupMemberRecoveryRequest, context: NetworkContext): Uint8Array => signingInput(groupMemberRecoveryRequestCodec.encode(value), context, ['device_signature']);
140
+ export function validateGroupMemberRecoveryRequest(value: GroupMemberRecoveryRequest): void { validateCompleteObject(groupMemberRecoveryRequestCodec.encode(value)); validateIdentifier('group', value.groupId); validateAccountId(value.account); requireLength(value.memberEncryptionPublicKey, 32, 'Member encryption public key'); requireLength(value.deviceSignature, 64, 'Recovery request signature'); }
141
+ export function verifyGroupApplication(value: GroupApplication, certificate: DeviceCertificate, context: NetworkContext): void {
142
+ validateGroupApplication(value); validateCertificate(certificate, context); if (certificate.account !== value.account) throw new ProtocolError('invalid_identity', 'Application certificate belongs to another account.');
143
+ if (!verifyDevice(groupApplicationInput(value, context), value.deviceSignature, certificate.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid group application signature.');
144
+ }
145
+ export function verifyGroupMemberRecoveryRequest(value: GroupMemberRecoveryRequest, certificate: DeviceCertificate, context: NetworkContext): void {
146
+ validateGroupMemberRecoveryRequest(value); validateCertificate(certificate, context); if (certificate.account !== value.account) throw new ProtocolError('invalid_identity', 'Recovery request certificate belongs to another account.');
147
+ if (!verifyDevice(groupMemberRecoveryRequestInput(value, context), value.deviceSignature, certificate.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid member recovery signature.');
148
+ }
149
+
150
+ export interface GroupState extends ExtensibleModel { readonly groupId: string; readonly name: string; readonly description?: string; readonly status: GroupStatus; readonly owner: string; readonly memberCapacity: number; readonly memberCount: number; readonly invitePolicy: GroupInvitePolicy }
151
+ export const groupStateCodec = defineCodec<GroupState>({ groupId: { wire: 'group_id', codec: text }, name: { wire: 'name', codec: text }, description: { wire: 'description', codec: text, optional: true }, status: { wire: 'status', codec: enumeration('active', 'closed') },
152
+ owner: { wire: 'owner', codec: text }, memberCapacity: { wire: 'member_capacity', codec: integer }, memberCount: { wire: 'member_count', codec: integer }, invitePolicy: { wire: 'invite_policy', codec: policy } });
153
+ /** A relay preview is structurally validated, but cannot establish membership or replace verified chain state. */
154
+ export function validateGroupState(value: GroupState): void { validateCompleteObject(groupStateCodec.encode(value)); validateIdentifier('group', value.groupId); name(value.name); description(value.description); validateAccountId(value.owner); requireSafeInteger(value.memberCapacity, 1); requireSafeInteger(value.memberCount, 1); }
155
+ export interface GroupMember extends GroupMemberKey { readonly role: GroupRole }
156
+ export const groupMemberCodec = defineCodec<GroupMember>({ account: { wire: 'account', codec: text }, memberEncryptionPublicKey: { wire: 'member_encryption_public_key', codec: bytes }, role: { wire: 'role', codec: role } });
157
+ export interface GroupSequenceQuery extends ExtensibleModel { readonly groupId: string; readonly after?: number; readonly limit?: number }
158
+ export const groupSequenceQueryCodec = defineCodec<GroupSequenceQuery>({ groupId: { wire: 'group_id', codec: text }, after: { wire: 'after', codec: integer, optional: true }, limit: { wire: 'limit', codec: integer, optional: true } });
159
+ export function validateGroupSequenceQuery(value: GroupSequenceQuery): void { validateCompleteObject(groupSequenceQueryCodec.encode(value)); validateIdentifier('group', value.groupId); if (value.after !== undefined) requireSafeInteger(value.after, -1); if (value.limit !== undefined) requireSafeInteger(value.limit, 1); }
160
+ const timelinePayload: ValueCodec<JsonObject> = {
161
+ decode(value) { const wire = requireObject(parseJson(canonicalJson(requireObject(value)))); if (typeof wire.$type !== 'string') throw new ProtocolError('invalid_type', 'A group event payload requires a string $type.'); return wire; },
162
+ encode(value) { return this.decode(value); },
163
+ };
164
+ /** Raw typed payload lets the receiver quarantine invalid ordinary messages without skipping unknown administration events. */
165
+ export interface GroupEvent extends ExtensibleModel { readonly sequence: number; readonly epoch: number; readonly payload: JsonObject; readonly acceptedAt: number; readonly signerDeviceId?: string }
166
+ export const groupEventCodec = defineCodec<GroupEvent>({ sequence: { wire: 'sequence', codec: integer }, epoch: { wire: 'epoch', codec: integer }, payload: { wire: 'payload', codec: timelinePayload }, acceptedAt: { wire: 'accepted_at', codec: integer }, signerDeviceId: { wire: 'signer_device_id', codec: text, optional: true } });
167
+ export function validateGroupEvent(value: GroupEvent): void {
168
+ const wire = groupEventCodec.encode(value); delete wire.payload; validateCompleteObject(wire); requireSafeInteger(value.sequence, 0); requireSafeInteger(value.epoch, 0); requireSafeInteger(value.acceptedAt, 0);
169
+ if (value.payload.$type === 'meshline.group.key.rotated') {
170
+ validateCompleteObject(value.payload); if (value.signerDeviceId !== undefined || Object.hasOwn(value.payload, 'device_signature')) throw new ProtocolError('invalid_rotation', 'Relay rotation cannot carry a device signer or signature.');
171
+ } else { if (value.signerDeviceId === undefined) throw new ProtocolError('missing_signer', 'A client group event requires a signing device.'); validateIdentifier('device', value.signerDeviceId); }
172
+ }
173
+ export interface GroupSyncPage extends ExtensibleModel { readonly events: readonly GroupEvent[]; readonly certificates: readonly DeviceCertificate[]; readonly hasMore: boolean }
174
+ export const groupSyncPageCodec = defineCodec<GroupSyncPage>({ events: { wire: 'events', codec: array(groupEventCodec) }, certificates: { wire: 'certificates', codec: array(deviceCertificateCodec) }, hasMore: { wire: 'has_more', codec: boolean } });
175
+ export function validateGroupSyncPage(value: GroupSyncPage, query: GroupSequenceQuery, context: NetworkContext): void {
176
+ validateGroupSequenceQuery(query); const wire = groupSyncPageCodec.encode(value); delete wire.events; validateCompleteObject(wire);
177
+ if (value.hasMore && !value.events.length || query.limit !== undefined && value.events.length > query.limit) throw new ProtocolError('invalid_pagination', 'Invalid group synchronization page length.');
178
+ const ids = new Set<string>(); for (const certificate of value.certificates) { validateCertificate(certificate, context); const id = certificateId(certificate, context); if (ids.has(id)) throw new ProtocolError('duplicate_device', 'Group page repeats a signing certificate.'); ids.add(id); }
179
+ let previous = query.after ?? -1;
180
+ for (const entry of value.events) { validateGroupEvent(entry); if (entry.sequence <= previous) throw new ProtocolError('invalid_sequence', 'Group events must be in increasing order after the requested cursor.'); previous = entry.sequence;
181
+ if (entry.signerDeviceId !== undefined && !ids.has(entry.signerDeviceId)) throw new ProtocolError('missing_certificate', 'Group event signing certificate is missing.'); }
182
+ }
@@ -0,0 +1,100 @@
1
+ import { requireLength, verifyDevice } from '../crypto/primitives.js';
2
+ import { ProtocolError } from '../errors.js';
3
+ import { validateIdentifier } from '../identity/identifiers.js';
4
+ import { validateAccountId, validateRelayId } from '../identity/neo.js';
5
+ import { array, bytes, defineCodec, integer, text, type ExtensibleModel, type ValueCodec } from '../protocol/codec.js';
6
+ import { signingInput, type NetworkContext } from '../protocol/context.js';
7
+ import { encodeUtf8 } from '../protocol/encoding.js';
8
+ import { canonicalJson, parseJson, requireObject, requireSafeInteger, type JsonObject } from '../protocol/json.js';
9
+ import { validateCompleteObject } from '../protocol/validation.js';
10
+ import { containsNonWhitespace, contentReferenceCodec, messageBodyCodec, validateAttachments, validateMessageBody, type ContentReference, type MessageBody } from './content.js';
11
+ import { certificateId, validateCertificate, type DeviceCertificate } from './identity.js';
12
+ import { encryptedPayloadCodec, validateEncryptedPayload, type EncryptedPayload } from './messages.js';
13
+
14
+ export interface GroupRef { readonly groupId: string; readonly relayId: string }
15
+ export function validateGroupRef(value: GroupRef): void { validateIdentifier('group', value.groupId); validateRelayId(value.relayId); }
16
+
17
+ export interface GroupSecretBox extends ExtensibleModel { readonly alg: string; readonly enc: Uint8Array; readonly sealedSecret: Uint8Array }
18
+ export const groupSecretBoxCodec = defineCodec<GroupSecretBox>({ alg: { wire: 'alg', codec: text }, enc: { wire: 'enc', codec: bytes }, sealedSecret: { wire: 'sealed_secret', codec: bytes } });
19
+ export function validateGroupSecretBox(value: GroupSecretBox): void {
20
+ validateCompleteObject(groupSecretBoxCodec.encode(value));
21
+ if (value.alg !== 'X25519-HKDF-SHA256-AES256GCM') throw new ProtocolError('unsupported_algorithm', 'Unsupported group secret box algorithm.');
22
+ requireLength(value.enc, 32, 'Ephemeral public key'); requireLength(value.sealedSecret, 60, 'Sealed group secret');
23
+ }
24
+
25
+ /** The sender identity is carried by the verified timeline event and certificate, never by this envelope. */
26
+ export interface GroupMessageEnvelope extends ExtensibleModel {
27
+ readonly messageId: string; readonly groupId: string; readonly epoch: number; readonly createdAt: number;
28
+ readonly payload: EncryptedPayload; readonly deviceSignature: Uint8Array;
29
+ }
30
+ export const groupMessageEnvelopeCodec = defineCodec<GroupMessageEnvelope>({ messageId: { wire: 'message_id', codec: text }, groupId: { wire: 'group_id', codec: text },
31
+ epoch: { wire: 'epoch', codec: integer }, createdAt: { wire: 'created_at', codec: integer }, payload: { wire: 'payload', codec: encryptedPayloadCodec },
32
+ deviceSignature: { wire: 'device_signature', codec: bytes } }, 'meshline.group.message');
33
+ export const groupMessageEnvelopeInput = (value: GroupMessageEnvelope, context: NetworkContext): Uint8Array => signingInput(groupMessageEnvelopeCodec.encode(value), context, ['device_signature']);
34
+ export function validateGroupMessageEnvelope(value: GroupMessageEnvelope): void {
35
+ validateCompleteObject(groupMessageEnvelopeCodec.encode(value)); validateIdentifier('message', value.messageId); validateIdentifier('group', value.groupId);
36
+ requireSafeInteger(value.epoch, 0); requireSafeInteger(value.createdAt, 0); requireLength(value.deviceSignature, 64, 'Group message signature'); validateEncryptedPayload(value.payload);
37
+ if (encodeUtf8(groupMessageEnvelopeCodec.stringify(value)).length > 262144) throw new ProtocolError('invalid_size', 'Group message envelope exceeds 262144 canonical UTF-8 bytes.');
38
+ }
39
+ /** Historical signature integrity only; membership, epoch and event ordering require verified management state. */
40
+ export function verifyGroupMessageEnvelope(value: GroupMessageEnvelope, sender: DeviceCertificate, signerDeviceId: string, context: NetworkContext): void {
41
+ validateGroupMessageEnvelope(value); validateCertificate(sender, context); validateIdentifier('device', signerDeviceId);
42
+ if (certificateId(sender, context) !== signerDeviceId) throw new ProtocolError('invalid_identity', 'Group event signing certificate belongs to another device.');
43
+ if (!verifyDevice(groupMessageEnvelopeInput(value, context), value.deviceSignature, sender.signingPublicKey)) throw new ProtocolError('invalid_signature', 'Invalid group message signature.');
44
+ }
45
+
46
+ export interface GroupMessage extends ExtensibleModel { readonly body?: MessageBody; readonly attachments?: readonly ContentReference[]; readonly replyToSeq?: number }
47
+ export const groupMessageCodec = defineCodec<GroupMessage>({ body: { wire: 'body', codec: messageBodyCodec, optional: true }, attachments: { wire: 'attachments', codec: array(contentReferenceCodec), optional: true },
48
+ replyToSeq: { wire: 'reply_to_seq', codec: integer, optional: true } }, 'meshline.group.message.content');
49
+ export function validateGroupMessage(value: GroupMessage): void {
50
+ validateCompleteObject(groupMessageCodec.encode(value)); if (value.body) validateMessageBody(value.body); if (value.attachments) validateAttachments(value.attachments);
51
+ if (!value.body && !value.attachments?.length) throw new ProtocolError('invalid_message', 'A group message requires a body or at least one attachment.');
52
+ if (value.replyToSeq !== undefined) requireSafeInteger(value.replyToSeq, 1);
53
+ }
54
+ export interface GroupMemberNicknameUpdate extends ExtensibleModel { readonly nickname: string | null }
55
+ export const groupMemberNicknameUpdateCodec = defineCodec<GroupMemberNicknameUpdate>({ nickname: { wire: 'nickname', codec: text, nullable: true } }, 'meshline.group.member.nickname.update');
56
+ export function validateGroupMemberNicknameUpdate(value: GroupMemberNicknameUpdate): void {
57
+ const wire = groupMemberNicknameUpdateCodec.encode(value); if (wire.nickname === null) delete wire.nickname; validateCompleteObject(wire);
58
+ if (value.nickname !== null && (!containsNonWhitespace(value.nickname) || encodeUtf8(value.nickname).length > 256)) throw new ProtocolError('invalid_nickname', 'A group nickname requires non-whitespace text and at most 256 UTF-8 bytes.');
59
+ }
60
+ /** Preserves authenticated but business-invalid content so the receiver can record diagnostics without stalling its cursor. */
61
+ export const groupPayloadCodec: ValueCodec<JsonObject> = {
62
+ decode(value) {
63
+ const object = requireObject(value); const copy = requireObject(parseJson(canonicalJson(object)));
64
+ if (typeof copy.$type !== 'string') throw new ProtocolError('invalid_type', 'Group content requires a string $type.');
65
+ const complete = { ...copy }; if (complete.$type === 'meshline.group.member.nickname.update' && complete.nickname === null) delete complete.nickname;
66
+ validateCompleteObject(complete); return copy;
67
+ },
68
+ encode(value) { return this.decode(value); },
69
+ };
70
+ export function validateGroupPayload(value: JsonObject): void {
71
+ const wire = groupPayloadCodec.decode(value);
72
+ if (wire.$type === 'meshline.group.message.content') validateGroupMessage(groupMessageCodec.decode(wire));
73
+ else if (wire.$type === 'meshline.group.member.nickname.update') validateGroupMemberNicknameUpdate(groupMemberNicknameUpdateCodec.decode(wire));
74
+ }
75
+
76
+ export interface GroupMemberKey extends ExtensibleModel { readonly account: string; readonly memberEncryptionPublicKey: Uint8Array }
77
+ export const groupMemberKeyCodec = defineCodec<GroupMemberKey>({ account: { wire: 'account', codec: text }, memberEncryptionPublicKey: { wire: 'member_encryption_public_key', codec: bytes } });
78
+ export function validateGroupMemberKey(value: GroupMemberKey): void { validateCompleteObject(groupMemberKeyCodec.encode(value)); validateAccountId(value.account); requireLength(value.memberEncryptionPublicKey, 32, 'Member encryption public key'); }
79
+ export interface GroupHistorySecret extends ExtensibleModel { readonly groupId: string; readonly epoch: number; readonly applicationSecret: Uint8Array }
80
+ export const groupHistorySecretCodec = defineCodec<GroupHistorySecret>({ groupId: { wire: 'group_id', codec: text }, epoch: { wire: 'epoch', codec: integer }, applicationSecret: { wire: 'application_secret', codec: bytes } });
81
+ export function validateGroupHistorySecret(value: GroupHistorySecret): void { validateCompleteObject(groupHistorySecretCodec.encode(value)); validateIdentifier('group', value.groupId); requireSafeInteger(value.epoch, 0); requireLength(value.applicationSecret, 32, 'Epoch application secret'); }
82
+ export interface GroupMemberPrivateState extends ExtensibleModel, GroupRef { readonly memberEncryptionPrivateKey: Uint8Array }
83
+ export const groupMemberPrivateStateCodec = defineCodec<GroupMemberPrivateState>({ groupId: { wire: 'group_id', codec: text }, relayId: { wire: 'relay_id', codec: text }, memberEncryptionPrivateKey: { wire: 'member_encryption_private_key', codec: bytes } });
84
+ export function validateGroupMemberPrivateState(value: GroupMemberPrivateState): void { validateCompleteObject(groupMemberPrivateStateCodec.encode(value)); validateGroupRef(value); requireLength(value.memberEncryptionPrivateKey, 32, 'Member encryption private key'); }
85
+ export interface AccountGroupHistorySecretSync extends ExtensibleModel { readonly secrets: readonly GroupHistorySecret[] }
86
+ export const accountGroupHistorySecretSyncCodec = defineCodec<AccountGroupHistorySecretSync>({ secrets: { wire: 'secrets', codec: array(groupHistorySecretCodec) } }, 'meshline.account.group.history_secret.sync');
87
+ export function validateAccountGroupHistorySecretSync(value: AccountGroupHistorySecretSync): void {
88
+ validateCompleteObject(accountGroupHistorySecretSyncCodec.encode(value)); if (!value.secrets.length) throw new ProtocolError('invalid_size', 'History secret synchronization requires at least one secret.');
89
+ const ids = new Set<string>(); for (const secret of value.secrets) { validateGroupHistorySecret(secret); const id = `${secret.groupId}|${secret.epoch}`;
90
+ if (ids.has(id)) throw new ProtocolError('duplicate_secret', 'History secret synchronization repeats a group and epoch.'); ids.add(id); }
91
+ }
92
+ export interface AccountGroupPrivateStateSync extends ExtensibleModel { readonly states: readonly GroupMemberPrivateState[] }
93
+ export const accountGroupPrivateStateSyncCodec = defineCodec<AccountGroupPrivateStateSync>({ states: { wire: 'states', codec: array(groupMemberPrivateStateCodec) } }, 'meshline.account.group.state.sync');
94
+ export function validateAccountGroupPrivateStateSync(value: AccountGroupPrivateStateSync): void {
95
+ validateCompleteObject(accountGroupPrivateStateSyncCodec.encode(value)); if (!value.states.length) throw new ProtocolError('invalid_size', 'Private state synchronization requires at least one state.');
96
+ const ids = new Set<string>(); for (const state of value.states) { validateGroupMemberPrivateState(state); if (ids.has(state.groupId)) throw new ProtocolError('duplicate_group', 'Private state synchronization repeats a group.'); ids.add(state.groupId); }
97
+ }
98
+ export interface AccountGroupPrivateStateRequest extends ExtensibleModel { readonly groupId?: string }
99
+ export const accountGroupPrivateStateRequestCodec = defineCodec<AccountGroupPrivateStateRequest>({ groupId: { wire: 'group_id', codec: text, optional: true } }, 'meshline.account.group.state.request');
100
+ export function validateAccountGroupPrivateStateRequest(value: AccountGroupPrivateStateRequest): void { validateCompleteObject(accountGroupPrivateStateRequestCodec.encode(value)); if (value.groupId !== undefined) validateIdentifier('group', value.groupId); }
@@ -0,0 +1,164 @@
1
+ import { requireLength, verifyAccount, verifyDevice } from '../crypto/primitives.js';
2
+ import { ProtocolError } from '../errors.js';
3
+ import { deriveDeviceId, validateIdentifier } from '../identity/identifiers.js';
4
+ import { matchesAccount, validateRelayId } from '../identity/neo.js';
5
+ import { array, bytes, defineCodec, integer, text, type ExtensibleModel } from '../protocol/codec.js';
6
+ import { NetworkContext, signingInput } from '../protocol/context.js';
7
+ import { encodeUtf8 } from '../protocol/encoding.js';
8
+ import { canonicalJson, requireSafeInteger, type JsonObject } from '../protocol/json.js';
9
+ import { validateCompleteObject } from '../protocol/validation.js';
10
+
11
+ export interface DeviceCertificate extends ExtensibleModel {
12
+ readonly account: string;
13
+ readonly accountPublicKey: Uint8Array;
14
+ readonly signingPublicKey: Uint8Array;
15
+ readonly encryptionPublicKey: Uint8Array;
16
+ readonly notBefore: number;
17
+ readonly expiresAt: number;
18
+ readonly deviceSignature: Uint8Array;
19
+ readonly accountSignature: Uint8Array;
20
+ }
21
+
22
+ export const deviceCertificateCodec = defineCodec<DeviceCertificate>({
23
+ account: { wire: 'account', codec: text },
24
+ accountPublicKey: { wire: 'account_public_key', codec: bytes },
25
+ signingPublicKey: { wire: 'signing_public_key', codec: bytes },
26
+ encryptionPublicKey: { wire: 'encryption_public_key', codec: bytes },
27
+ notBefore: { wire: 'not_before', codec: integer },
28
+ expiresAt: { wire: 'expires_at', codec: integer },
29
+ deviceSignature: { wire: 'device_signature', codec: bytes },
30
+ accountSignature: { wire: 'account_signature', codec: bytes },
31
+ }, 'meshline.device.certificate');
32
+
33
+ export function certificateId(certificate: DeviceCertificate, context: NetworkContext): string {
34
+ return deriveDeviceId(certificate.account, certificate.signingPublicKey, certificate.encryptionPublicKey, context);
35
+ }
36
+
37
+ export function certificateDeviceInput(certificate: DeviceCertificate, context: NetworkContext): Uint8Array {
38
+ return signingInput(deviceCertificateCodec.encode(certificate), context, ['device_signature', 'account_signature']);
39
+ }
40
+
41
+ export function certificateAccountInput(certificate: DeviceCertificate, context: NetworkContext): Uint8Array {
42
+ return signingInput(deviceCertificateCodec.encode(certificate), context, ['account_signature']);
43
+ }
44
+
45
+ /** Checks complete certificate integrity; current account authorization is a separate operation. */
46
+ export function validateCertificate(certificate: DeviceCertificate, context: NetworkContext): void {
47
+ validateCompleteObject(deviceCertificateCodec.encode(certificate));
48
+ requireLength(certificate.signingPublicKey, 32, 'Signing public key');
49
+ requireLength(certificate.encryptionPublicKey, 32, 'Encryption public key');
50
+ requireLength(certificate.deviceSignature, 64, 'Device signature');
51
+ requireLength(certificate.accountSignature, 64, 'Account signature');
52
+ requireSafeInteger(certificate.notBefore, 0);
53
+ requireSafeInteger(certificate.expiresAt, 0);
54
+ if (certificate.expiresAt <= certificate.notBefore || certificate.expiresAt - certificate.notBefore > 720 * 86400)
55
+ throw new ProtocolError('invalid_time', 'Certificate validity must be positive and at most 720 days.');
56
+ if (!matchesAccount(certificate.account, certificate.accountPublicKey)) throw new ProtocolError('invalid_identity', 'Certificate account does not match its public key.');
57
+ if (encodeUtf8(deviceCertificateCodec.stringify(certificate)).length > 4096) throw new ProtocolError('invalid_size', 'Certificate exceeds 4096 bytes.');
58
+ if (!verifyDevice(certificateDeviceInput(certificate, context), certificate.deviceSignature, certificate.signingPublicKey))
59
+ throw new ProtocolError('invalid_signature', 'Invalid certificate device signature.');
60
+ if (!verifyAccount(certificateAccountInput(certificate, context), certificate.accountSignature, certificate.accountPublicKey))
61
+ throw new ProtocolError('invalid_signature', 'Invalid certificate account signature.');
62
+ }
63
+
64
+ export interface AccountDeviceState extends ExtensibleModel {
65
+ readonly account: string;
66
+ readonly accountPublicKey: Uint8Array;
67
+ readonly revision: number;
68
+ readonly certificates: readonly DeviceCertificate[];
69
+ readonly accountSignature: Uint8Array;
70
+ }
71
+
72
+ export const accountDeviceStateCodec = defineCodec<AccountDeviceState>({
73
+ account: { wire: 'account', codec: text }, accountPublicKey: { wire: 'account_public_key', codec: bytes },
74
+ revision: { wire: 'revision', codec: integer }, certificates: { wire: 'certificates', codec: array(deviceCertificateCodec) },
75
+ accountSignature: { wire: 'account_signature', codec: bytes },
76
+ }, 'meshline.account.device.state');
77
+
78
+ export function deviceStateInput(state: AccountDeviceState, context: NetworkContext): Uint8Array {
79
+ return signingInput(accountDeviceStateCodec.encode(state), context, ['account_signature']);
80
+ }
81
+
82
+ export function validateDeviceState(state: AccountDeviceState, context: NetworkContext): void {
83
+ validateCompleteObject(accountDeviceStateCodec.encode(state));
84
+ requireSafeInteger(state.revision, 0);
85
+ if (!matchesAccount(state.account, state.accountPublicKey)) throw new ProtocolError('invalid_identity', 'Device state account does not match its key.');
86
+ if (state.certificates.length > 8) throw new ProtocolError('invalid_size', 'At most eight devices may be authorized.');
87
+ if (encodeUtf8(accountDeviceStateCodec.stringify(state)).length > 131072) throw new ProtocolError('invalid_size', 'Device state exceeds 131072 bytes.');
88
+ const identities = new Set<string>();
89
+ for (const certificate of state.certificates) {
90
+ validateCertificate(certificate, context);
91
+ if (certificate.account !== state.account) throw new ProtocolError('invalid_identity', 'Certificate belongs to another account.');
92
+ const id = certificateId(certificate, context);
93
+ if (identities.has(id)) throw new ProtocolError('duplicate_device', 'Duplicate device identity.');
94
+ identities.add(id);
95
+ }
96
+ if (!verifyAccount(deviceStateInput(state, context), state.accountSignature, state.accountPublicKey))
97
+ throw new ProtocolError('invalid_signature', 'Invalid device state signature.');
98
+ }
99
+
100
+ /** The state must have passed validateDeviceState before use as trusted authorization. */
101
+ export function authorizedDevice(state: AccountDeviceState, deviceId: string, context: NetworkContext, now: number): DeviceCertificate {
102
+ requireSafeInteger(now, 0);
103
+ validateIdentifier('device', deviceId);
104
+ const certificate = state.certificates.find(item => certificateId(item, context) === deviceId);
105
+ if (!certificate) throw new ProtocolError('unauthorized_device', 'Device is not registered in the account state.');
106
+ if (now < certificate.notBefore || now >= certificate.expiresAt) throw new ProtocolError('unauthorized_device', 'Device is outside its authorization window.');
107
+ return certificate;
108
+ }
109
+
110
+ export interface AccountRoute extends ExtensibleModel {
111
+ readonly account: string;
112
+ readonly accountPublicKey: Uint8Array;
113
+ readonly revision: number;
114
+ readonly relayId: string;
115
+ readonly updatedAt: number;
116
+ readonly expiresAt: number;
117
+ readonly accountSignature: Uint8Array;
118
+ readonly relaySignature?: Uint8Array;
119
+ }
120
+
121
+ export const accountRouteCodec = defineCodec<AccountRoute>({
122
+ account: { wire: 'account', codec: text }, accountPublicKey: { wire: 'account_public_key', codec: bytes },
123
+ revision: { wire: 'revision', codec: integer }, relayId: { wire: 'relay_id', codec: text },
124
+ updatedAt: { wire: 'updated_at', codec: integer }, expiresAt: { wire: 'expires_at', codec: integer },
125
+ accountSignature: { wire: 'account_signature', codec: bytes }, relaySignature: { wire: 'relay_signature', codec: bytes, optional: true },
126
+ }, 'meshline.account.route');
127
+
128
+ export function routeAccountInput(route: AccountRoute, context: NetworkContext): Uint8Array {
129
+ return signingInput(accountRouteCodec.encode(route), context, ['account_signature', 'relay_signature']);
130
+ }
131
+
132
+ export function routeRelayInput(route: AccountRoute, context: NetworkContext): Uint8Array {
133
+ return signingInput(accountRouteCodec.encode(route), context, ['relay_signature']);
134
+ }
135
+
136
+ /** Relay acknowledgement, when present, must additionally be checked against the relay key. */
137
+ export function validateRoute(route: AccountRoute, context: NetworkContext, now: number): void {
138
+ validateCompleteObject(accountRouteCodec.encode(route));
139
+ requireSafeInteger(now, 0);
140
+ requireSafeInteger(route.revision, 0);
141
+ requireSafeInteger(route.updatedAt, 0);
142
+ requireSafeInteger(route.expiresAt, 0);
143
+ validateRelayId(route.relayId);
144
+ if (route.expiresAt <= route.updatedAt || route.expiresAt - route.updatedAt > 315_360_000 || route.expiresAt <= now)
145
+ throw new ProtocolError('invalid_time', 'Route is expired or has an invalid validity window.');
146
+ if (!matchesAccount(route.account, route.accountPublicKey)) throw new ProtocolError('invalid_identity', 'Route account does not match its key.');
147
+ if (route.relaySignature !== undefined) requireLength(route.relaySignature, 64, 'Relay signature');
148
+ if (encodeUtf8(accountRouteCodec.stringify(route)).length > 4096) throw new ProtocolError('invalid_size', 'Route exceeds 4096 bytes.');
149
+ if (!verifyAccount(routeAccountInput(route, context), route.accountSignature, route.accountPublicKey))
150
+ throw new ProtocolError('invalid_signature', 'Invalid account route signature.');
151
+ }
152
+
153
+ export function compareRoutes(left: AccountRoute, right: AccountRoute): 'older' | 'newer' | 'equivalent' | 'conflict' {
154
+ if (left.account !== right.account) throw new ProtocolError('invalid_identity', 'Routes belong to different accounts.');
155
+ if (left.revision < right.revision) return 'older';
156
+ if (left.revision > right.revision) return 'newer';
157
+ const unsigned = (value: AccountRoute): JsonObject => {
158
+ const object = accountRouteCodec.encode(value);
159
+ delete object.account_signature;
160
+ delete object.relay_signature;
161
+ return object;
162
+ };
163
+ return canonicalJson(unsigned(left)) === canonicalJson(unsigned(right)) ? 'equivalent' : 'conflict';
164
+ }