@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,648 @@
1
+ import { agreeKey, devicePublicKey, encryptionPublicKey, requireLength, signDevice, verifyDevice, systemRandom } from '../crypto/primitives.js';
2
+ import { ProtocolError, StateConflictError } from '../errors.js';
3
+ import { validateIdentifier } from '../identity/identifiers.js';
4
+ import { matchesAccount, validateAccountId, validateRelayId } from '../identity/neo.js';
5
+ import { deviceStatePublishResponseCodec, validateDeviceStatePublishResponse } from '../models/device-state.js';
6
+ import { contactAuthorizationCodec, contactGrantCodec, contactGrantInput, contactRecordCodec, deviceStateQueryInput, signedDeviceStateQueryCodec, validateContactGrant, validateContactInvite, validateDeviceStateQuery, verifyContactGrant, verifyContactInvite } from '../models/contacts.js';
7
+ import { accountDeviceStateCodec, certificateAccountInput, certificateDeviceInput, certificateId, deviceCertificateCodec, deviceStateInput, validateCertificate, validateDeviceState, } from '../models/identity.js';
8
+ import { nextRevision } from '../protocol/context.js';
9
+ import { decodeBase64Url, encodeBase64Url, equalBytes } from '../protocol/encoding.js';
10
+ import { canonicalJson, requireSafeInteger } from '../protocol/json.js';
11
+ import { AsyncGate } from '../runtime/async-gate.js';
12
+ import { AsyncPulse } from '../runtime/async-pulse.js';
13
+ import { throwIfAborted } from '../runtime/clock.js';
14
+ import { EventHub } from '../runtime/events.js';
15
+ import { finishSignedRequest, putSignedRequest, readSignedRequest, requestKey } from '../storage/signed-request.js';
16
+ import { updateStore } from '../storage/transaction.js';
17
+ import { RelayError } from '../transport/relay-error.js';
18
+ import { ClientComponent } from './component.js';
19
+ const method = 'device.state.publish';
20
+ const localKey = { collection: 'local_device', key: 'current' };
21
+ const bindingKey = { collection: 'identity_binding', key: 'device' };
22
+ const stateKey = (account) => ({ collection: 'device_states', key: account });
23
+ const clone = (value) => deviceCertificateCodec.decode(deviceCertificateCodec.encode(value));
24
+ const copyState = (value) => accountDeviceStateCodec.decode(accountDeviceStateCodec.encode(value));
25
+ const fingerprint = (value) => canonicalJson(value ?? null);
26
+ function validity(seconds) { requireSafeInteger(seconds, 1); if (seconds > 720 * 86400)
27
+ throw new RangeError('Device validity cannot exceed 720 days.'); }
28
+ /** Protected local device keys and explicit account-authorized device-state publication. */
29
+ export class DeviceManager extends ClientComponent {
30
+ relayClients;
31
+ #store;
32
+ #account;
33
+ #accountSigner;
34
+ #protector;
35
+ #random;
36
+ #keyGate = new AsyncGate();
37
+ #publicationGate = new AsyncGate();
38
+ #stateGate = new AsyncGate();
39
+ #events = new EventHub();
40
+ #local;
41
+ #state;
42
+ #signingKey;
43
+ #encryptionKey;
44
+ #homeClient;
45
+ #homeListeners = [];
46
+ #refresh;
47
+ #homeRefresh = Promise.resolve();
48
+ #homePolling = Promise.resolve();
49
+ #requiredRevisions = new Map();
50
+ constructor(options) {
51
+ super(options);
52
+ for (const dependency of [options.relayClients, options.accountManager])
53
+ if (dependency.accountId !== this.accountId || dependency.context.toString() !== this.context.toString())
54
+ throw new ProtocolError('invalid_context', 'Device dependencies belong to another account or network.');
55
+ this.#store = options.store;
56
+ this.#account = options.accountManager;
57
+ this.relayClients = options.relayClients;
58
+ this.#accountSigner = options.accountSigner;
59
+ this.#protector = options.secretProtector;
60
+ this.#random = options.random ?? systemRandom;
61
+ }
62
+ get local() { return this.#local && clone(this.#local); }
63
+ get deviceState() { return this.#state && copyState(this.#state); }
64
+ get certificate() { this.ensureInitialized(); if (!this.#local)
65
+ throw new ProtocolError('device_required', 'No local device has been created.'); return clone(this.#local); }
66
+ on(event, listener) { return this.#events.on(event, listener); }
67
+ #notify(event, value) {
68
+ this.#events.notify(event, value, error => this.notifyBackgroundError({ operation: 'observer', resource: event, error }));
69
+ }
70
+ async onInitialize(signal) {
71
+ this.#account.ensureInitialized();
72
+ await this.#store.initialize({ context: this.context.toString(), accountId: this.accountId }, signal);
73
+ const snapshot = await this.#store.read([localKey, bindingKey, stateKey(this.accountId)], signal);
74
+ const local = snapshot.sets[0][0]?.value;
75
+ const binding = snapshot.sets[1][0]?.value;
76
+ if (Boolean(local) !== Boolean(binding))
77
+ throw new ProtocolError('invalid_storage', 'Device binding and protected keys must exist together.');
78
+ if (local) {
79
+ const certificate = this.#readCertificate(local);
80
+ if (binding.deviceId !== local.deviceId)
81
+ throw new ProtocolError('invalid_storage', 'Local device differs from the persisted device binding.');
82
+ this.#local = certificate;
83
+ }
84
+ const state = snapshot.sets[2][0]?.value;
85
+ if (state) {
86
+ const value = accountDeviceStateCodec.decode(state);
87
+ validateDeviceState(value, this.context);
88
+ if (value.account !== this.accountId)
89
+ throw new ProtocolError('invalid_storage', 'Stored device state belongs to another account.');
90
+ this.#state = value;
91
+ }
92
+ }
93
+ #readCertificate(record) {
94
+ if (record.certificate === undefined || typeof record.deviceId !== 'string' || typeof record.protectedSigningKey !== 'string' || typeof record.protectedEncryptionKey !== 'string')
95
+ throw new ProtocolError('invalid_storage', 'Malformed local device record.');
96
+ const certificate = deviceCertificateCodec.decode(record.certificate);
97
+ validateCertificate(certificate, this.context);
98
+ if (certificate.account !== this.accountId || certificateId(certificate, this.context) !== record.deviceId)
99
+ throw new ProtocolError('invalid_storage', 'Local device does not match its account and identifier.');
100
+ return certificate;
101
+ }
102
+ #purpose(id, kind) { return `Meshline/device-${kind}/v1/${this.context}/${this.accountId}/${id}`; }
103
+ async #protect(key, id, kind, signal) {
104
+ if (!this.#protector)
105
+ throw new ProtocolError('protector_required', 'A secret protector is required to persist local keys.');
106
+ const input = key.slice();
107
+ try {
108
+ const protectedData = await this.#protector.protect(input, this.#purpose(id, kind), signal);
109
+ throwIfAborted(signal);
110
+ if (!(protectedData instanceof Uint8Array) || protectedData.length === 0)
111
+ throw new ProtocolError('invalid_protection', 'Secret protector returned no protected data.');
112
+ return encodeBase64Url(protectedData);
113
+ }
114
+ finally {
115
+ input.fill(0);
116
+ }
117
+ }
118
+ async #issue(signingKey, encryptionKey, seconds, signal) {
119
+ const signer = this.#accountSigner;
120
+ if (!signer)
121
+ throw new ProtocolError('signer_required', 'An account signer is required to authorize a device.');
122
+ const publicKey = signer.publicKey.slice();
123
+ if (signer.accountId !== this.accountId || !matchesAccount(this.accountId, publicKey))
124
+ throw new ProtocolError('invalid_identity', 'Account signer belongs to another account.');
125
+ const now = this.clock.nowSeconds();
126
+ requireSafeInteger(now, 0);
127
+ requireSafeInteger(now + seconds, 0);
128
+ let certificate = { account: this.accountId, accountPublicKey: publicKey, signingPublicKey: devicePublicKey(signingKey), encryptionPublicKey: encryptionKey.slice(),
129
+ notBefore: now, expiresAt: now + seconds, deviceSignature: new Uint8Array(64), accountSignature: new Uint8Array(64) };
130
+ certificate = { ...certificate, deviceSignature: signDevice(certificateDeviceInput(certificate, this.context), signingKey) };
131
+ certificate = { ...certificate, accountSignature: (await signer.sign(certificateAccountInput(certificate, this.context), signal)).slice() };
132
+ throwIfAborted(signal);
133
+ validateCertificate(certificate, this.context);
134
+ return certificate;
135
+ }
136
+ async createDevice(validitySeconds, signal) {
137
+ validity(validitySeconds);
138
+ if (!this.#protector || !this.#accountSigner)
139
+ throw new ProtocolError('integration_required', 'Creating a device requires an account signer and a secret protector.');
140
+ const certificate = await this.runOperation(scope => this.#keyGate.run(async () => {
141
+ const existing = await this.#store.read([localKey, bindingKey], scope);
142
+ if (existing.sets.some(rows => rows.length))
143
+ throw new StateConflictError('A local device already exists in this database.');
144
+ let signing;
145
+ let encryption;
146
+ try {
147
+ signing = this.#random.bytes(32);
148
+ encryption = this.#random.bytes(32);
149
+ requireLength(signing, 32, 'Device signing key');
150
+ requireLength(encryption, 32, 'Device encryption key');
151
+ const certificate = await this.#issue(signing, encryptionPublicKey(encryption), validitySeconds, scope);
152
+ const id = certificateId(certificate, this.context);
153
+ const protectedSigningKey = await this.#protect(signing, id, 'signing', scope);
154
+ const protectedEncryptionKey = await this.#protect(encryption, id, 'encryption', scope);
155
+ await updateStore(this.#store, [localKey, bindingKey], snapshot => {
156
+ if (snapshot.sets.some(rows => rows.length))
157
+ throw new StateConflictError('Another writer created a local device.');
158
+ return { mutations: [{ ...localKey, kind: 'put', value: { deviceId: id, certificate: deviceCertificateCodec.encode(certificate), protectedSigningKey, protectedEncryptionKey } },
159
+ { ...bindingKey, kind: 'put', value: { deviceId: id } }], result: undefined };
160
+ }, scope);
161
+ this.#clearKeys();
162
+ this.#signingKey = signing;
163
+ this.#encryptionKey = encryption;
164
+ signing = encryption = undefined;
165
+ this.#local = certificate;
166
+ return certificate;
167
+ }
168
+ finally {
169
+ signing?.fill(0);
170
+ encryption?.fill(0);
171
+ }
172
+ }, scope), signal);
173
+ this.#notify('deviceChanged', certificateId(certificate, this.context));
174
+ return clone(certificate);
175
+ }
176
+ async renewDevice(validitySeconds, signal) {
177
+ validity(validitySeconds);
178
+ const certificate = await this.runOperation(scope => this.#keyGate.run(async () => {
179
+ const saved = (await this.#store.read([localKey], scope)).sets[0][0]?.value;
180
+ if (!saved)
181
+ throw new ProtocolError('device_required', 'No local device has been created.');
182
+ const previous = this.#readCertificate(saved);
183
+ const key = await this.#getKey('signing', scope);
184
+ const issued = await this.#issue(key, previous.encryptionPublicKey, validitySeconds, scope);
185
+ await updateStore(this.#store, [localKey], snapshot => {
186
+ if (fingerprint(snapshot.sets[0][0]?.value) !== fingerprint(saved))
187
+ throw new StateConflictError('Local certificate changed while being renewed.');
188
+ return { mutations: [{ ...localKey, kind: 'put', value: { ...saved, certificate: deviceCertificateCodec.encode(issued) } }], result: undefined };
189
+ }, scope);
190
+ this.#local = issued;
191
+ return issued;
192
+ }, scope), signal);
193
+ this.#notify('deviceChanged', certificateId(certificate, this.context));
194
+ return clone(certificate);
195
+ }
196
+ async #getKey(kind, signal) {
197
+ const cached = kind === 'signing' ? this.#signingKey : this.#encryptionKey;
198
+ if (cached)
199
+ return cached;
200
+ if (!this.#protector)
201
+ throw new ProtocolError('protector_required', 'A secret protector is required to access device keys.');
202
+ const record = (await this.#store.read([localKey], signal)).sets[0][0]?.value;
203
+ if (!record)
204
+ throw new ProtocolError('device_required', 'No local device has been created.');
205
+ const certificate = this.#readCertificate(record);
206
+ const key = await this.#protector.unprotect(decodeBase64Url(record[kind === 'signing' ? 'protectedSigningKey' : 'protectedEncryptionKey']), this.#purpose(record.deviceId, kind), signal);
207
+ try {
208
+ throwIfAborted(signal);
209
+ requireLength(key, 32, 'Unprotected device key');
210
+ const derived = kind === 'signing' ? devicePublicKey(key) : encryptionPublicKey(key);
211
+ if (!equalBytes(derived, kind === 'signing' ? certificate.signingPublicKey : certificate.encryptionPublicKey))
212
+ throw new ProtocolError('invalid_key', 'Unprotected key does not match the local certificate.');
213
+ if (kind === 'signing')
214
+ this.#signingKey = key;
215
+ else
216
+ this.#encryptionKey = key;
217
+ return key;
218
+ }
219
+ catch (error) {
220
+ if (key instanceof Uint8Array)
221
+ key.fill(0);
222
+ throw error;
223
+ }
224
+ }
225
+ sign(input, signal) {
226
+ const copy = input.slice();
227
+ return this.runOperation(scope => this.#keyGate.run(async () => { const key = await this.#getKey('signing', scope); throwIfAborted(scope); return signDevice(copy, key); }, scope), signal);
228
+ }
229
+ /** Caller owns and should erase the returned shared secret after deriving its purpose-specific key. */
230
+ deriveSharedSecret(peerPublicKey, signal) {
231
+ requireLength(peerPublicKey, 32, 'Peer X25519 key');
232
+ const copy = peerPublicKey.slice();
233
+ return this.runOperation(scope => this.#keyGate.run(async () => { const key = await this.#getKey('encryption', scope); throwIfAborted(scope); return agreeKey(key, copy); }, scope), signal);
234
+ }
235
+ getCertificate(deviceId) {
236
+ this.ensureInitialized();
237
+ validateIdentifier('device', deviceId);
238
+ const value = this.#state?.certificates.find(value => certificateId(value, this.context) === deviceId);
239
+ return value && clone(value);
240
+ }
241
+ getAuthorizationState(deviceId) {
242
+ this.ensureInitialized();
243
+ validateIdentifier('device', deviceId);
244
+ if (!this.#state)
245
+ return 'unknown';
246
+ const certificate = this.getCertificate(deviceId);
247
+ if (!certificate)
248
+ return 'notRegistered';
249
+ const now = this.clock.nowSeconds();
250
+ return now < certificate.notBefore ? 'notYetValid' : now >= certificate.expiresAt ? 'expired' : 'authorized';
251
+ }
252
+ async #home(signal) {
253
+ const route = await this.#account.getRoute(undefined, signal);
254
+ if (!route)
255
+ throw new ProtocolError('route_required', 'Establish or recover the account route first.');
256
+ return route.relayId;
257
+ }
258
+ async getDeviceState(target = this.accountId, signal) {
259
+ if (typeof target === 'string')
260
+ return this.#getState(target, undefined, signal);
261
+ const authorization = contactAuthorizationCodec.decode(contactAuthorizationCodec.encode(target));
262
+ let accountId;
263
+ if ('grantor' in authorization) {
264
+ validateContactGrant(authorization, this.clock.nowSeconds());
265
+ accountId = authorization.grantor;
266
+ if (authorization.grantee !== this.accountId)
267
+ throw new ProtocolError('invalid_authorization', 'Contact grant must authorize this account.');
268
+ }
269
+ else {
270
+ validateContactInvite(authorization, this.clock.nowSeconds());
271
+ accountId = authorization.inviter;
272
+ }
273
+ if (accountId === this.accountId)
274
+ throw new ProtocolError('invalid_authorization', 'Signed device queries cannot target the current account.');
275
+ return this.#getState(accountId, undefined, signal, authorization);
276
+ }
277
+ async getOwnDeviceState(relayId, signal) {
278
+ validateRelayId(relayId);
279
+ return this.#getState(this.accountId, relayId, signal);
280
+ }
281
+ async #getState(accountId, relayId, signal, authorization) {
282
+ validateAccountId(accountId);
283
+ let changed = false;
284
+ const state = await this.runOperation(async (scope) => {
285
+ const relay = relayId ?? await this.#home(scope);
286
+ const useAccount = accountId === this.accountId && this.#accountSigner !== undefined && (!this.#local || this.getAuthorizationState(certificateId(this.#local, this.context)) !== 'authorized');
287
+ let parameters = { account: accountId };
288
+ if (authorization) {
289
+ let query = { account: accountId, authorization, signerCertificate: this.certificate, createdAt: this.clock.nowSeconds(), deviceSignature: new Uint8Array(64) };
290
+ query = { ...query, deviceSignature: await this.sign(deviceStateQueryInput(query, this.context), scope) };
291
+ validateDeviceStateQuery(query, this.context, this.clock.nowSeconds());
292
+ parameters = signedDeviceStateQueryCodec.encode(query);
293
+ }
294
+ const read = async (account) => {
295
+ const client = await this.relayClients.get(relay, account ? { mode: 'account', signer: this.#accountSigner } : { mode: 'device', signer: this }, scope);
296
+ return client.requestHttp(authorization ? 'POST' : 'GET', 'device.state.resolve', parameters, { signal: scope });
297
+ };
298
+ let response;
299
+ try {
300
+ try {
301
+ response = await read(useAccount);
302
+ }
303
+ catch (error) {
304
+ if (!useAccount && accountId === this.accountId && this.#accountSigner && error instanceof RelayError && ['unauthorized', 'device_unknown'].includes(error.code))
305
+ response = await read(true);
306
+ else
307
+ throw error;
308
+ }
309
+ }
310
+ catch (error) {
311
+ if (error instanceof RelayError && error.code === 'not_found')
312
+ return undefined;
313
+ throw error;
314
+ }
315
+ if (response === undefined)
316
+ throw new ProtocolError('invalid_response', 'Device resolution returned no state.');
317
+ const state = accountDeviceStateCodec.decode(response);
318
+ validateDeviceState(state, this.context);
319
+ if (state.account !== accountId)
320
+ throw new ProtocolError('invalid_identity', 'Resolved device state belongs to another account.');
321
+ if (authorization) {
322
+ if ('grantor' in authorization)
323
+ verifyContactGrant(authorization, state, this.context, this.clock.nowSeconds());
324
+ else
325
+ verifyContactInvite(authorization, state, this.context, this.clock.nowSeconds());
326
+ }
327
+ changed = await this.#save(state, scope);
328
+ return state;
329
+ }, signal);
330
+ if (changed && state)
331
+ this.#notify('deviceStateChanged', copyState(state));
332
+ return state && copyState(state);
333
+ }
334
+ async publishDeviceState(relayId, options = {}) {
335
+ validateRelayId(relayId);
336
+ options = { ...options, ...(options.certificates === undefined ? {} : { certificates: options.certificates.map(clone) }),
337
+ ...(options.previousState === undefined ? {} : { previousState: copyState(options.previousState) }) };
338
+ if (options.previousState) {
339
+ validateDeviceState(options.previousState, this.context);
340
+ if (options.previousState.account !== this.accountId)
341
+ throw new ProtocolError('invalid_identity', 'Previous device state belongs to another account.');
342
+ }
343
+ if (options.revision !== undefined)
344
+ requireSafeInteger(options.revision, 0);
345
+ let changed = false;
346
+ const result = await this.runOperation(signal => this.#publicationGate.run(async () => {
347
+ const signer = this.#accountSigner;
348
+ if (!signer)
349
+ throw new ProtocolError('signer_required', 'An account signer is required to publish device state.');
350
+ const signed = await this.#prepare(relayId, options, signer, signal);
351
+ const submitted = await this.#submit(signed, signer, signal);
352
+ changed = submitted.changed;
353
+ return submitted.result;
354
+ }, signal), options.signal);
355
+ if (changed)
356
+ this.#notify('deviceStateChanged', copyState(result.deviceState));
357
+ return { ...result, deviceState: copyState(result.deviceState) };
358
+ }
359
+ async #submit(signed, signer, signal) {
360
+ const state = accountDeviceStateCodec.decode(signed.document);
361
+ const client = await this.relayClients.get(signed.relayId, { mode: 'account', signer }, signal);
362
+ let response;
363
+ try {
364
+ response = await client.requestHttp('PUT', method, signed.document, { signal });
365
+ }
366
+ catch (error) {
367
+ if (error instanceof RelayError && error.isDefinitiveRejection) {
368
+ try {
369
+ await finishSignedRequest(this.#store, method, signed);
370
+ }
371
+ catch (storageError) {
372
+ throw new AggregateError([error, storageError], 'Device-state rejection could not be persisted.');
373
+ }
374
+ }
375
+ throw error;
376
+ }
377
+ if (response === undefined)
378
+ throw new ProtocolError('invalid_response', 'Device publication returned no result.');
379
+ const receipt = deviceStatePublishResponseCodec.decode(response);
380
+ validateDeviceStatePublishResponse(receipt, this.clock.nowSeconds());
381
+ let changed = false;
382
+ if (receipt.status === 'accepted')
383
+ changed = await this.#save(state, signal);
384
+ else
385
+ await finishSignedRequest(this.#store, method, signed);
386
+ return { changed, result: { ...receipt, deviceState: state } };
387
+ }
388
+ /** Prevents removal of the last usable signature on a locally issued contact grant before replacement delivery acceptance. */
389
+ async removeDevice(deviceId, signal) {
390
+ validateIdentifier('device', deviceId);
391
+ let changed;
392
+ await this.runOperation(async (scope) => {
393
+ const relayId = await this.#home(scope);
394
+ const current = await this.#getState(this.accountId, relayId, scope);
395
+ if (!current)
396
+ throw new ProtocolError('device_state_required', 'The account has no published device state.');
397
+ const certificates = current.certificates.filter(value => certificateId(value, this.context) !== deviceId);
398
+ if (certificates.length === current.certificates.length)
399
+ return;
400
+ await this.#publicationGate.run(async () => {
401
+ const signer = this.#accountSigner;
402
+ if (!signer)
403
+ throw new ProtocolError('signer_required', 'An account signer is required to remove a device.');
404
+ const signed = await this.#prepare(relayId, { certificates, previousState: current }, signer, scope, deviceId);
405
+ const result = await this.#submit(signed, signer, scope);
406
+ if (result.result.status !== 'accepted')
407
+ throw new ProtocolError('removal_staged', 'Device removal was staged and is not yet authoritative.');
408
+ if (result.changed)
409
+ changed = result.result.deviceState;
410
+ }, scope);
411
+ }, signal);
412
+ if (changed)
413
+ this.#notify('deviceStateChanged', copyState(changed));
414
+ }
415
+ #checkRemoval(deviceId, certificates, contacts) {
416
+ const now = this.clock.nowSeconds();
417
+ for (const row of contacts) {
418
+ const contact = contactRecordCodec.decode(row.value.record);
419
+ const issued = contact.grantToContact;
420
+ if (contact.status !== 'active' || !issued || !issued.signatures[deviceId] || (issued.expiresAt ?? Infinity) <= now)
421
+ continue;
422
+ const confirmed = row.value.confirmedGrantTo === undefined ? undefined : contactGrantCodec.decode(row.value.confirmedGrantTo);
423
+ if (confirmed && (confirmed.expiresAt ?? Infinity) <= now)
424
+ continue;
425
+ if (!confirmed)
426
+ throw new ProtocolError('contact_grant_at_risk', 'Device removal would invalidate a contact grant before replacement signatures have been accepted for delivery.');
427
+ validateContactGrant(confirmed, now);
428
+ if (confirmed.grantor !== this.accountId || confirmed.grantee !== contact.account || contact.account !== row.key)
429
+ throw new ProtocolError('invalid_storage', 'Confirmed contact grant has inconsistent account binding.');
430
+ const input = contactGrantInput(confirmed, this.context);
431
+ if (!certificates.some(certificate => certificate.notBefore <= now && now < certificate.expiresAt && confirmed.signatures[certificateId(certificate, this.context)] !== undefined
432
+ && verifyDevice(input, confirmed.signatures[certificateId(certificate, this.context)], certificate.signingPublicKey)))
433
+ throw new ProtocolError('contact_grant_at_risk', 'Device removal would invalidate a contact grant before replacement signatures have been accepted for delivery.');
434
+ }
435
+ }
436
+ async #prepare(relayId, options, signer, signal, removedDevice) {
437
+ const queries = [stateKey(this.accountId), requestKey(method), ...(removedDevice ? [{ collection: 'contacts' }] : [])];
438
+ const snapshot = await this.#store.read(queries, signal);
439
+ const known = snapshot.sets[0][0]?.value;
440
+ const saved = snapshot.sets[1][0]?.value;
441
+ if (removedDevice) {
442
+ if (!known || !options.previousState || canonicalJson(known) !== accountDeviceStateCodec.stringify(options.previousState))
443
+ throw new StateConflictError('Device state changed before preparing removal.');
444
+ this.#checkRemoval(removedDevice, options.certificates, snapshot.sets[2]);
445
+ }
446
+ const signed = readSignedRequest(saved);
447
+ const knownState = known && accountDeviceStateCodec.decode(known);
448
+ const knownRevision = Math.max(knownState?.revision ?? -1, signed?.revision ?? -1, options.previousState?.revision ?? -1);
449
+ const refreshStaging = options.certificates === undefined && !options.recovery && options.revision === undefined;
450
+ let certificates = options.certificates;
451
+ if (certificates === undefined) {
452
+ let previous = options.previousState;
453
+ if (knownState && (!previous || knownState.revision > previous.revision))
454
+ previous = knownState;
455
+ if (refreshStaging && signed?.pending && signed.relayId !== relayId)
456
+ throw new StateConflictError('Resolve pending device state before transferring it to another relay.');
457
+ if (signed && signed.relayId === relayId && (!previous || signed.revision > previous.revision))
458
+ previous = accountDeviceStateCodec.decode(signed.document);
459
+ if (!previous && !options.recovery)
460
+ throw new StateConflictError('The complete device state is unavailable. Supply all certificates or recover explicitly.');
461
+ certificates = previous?.certificates ?? [];
462
+ if (this.#local) {
463
+ const local = this.#local;
464
+ const id = certificateId(local, this.context);
465
+ const registered = certificates.some(value => certificateId(value, this.context) === id);
466
+ if (!registered && !options.recovery)
467
+ throw new StateConflictError('The local device is absent from the complete device state.');
468
+ certificates = registered ? certificates.map(value => certificateId(value, this.context) === id ? local : value) : [...certificates, local];
469
+ }
470
+ }
471
+ const pending = signed?.pending ? accountDeviceStateCodec.decode(signed.document) : undefined;
472
+ if (!refreshStaging && pending && signed.relayId === relayId && (options.revision === undefined || options.revision === signed.revision)
473
+ && canonicalJson(pending.certificates.map(value => deviceCertificateCodec.encode(value))) === canonicalJson(certificates.map(value => deviceCertificateCodec.encode(value)))) {
474
+ validateDeviceState(pending, this.context);
475
+ if (pending.account !== this.accountId)
476
+ throw new ProtocolError('invalid_storage', 'Pending device state belongs to another account.');
477
+ return signed;
478
+ }
479
+ if (pending && !refreshStaging && !options.recovery && options.revision === undefined)
480
+ throw new StateConflictError('Device publication has an unknown result. Retry the original certificates or resolve current state.');
481
+ const publicKey = signer.publicKey.slice();
482
+ if (signer.accountId !== this.accountId || !matchesAccount(this.accountId, publicKey))
483
+ throw new ProtocolError('invalid_identity', 'Account signer changed identity.');
484
+ const revision = nextRevision(knownRevision, options.revision ?? (options.recovery ? Math.max(nextRevision(knownRevision), this.clock.nowSeconds() * 1000) : undefined));
485
+ let state = { account: this.accountId, accountPublicKey: publicKey, revision, certificates: certificates.map(clone), accountSignature: new Uint8Array(64) };
486
+ state = { ...state, accountSignature: (await signer.sign(deviceStateInput(state, this.context), signal)).slice() };
487
+ validateDeviceState(state, this.context);
488
+ const prepared = { relayId, revision, document: accountDeviceStateCodec.encode(state), pending: true };
489
+ await updateStore(this.#store, queries, current => {
490
+ if (fingerprint(current.sets[0][0]?.value) !== fingerprint(known) || fingerprint(current.sets[1][0]?.value) !== fingerprint(saved))
491
+ throw new StateConflictError('Device state changed while signing.');
492
+ if (removedDevice && canonicalJson(current.sets[2].map(row => ({ key: row.key, value: row.value }))) !== canonicalJson(snapshot.sets[2].map(row => ({ key: row.key, value: row.value }))))
493
+ throw new StateConflictError('Contact grants changed while signing device removal.');
494
+ return { mutations: [putSignedRequest(method, prepared)], result: undefined };
495
+ }, signal);
496
+ return prepared;
497
+ }
498
+ async #save(state, signal) {
499
+ return this.#stateGate.run(async () => {
500
+ const document = accountDeviceStateCodec.encode(state);
501
+ const json = canonicalJson(document);
502
+ await updateStore(this.#store, [stateKey(state.account), requestKey(method)], snapshot => {
503
+ const previous = snapshot.sets[0][0]?.value;
504
+ if (previous) {
505
+ const known = accountDeviceStateCodec.decode(previous);
506
+ if (state.revision < known.revision || state.revision === known.revision && json !== canonicalJson(previous))
507
+ throw new ProtocolError('stale_device_state', 'Device state is older or conflicts at the same revision.');
508
+ }
509
+ const mutations = [{ ...stateKey(state.account), kind: 'put', value: document }];
510
+ const signed = readSignedRequest(snapshot.sets[1][0]?.value);
511
+ if (state.account === this.accountId && signed?.pending && (state.revision > signed.revision || state.revision === signed.revision && json === canonicalJson(signed.document)))
512
+ mutations.push(putSignedRequest(method, { ...signed, pending: false }));
513
+ return { mutations, result: undefined };
514
+ }, signal);
515
+ if (state.account !== this.accountId)
516
+ return false;
517
+ const changed = this.#state?.revision !== state.revision;
518
+ this.#state = copyState(state);
519
+ if (this.#local && this.getAuthorizationState(certificateId(this.#local, this.context)) !== 'authorized')
520
+ await this.relayClients.invalidateDevice();
521
+ return changed;
522
+ }, signal);
523
+ }
524
+ #clearKeys() { this.#signingKey?.fill(0); this.#encryptionKey?.fill(0); this.#signingKey = this.#encryptionKey = undefined; }
525
+ async onStart(signal) {
526
+ if (!this.#local)
527
+ throw new ProtocolError('device_required', 'Create and authorize a local device before starting.');
528
+ const state = await this.#getState(this.accountId, undefined, signal);
529
+ if (!state || this.getAuthorizationState(certificateId(this.#local, this.context)) !== 'authorized')
530
+ throw new ProtocolError('unauthorized_device', 'The local device has no current published authorization.');
531
+ this.#refresh = new AsyncPulse();
532
+ this.#requiredRevisions.clear();
533
+ await this.#useHome(await this.#home(signal), signal);
534
+ const runtime = this.runtimeSignal;
535
+ this.#homeRefresh = this.#refreshHome(runtime);
536
+ this.#homePolling = this.#pollHome(runtime);
537
+ // Retain terminal observer/clock failures until stop/dispose; never create unhandled rejections.
538
+ this.#homeRefresh.catch(() => undefined);
539
+ this.#homePolling.catch(() => undefined);
540
+ this.#refresh.pulse();
541
+ }
542
+ async #useHome(relayId, signal) {
543
+ const client = await this.relayClients.get(relayId, { mode: 'device', signer: this }, signal);
544
+ if (client === this.#homeClient)
545
+ return;
546
+ const descriptor = await client.getDescriptor(signal);
547
+ const previous = this.#homeClient;
548
+ const listeners = [
549
+ client.on('notificationReceived', notification => {
550
+ if (this.#homeClient !== client || notification.method !== 'device.state.changed')
551
+ return;
552
+ const revision = notification.params?.revision;
553
+ requireSafeInteger(revision, 0);
554
+ this.#requiredRevisions.set(relayId, Math.max(revision, this.#requiredRevisions.get(relayId) ?? -1));
555
+ this.#refresh?.pulse();
556
+ }),
557
+ client.on('socketConnected', () => { if (this.#homeClient === client)
558
+ this.#refresh?.pulse(); }),
559
+ client.on('errorOccurred', error => { if (this.#homeClient === client)
560
+ this.notifyBackgroundError({ operation: 'connect', resource: relayId, error }); }),
561
+ ];
562
+ this.#homeClient = client;
563
+ try {
564
+ if (descriptor.endpoints.some(value => value.startsWith('wss://')))
565
+ client.startNotifications();
566
+ }
567
+ catch (error) {
568
+ this.#homeClient = previous;
569
+ for (const remove of listeners)
570
+ remove();
571
+ throw error;
572
+ }
573
+ for (const remove of this.#homeListeners)
574
+ remove();
575
+ this.#homeListeners = listeners;
576
+ if (previous && previous.relayId !== relayId)
577
+ this.#requiredRevisions.delete(previous.relayId);
578
+ }
579
+ async #pollHome(signal) {
580
+ try {
581
+ for (;;) {
582
+ await this.clock.delay(30_000, signal);
583
+ this.#refresh?.pulse();
584
+ }
585
+ }
586
+ catch (error) {
587
+ if (!signal.aborted)
588
+ throw error;
589
+ }
590
+ }
591
+ async #refreshHome(signal) {
592
+ try {
593
+ for (;;) {
594
+ await this.#refresh.wait(signal);
595
+ if (!this.#accountSigner && this.getAuthorizationState(certificateId(this.#local, this.context)) !== 'authorized')
596
+ continue;
597
+ try {
598
+ const relayId = await this.#home(signal);
599
+ const state = await this.#getState(this.accountId, relayId, signal);
600
+ if (!state)
601
+ throw new ProtocolError('device_state_required', 'The home relay has no published device state.');
602
+ if (this.getAuthorizationState(certificateId(this.#local, this.context)) !== 'authorized')
603
+ throw new ProtocolError('unauthorized_device', 'The local device is no longer authorized.');
604
+ if (state.revision < (this.#requiredRevisions.get(relayId) ?? -1))
605
+ throw new ProtocolError('stale_device_state', 'Resolved device state is below the announced revision.');
606
+ await this.#useHome(relayId, signal);
607
+ }
608
+ catch (error) {
609
+ throwIfAborted(signal);
610
+ this.notifyBackgroundError({ operation: 'synchronize', ...(this.#homeClient ? { resource: this.#homeClient.relayId } : {}), error });
611
+ await this.clock.delay(5000, signal);
612
+ this.#refresh?.pulse();
613
+ }
614
+ }
615
+ }
616
+ catch (error) {
617
+ if (!signal.aborted)
618
+ throw error;
619
+ }
620
+ }
621
+ async onStop() {
622
+ try {
623
+ const results = await Promise.allSettled([this.#homeRefresh, this.#homePolling]);
624
+ const errors = results.filter((result) => result.status === 'rejected').map(result => result.reason);
625
+ if (errors.length)
626
+ throw new AggregateError(errors, 'Device background processing failed.');
627
+ }
628
+ finally {
629
+ for (const remove of this.#homeListeners)
630
+ remove();
631
+ this.#homeListeners = [];
632
+ this.#homeClient = undefined;
633
+ this.#refresh = undefined;
634
+ this.#requiredRevisions.clear();
635
+ }
636
+ }
637
+ async onDispose() {
638
+ try {
639
+ if (this.#local)
640
+ await this.relayClients.invalidateDevice();
641
+ }
642
+ finally {
643
+ this.#clearKeys();
644
+ this.#events.clear();
645
+ }
646
+ }
647
+ }
648
+ //# sourceMappingURL=device.js.map