@cero-base/cero 1.19.0 → 2.0.0

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 (40) hide show
  1. package/README.md +38 -643
  2. package/package.json +9 -6
  3. package/src/build/index.js +21 -50
  4. package/src/build/internal.js +121 -0
  5. package/src/build/schemas.js +2 -8
  6. package/src/extensions/handle-sync.js +3 -10
  7. package/src/extensions/index.js +6 -0
  8. package/src/extensions/profile-sync.js +0 -5
  9. package/src/handle/index.js +241 -266
  10. package/src/index.js +20 -51
  11. package/src/lib/bluetooth.js +25 -56
  12. package/src/lib/constants.js +0 -15
  13. package/src/lib/operators.js +22 -72
  14. package/src/lib/peek.js +4 -8
  15. package/src/lib/refs.js +2 -5
  16. package/src/lib/spec.js +2 -3
  17. package/src/local/index.js +2 -3
  18. package/src/rpc/client.js +20 -34
  19. package/src/rpc/index.js +3 -3
  20. package/src/rpc/server.js +23 -35
  21. package/types/build/index.d.ts +1 -9
  22. package/types/build/{builtins.d.ts → internal.d.ts} +20 -38
  23. package/types/extensions/handle-sync.d.ts +2 -7
  24. package/types/extensions/index.d.ts +22 -0
  25. package/types/extensions/profile-sync.d.ts +0 -4
  26. package/types/handle/index.d.ts +83 -101
  27. package/types/index.d.ts +4 -8
  28. package/types/lib/bluetooth.d.ts +8 -32
  29. package/types/lib/constants.d.ts +0 -11
  30. package/types/lib/operators.d.ts +18 -46
  31. package/types/lib/peek.d.ts +2 -3
  32. package/types/lib/refs.d.ts +1 -3
  33. package/types/lib/spec.d.ts +2 -3
  34. package/types/local/index.d.ts +2 -3
  35. package/types/rpc/client.d.ts +10 -18
  36. package/types/rpc/index.d.ts +3 -3
  37. package/types/rpc/server.d.ts +6 -9
  38. package/src/build/builtins.js +0 -174
  39. package/src/lib/internal.js +0 -9
  40. package/types/lib/internal.d.ts +0 -24
@@ -87,10 +87,6 @@ export { Ref } from '../lib/refs.js'
87
87
 
88
88
  /**
89
89
  * A cero handle — a single writable database session attached to a swarm.
90
- * The "root" handle is the user's facade; child handles (created via
91
- * `_create`/`_join`/`_load`) live under it and share the same identity,
92
- * network and corestore. Ref properties (`profile`, `members`, ...) are
93
- * attached dynamically per the schema; child handles also carry a `name`.
94
90
  */
