@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,933 @@
1
+ import { encryptGroupMessage, groupClientSecretCommitment, GroupKeyAccessError, sealGroupClientSecret } from '../crypto/groups.js';
2
+ import { requireLength, systemRandom, verifyDevice } from '../crypto/primitives.js';
3
+ import { ProtocolError, StateConflictError } from '../errors.js';
4
+ import { GroupMessageProcessor } from '../groups/messages.js';
5
+ import { GroupAccountSync } from '../groups/account-sync.js';
6
+ import { GroupOperations, groupOperationMethods, groupOperationPayload } from '../groups/operations.js';
7
+ import { decodeGroupProjection, GroupRepository, groupKey } from '../groups/repository.js';
8
+ import { GroupSecrets } from '../groups/secrets.js';
9
+ import { applyGroupEvent } from '../groups/state.js';
10
+ import { createIdentifier, deriveResourceId, validateIdentifier } from '../identity/identifiers.js';
11
+ import { validateAccountId, validateRelayId } from '../identity/neo.js';
12
+ import { snapshotReader } from '../messages/repository.js';
13
+ import { groupKeyPageCodec, groupRotationPrepareRequestCodec, groupRotationPrepareResultCodec, validateGroupRotationPrepareRequest, validateGroupRotationPrepareResult } from '../models/group-keys.js';
14
+ import { groupChangedNotificationCodec, groupInvitePageCodec, groupInviteQueryCodec, groupInviteResolveResultCodec, groupListQueryCodec, groupTimelineChangedNotificationCodec, validateGroupChangedNotification, validateGroupInvitePage, validateGroupInviteQuery, validateGroupInviteResolveResult, validateGroupListQuery, validateGroupTimelineChangedNotification } from '../models/group-admission.js';
15
+ import * as admission from '../models/group-admission.js';
16
+ import { groupApplicationCodec, groupCreateRequestCodec, groupEventCodec, groupInviteCodec, groupInviteInput, groupManagementInput, groupManagementPayloadCodec, groupMemberRecoveryRequestCodec, groupSequenceQueryCodec, groupStateCodec, groupSyncPageCodec, validateGroupApplication, validateGroupInvite, validateGroupManagementPayload, validateGroupMemberRecoveryRequest, validateGroupState, verifyGroupApplication, verifyGroupInvite, verifyGroupManagementPayload, verifyGroupMemberRecoveryRequest } from '../models/group-management.js';
17
+ import { groupMessageCodec, groupMessageEnvelopeCodec, groupMemberNicknameUpdateCodec, validateGroupPayload, validateGroupRef } from '../models/groups.js';
18
+ import { certificateId } from '../models/identity.js';
19
+ import { signingInput } from '../protocol/context.js';
20
+ import { canonicalJson, requireObject, requireSafeInteger } from '../protocol/json.js';
21
+ import { AsyncGate } from '../runtime/async-gate.js';
22
+ import { AsyncPulse } from '../runtime/async-pulse.js';
23
+ import { throwIfAborted } from '../runtime/clock.js';
24
+ import { EventHub } from '../runtime/events.js';
25
+ import { RelayError } from '../transport/relay-error.js';
26
+ import { ClientComponent } from './component.js';
27
+ function ordinal(left, right) { return left < right ? -1 : left > right ? 1 : 0; }
28
+ /** Verified history determines authority; local secrets and exact signed submissions survive restart. */
29
+ export class GroupManager extends ClientComponent {
30
+ #store;
31
+ #pool;
32
+ #device;
33
+ #random;
34
+ #repository;
35
+ #secrets;
36
+ #messages;
37
+ #operations;
38
+ #accountMessages;
39
+ #accountSync;
40
+ #detachMessages;
41
+ #writes = new AsyncGate();
42
+ #events = new EventHub();
43
+ #subscriptions = new Map();
44
+ #pulse;
45
+ #jobs = [];
46
+ constructor(options) {
47
+ super(options);
48
+ for (const dependency of [options.relayClients, options.deviceManager, options.messageManager])
49
+ if (dependency.accountId !== this.accountId || dependency.context.toString() !== this.context.toString())
50
+ throw new ProtocolError('invalid_context', 'Group dependencies belong to another network or account.');
51
+ this.#store = options.store;
52
+ this.#pool = options.relayClients;
53
+ this.#device = options.deviceManager;
54
+ this.#random = options.random ?? systemRandom;
55
+ this.#repository = new GroupRepository(this.#store, this.context, this.accountId);
56
+ this.#secrets = new GroupSecrets({ store: this.#store, context: this.context, accountId: this.accountId, deviceId: () => certificateId(this.#device.certificate, this.context), protector: options.secretProtector });
57
+ this.#messages = new GroupMessageProcessor(this.#store, this.context, this.accountId, this.#secrets);
58
+ this.#operations = new GroupOperations(this.#store, this.context, this.accountId);
59
+ this.#accountMessages = options.messageManager;
60
+ this.#accountSync = new GroupAccountSync({ store: this.#store, context: this.context, accountId: this.accountId, clock: this.clock, device: this.#device, messages: this.#accountMessages, secrets: this.#secrets,
61
+ rejected: (messageId, error) => this.notifyBackgroundError({ operation: 'synchronize_group_account', resource: messageId, error }) });
62
+ }
63
+ on(event, listener) { return this.#events.on(event, listener); }
64
+ #notify(event, value) { this.#events.notify(event, value, error => this.notifyBackgroundError({ operation: 'observer', resource: event, error })); }
65
+ async onInitialize(signal) { this.#device.ensureInitialized(); this.#accountMessages.ensureInitialized(); await this.#store.initialize({ context: this.context.toString(), accountId: this.accountId }, signal); }
66
+ async #hosting(relayId, signal) {
67
+ validateRelayId(relayId);
68
+ const relay = await this.#pool.get(relayId, { mode: 'device', signer: this.#device }, signal);
69
+ if (!(await relay.getDescriptor(signal)).capabilities?.includes('group.host.v1'))
70
+ throw new ProtocolError('unsupported_capability', 'Relay does not host groups.');
71
+ return relay;
72
+ }
73
+ #snapshot(group, row) {
74
+ if (row.relayId !== group.relayId)
75
+ throw new ProtocolError('invalid_binding', 'Group belongs to another hosting relay.');
76
+ if (!row.projection) {
77
+ if (!row.preview)
78
+ return undefined;
79
+ const preview = groupStateCodec.decode(row.preview);
80
+ validateGroupState(preview);
81
+ if (preview.groupId !== group.groupId)
82
+ throw new ProtocolError('invalid_storage', 'Stored preview belongs to another group.');
83
+ return { ref: group, group: preview, membership: row.pendingApplication === true ? 'pending' : 'unknown' };
84
+ }
85
+ const projection = decodeGroupProjection(requireObject(row.projection), group);
86
+ const member = projection.members.find(value => value.account === this.accountId);
87
+ const departure = projection.departures[this.accountId];
88
+ let membership = member ? 'member' : departure === 'not_member' ? 'notMember' : departure ?? 'notMember';
89
+ if (!member && row.pendingApplication === true && membership !== 'banned')
90
+ membership = 'pending';
91
+ if (row.localDepartureAfter !== undefined) {
92
+ requireSafeInteger(row.localDepartureAfter, 0);
93
+ if (projection.sequence <= row.localDepartureAfter)
94
+ membership = 'left';
95
+ }
96
+ return { ref: { ...group }, group: row.locallyClosed === true ? { ...projection.state, status: 'closed' } : projection.state, membership, ...(membership === 'member' ? { role: member.role } : {}) };
97
+ }
98
+ async #info(group, signal) {
99
+ const row = (await this.#store.read([groupKey(group)], signal)).sets[0][0]?.value;
100
+ const result = row && this.#snapshot(group, row);
101
+ if (!result)
102
+ throw new ProtocolError('unverified_group', 'Group history has not established a verified state.');
103
+ return result;
104
+ }
105
+ async #writable(group, signal) {
106
+ const info = await this.#info(group, signal);
107
+ if (info.group.status !== 'active' || info.membership !== 'member')
108
+ throw new ProtocolError('forbidden', 'Group is not writable by this account.');
109
+ return (await this.#repository.get(group, signal));
110
+ }
111
+ async #sign(source, signal) {
112
+ const payload = groupManagementPayloadCodec.decode(groupManagementPayloadCodec.encode(source));
113
+ validateGroupManagementPayload(payload);
114
+ const certificate = this.#device.certificate;
115
+ const id = certificateId(certificate, this.context);
116
+ const signature = await this.#device.sign(groupManagementInput(payload, this.context), signal);
117
+ throwIfAborted(signal);
118
+ if (id !== certificateId(this.#device.certificate, this.context))
119
+ throw new StateConflictError('Group signing device changed during signing.');
120
+ const result = { ...payload, value: { ...payload.value, deviceSignature: signature } };
121
+ verifyGroupManagementPayload(result, certificate, this.context);
122
+ return result;
123
+ }
124
+ async #signRequest(source, codec, signal) {
125
+ const copy = codec.decode(codec.encode(source));
126
+ const certificate = this.#device.certificate;
127
+ const id = certificateId(certificate, this.context);
128
+ const input = signingInput(codec.encode(copy), this.context, ['device_signature']);
129
+ const signature = await this.#device.sign(input, signal);
130
+ throwIfAborted(signal);
131
+ if (id !== certificateId(this.#device.certificate, this.context) || !verifyDevice(input, signature, certificate.signingPublicKey))
132
+ throw new StateConflictError('Local member-request signer changed or returned an invalid signature.');
133
+ return { ...copy, deviceSignature: signature };
134
+ }
135
+ #checkManagement(state, payload) {
136
+ const advances = ['applicationApproval', 'memberRecoveryApproval', 'memberLeave', 'memberRemoval', 'secretRotation'].includes(payload.kind)
137
+ || payload.kind === 'memberBan' && payload.value.accounts.some(account => state.members.some(member => member.account === account));
138
+ applyGroupEvent(state, { sequence: state.sequence + 1, epoch: state.epoch + (advances ? 1 : 0), acceptedAt: this.clock.nowSeconds(), signerDeviceId: certificateId(this.#device.certificate, this.context), payload: requireObject(groupManagementPayloadCodec.encode(payload)) }, this.#device.certificate, state, this.context);
139
+ }
140
+ #operationChanged(operation) {
141
+ if (operation.method.startsWith('group.application.'))
142
+ this.#notify('applicationsChanged', operation.group);
143
+ if (operation.method.startsWith('group.member.recovery.'))
144
+ this.#notify('keyRecoveryChanged', operation.group);
145
+ }
146
+ async #localRecovery(group, signal) {
147
+ const recovery = await this.#secrets.derivePending(group, this.#device, signal);
148
+ for (const failure of recovery.rejected)
149
+ this.notifyBackgroundError({ operation: 'recover_group_key', resource: `${group.groupId}/${failure.epoch}`, error: failure.error });
150
+ await this.#messages.processPending(group, (sequence, result) => {
151
+ if (result.error)
152
+ this.notifyBackgroundError({ operation: 'decrypt_group', resource: `${group.groupId}/${sequence}`, error: result.error });
153
+ if (result.groupRecord)
154
+ this.#notify('groupChanged', { ...this.#snapshot(group, result.groupRecord), kinds: ['nickname'] });
155
+ this.#notify('timelineChanged', { group, sequence, ...(result.message ? { message: result.message } : {}) });
156
+ }, signal);
157
+ }
158
+ async #synchronize(relay, group, signal, includeKeys = true) {
159
+ let projection;
160
+ for (;;) {
161
+ const page = await this.#repository.synchronizePage(group, { read: async (query) => groupSyncPageCodec.decode((await relay.requestHttp('GET', 'group.sync', groupSequenceQueryCodec.encode(query), { signal }))) }, signal);
162
+ projection = page.projection;
163
+ for (const failure of page.rejected)
164
+ this.notifyBackgroundError({ operation: 'synchronize_group', resource: `${group.groupId}/${failure.sequence}`, error: failure.error });
165
+ if (page.change)
166
+ this.#notify('groupChanged', { ...this.#snapshot(group, page.change.record), kinds: page.change.kinds });
167
+ if (!page.hasMore)
168
+ break;
169
+ }
170
+ if (!includeKeys || !projection)
171
+ return;
172
+ if (projection.members.some(member => member.account === this.accountId)) {
173
+ let after = await this.#repository.keyProgress(group, signal);
174
+ while (after < projection.epoch) {
175
+ const page = groupKeyPageCodec.decode((await relay.requestHttp('GET', 'group.key.sync', groupSequenceQueryCodec.encode({ groupId: group.groupId, after }), { signal })));
176
+ await this.#repository.saveKeyPage(group, page, after, signal);
177
+ if (!page.hasMore)
178
+ break;
179
+ after = page.keys.at(-1).epoch;
180
+ }
181
+ }
182
+ await this.#localRecovery(group, signal);
183
+ await this.#accountSync.shareCurrent(group, signal);
184
+ }
185
+ async #execute(relay, initial, recovering, signal) {
186
+ let operation = initial;
187
+ const timelinePayload = groupOperationPayload(operation);
188
+ const unconfirmedMemberRequest = operation.method === 'group.application.submit' && !operation.accepted
189
+ || operation.method === 'group.member.recovery.submit' && operation.acceptedResult === undefined;
190
+ if (recovering && unconfirmedMemberRequest) {
191
+ let approved = await this.#operations.acceptedMemberRequest(operation, signal);
192
+ if (!approved) {
193
+ try {
194
+ await this.#synchronize(relay, operation.group, signal, false);
195
+ }
196
+ catch (error) {
197
+ if (!(error instanceof RelayError && ['forbidden', 'not_found'].includes(error.code)))
198
+ throw error;
199
+ }
200
+ approved = await this.#operations.acceptedMemberRequest(operation, signal);
201
+ }
202
+ if (approved) {
203
+ // The signed history already owns membership, including any later departure
204
+ // or key replacement. Completing the request must not mark it pending again.
205
+ await this.#operations.complete(operation);
206
+ this.#operationChanged(operation);
207
+ return undefined;
208
+ }
209
+ }
210
+ if (recovering && operation.method === 'group.invite.create' && !operation.accepted) {
211
+ const expected = groupInviteCodec.decode(operation.request);
212
+ try {
213
+ const existing = await this.#readInvite({ group: operation.group, inviteId: expected.inviteId }, signal);
214
+ if (groupInviteCodec.stringify(existing.invite.document) !== groupInviteCodec.stringify(expected))
215
+ throw new StateConflictError('Invitation identifier is occupied by different signed content.');
216
+ await this.#operations.complete(operation);
217
+ return undefined;
218
+ }
219
+ catch (error) {
220
+ if (!(error instanceof RelayError && error.code === 'not_found'))
221
+ throw error;
222
+ }
223
+ }
224
+ if (recovering && timelinePayload) {
225
+ try {
226
+ await this.#synchronize(relay, operation.group, signal, false);
227
+ }
228
+ catch (error) {
229
+ const noHistory = error instanceof RelayError && (error.code === 'not_found' && operation.method === 'group.create' || error.code === 'forbidden' && ['group.close', 'group.member.leave'].includes(operation.method));
230
+ if (!noHistory)
231
+ throw error;
232
+ }
233
+ }
234
+ let evidence = await this.#operations.acceptedEvent(operation, signal);
235
+ if (!evidence && !operation.accepted) {
236
+ let result;
237
+ try {
238
+ result = await relay.requestHttp(groupOperationMethods[operation.method], operation.method, operation.request, { signal });
239
+ }
240
+ catch (error) {
241
+ if (!recovering && error instanceof RelayError && error.isDefinitiveRejection)
242
+ await this.#operations.complete(operation);
243
+ throw error;
244
+ }
245
+ let sequence;
246
+ if (operation.method === 'group.message.send') {
247
+ try {
248
+ sequence = requireObject(result).sequence;
249
+ requireSafeInteger(sequence, 1);
250
+ }
251
+ catch (error) {
252
+ await this.#operations.acknowledge(operation);
253
+ throw error;
254
+ }
255
+ }
256
+ let acceptedResult;
257
+ if (operation.method === 'group.member.recovery.submit') {
258
+ try {
259
+ const value = admission.groupRecoverySubmitResultCodec.decode(result);
260
+ admission.validateGroupRecoverySubmitResult(value);
261
+ acceptedResult = admission.groupRecoverySubmitResultCodec.encode(value);
262
+ }
263
+ catch (error) {
264
+ await this.#operations.acknowledge(operation);
265
+ throw error;
266
+ }
267
+ }
268
+ operation = await this.#operations.acknowledge(operation, sequence, acceptedResult);
269
+ }
270
+ if (['group.close', 'group.member.leave'].includes(operation.method)) {
271
+ const record = await this.#operations.complete(operation, true);
272
+ if (record)
273
+ this.#notify('groupChanged', { ...this.#snapshot(operation.group, record), kinds: [operation.method === 'group.close' ? 'status' : 'members'] });
274
+ return undefined;
275
+ }
276
+ if (!timelinePayload) {
277
+ if (operation.method === 'group.member.recovery.submit' && operation.acceptedResult === undefined)
278
+ throw new ProtocolError('unconfirmed_recovery', 'Accepted recovery request has no valid persisted acceptance interval.');
279
+ const record = await this.#operations.complete(operation, operation.method === 'group.application.submit');
280
+ this.#operationChanged(operation);
281
+ if (record && operation.method === 'group.application.submit')
282
+ this.#notify('groupChanged', { ...this.#snapshot(operation.group, record), kinds: ['members'] });
283
+ return operation.acceptedResult;
284
+ }
285
+ await this.#synchronize(relay, operation.group, signal);
286
+ evidence = await this.#operations.acceptedEvent(operation, signal);
287
+ if (!evidence)
288
+ throw new ProtocolError('unconfirmed_operation', 'Accepted group operation is absent from verified history.');
289
+ if (operation.method === 'group.message.send') {
290
+ if (evidence.record.rejection !== undefined)
291
+ throw new ProtocolError('rejected_message', 'Accepted group message failed local content validation.');
292
+ if (evidence.record.decryptedPayload === undefined)
293
+ throw new GroupKeyAccessError('Accepted group message awaits an available epoch key.');
294
+ }
295
+ await this.#operations.complete(operation, true);
296
+ this.#operationChanged(operation);
297
+ return evidence.record;
298
+ }
299
+ createGroup(relayId, options, signal) {
300
+ validateRelayId(relayId);
301
+ const copy = { ...options };
302
+ return this.runOperation(scope => this.#writes.run(async () => {
303
+ const relay = await this.#hosting(relayId, scope);
304
+ const limit = (await relay.getInfo(scope)).limits.maxGroupMembers;
305
+ if (copy.memberCapacity > limit)
306
+ throw new ProtocolError('invalid_capacity', 'Group capacity exceeds the hosting relay limit.');
307
+ const nonce = this.#random.bytes(16);
308
+ requireLength(nonce, 16, 'Group nonce');
309
+ const group = { relayId, groupId: deriveResourceId('group', this.accountId, relayId, nonce, this.context) };
310
+ const privateKey = this.#random.bytes(32);
311
+ let secret;
312
+ try {
313
+ secret = this.#random.bytes(32);
314
+ const member = await this.#secrets.prepareMemberState({ ...group, memberEncryptionPrivateKey: privateKey }, scope);
315
+ const commitment = groupClientSecretCommitment(group.groupId, secret, this.context);
316
+ const signed = await this.#sign({ kind: 'create', value: { ...copy, invitePolicy: copy.invitePolicy ?? 'administrators', groupId: group.groupId, nonce,
317
+ owner: { account: this.accountId, memberEncryptionPublicKey: member.publicKey }, clientSecretCommitment: commitment, deviceSignature: new Uint8Array(64) } }, scope);
318
+ const request = groupCreateRequestCodec.encode({ create: signed.value, clientSecretBox: sealGroupClientSecret({ groupId: group.groupId, account: this.accountId, memberEncryptionPublicKey: member.publicKey, clientSecretCommitment: commitment }, secret, this.context, this.#random) });
319
+ privateKey.fill(0);
320
+ secret.fill(0);
321
+ if (member.deviceId !== certificateId(this.#device.certificate, this.context))
322
+ throw new GroupKeyAccessError('Local group key protection device changed.');
323
+ const operation = { group, method: 'group.create', request };
324
+ await this.#operations.save(operation, member, scope);
325
+ this.#pulse?.pulse();
326
+ await this.#execute(relay, operation, false, scope);
327
+ return this.#info(group, scope);
328
+ }
329
+ finally {
330
+ privateKey.fill(0);
331
+ secret?.fill(0);
332
+ }
333
+ }, scope), signal);
334
+ }
335
+ /** Invitation previews validate local fields and recipient; the host checks its accepted invitation by ID. Preview data grants no membership or key authority. */
336
+ getGroup(source, signal) {
337
+ if ('document' in source) {
338
+ const invitation = this.#copyInvitation(source);
339
+ return this.runOperation(scope => this.#writes.run(async () => { const preview = await this.#previewInvitation(invitation, scope); await this.#repository.savePreview(invitation.group, preview.state, scope); return this.#info(invitation.group, scope); }, scope), signal);
340
+ }
341
+ const group = { ...source };
342
+ validateGroupRef(group);
343
+ return this.runOperation(scope => this.#writes.run(async () => { await this.#synchronize(await this.#hosting(group.relayId, scope), group, scope); return this.#info(group, scope); }, scope), signal);
344
+ }
345
+ #manage(group, method, create, signal) {
346
+ group = { ...group };
347
+ validateGroupRef(group);
348
+ return this.runOperation(scope => this.#writes.run(async () => {
349
+ await this.#operations.ensureAvailable(group, method, scope);
350
+ const relay = await this.#hosting(group.relayId, scope);
351
+ await this.#synchronize(relay, group, scope);
352
+ const state = await this.#writable(group, scope);
353
+ const payload = await this.#sign(create(state), scope);
354
+ this.#checkManagement(state, payload);
355
+ const operation = { group, method, request: requireObject(groupManagementPayloadCodec.encode(payload)) };
356
+ await this.#operations.save(operation, undefined, scope);
357
+ this.#pulse?.pulse();
358
+ await this.#execute(relay, operation, false, scope);
359
+ return this.#info(group, scope);
360
+ }, scope), signal);
361
+ }
362
+ updateGroup(group, update, signal) { const copy = { ...update }; return this.#manage(group, 'group.update', state => ({ kind: 'update', value: { ...copy, groupId: state.groupId, prevHash: state.managementHash, deviceSignature: new Uint8Array(64) } }), signal); }
363
+ async closeGroup(group, signal) { await this.#manage(group, 'group.close', state => ({ kind: 'close', value: { groupId: state.groupId, prevHash: state.managementHash, deviceSignature: new Uint8Array(64) } }), signal); }
364
+ async leaveGroup(group, signal) { await this.#manage(group, 'group.member.leave', state => ({ kind: 'memberLeave', value: { groupId: state.groupId, prevHash: state.managementHash, account: this.accountId, deviceSignature: new Uint8Array(64) } }), signal); }
365
+ async removeMembers(group, accounts, signal) { const copy = [...accounts]; await this.#manage(group, 'group.member.remove', state => ({ kind: 'memberRemoval', value: { groupId: state.groupId, prevHash: state.managementHash, accounts: copy, deviceSignature: new Uint8Array(64) } }), signal); }
366
+ async ban(group, accounts, signal) { const copy = [...accounts]; await this.#manage(group, 'group.member.ban', state => ({ kind: 'memberBan', value: { groupId: state.groupId, prevHash: state.managementHash, accounts: copy, deviceSignature: new Uint8Array(64) } }), signal); }
367
+ async unban(group, accounts, signal) { const copy = [...accounts]; await this.#manage(group, 'group.member.unban', state => ({ kind: 'memberUnban', value: { groupId: state.groupId, prevHash: state.managementHash, accounts: copy, deviceSignature: new Uint8Array(64) } }), signal); }
368
+ async setRole(group, account, role, signal) { await this.#manage(group, 'group.role.update', state => ({ kind: 'roleUpdate', value: { groupId: state.groupId, prevHash: state.managementHash, account, role, deviceSignature: new Uint8Array(64) } }), signal); }
369
+ async transferOwnership(group, account, signal) { await this.#manage(group, 'group.owner.transfer', state => ({ kind: 'ownerTransfer', value: { groupId: state.groupId, prevHash: state.managementHash, newOwnerAccount: account, deviceSignature: new Uint8Array(64) } }), signal); }
370
+ createInvite(group, options, signal) {
371
+ group = { ...group };
372
+ validateGroupRef(group);
373
+ const copy = { ...options };
374
+ return this.runOperation(scope => this.#writes.run(async () => {
375
+ await this.#operations.ensureAvailable(group, 'group.invite.create', scope);
376
+ const relay = await this.#hosting(group.relayId, scope);
377
+ await this.#synchronize(relay, group, scope, false);
378
+ const state = await this.#writable(group, scope);
379
+ const role = state.members.find(member => member.account === this.accountId).role;
380
+ if (role === 'member' && (state.state.invitePolicy === 'administrators' || copy.invitee === undefined && state.state.invitePolicy !== 'members_shareable'))
381
+ throw new ProtocolError('forbidden', 'Current membership cannot issue this group invitation.');
382
+ const now = this.clock.nowSeconds();
383
+ const limits = (await relay.getInfo(scope)).limits;
384
+ if (copy.expiresAt - now > limits.maxGroupInviteTtl)
385
+ throw new ProtocolError('invalid_expiry', 'Invitation lifetime exceeds the hosting relay limit.');
386
+ let invite = { ...copy, groupId: group.groupId, inviteId: createIdentifier('invite', this.#random), inviter: this.accountId, createdAt: now, deviceSignature: new Uint8Array(64) };
387
+ validateGroupInvite(invite, now);
388
+ const certificate = this.#device.certificate;
389
+ const id = certificateId(certificate, this.context);
390
+ invite = { ...invite, deviceSignature: await this.#device.sign(groupInviteInput(invite, this.context), scope) };
391
+ if (id !== certificateId(this.#device.certificate, this.context))
392
+ throw new StateConflictError('Local invitation signer changed during signing.');
393
+ verifyGroupInvite(invite, certificate, this.context, now);
394
+ const operation = { group, method: 'group.invite.create', request: groupInviteCodec.encode(invite) };
395
+ await this.#operations.save(operation, undefined, scope);
396
+ this.#pulse?.pulse();
397
+ await this.#execute(relay, operation, false, scope);
398
+ return { group, document: invite };
399
+ }, scope), signal);
400
+ }
401
+ async #readInvite(reference, signal) {
402
+ const query = { groupId: reference.group.groupId, inviteId: reference.inviteId };
403
+ validateGroupInviteQuery(query);
404
+ const relay = await this.#hosting(reference.group.relayId, signal);
405
+ const result = groupInviteResolveResultCodec.decode((await relay.requestHttp('GET', 'group.invite.resolve', groupInviteQueryCodec.encode(query), { signal })));
406
+ validateGroupInviteResolveResult(result, query, this.context, this.clock.nowSeconds());
407
+ return { invite: { group: reference.group, document: result.invite }, uses: result.uses };
408
+ }
409
+ getInvite(reference, signal) {
410
+ reference = { group: { ...reference.group }, inviteId: reference.inviteId };
411
+ validateGroupRef(reference.group);
412
+ validateIdentifier('invite', reference.inviteId);
413
+ return this.runOperation(scope => this.#readInvite(reference, scope), signal);
414
+ }
415
+ getInvites(group, page = {}, signal) {
416
+ group = { ...group };
417
+ validateGroupRef(group);
418
+ const query = { ...page, groupId: group.groupId };
419
+ validateGroupListQuery(query);
420
+ return this.runOperation(async (scope) => {
421
+ const relay = await this.#hosting(group.relayId, scope);
422
+ const result = groupInvitePageCodec.decode((await relay.requestHttp('GET', 'group.invite.list', groupListQueryCodec.encode(query), { signal: scope })));
423
+ validateGroupInvitePage(result, query, this.context, this.clock.nowSeconds());
424
+ return { items: result.invites.map(entry => ({ invite: { group, document: entry.invite }, uses: entry.uses })), ...(result.next === undefined ? {} : { nextCursor: result.next }) };
425
+ }, signal);
426
+ }
427
+ revokeInvite(reference, signal) {
428
+ reference = { group: { ...reference.group }, inviteId: reference.inviteId };
429
+ validateGroupRef(reference.group);
430
+ const query = { groupId: reference.group.groupId, inviteId: reference.inviteId };
431
+ validateGroupInviteQuery(query);
432
+ return this.runOperation(scope => this.#writes.run(async () => {
433
+ await this.#operations.ensureAvailable(reference.group, 'group.invite.revoke', scope);
434
+ const relay = await this.#hosting(reference.group.relayId, scope);
435
+ const operation = { group: reference.group, method: 'group.invite.revoke', request: groupInviteQueryCodec.encode(query) };
436
+ await this.#operations.save(operation, undefined, scope);
437
+ this.#pulse?.pulse();
438
+ await this.#execute(relay, operation, false, scope);
439
+ }, scope), signal);
440
+ }
441
+ #copyInvitation(value) {
442
+ const copy = { group: { ...value.group }, document: groupInviteCodec.decode(groupInviteCodec.encode(value.document)) };
443
+ validateGroupRef(copy.group);
444
+ validateGroupInvite(copy.document, this.clock.nowSeconds());
445
+ if (copy.group.groupId !== copy.document.groupId)
446
+ throw new ProtocolError('invalid_binding', 'Invitation belongs to another group.');
447
+ return copy;
448
+ }
449
+ async #previewInvitation(invite, signal) {
450
+ if (invite.document.invitee !== undefined && invite.document.invitee !== this.accountId)
451
+ throw new ProtocolError('invalid_invitation', 'Invitation targets another account.');
452
+ // Invitation-record reads require membership. The host checks its accepted invitation,
453
+ // including remaining uses and revocation; this preview does not verify the supplied signature.
454
+ const relay = await this.#hosting(invite.group.relayId, signal);
455
+ const state = groupStateCodec.decode((await relay.requestHttp('GET', 'group.resolve', admission.groupResolveQueryCodec.encode({ groupId: invite.group.groupId, inviteId: invite.document.inviteId }), { signal })));
456
+ validateGroupState(state);
457
+ if (state.groupId !== invite.group.groupId)
458
+ throw new ProtocolError('invalid_binding', 'Relay preview belongs to another group.');
459
+ return { state };
460
+ }
461
+ applyToGroup(source, signal) {
462
+ const invite = this.#copyInvitation(source);
463
+ const group = invite.group;
464
+ return this.runOperation(scope => this.#writes.run(async () => {
465
+ await this.#operations.ensureAvailable(group, 'group.application.submit', scope);
466
+ const preview = await this.#previewInvitation(invite, scope);
467
+ if (preview.state.status !== 'active')
468
+ throw new ProtocolError('invalid_invitation', 'Group is closed.');
469
+ const relay = await this.#hosting(group.relayId, scope);
470
+ const privateKey = this.#random.bytes(32);
471
+ try {
472
+ const member = await this.#secrets.prepareMemberState({ ...group, memberEncryptionPrivateKey: privateKey }, scope);
473
+ privateKey.fill(0);
474
+ const basis = { groupId: group.groupId, account: this.accountId, inviteId: invite.document.inviteId, memberEncryptionPublicKey: member.publicKey, deviceSignature: new Uint8Array(64) };
475
+ validateGroupApplication(basis);
476
+ const application = await this.#signRequest(basis, groupApplicationCodec, scope);
477
+ verifyGroupApplication(application, this.#device.certificate, this.context);
478
+ if (member.deviceId !== certificateId(this.#device.certificate, this.context))
479
+ throw new GroupKeyAccessError('Local admission key protection device changed.');
480
+ const operation = { group, method: 'group.application.submit', request: groupApplicationCodec.encode(application) };
481
+ await this.#operations.save(operation, member, scope, preview.state);
482
+ this.#pulse?.pulse();
483
+ await this.#execute(relay, operation, false, scope);
484
+ }
485
+ finally {
486
+ privateKey.fill(0);
487
+ }
488
+ }, scope), signal);
489
+ }
490
+ async #readApplications(group, page, signal) {
491
+ const query = { ...page, groupId: group.groupId };
492
+ validateGroupListQuery(query);
493
+ const relay = await this.#hosting(group.relayId, signal);
494
+ const result = admission.groupApplicationPageCodec.decode((await relay.requestHttp('GET', 'group.application.list', groupListQueryCodec.encode(query), { signal })));
495
+ admission.validateGroupApplicationPage(result, query, this.context);
496
+ return { items: result.applications.map(value => ({ ...value, group })), ...(result.next === undefined ? {} : { nextCursor: result.next }) };
497
+ }
498
+ getApplications(group, page = {}, signal) {
499
+ group = { ...group };
500
+ validateGroupRef(group);
501
+ const copy = { ...page };
502
+ return this.runOperation(scope => this.#readApplications(group, copy, scope), signal);
503
+ }
504
+ async #readRecoveries(group, page, signal) {
505
+ const query = { ...page, groupId: group.groupId };
506
+ validateGroupListQuery(query);
507
+ const relay = await this.#hosting(group.relayId, signal);
508
+ const result = admission.groupRecoveryPageCodec.decode((await relay.requestHttp('GET', 'group.member.recovery.list', groupListQueryCodec.encode(query), { signal })));
509
+ admission.validateGroupRecoveryPage(result, query, this.context);
510
+ return { items: result.requests.map(value => ({ ...value, group })), ...(result.next === undefined ? {} : { nextCursor: result.next }) };
511
+ }
512
+ getKeyRecoveryRequests(group, page = {}, signal) {
513
+ group = { ...group };
514
+ validateGroupRef(group);
515
+ const copy = { ...page };
516
+ return this.runOperation(scope => this.#readRecoveries(group, copy, scope), signal);
517
+ }
518
+ requestKeyRecovery(group, signal) {
519
+ group = { ...group };
520
+ validateGroupRef(group);
521
+ return this.runOperation(scope => this.#writes.run(async () => {
522
+ await this.#operations.ensureAvailable(group, 'group.member.recovery.submit', scope);
523
+ const relay = await this.#hosting(group.relayId, scope);
524
+ await this.#synchronize(relay, group, scope, false);
525
+ await this.#writable(group, scope);
526
+ const privateKey = this.#random.bytes(32);
527
+ try {
528
+ const member = await this.#secrets.prepareMemberState({ ...group, memberEncryptionPrivateKey: privateKey }, scope);
529
+ privateKey.fill(0);
530
+ const basis = { groupId: group.groupId, account: this.accountId, memberEncryptionPublicKey: member.publicKey, deviceSignature: new Uint8Array(64) };
531
+ validateGroupMemberRecoveryRequest(basis);
532
+ const request = await this.#signRequest(basis, groupMemberRecoveryRequestCodec, scope);
533
+ const signerCertificate = this.#device.certificate;
534
+ verifyGroupMemberRecoveryRequest(request, signerCertificate, this.context);
535
+ if (member.deviceId !== certificateId(signerCertificate, this.context))
536
+ throw new GroupKeyAccessError('Local recovery key protection device changed.');
537
+ const operation = { group, method: 'group.member.recovery.submit', request: groupMemberRecoveryRequestCodec.encode(request) };
538
+ await this.#operations.save(operation, member, scope);
539
+ this.#pulse?.pulse();
540
+ const result = admission.groupRecoverySubmitResultCodec.decode((await this.#execute(relay, operation, false, scope)));
541
+ return { group, request, signerCertificate, ...result };
542
+ }
543
+ finally {
544
+ privateKey.fill(0);
545
+ }
546
+ }, scope), signal);
547
+ }
548
+ #rejectRequests(group, accounts, recovery, signal) {
549
+ group = { ...group };
550
+ validateGroupRef(group);
551
+ const request = { groupId: group.groupId, accounts: [...accounts] };
552
+ admission.validateGroupAccountsRequest(request);
553
+ const method = recovery ? 'group.member.recovery.reject' : 'group.application.reject';
554
+ return this.runOperation(scope => this.#writes.run(async () => {
555
+ await this.#operations.ensureAvailable(group, method, scope);
556
+ const relay = await this.#hosting(group.relayId, scope);
557
+ const operation = { group, method, request: admission.groupAccountsRequestCodec.encode(request) };
558
+ await this.#operations.save(operation, undefined, scope);
559
+ this.#pulse?.pulse();
560
+ await this.#execute(relay, operation, false, scope);
561
+ }, scope), signal);
562
+ }
563
+ rejectApplications(group, accounts, signal) { return this.#rejectRequests(group, accounts, false, signal); }
564
+ rejectKeyRecovery(group, accounts, signal) { return this.#rejectRequests(group, accounts, true, signal); }
565
+ withdrawKeyRecovery(group, signal) { return this.rejectKeyRecovery(group, [this.accountId], signal); }
566
+ #approve(group, accounts, recovery, signal) {
567
+ group = { ...group };
568
+ validateGroupRef(group);
569
+ const copy = [...accounts];
570
+ admission.validateGroupAccountsRequest({ groupId: group.groupId, accounts: copy });
571
+ const method = recovery ? 'group.member.recovery.approve' : 'group.application.approve';
572
+ return this.runOperation(scope => this.#writes.run(async () => {
573
+ await this.#operations.ensureAvailable(group, method, scope);
574
+ const relay = await this.#hosting(group.relayId, scope);
575
+ await this.#synchronize(relay, group, scope);
576
+ const state = await this.#writable(group, scope);
577
+ const selected = new Map();
578
+ const targets = new Set(copy);
579
+ const seenAccounts = new Set();
580
+ const cursors = new Set();
581
+ let cursor;
582
+ const invitations = new Map();
583
+ do {
584
+ const page = recovery ? await this.#readRecoveries(group, cursor === undefined ? {} : { cursor }, scope) : await this.#readApplications(group, cursor === undefined ? {} : { cursor }, scope);
585
+ for (const item of page.items) {
586
+ const request = 'request' in item ? item.request : item.application;
587
+ if (seenAccounts.has(request.account))
588
+ throw new ProtocolError('invalid_pagination', 'Request account repeats across group pages.');
589
+ seenAccounts.add(request.account);
590
+ if (!targets.has(request.account))
591
+ continue;
592
+ if ('request' in item) {
593
+ if (item.expiresAt <= this.clock.nowSeconds())
594
+ throw new ProtocolError('expired_recovery', 'Selected member recovery request has expired.');
595
+ }
596
+ else {
597
+ let chosen = invitations.get(item.application.inviteId);
598
+ if (!chosen) {
599
+ chosen = { value: await this.#readInvite({ group, inviteId: item.application.inviteId }, scope), selected: 0 };
600
+ invitations.set(item.application.inviteId, chosen);
601
+ }
602
+ const invite = chosen.value.invite.document;
603
+ if (invite.invitee !== undefined && invite.invitee !== request.account)
604
+ throw new ProtocolError('invalid_invitation', 'Application invitation targets another account.');
605
+ chosen.selected++;
606
+ const maximum = invite.invitee === undefined ? invite.maxUses : 1;
607
+ if (maximum !== undefined && chosen.value.uses + chosen.selected > maximum)
608
+ throw new ProtocolError('invalid_invitation', 'Selected applications exceed the remaining invitation uses.');
609
+ }
610
+ selected.set(request.account, { account: request.account, memberEncryptionPublicKey: request.memberEncryptionPublicKey });
611
+ }
612
+ cursor = page.nextCursor;
613
+ if (cursor !== undefined) {
614
+ if (cursors.has(cursor))
615
+ throw new ProtocolError('invalid_pagination', 'Group request list repeated a continuation cursor.');
616
+ cursors.add(cursor);
617
+ }
618
+ } while (cursor !== undefined && selected.size < targets.size);
619
+ if (selected.size !== targets.size)
620
+ throw new StateConflictError('At least one selected group request is missing or changed during pagination.');
621
+ const basis = { groupId: group.groupId, prevHash: state.managementHash, members: copy.map(account => selected.get(account)), deviceSignature: new Uint8Array(64) };
622
+ const payload = await this.#sign(recovery ? { kind: 'memberRecoveryApproval', value: basis } : { kind: 'applicationApproval', value: basis }, scope);
623
+ this.#checkManagement(state, payload);
624
+ const secret = await this.#secrets.readClientSecret(group, state.clientSecretCommitment, scope);
625
+ if (!secret)
626
+ throw new GroupKeyAccessError('Current client group secret is unavailable for approval.');
627
+ let operation;
628
+ try {
629
+ const boxes = Object.create(null);
630
+ for (const member of basis.members)
631
+ boxes[member.account] = sealGroupClientSecret({ groupId: group.groupId, account: member.account, memberEncryptionPublicKey: member.memberEncryptionPublicKey, clientSecretCommitment: state.clientSecretCommitment }, secret, this.context, this.#random);
632
+ const value = { approval: payload.value, clientSecretCommitment: state.clientSecretCommitment, clientSecretBoxes: boxes };
633
+ const request = recovery ? admission.groupRecoveryApproveRequestCodec.encode(value) : admission.groupApplicationApproveRequestCodec.encode(value);
634
+ operation = { group, method, request };
635
+ await this.#operations.save(operation, undefined, scope);
636
+ this.#pulse?.pulse();
637
+ }
638
+ finally {
639
+ secret.fill(0);
640
+ }
641
+ await this.#execute(relay, operation, false, scope);
642
+ }, scope), signal);
643
+ }
644
+ approveApplications(group, accounts, signal) { return this.#approve(group, accounts, false, signal); }
645
+ approveKeyRecovery(group, accounts, signal) { return this.#approve(group, accounts, true, signal); }
646
+ rotateSecret(group, options = {}, signal) {
647
+ group = { ...group };
648
+ validateGroupRef(group);
649
+ const rotateOwnerMemberKey = options.rotateOwnerMemberKey ?? false;
650
+ return this.runOperation(scope => this.#writes.run(async () => {
651
+ await this.#operations.ensureAvailable(group, 'group.secret.rotation.commit', scope);
652
+ const relay = await this.#hosting(group.relayId, scope);
653
+ await this.#synchronize(relay, group, scope);
654
+ const state = await this.#writable(group, scope);
655
+ if (state.state.owner !== this.accountId)
656
+ throw new ProtocolError('forbidden', 'Only the current owner can rotate the client group secret.');
657
+ const row = (await this.#store.read([groupKey(group)], scope)).sets[0][0].value;
658
+ const ownerSequence = row.ownerSequence ?? 0;
659
+ requireSafeInteger(ownerSequence, 0);
660
+ let rotation = await this.#secrets.rotation(group, scope);
661
+ if (rotation && (rotation.baseCommitment !== state.clientSecretCommitment || rotation.ownerSequence !== ownerSequence || rotation.expiresAt !== undefined && rotation.expiresAt <= this.clock.nowSeconds())) {
662
+ await this.#secrets.discardRotation(rotation, scope);
663
+ rotation = undefined;
664
+ }
665
+ if (rotation && Boolean(rotation.ownerPublicKey) !== rotateOwnerMemberKey)
666
+ throw new StateConflictError('Retry the original owner-key choice while this rotation is staged.');
667
+ let secret;
668
+ try {
669
+ if (rotation)
670
+ secret = await this.#secrets.readRotationSecret(rotation, scope);
671
+ else {
672
+ let member;
673
+ if (rotateOwnerMemberKey) {
674
+ const privateKey = this.#random.bytes(32);
675
+ try {
676
+ member = await this.#secrets.prepareMemberState({ ...group, memberEncryptionPrivateKey: privateKey }, scope);
677
+ }
678
+ finally {
679
+ privateKey.fill(0);
680
+ }
681
+ }
682
+ secret = this.#random.bytes(32);
683
+ rotation = await this.#secrets.beginRotation(group, state.clientSecretCommitment, secret, member, scope);
684
+ }
685
+ const members = [...state.members].sort((a, b) => a.account < b.account ? -1 : a.account > b.account ? 1 : 0);
686
+ for (let offset = 0; offset < members.length; offset += 64) {
687
+ const batch = members.slice(offset, offset + 64);
688
+ const boxes = Object.create(null);
689
+ for (const member of batch)
690
+ boxes[member.account] = sealGroupClientSecret({ groupId: group.groupId, account: member.account, memberEncryptionPublicKey: member.account === this.accountId && rotation.ownerPublicKey ? rotation.ownerPublicKey : member.memberEncryptionPublicKey, clientSecretCommitment: rotation.commitment }, secret, this.context, this.#random);
691
+ const request = { groupId: group.groupId, baseCommitment: rotation.baseCommitment, clientSecretCommitment: rotation.commitment, clientSecretBoxes: boxes };
692
+ validateGroupRotationPrepareRequest(request);
693
+ const result = groupRotationPrepareResultCodec.decode((await relay.requestHttp('PATCH', 'group.secret.rotation.prepare', groupRotationPrepareRequestCodec.encode(request), { signal: scope })));
694
+ validateGroupRotationPrepareResult(result, this.clock.nowSeconds());
695
+ if (result.prepared < batch.length)
696
+ throw new ProtocolError('invalid_preparation', 'Relay did not retain all submitted rotation boxes.');
697
+ rotation = await this.#secrets.acknowledgeRotation(rotation, result.expiresAt);
698
+ }
699
+ secret.fill(0);
700
+ const payload = await this.#sign({ kind: 'secretRotation', value: { groupId: group.groupId, prevHash: state.managementHash, clientSecretCommitment: rotation.commitment, ...(rotation.ownerPublicKey ? { ownerEncryptionPublicKey: rotation.ownerPublicKey } : {}), deviceSignature: new Uint8Array(64) } }, scope);
701
+ this.#checkManagement(state, payload);
702
+ const operation = { group, method: 'group.secret.rotation.commit', request: requireObject(groupManagementPayloadCodec.encode(payload)) };
703
+ await this.#operations.save(operation, undefined, scope);
704
+ this.#pulse?.pulse();
705
+ await this.#execute(relay, operation, false, scope);
706
+ }
707
+ finally {
708
+ secret?.fill(0);
709
+ }
710
+ }, scope), signal);
711
+ }
712
+ #messageInfo(group, record) {
713
+ if (!record.isMessage || !record.decryptedPayload)
714
+ return undefined;
715
+ const event = groupEventCodec.decode(record.event);
716
+ const envelope = groupMessageEnvelopeCodec.decode(event.payload);
717
+ const sender = requireObject(record.message);
718
+ return { ...groupMessageCodec.decode(record.decryptedPayload), group, sequence: event.sequence, messageId: envelope.messageId, sender: String(sender.sender), senderDeviceId: event.signerDeviceId, createdAt: envelope.createdAt, acceptedAt: event.acceptedAt };
719
+ }
720
+ #sendPayload(group, payload, signal) {
721
+ group = { ...group };
722
+ validateGroupRef(group);
723
+ validateGroupPayload(payload);
724
+ return this.runOperation(scope => this.#writes.run(async () => {
725
+ await this.#operations.ensureAvailable(group, 'group.message.send', scope);
726
+ const relay = await this.#hosting(group.relayId, scope);
727
+ await this.#synchronize(relay, group, scope);
728
+ const state = await this.#writable(group, scope);
729
+ const reply = payload.reply_to_seq;
730
+ if (reply !== undefined && (typeof reply !== 'number' || reply > state.sequence))
731
+ throw new ProtocolError('invalid_reply', 'Group reply must refer to preceding verified history.');
732
+ const secret = await this.#secrets.readApplicationSecret(group, state.epoch, scope);
733
+ if (!secret)
734
+ throw new GroupKeyAccessError('Current group epoch key is unavailable.');
735
+ let request;
736
+ try {
737
+ request = groupMessageEnvelopeCodec.encode(await encryptGroupMessage({ context: this.context, signer: this.#device, groupId: group.groupId, epoch: state.epoch,
738
+ messageId: createIdentifier('message', this.#random), createdAt: this.clock.nowSeconds(), applicationSecret: secret, payload, random: this.#random, signal: scope }));
739
+ }
740
+ finally {
741
+ secret.fill(0);
742
+ }
743
+ const operation = { group, method: 'group.message.send', request };
744
+ await this.#operations.save(operation, undefined, scope);
745
+ this.#pulse?.pulse();
746
+ const record = (await this.#execute(relay, operation, false, scope));
747
+ if (canonicalJson(record.decryptedPayload) !== canonicalJson(payload))
748
+ throw new ProtocolError('conflicting_message', 'Accepted group message differs from the submitted content.');
749
+ return this.#messageInfo(group, record);
750
+ }, scope), signal);
751
+ }
752
+ async sendMessage(group, draft, signal) { return (await this.#sendPayload(group, groupMessageCodec.encode(draft), signal)); }
753
+ async setNickname(group, nickname, signal) { await this.#sendPayload(group, groupMemberNicknameUpdateCodec.encode({ nickname }), signal); }
754
+ getGroups(filter = {}, signal) {
755
+ const copy = { ...filter };
756
+ if (copy.relayId !== undefined)
757
+ validateRelayId(copy.relayId);
758
+ if (copy.membership !== undefined && !['unknown', 'notMember', 'pending', 'member', 'left', 'removed', 'banned'].includes(copy.membership) || copy.role !== undefined && !['owner', 'administrator', 'member'].includes(copy.role))
759
+ throw new TypeError('Invalid group membership or role filter.');
760
+ return this.runOperation(async (scope) => {
761
+ const rows = (await this.#store.read([{ collection: 'groups' }], scope)).sets[0];
762
+ return snapshotReader(rows.map(row => this.#snapshot({ groupId: row.key, relayId: String(row.value.relayId) }, row.value)).filter((value) => !!value && (!copy.membership || copy.membership === value.membership) && (!copy.role || copy.role === value.role) && (!copy.relayId || copy.relayId === value.ref.relayId)));
763
+ }, signal);
764
+ }
765
+ getMembers(group, filter = {}, signal) {
766
+ group = { ...group };
767
+ validateGroupRef(group);
768
+ const copy = { ...filter };
769
+ if (copy.role !== undefined && !['owner', 'administrator', 'member'].includes(copy.role))
770
+ throw new TypeError('Invalid group role filter.');
771
+ return this.runOperation(async (scope) => snapshotReader((await this.#repository.get(group, scope))?.members.filter(member => (!copy.role || copy.role === member.role) && (!copy.search || member.account.includes(copy.search) || member.nickname?.includes(copy.search))).map(member => ({ accountId: member.account, role: member.role, memberEncryptionPublicKey: member.memberEncryptionPublicKey, ...(member.nickname === undefined ? {} : { nickname: member.nickname }) })).sort((a, b) => ordinal(a.accountId, b.accountId)) ?? []), signal);
772
+ }
773
+ getBans(group, search, signal) { group = { ...group }; validateGroupRef(group); return this.runOperation(async (scope) => snapshotReader((await this.#repository.get(group, scope))?.bans.filter(account => !search || account.includes(search)).sort() ?? []), signal); }
774
+ getMessages(filter = {}, signal) {
775
+ const copy = { ...filter };
776
+ if (copy.groupId !== undefined)
777
+ validateIdentifier('group', copy.groupId);
778
+ if (copy.sender !== undefined)
779
+ validateAccountId(copy.sender);
780
+ return this.runOperation(async (scope) => {
781
+ const snapshot = await this.#store.read([{ collection: 'groups' }, { collection: 'group_events', ...(copy.groupId ? { prefix: `${copy.groupId}|` } : {}) }], scope);
782
+ const relays = new Map(snapshot.sets[0].map(row => [row.key, String(row.value.relayId)]));
783
+ const messages = snapshot.sets[1].flatMap(row => { const groupId = row.key.split('|')[0]; const relayId = relays.get(groupId); if (!relayId)
784
+ throw new ProtocolError('invalid_storage', 'Stored group event has no hosting association.'); const message = this.#messageInfo({ groupId, relayId }, row.value); return message && (!copy.sender || copy.sender === message.sender) ? [message] : []; });
785
+ return snapshotReader(messages.sort((a, b) => a.createdAt - b.createdAt || ordinal(a.group.groupId, b.group.groupId) || a.sequence - b.sequence));
786
+ }, signal);
787
+ }
788
+ async onStart(signal) {
789
+ void this.#device.certificate;
790
+ this.#pulse = new AsyncPulse();
791
+ this.#detachMessages = this.#accountMessages.on('timelineChanged', () => this.#pulse?.pulse());
792
+ await this.#accountSync.request(signal);
793
+ this.#jobs = [this.#refresh(this.runtimeSignal), this.#poll(this.runtimeSignal)];
794
+ this.#pulse.pulse();
795
+ }
796
+ async #poll(signal) { try {
797
+ for (;;) {
798
+ await this.clock.delay(30000, signal);
799
+ this.#pulse?.pulse();
800
+ }
801
+ }
802
+ catch (error) {
803
+ if (!signal.aborted)
804
+ throw error;
805
+ } }
806
+ async #subscribe(client, groups, signal) {
807
+ if (!(await client.getDescriptor(signal)).endpoints.some(value => value.startsWith('wss://')))
808
+ return;
809
+ const selected = groups.slice(0, (await client.getInfo(signal)).limits.maxGroupSubscriptions ?? 0);
810
+ let observed = this.#subscriptions.get(client.relayId);
811
+ if (observed?.client !== client) {
812
+ if (observed) {
813
+ for (const detach of observed.detach)
814
+ detach();
815
+ await observed.subscription.dispose();
816
+ }
817
+ const subscription = client.createSubscription('group.subscribe', { group_ids: [] }, { signal, onSubscribed: () => { this.#pulse?.pulse(); }, onError: error => this.notifyBackgroundError({ operation: 'subscribe_group', resource: client.relayId, error }) });
818
+ observed = { client, subscription, selected: new Set(), detach: [] };
819
+ this.#subscriptions.set(client.relayId, observed);
820
+ const owner = observed;
821
+ owner.detach.push(client.on('notificationReceived', notification => {
822
+ if (!['group.timeline.changed', 'group.application.changed', 'group.member.recovery.changed'].includes(notification.method) || !owner.selected.size)
823
+ return;
824
+ const value = groupChangedNotificationCodec.decode(notification.params);
825
+ validateGroupChangedNotification(value);
826
+ if (notification.method === 'group.timeline.changed')
827
+ validateGroupTimelineChangedNotification(groupTimelineChangedNotificationCodec.decode(notification.params));
828
+ if (!owner.selected.has(value.groupId))
829
+ return;
830
+ const group = { groupId: value.groupId, relayId: client.relayId };
831
+ if (notification.method === 'group.application.changed')
832
+ this.#notify('applicationsChanged', group);
833
+ else if (notification.method === 'group.member.recovery.changed')
834
+ this.#notify('keyRecoveryChanged', group);
835
+ else
836
+ this.#pulse?.pulse();
837
+ }), client.on('socketConnected', () => { this.#pulse?.pulse(); }), client.on('errorOccurred', error => this.notifyBackgroundError({ operation: 'connect_group', resource: client.relayId, error })));
838
+ }
839
+ observed.selected = new Set(selected);
840
+ observed.subscription.update({ group_ids: selected });
841
+ }
842
+ async #refresh(signal) {
843
+ try {
844
+ for (;;) {
845
+ await this.#pulse.wait(signal);
846
+ try {
847
+ await this.#writes.run(() => this.#accountSync.consume(signal), signal);
848
+ await this.#writes.run(async () => {
849
+ for (const operation of await this.#operations.all(signal)) {
850
+ try {
851
+ await this.#execute(await this.#hosting(operation.group.relayId, signal), operation, true, signal);
852
+ }
853
+ catch (error) {
854
+ throwIfAborted(signal);
855
+ this.notifyBackgroundError({ operation: 'recover_group_operation', resource: operation.group.groupId, error });
856
+ }
857
+ }
858
+ }, signal);
859
+ const rows = (await this.#store.read([{ collection: 'groups' }], signal)).sets[0];
860
+ const selected = new Map();
861
+ for (const row of rows) {
862
+ const group = { groupId: row.key, relayId: String(row.value.relayId) };
863
+ const info = this.#snapshot(group, row.value);
864
+ try {
865
+ await this.#writes.run(() => this.#localRecovery(group, signal), signal);
866
+ }
867
+ catch (error) {
868
+ throwIfAborted(signal);
869
+ this.notifyBackgroundError({ operation: 'recover_group_local', resource: group.groupId, error });
870
+ }
871
+ if (info?.group.status === 'closed' || info && !['member', 'pending', 'unknown'].includes(info.membership))
872
+ continue;
873
+ const groups = selected.get(group.relayId) ?? [];
874
+ groups.push(group);
875
+ selected.set(group.relayId, groups);
876
+ }
877
+ for (const [id, observed] of this.#subscriptions)
878
+ if (!selected.has(id)) {
879
+ observed.selected.clear();
880
+ observed.subscription.update({ group_ids: [] });
881
+ }
882
+ for (const [relayId, groups] of selected) {
883
+ try {
884
+ const relay = await this.#hosting(relayId, signal);
885
+ try {
886
+ await this.#subscribe(relay, groups.filter(group => { const row = rows.find(value => value.key === group.groupId); return this.#snapshot(group, row.value)?.membership === 'member'; }).map(group => group.groupId), signal);
887
+ }
888
+ catch (error) {
889
+ throwIfAborted(signal);
890
+ this.notifyBackgroundError({ operation: 'subscribe_group', resource: relayId, error });
891
+ }
892
+ for (const group of groups) {
893
+ try {
894
+ await this.#writes.run(() => this.#synchronize(relay, group, signal), signal);
895
+ }
896
+ catch (error) {
897
+ throwIfAborted(signal);
898
+ this.notifyBackgroundError({ operation: 'synchronize_group', resource: group.groupId, error });
899
+ }
900
+ }
901
+ }
902
+ catch (error) {
903
+ throwIfAborted(signal);
904
+ this.notifyBackgroundError({ operation: 'connect_group', resource: relayId, error });
905
+ }
906
+ }
907
+ }
908
+ catch (error) {
909
+ throwIfAborted(signal);
910
+ this.notifyBackgroundError({ operation: 'refresh_groups', error });
911
+ }
912
+ }
913
+ }
914
+ catch (error) {
915
+ if (!signal.aborted)
916
+ throw error;
917
+ }
918
+ }
919
+ async onStop() { this.#detachMessages?.(); this.#detachMessages = undefined; try {
920
+ await Promise.all(this.#jobs);
921
+ }
922
+ finally {
923
+ for (const observed of this.#subscriptions.values())
924
+ for (const detach of observed.detach)
925
+ detach();
926
+ await Promise.all([...this.#subscriptions.values()].map(observed => observed.subscription.dispose()));
927
+ this.#subscriptions.clear();
928
+ this.#jobs = [];
929
+ this.#pulse = undefined;
930
+ } }
931
+ async onDispose() { this.#events.clear(); }
932
+ }
933
+ //# sourceMappingURL=group.js.map