95
91
  export class Handle extends ReadyResource {
96
92
  /** @param {HandleOpts} [opts] */
@@ -150,6 +146,78 @@ export class Handle extends ReadyResource {
150
146
  this._wantsPair = opts.pair !== false
151
147
  }
152
148
 
149
+ /**
150
+ * An `AbortSignal` that fires when this handle closes.
151
+ *
152
+ * @returns {AbortSignal}
153
+ */
154
+ get signal() {
155
+ if (!this._ac) {
156
+ this._ac = new AbortController()
157
+ if (this.closed) this._ac.abort()
158
+ else this.once('close', () => this._ac.abort())
159
+ }
160
+ return this._ac.signal
161
+ }
162
+
163
+ /** The top-most handle in the parent chain — itself for a root handle. */
164
+ get root() {
165
+ let h = this
166
+ while (h.parent) h = h.parent
167
+ return h
168
+ }
169
+
170
+ /**
171
+ * Lazily-built file server for this identity. Root-only — child handles
172
+ * reach it through `this.root.fileServer`.
173
+ *
174
+ * @returns {FileServer}
175
+ */
176
+ get fileServer() {
177
+ if (this.parent) return this.root.fileServer
178
+ if (!this._fileServer) {
179
+ this._fileServer = new FileServer({
180
+ store: this.store.store,
181
+ resolve: (coreKey, info) => this._resolveCore(coreKey, info)
182
+ })
183
+ }
184
+ return this._fileServer
185
+ }
186
+
187
+ /**
188
+ * Lazily-built blob store for THIS handle's writing device, at the current rotation epoch.
189
+ *
190
+ * @returns {Blobs}
191
+ */
192
+ get blobs() {
193
+ const stamp = this.store.keyring.current
194
+ if (!stamp) return this._baseBlobs()
195
+ let blobs = this._epochBlobs?.get(stamp)
196
+ if (!blobs) {
197
+ const entropy = this.store.keyring.entropy(stamp)
198
+ blobs = this._makeBlobs(`blobs-${stamp}`, blobEpochKey(entropy), stamp)
199
+ ;(this._epochBlobs ??= new Map()).set(stamp, blobs)
200
+ }
201
+ return blobs
202
+ }
203
+
204
+ /** Canonical id — identity id for the root handle, store key for children. */
205
+ get id() {
206
+ if (!this.parent) return this.identity.id
207
+ return this.store?.key ? hid.encode(this.store.key) : null
208
+ }
209
+
210
+ /** This device's id + name. `null` on child handles. */
211
+ get device() {
212
+ if (this.parent) return null
213
+ const k = this.store?.writerKey
214
+ return k ? { id: hid.encode(k), name: this._opts.name || null } : null
215
+ }
216
+
217
+ get suspended() {
218
+ return this._sus?.suspended === true
219
+ }
220
+
153
221
  async _open() {
154
222
  await this.store.ready()
155
223
  if (this._wantsPair) {
@@ -163,8 +231,7 @@ export class Handle extends ReadyResource {
163
231
  }
164
232
  })
165
233
  await this.pair.ready()
166
- // serve invites persisted by any member, and keep the set in step as rows
167
- // replicate in or disappear (minted, revoked or consumed elsewhere)
234
+ // serve invites persisted by any member, in step with the rows
168
235
  await this._syncInvites().catch(safetyCatch)
169
236
  this._invitesSync = () => this._syncInvites().catch(safetyCatch)
170
237
  this.store.on('update', this._invitesSync)
@@ -216,9 +283,9 @@ export class Handle extends ReadyResource {
216
283
  }
217
284
 
218
285
  /**
219
- * Tie a destroyable resource (a `watch` stream, a timer, any `{ destroy }`)
220
- * to this handle's lifecycle — it's destroyed automatically on close, so
221
- * callers don't track cleanup. De-registers itself if destroyed earlier.
286
+ * Tie a destroyable resource (a `watch` stream, a timer, any `{ destroy }`) to this
287
+ * handle's lifecycle — it's destroyed automatically on close, so callers don't track
288
+ * cleanup.
222
289
  *
223
290
  * @template {{ destroy?: Function, once?: Function }} T
224
291
  * @param {T} resource
@@ -234,22 +301,6 @@ export class Handle extends ReadyResource {
234
301
  return resource
235
302
  }
236
303
 
237
- /**
238
- * An `AbortSignal` that fires when this handle closes. Pass it as
239
- * `{ signal }` to `on`/`after`/`before`/`watch` to drop a subscription on
240
- * close — or use your own `AbortController` for a finer scope.
241
- *
242
- * @returns {AbortSignal}
243
- */
244
- get signal() {
245
- if (!this._ac) {
246
- this._ac = new AbortController()
247
- if (this.closed) this._ac.abort()
248
- else this.once('close', () => this._ac.abort())
249
- }
250
- return this._ac.signal
251
- }
252
-
253
304
  /**
254
305
  * `EventEmitter.on` plus an optional `{ signal }` that removes the listener
255
306
  * when the signal aborts — e.g. `me.on('handle', fn, { signal: me.signal })`.
@@ -265,42 +316,6 @@ export class Handle extends ReadyResource {
265
316
  return this
266
317
  }
267
318
 
268
- /** The top-most handle in the parent chain — itself for a root handle. */
269
- get root() {
270
- let h = this
271
- while (h.parent) h = h.parent
272
- return h
273
- }
274
-
275
- /**
276
- * Lazily-built file server for this identity. Root-only — child handles
277
- * reach it through `this.root.fileServer`.
278
- *
279
- * @returns {FileServer}
280
- */
281
- get fileServer() {
282
- if (this.parent) return this.root.fileServer
283
- if (!this._fileServer) {
284
- this._fileServer = new FileServer({
285
- store: this.store.store,
286
- resolve: (coreKey, info) => this._resolveCore(coreKey, info)
287
- })
288
- }
289
- return this._fileServer
290
- }
291
-
292
- /**
293
- * @param {Uint8Array} coreKey
294
- * @param {object} info
295
- * @returns {{ key: Uint8Array, encryptionKey: Uint8Array } | null}
296
- */
297
- _resolveCore(coreKey, info) {
298
- const hex = b4a.toString(coreKey, 'hex')
299
- const encryptionKey = this.root._coreKeys.get(hex)
300
- if (encryptionKey !== undefined) return { key: coreKey, encryptionKey }
301
- return null
302
- }
303
-
304
319
  /**
305
320
  * Resolve a durable file id to an ephemeral download url via this identity's
306
321
  * file server.
@@ -312,112 +327,6 @@ export class Handle extends ReadyResource {
312
327
  return this.root.fileServer.getLink(id)
313
328
  }
314
329
 
315
- /**
316
- * Lazily-built blob store for THIS handle's writing device, at the current
317
- * rotation epoch. One core per writer per epoch that writes files: the base
318
- * era uses the handle's encryptionKey, rotated epochs use a key derived
319
- * from the epoch entropy — so a removed member cannot decrypt files added
320
- * after the rotation. Instances carry `.stamp` so the file row records
321
- * which era its core belongs to.
322
- *
323
- * @returns {Blobs}
324
- */
325
- get blobs() {
326
- const stamp = this.store.keyring.current
327
- if (!stamp) return this._baseBlobs()
328
- let blobs = this._epochBlobs?.get(stamp)
329
- if (!blobs) {
330
- const entropy = this.store.keyring.entropy(stamp)
331
- blobs = this._makeBlobs(`blobs-${stamp}`, blobEpochKey(entropy), stamp)
332
- ;(this._epochBlobs ??= new Map()).set(stamp, blobs)
333
- }
334
- return blobs
335
- }
336
-
337
- _baseBlobs() {
338
- if (!this._blobs) this._blobs = this._makeBlobs('blobs', this.store.encryptionKey, 0)
339
- return this._blobs
340
- }
341
-
342
- _makeBlobs(name, encryptionKey, stamp) {
343
- const blobs = new Blobs({
344
- store: this.store.store,
345
- network: this.network,
346
- encryptionKey,
347
- name
348
- })
349
- blobs.stamp = stamp
350
- blobs
351
- .ready()
352
- .then(() => {
353
- // close prunes _coreKeys first, a late ready() must not re-insert the entry
354
- if (this.closing || this.closed || !blobs.key) return
355
- this.root._coreKeys.set(b4a.toString(blobs.key, 'hex'), encryptionKey)
356
- })
357
- .catch(this._onerror)
358
- return blobs
359
- }
360
-
361
- /**
362
- * Remember the blob-core key a file id points at so the file server can
363
- * open the core. Lives on the Handle (not the shared operators) because the
364
- * epoch-key derivation pulls native crypto — the RPC client must stay
365
- * bundleable without it.
366
- *
367
- * @param {string} id
368
- * @param {number} [stamp]
369
- */
370
- _registerBlobCore(id, stamp) {
371
- if (!id || !this.root?._coreKeys) return
372
- try {
373
- const { coreKey } = decodeId(id)
374
- const hex = b4a.toString(coreKey, 'hex')
375
- if (!this.root._coreKeys.has(hex)) {
376
- // no stamp on a file-field value, look it up (fire-and-forget, idempotent)
377
- if (stamp === undefined) {
378
- this.store
379
- .get('files', id)
380
- .then(({ data }) => {
381
- if (data && !this.closing && !this.closed) this._registerBlobCore(id, data.stamp || 0)
382
- })
383
- .catch(safetyCatch)
384
- return
385
- }
386
- const key = this._blobCoreKey(stamp)
387
- if (!key) return // unknown epoch — this device is not entitled to the core
388
- this.root._coreKeys.set(hex, key)
389
- }
390
- // remember which handle read it, so close prunes the entry
391
- if (this !== this.root) (this._blobKeys ??= new Set()).add(hex)
392
- } catch {
393
- // ignore invalid ids
394
- }
395
- }
396
-
397
- // base-era cores use the OWNING handle's key, rooms have their own
398
- _blobCoreKey(stamp) {
399
- if (!stamp) return this.store.encryptionKey
400
- const entropy = this.store.keyring.entropy(stamp)
401
- return entropy ? blobEpochKey(entropy) : null
402
- }
403
-
404
- /** Canonical id — identity id for the root handle, store key for children. */
405
- get id() {
406
- if (!this.parent) return this.identity.id
407
- return this.store?.key ? hid.encode(this.store.key) : null
408
- }
409
-
410
- /** This device's id + name. `null` on child handles. */
411
- get device() {
412
- if (this.parent) return null
413
- const k = this.store?.writerKey
414
- return k ? { id: hid.encode(k), name: this._opts.name || null } : null
415
- }
416
-
417
- get suspended() {
418
- return this._sus?.suspended === true
419
- }
420
-
421
330
  /**
422
331
  * Initialise a fresh database: write the genesis claim, derive the writer.
423
332
  * Forwards to `Database.bootstrap`.
@@ -440,9 +349,9 @@ export class Handle extends ReadyResource {
440
349
  }
441
350
 
442
351
  /**
443
- * Flip this handle's swarm announce mode — `setActive(false)` demotes an
444
- * idle/background room to server-only (still reachable, stops searching);
445
- * `setActive(true)` promotes it back on focus. Cheap, safe to call often.
352
+ * Flip this handle's swarm announce mode — `setActive(false)` demotes an idle/background
353
+ * room to server-only (still reachable, stops searching); `setActive(true)` promotes it
354
+ * back on focus.
446
355
  *
447
356
  * @param {boolean} active
448
357
  * @returns {Promise<void>}
@@ -485,12 +394,6 @@ export class Handle extends ReadyResource {
485
394
  return str
486
395
  }
487
396
 
488
- async _syncInvites() {
489
- if (!this.pair) return
490
- const { data } = await this.store.get('invites')
491
- this.pair.syncRows(data || [])
492
- }
493
-
494
397
  /**
495
398
  * Revoke a previously-minted invite by its string form.
496
399
  *
@@ -538,10 +441,8 @@ export class Handle extends ReadyResource {
538
441
  throw CeroError.INVALID(`role '${role}' is not a rank (owner, admin, member, reader)`)
539
442
  }
540
443
 
541
- // confirm must answer within the pairing request's lifetime, any await before
542
- // it (even ~100ms) drops the response. So the key is revealed before the
543
- // membership writes land, and a failure there is recoverable by re-pairing.
544
- // Epoch secrets ride along so a post-rotation joiner reads full history.
444
+ // confirm must answer within the request's lifetime, so the key goes out before the membership
445
+ // writes land; epoch secrets ride along so a post-rotation joiner reads full history
545
446
  const epochs = this.store.keyring.all()
546
447
  await candidate.confirm({
547
448
  key: this.store.key,
@@ -579,6 +480,118 @@ export class Handle extends ReadyResource {
579
480
  })
580
481
  }
581
482
 
483
+ /**
484
+ * Leave a child handle — removes it from the parent's `handles` collection
485
+ * and closes the session. No-op on root handles.
486
+ *
487
+ * @returns {Promise<void>}
488
+ */
489
+ async leave() {
490
+ if (!this.parent) return
491
+ await this.parent.store.call('del-handle', { id: hid.encode(this.store.key) })
492
+ await this.close()
493
+ }
494
+
495
+ /**
496
+ * Pause networking + storage. Idempotent; no-op on child handles.
497
+ *
498
+ * @returns {Promise<void>}
499
+ */
500
+ async suspend() {
501
+ if (this._sus) await this._sus.suspend()
502
+ }
503
+
504
+ /**
505
+ * Resume a suspended root handle. Idempotent; no-op on child handles.
506
+ *
507
+ * @returns {Promise<void>}
508
+ */
509
+ async resume() {
510
+ if (this._sus) await this._sus.resume()
511
+ }
512
+
513
+ /**
514
+ * @param {Uint8Array} coreKey
515
+ * @param {object} info
516
+ * @returns {{ key: Uint8Array, encryptionKey: Uint8Array } | null}
517
+ */
518
+ _resolveCore(coreKey, info) {
519
+ const hex = b4a.toString(coreKey, 'hex')
520
+ const encryptionKey = this.root._coreKeys.get(hex)
521
+ if (encryptionKey !== undefined) return { key: coreKey, encryptionKey }
522
+ return null
523
+ }
524
+
525
+ _baseBlobs() {
526
+ if (!this._blobs) this._blobs = this._makeBlobs('blobs', this.store.encryptionKey, 0)
527
+ return this._blobs
528
+ }
529
+
530
+ _makeBlobs(name, encryptionKey, stamp) {
531
+ const blobs = new Blobs({
532
+ store: this.store.store,
533
+ network: this.network,
534
+ encryptionKey,
535
+ name
536
+ })
537
+ blobs.stamp = stamp
538
+ blobs
539
+ .ready()
540
+ .then(() => {
541
+ // close prunes _coreKeys first, a late ready() must not re-insert the entry
542
+ if (this.closing || this.closed || !blobs.key) return
543
+ this.root._coreKeys.set(b4a.toString(blobs.key, 'hex'), encryptionKey)
544
+ })
545
+ .catch(this._onerror)
546
+ return blobs
547
+ }
548
+
549
+ /**
550
+ * Remember the blob-core key a file id points at so the file server can open the core.
551
+ *
552
+ * @param {string} id
553
+ * @param {number} [stamp]
554
+ */
555
+ _registerBlobCore(id, stamp) {
556
+ if (!id || !this.root?._coreKeys) return
557
+ try {
558
+ const { coreKey } = decodeId(id)
559
+ const hex = b4a.toString(coreKey, 'hex')
560
+ if (!this.root._coreKeys.has(hex)) {
561
+ // no stamp on a file-field value, look it up (fire-and-forget, idempotent)
562
+ if (stamp === undefined) {
563
+ this.store
564
+ .get('files', id)
565
+ .then(({ data }) => {
566
+ if (data && !this.closing && !this.closed) this._registerBlobCore(id, data.stamp || 0)
567
+ })
568
+ .catch(safetyCatch)
569
+ return
570
+ }
571
+ const key = this._blobCoreKey(stamp)
572
+ if (!key) return // unknown epoch — this device is not entitled to the core
573
+ this.root._coreKeys.set(hex, key)
574
+ }
575
+ // remember which handle read it, so close prunes the entry
576
+ if (this !== this.root) (this._blobKeys ??= new Set()).add(hex)
577
+ } catch {
578
+ // ignore invalid ids
579
+ }
580
+ }
581
+
582
+ // base-era cores use the OWNING handle's key, rooms have their own
583
+ _blobCoreKey(stamp) {
584
+ if (!stamp) return this.store.encryptionKey
585
+ const entropy = this.store.keyring.entropy(stamp)
586
+ return entropy ? blobEpochKey(entropy) : null
587
+ }
588
+
589
+ async _syncInvites() {
590
+ if (!this.pair) return
591
+ const { data } = await this.store.get('invites')
592
+ this.pair.syncRows(data || [])
593
+ }
594
+
582
595
  // `grants` treats an unknown role as "no", so an app role name must fail loudly
583
596
  async _checkGrant(role) {
584
597
  if (!isRank(role)) {
@@ -593,21 +606,8 @@ export class Handle extends ReadyResource {
593
606
  }
594
607
 
595
608
  /**
596
- * Leave a child handle — removes it from the parent's `handles` collection
597
- * and closes the session. No-op on root handles.
598
- *
599
- * @returns {Promise<void>}
600
- */
601
- async leave() {
602
- if (!this.parent) return
603
- await this.parent.store.call('del-handle', { id: hid.encode(this.store.key) })
604
- await this.close()
605
- }
606
-
607
- /**
608
- * Create a new child handle of `type`. Owner-flow — generates a fresh
609
- * writer, adds it as a writer + member, and registers the child on the
610
- * parent's `handles` collection.
609
+ * Create a new child handle of `type`. Owner-flow generates a fresh writer, adds it as a
610
+ * writer + member, and registers the child on the parent's `handles` collection.
611
611
  *
612
612
  * @param {string} type
613
613
  * @param {CreateChildOpts} [opts]
@@ -635,8 +635,7 @@ export class Handle extends ReadyResource {
635
635
  child.name = name
636
636
 
637
637
  const id = hid.encode(child.store.key)
638
- // add-handle makes the row visible before this create finishes, and a
639
- // duplicate Handle over the same core deadlocks in ready()
638
+ // add-handle makes the row visible before create finishes; a second Handle on the same core deadlocks
640
639
  const inflight = new Promise((resolve, reject) => {
641
640
  publish = resolve
642
641
  abort = reject
@@ -691,9 +690,7 @@ export class Handle extends ReadyResource {
691
690
  }
692
691
 
693
692
  /**
694
- * Join a child handle by invite (joiner-flow). Waits for writer
695
- * capability and registers the child on the parent's `handles`
696
- * collection.
693
+ * Join a child handle by invite (joiner-flow).
697
694
  *
698
695
  * @param {string} invite
699
696
  * @param {string} type
@@ -726,10 +723,8 @@ export class Handle extends ReadyResource {
726
723
  async _pair(invite, type, { routes, timeout } = {}, target = null) {
727
724
  const deadline = timeout || TIMEOUT
728
725
 
729
- // idempotent while our stored writer is still admitted, so no waiting on a
730
- // peer to confirm. A removed writer's core is frozen and reusing its keypair
731
- // only gets it re-removed, so fall through to a real pairing, which mints a
732
- // fresh keypair. A member row outlives a revoked device, so test the device.
726
+ // a stored writer still admitted reopens without waiting; a removed writer's core is
727
+ // frozen, so fall through to a real pairing and a fresh keypair
733
728
  if (target) {
734
729
  const { data: joined } = await this.store.get('handles')
735
730
  const existing = joined.find(
@@ -801,9 +796,8 @@ export class Handle extends ReadyResource {
801
796
  }
802
797
 
803
798
  /**
804
- * Get an open child by id, or re-open it. Concurrent calls for the same id
805
- * share one in-flight load, so the child is built — and `handle` emitted —
806
- * exactly once.
799
+ * Get an open child by id, or re-open it. Concurrent calls for the same id share one
800
+ * in-flight load, so the child is built — and `handle` emitted — exactly once.
807
801
  *
808
802
  * @param {string} type
809
803
  * @param {string} id
@@ -864,15 +858,6 @@ export class Handle extends ReadyResource {
864
858
  return child
865
859
  }
866
860
 
867
- /**
868
- * Pause networking + storage. Idempotent; no-op on child handles.
869
- *
870
- * @returns {Promise<void>}
871
- */
872
- async suspend() {
873
- if (this._sus) await this._sus.suspend()
874
- }
875
-
876
861
  async _suspend() {
877
862
  if (this.closing || this.closed) return
878
863
  await Promise.all([...this.children].map((c) => c.pair?.suspend()))
@@ -885,15 +870,6 @@ export class Handle extends ReadyResource {
885
870
  }
886
871
  }
887
872
 
888
- /**
889
- * Resume a suspended root handle. Idempotent; no-op on child handles.
890
- *
891
- * @returns {Promise<void>}
892
- */
893
- async resume() {
894
- if (this._sus) await this._sus.resume()
895
- }
896
-
897
873
  async _resume() {
898
874
  if (this.closing || this.closed) return
899
875
  try {
@@ -906,6 +882,45 @@ export class Handle extends ReadyResource {
906
882
  await Promise.all([...this.children].map((child) => child.pair?.resume()))
907
883
  }
908
884
 
885
+ /**
886
+ * @param {Handle} child
887
+ * @param {{ role?: string }} [opts]
888
+ */
889
+ _wireAccept(child, { role } = {}) {
890
+ child.pair.on('candidate', (cand) => {
891
+ if (this.closing || this.closed || child.closing || child.closed) return
892
+ child.accept(cand, { role }).catch(this._onerror)
893
+ })
894
+ }
895
+
896
+ /**
897
+ * @param {string} id
898
+ * @param {KeyPair | { publicKey: Uint8Array, secretKey: Uint8Array } | null} keyPair
899
+ * @returns {Promise<void>}
900
+ */
901
+ async _saveKeyPair(id, keyPair) {
902
+ if (!this.local || !keyPair) return
903
+ await this.local.store.put('handle-keypairs', {
904
+ id,
905
+ publicKey: keyPair.publicKey,
906
+ secretKey: keyPair.secretKey
907
+ })
908
+ }
909
+
910
+ /**
911
+ * @param {string} id
912
+ * @returns {Promise<{ publicKey: Uint8Array, secretKey: Uint8Array } | null>}
913
+ */
914
+ async _loadKeyPair(id) {
915
+ if (!this.local) return null
916
+ const { data } = await this.local.store.get('handle-keypairs', id)
917
+ if (!data) return null
918
+ return {
919
+ publicKey: data.publicKey,
920
+ secretKey: data.secretKey
921
+ }
922
+ }
923
+
909
924
  /**
910
925
  * Pair into an existing handle via an invite, returning a brand-new
911
926
  * `Handle` already configured with the resolved key + encryption key.
@@ -926,8 +941,7 @@ export class Handle extends ReadyResource {
926
941
  if (!spec) throw CeroError.REQUIRED('spec')
927
942
 
928
943
  const writer = Identity.randomKeyPair()
929
- // join-only: no member listener, so concurrent joins never collide on the
930
- // identity-derived default topic ('Active member already exist')
944
+ // join-only: a member listener here would collide with concurrent joins on the identity topic
931
945
  const pair = new Pairing({ network: net, identity: id, host: false })
932
946
  await pair.ready()
933
947
 
@@ -963,45 +977,6 @@ export class Handle extends ReadyResource {
963
977
  keyPair: writer
964
978
  })
965
979
  }
966
-
967
- /**
968
- * @param {Handle} child
969
- * @param {{ role?: string }} [opts]
970
- */
971
- _wireAccept(child, { role } = {}) {
972
- child.pair.on('candidate', (cand) => {
973
- if (this.closing || this.closed || child.closing || child.closed) return
974
- child.accept(cand, { role }).catch(this._onerror)
975
- })
976
- }
977
-
978
- /**
979
- * @param {string} id
980
- * @param {KeyPair | { publicKey: Uint8Array, secretKey: Uint8Array } | null} keyPair
981
- * @returns {Promise<void>}
982
- */
983
- async _saveKeyPair(id, keyPair) {
984
- if (!this.local || !keyPair) return
985
- await this.local.store.put('handle-keypairs', {
986
- id,
987
- publicKey: keyPair.publicKey,
988
- secretKey: keyPair.secretKey
989
- })
990
- }
991
-
992
- /**
993
- * @param {string} id
994
- * @returns {Promise<{ publicKey: Uint8Array, secretKey: Uint8Array } | null>}
995
- */
996
- async _loadKeyPair(id) {
997
- if (!this.local) return null
998
- const { data } = await this.local.store.get('handle-keypairs', id)
999
- if (!data) return null
1000
- return {
1001
- publicKey: data.publicKey,
1002
- secretKey: data.secretKey
1003
- }
1004
- }
1005
980
  }
1006
981
 
1007
982
  function pickHandle(spec, type) {