@cero-base/cero 0.8.10 → 1.0.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.
- package/README.md +64 -14
- package/package.json +20 -10
- package/src/build/builtins.js +10 -3
- package/src/build/index.js +24 -4
- package/src/build/schemas.js +20 -1
- package/src/handle/index.js +178 -69
- package/src/index.js +85 -64
- package/src/lib/constants.js +2 -1
- package/src/lib/operators.js +114 -53
- package/src/lib/peek.js +42 -0
- package/src/lib/utils.js +16 -3
- package/src/rpc/client.js +128 -14
- package/src/rpc/server.js +54 -7
- package/types/build/builtins.d.ts +5 -1
- package/types/build/index.d.ts +4 -21
- package/types/build/schemas.d.ts +19 -0
- package/types/handle/index.d.ts +49 -8
- package/types/index.d.ts +10 -5
- package/types/lib/constants.d.ts +1 -0
- package/types/lib/operators.d.ts +30 -10
- package/types/lib/peek.d.ts +10 -0
- package/types/lib/utils.d.ts +6 -3
- package/types/rpc/client.d.ts +3 -1
- package/types/rpc/server.d.ts +6 -5
package/src/handle/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import AbortController from 'abort-controller'
|
|
1
|
+
import AbortController from 'bare-abort-controller'
|
|
2
2
|
import b4a from 'b4a'
|
|
3
3
|
import Hypercore from 'hypercore'
|
|
4
4
|
import { discoveryKey } from 'hypercore-crypto'
|
|
@@ -9,8 +9,10 @@ import z32 from 'z32'
|
|
|
9
9
|
import { Identity } from '@cero-base/core/identity'
|
|
10
10
|
import { Database } from '@cero-base/core/database'
|
|
11
11
|
import { Pairing } from '@cero-base/core/pairing'
|
|
12
|
-
import { toId } from '@cero-base/core/utils'
|
|
12
|
+
import { toId, grants } from '@cero-base/core/utils'
|
|
13
13
|
import { CeroError } from '@cero-base/core/errors'
|
|
14
|
+
import { Blobs } from '@cero-base/core/blobs'
|
|
15
|
+
import { FileServer } from '@cero-base/core/blobs/server'
|
|
14
16
|
|
|
15
17
|
import { NS, TIMEOUT } from '../lib/constants.js'
|
|
16
18
|
|
|
@@ -63,7 +65,7 @@ export { Ref } from '../lib/utils.js'
|
|
|
63
65
|
* @property {number} [timeout]
|
|
64
66
|
*
|
|
65
67
|
* @typedef {object} AcceptOpts
|
|
66
|
-
* @property {string} [role] Role to grant the joining peer. Falls back to the invite's role, then `'
|
|
68
|
+
* @property {string} [role] Role to grant the joining peer. Falls back to the invite's role, then `'member'`.
|
|
67
69
|
* @property {string | null} [name]
|
|
68
70
|
*
|
|
69
71
|
* @typedef {object} RecoverOpts
|
|
@@ -75,6 +77,8 @@ export { Ref } from '../lib/utils.js'
|
|
|
75
77
|
* @property {import('../lib/utils.js').Ref} [members] `members` ref, attached dynamically when the schema declares one.
|
|
76
78
|
*
|
|
77
79
|
* @typedef {Handle & HandleExtra} Child A child handle plus its dynamically-attached refs.
|
|
80
|
+
*
|
|
81
|
+
* @typedef {Handle & Record<string, import('../lib/utils.js').Ref>} CeroHandle A handle with every schema ref reachable as a `Ref` property (e.g. `me.profile`, `room.messages`).
|
|
78
82
|
*/
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -113,6 +117,9 @@ export class Handle extends ReadyResource {
|
|
|
113
117
|
this._onerror = this._opts.onerror || safetyCatch
|
|
114
118
|
this.children = parent ? null : new Set()
|
|
115
119
|
this._loading = parent ? null : new Map()
|
|
120
|
+
this._coreKeys = parent ? null : new Map()
|
|
121
|
+
this._fileServer = null
|
|
122
|
+
this._blobs = null
|
|
116
123
|
this._owned = new Set()
|
|
117
124
|
|
|
118
125
|
this.store = new Database({
|
|
@@ -124,7 +131,8 @@ export class Handle extends ReadyResource {
|
|
|
124
131
|
key: opts.key,
|
|
125
132
|
encryptionKey: opts.encryptionKey,
|
|
126
133
|
namespace: opts.namespace,
|
|
127
|
-
keyPair: opts.keyPair
|
|
134
|
+
keyPair: opts.keyPair,
|
|
135
|
+
onerror: this._onerror
|
|
128
136
|
})
|
|
129
137
|
this.pair = null
|
|
130
138
|
this._wantsPair = opts.pair !== false
|
|
@@ -136,16 +144,21 @@ export class Handle extends ReadyResource {
|
|
|
136
144
|
this.pair = new Pairing({
|
|
137
145
|
network: this.network,
|
|
138
146
|
identity: this.identity,
|
|
139
|
-
topic: this.store.key
|
|
147
|
+
topic: this.store.key,
|
|
148
|
+
onerror: this._onerror
|
|
140
149
|
})
|
|
141
150
|
await this.pair.ready()
|
|
142
151
|
}
|
|
143
152
|
attachRefs(this, this.store.refs)
|
|
153
|
+
this.root._coreKeys.set(b4a.toString(this.store.key, 'hex'), this.store.encryptionKey)
|
|
154
|
+
if (!this.parent) await this.fileServer.listen()
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
async _close() {
|
|
158
|
+
this.root._coreKeys.delete(b4a.toString(this.store.key, 'hex'))
|
|
147
159
|
for (const r of [...this._owned]) r.destroy?.()
|
|
148
160
|
this._owned.clear()
|
|
161
|
+
if (this._blobs) await this._blobs.close()
|
|
149
162
|
if (this.children) {
|
|
150
163
|
for (const c of [...this.children]) await c.close()
|
|
151
164
|
this.children.clear()
|
|
@@ -159,6 +172,10 @@ export class Handle extends ReadyResource {
|
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
if (this.local) await this.local.close()
|
|
175
|
+
if (this._fileServer) {
|
|
176
|
+
await this._fileServer.close()
|
|
177
|
+
this._fileServer = null
|
|
178
|
+
}
|
|
162
179
|
const store = this.store.store
|
|
163
180
|
await this.store.close()
|
|
164
181
|
if (this._discovery) await this._discovery.destroy()
|
|
@@ -217,6 +234,79 @@ export class Handle extends ReadyResource {
|
|
|
217
234
|
return this
|
|
218
235
|
}
|
|
219
236
|
|
|
237
|
+
/** The top-most handle in the parent chain — itself for a root handle. */
|
|
238
|
+
get root() {
|
|
239
|
+
let h = this
|
|
240
|
+
while (h.parent) h = h.parent
|
|
241
|
+
return h
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Lazily-built file server for this identity. Root-only — child handles
|
|
246
|
+
* reach it through `this.root.fileServer`.
|
|
247
|
+
*
|
|
248
|
+
* @returns {FileServer}
|
|
249
|
+
*/
|
|
250
|
+
get fileServer() {
|
|
251
|
+
if (this.parent) return this.root.fileServer
|
|
252
|
+
if (!this._fileServer) {
|
|
253
|
+
this._fileServer = new FileServer({
|
|
254
|
+
store: this.store.store,
|
|
255
|
+
resolve: (coreKey, info) => this._resolveCore(coreKey, info)
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
return this._fileServer
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {Uint8Array} coreKey
|
|
263
|
+
* @param {object} info
|
|
264
|
+
* @returns {{ key: Uint8Array, encryptionKey: Uint8Array } | null}
|
|
265
|
+
*/
|
|
266
|
+
_resolveCore(coreKey, info) {
|
|
267
|
+
const hex = b4a.toString(coreKey, 'hex')
|
|
268
|
+
const encryptionKey = this.root._coreKeys.get(hex)
|
|
269
|
+
if (encryptionKey !== undefined) return { key: coreKey, encryptionKey }
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolve a durable file id to an ephemeral download url via this identity's
|
|
275
|
+
* file server.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} id
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
getLink(id) {
|
|
281
|
+
return this.root.fileServer.getLink(id)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Lazily-built blob store for THIS handle's writing device. One core per
|
|
286
|
+
* writer, encrypted with the handle's encryptionKey and attached to the
|
|
287
|
+
* shared network for replication.
|
|
288
|
+
*
|
|
289
|
+
* @returns {Blobs}
|
|
290
|
+
*/
|
|
291
|
+
get blobs() {
|
|
292
|
+
if (!this._blobs) {
|
|
293
|
+
this._blobs = new Blobs({
|
|
294
|
+
store: this.store.store,
|
|
295
|
+
network: this.network,
|
|
296
|
+
encryptionKey: this.store.encryptionKey
|
|
297
|
+
})
|
|
298
|
+
this._blobs
|
|
299
|
+
.ready()
|
|
300
|
+
.then(() => {
|
|
301
|
+
if (this._blobs && this._blobs.key) {
|
|
302
|
+
this.root._coreKeys.set(b4a.toString(this._blobs.key, 'hex'), this.store.encryptionKey)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
.catch(() => {})
|
|
306
|
+
}
|
|
307
|
+
return this._blobs
|
|
308
|
+
}
|
|
309
|
+
|
|
220
310
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
221
311
|
get id() {
|
|
222
312
|
if (!this.parent) return this.identity.id
|
|
@@ -249,11 +339,10 @@ export class Handle extends ReadyResource {
|
|
|
249
339
|
* Claim writer capability on an existing database (paired-device flow).
|
|
250
340
|
* Forwards to `Database.claim`.
|
|
251
341
|
*
|
|
252
|
-
* @param {{ name?: string | null, isMobile?: boolean }} [opts]
|
|
253
342
|
* @returns {Promise<void>}
|
|
254
343
|
*/
|
|
255
|
-
claim(
|
|
256
|
-
return this.store.claim(
|
|
344
|
+
claim() {
|
|
345
|
+
return this.store.claim()
|
|
257
346
|
}
|
|
258
347
|
|
|
259
348
|
/**
|
|
@@ -277,6 +366,10 @@ export class Handle extends ReadyResource {
|
|
|
277
366
|
* @returns {Promise<string>} Z32-encoded invite string.
|
|
278
367
|
*/
|
|
279
368
|
invite(opts) {
|
|
369
|
+
if (!this.pair)
|
|
370
|
+
throw CeroError.INVALID(
|
|
371
|
+
'invite() is not available on the root handle — open a child handle first'
|
|
372
|
+
)
|
|
280
373
|
return this.pair.createInvite(opts)
|
|
281
374
|
}
|
|
282
375
|
|
|
@@ -287,6 +380,7 @@ export class Handle extends ReadyResource {
|
|
|
287
380
|
* @returns {boolean} `true` if the invite was found and removed.
|
|
288
381
|
*/
|
|
289
382
|
revoke(invite) {
|
|
383
|
+
if (!this.pair) throw CeroError.INVALID('revoke() is not available on the root handle')
|
|
290
384
|
return this.pair.revoke(invite)
|
|
291
385
|
}
|
|
292
386
|
|
|
@@ -305,7 +399,11 @@ export class Handle extends ReadyResource {
|
|
|
305
399
|
'candidate userData must be a 64-byte buffer (identity + writer pubkey)'
|
|
306
400
|
)
|
|
307
401
|
}
|
|
308
|
-
|
|
402
|
+
if (candidate.invite.expired) throw CeroError.EXPIRED()
|
|
403
|
+
role = role || candidate.invite.role || 'member'
|
|
404
|
+
if (candidate.invite.role && !grants(candidate.invite.role, role)) {
|
|
405
|
+
throw CeroError.INVALID(`role '${role}' exceeds the invite role '${candidate.invite.role}'`)
|
|
406
|
+
}
|
|
309
407
|
|
|
310
408
|
await candidate.confirm({ key: this.store.key, encryptionKey: this.store.encryptionKey })
|
|
311
409
|
|
|
@@ -320,12 +418,12 @@ export class Handle extends ReadyResource {
|
|
|
320
418
|
updatedAt: ts
|
|
321
419
|
}
|
|
322
420
|
|
|
323
|
-
if (role === '
|
|
421
|
+
if (role === 'reader') {
|
|
324
422
|
await this.store.call('add-member', member)
|
|
325
423
|
return
|
|
326
424
|
}
|
|
327
425
|
|
|
328
|
-
const sig = this.identity.sign(writerKey)
|
|
426
|
+
const sig = this.identity.sign(b4a.concat([writerKey, this.store.writerKey]))
|
|
329
427
|
await this.store.tx(async () => {
|
|
330
428
|
await this.store.call('add-writer', {
|
|
331
429
|
sig,
|
|
@@ -368,42 +466,49 @@ export class Handle extends ReadyResource {
|
|
|
368
466
|
keyPair: writer
|
|
369
467
|
})
|
|
370
468
|
)
|
|
371
|
-
|
|
372
|
-
|
|
469
|
+
// a failure after the child opens must close it — else it leaks its Database,
|
|
470
|
+
// swarm session and attached bee, never reaped by the parent (not yet a child)
|
|
471
|
+
try {
|
|
472
|
+
await child.ready()
|
|
473
|
+
child.name = name
|
|
373
474
|
|
|
374
|
-
|
|
375
|
-
|
|
475
|
+
const id = toId(child.store.key)
|
|
476
|
+
await this._saveKeyPair(id, writer)
|
|
376
477
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
478
|
+
const ts = Date.now()
|
|
479
|
+
const writerKey = child.store.writerKey
|
|
480
|
+
await child.store.call('add-writer', {
|
|
481
|
+
master: this.identity.publicKey,
|
|
482
|
+
writer: writerKey,
|
|
483
|
+
sig: this.identity.sign(b4a.concat([writerKey, child.store.writerKey]))
|
|
484
|
+
})
|
|
485
|
+
await child.store.call('add-member', {
|
|
486
|
+
id: this.identity.id,
|
|
487
|
+
key: writerKey,
|
|
488
|
+
role: 'owner',
|
|
489
|
+
name: null,
|
|
490
|
+
createdAt: ts,
|
|
491
|
+
updatedAt: ts
|
|
492
|
+
})
|
|
493
|
+
await this.store.call('add-handle', {
|
|
494
|
+
id,
|
|
495
|
+
type,
|
|
496
|
+
key: child.store.key,
|
|
497
|
+
encryptionKey: child.store.encryptionKey,
|
|
498
|
+
name,
|
|
499
|
+
createdAt: ts,
|
|
500
|
+
updatedAt: ts
|
|
501
|
+
})
|
|
401
502
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
503
|
+
if (accept !== false) this._wireAccept(child, { role })
|
|
504
|
+
bind(child, type)
|
|
505
|
+
this.children.add(child)
|
|
506
|
+
this.emit('handle', child, { name, role })
|
|
507
|
+
return child
|
|
508
|
+
} catch (err) {
|
|
509
|
+
await child.close().catch(safetyCatch)
|
|
510
|
+
throw err
|
|
511
|
+
}
|
|
407
512
|
}
|
|
408
513
|
|
|
409
514
|
/**
|
|
@@ -439,27 +544,34 @@ export class Handle extends ReadyResource {
|
|
|
439
544
|
timeout
|
|
440
545
|
})
|
|
441
546
|
)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
547
|
+
// whenWritable timing out (host offline) is a normal failure — close the
|
|
548
|
+
// fully-opened child rather than leak its Database/pairing/swarm session
|
|
549
|
+
try {
|
|
550
|
+
await child.ready()
|
|
551
|
+
if (!child.store.writable) await child.store.whenWritable({ timeout: deadline })
|
|
552
|
+
|
|
553
|
+
const id = toId(child.store.key)
|
|
554
|
+
await this._saveKeyPair(id, child.store.keyPair)
|
|
555
|
+
|
|
556
|
+
const ts = Date.now()
|
|
557
|
+
await this.store.call('add-handle', {
|
|
558
|
+
id,
|
|
559
|
+
type,
|
|
560
|
+
key: child.store.key,
|
|
561
|
+
encryptionKey: child.store.encryptionKey,
|
|
562
|
+
name: null,
|
|
563
|
+
createdAt: ts,
|
|
564
|
+
updatedAt: ts
|
|
565
|
+
})
|
|
566
|
+
this._wireAccept(child)
|
|
567
|
+
bind(child, type)
|
|
568
|
+
this.children.add(child)
|
|
569
|
+
this.emit('handle', child, {})
|
|
570
|
+
return child
|
|
571
|
+
} catch (err) {
|
|
572
|
+
await child.close().catch(safetyCatch)
|
|
573
|
+
throw err
|
|
574
|
+
}
|
|
463
575
|
}
|
|
464
576
|
|
|
465
577
|
/**
|
|
@@ -511,10 +623,7 @@ export class Handle extends ReadyResource {
|
|
|
511
623
|
})
|
|
512
624
|
await child.ready()
|
|
513
625
|
if (firstTime && !child.store.writable) {
|
|
514
|
-
await child.store.claim(
|
|
515
|
-
name: this._opts.name || null,
|
|
516
|
-
isMobile: this._opts.isMobile === true
|
|
517
|
-
})
|
|
626
|
+
await child.store.claim()
|
|
518
627
|
}
|
|
519
628
|
if (firstTime) {
|
|
520
629
|
await this._saveKeyPair(id, writer)
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import HypercoreStorage from 'hypercore-storage'
|
|
2
2
|
import Corestore from 'corestore'
|
|
3
|
+
import safetyCatch from 'safety-catch'
|
|
3
4
|
import fs from 'fs'
|
|
4
5
|
|
|
5
6
|
import { Identity } from '@cero-base/core/identity'
|
|
@@ -20,9 +21,9 @@ import {
|
|
|
20
21
|
before,
|
|
21
22
|
after,
|
|
22
23
|
bind,
|
|
23
|
-
define
|
|
24
|
-
peek
|
|
24
|
+
define
|
|
25
25
|
} from './lib/operators.js'
|
|
26
|
+
import { peek } from './lib/peek.js'
|
|
26
27
|
import { t, schema } from './lib/spec.js'
|
|
27
28
|
import { FLUSH } from './lib/constants.js'
|
|
28
29
|
import { internal } from './lib/internal.js'
|
|
@@ -40,11 +41,15 @@ export {
|
|
|
40
41
|
before,
|
|
41
42
|
after,
|
|
42
43
|
bind,
|
|
43
|
-
define
|
|
44
|
-
peek
|
|
44
|
+
define
|
|
45
45
|
} from './lib/operators.js'
|
|
46
|
+
export { peek } from './lib/peek.js'
|
|
46
47
|
export { t, schema } from './lib/spec.js'
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {import('./handle/index.js').CeroHandle} CeroHandle
|
|
51
|
+
*/
|
|
52
|
+
|
|
48
53
|
/**
|
|
49
54
|
* @typedef {object} CeroOpts
|
|
50
55
|
* @property {Identity} [identity] Pre-resolved identity. If absent, derived from `seed`/`phrase` or generated.
|
|
@@ -70,87 +75,103 @@ export { t, schema } from './lib/spec.js'
|
|
|
70
75
|
* @param {string} dir Data directory.
|
|
71
76
|
* @param {any} spec Built spec — output of `cero/build`.
|
|
72
77
|
* @param {CeroOpts} [opts]
|
|
73
|
-
* @returns {Promise<
|
|
78
|
+
* @returns {Promise<CeroHandle>}
|
|
74
79
|
*/
|
|
75
80
|
export async function cero(dir, spec, opts = {}) {
|
|
76
81
|
if (typeof dir !== 'string' || !dir) throw CeroError.INVALID('dir must be a non-empty string')
|
|
77
82
|
if (!spec) throw CeroError.REQUIRED('spec')
|
|
78
83
|
|
|
79
84
|
const storage = new HypercoreStorage(`${dir}/main`)
|
|
80
|
-
|
|
81
|
-
const store = new Corestore(storage, { manifestVersion: 2 })
|
|
82
|
-
await store.ready()
|
|
83
|
-
|
|
85
|
+
let store = null
|
|
84
86
|
let local = null
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
let network = null
|
|
88
|
+
let discovery = null
|
|
89
|
+
let me = null
|
|
90
|
+
// Any failure during open (bad phrase, network/bootstrap error, recovery
|
|
91
|
+
// timeout, extension setup) must close whatever opened — else the corestore
|
|
92
|
+
// lock leaks and a retry in the same process hits locked storage.
|
|
93
|
+
try {
|
|
94
|
+
await storage.ready()
|
|
95
|
+
store = new Corestore(storage, { manifestVersion: 2 })
|
|
96
|
+
await store.ready()
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await network.ready()
|
|
95
|
-
const discovery = network.join(identity.topic)
|
|
96
|
-
await Promise.race([discovery.flush(), new Promise((r) => setTimeout(r, FLUSH))])
|
|
97
|
-
|
|
98
|
-
const me = new Handle({
|
|
99
|
-
storage,
|
|
100
|
-
store,
|
|
101
|
-
local,
|
|
102
|
-
discovery,
|
|
103
|
-
identity,
|
|
104
|
-
network,
|
|
105
|
-
spec,
|
|
106
|
-
opts,
|
|
107
|
-
dir,
|
|
108
|
-
routes: opts.routes,
|
|
109
|
-
key: opts.key,
|
|
110
|
-
encryptionKey: opts.encryptionKey,
|
|
111
|
-
keyPair: writer ? { publicKey: writer.publicKey, secretKey: writer.secretKey } : undefined,
|
|
112
|
-
pair: false
|
|
113
|
-
})
|
|
114
|
-
await me.ready()
|
|
98
|
+
if (spec.local && spec.meta?.local) {
|
|
99
|
+
local = new Local(null, spec, { store })
|
|
100
|
+
await local.ready()
|
|
101
|
+
}
|
|
115
102
|
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
const identity = await resolveIdentity(opts, local)
|
|
104
|
+
const writer = local ? (await local.store.get('keypair')).data : null
|
|
105
|
+
|
|
106
|
+
network = new Network({ bootstrap: opts.bootstrap })
|
|
107
|
+
await network.ready()
|
|
108
|
+
discovery = network.join(identity.topic)
|
|
109
|
+
await Promise.race([discovery.flush(), new Promise((r) => setTimeout(r, FLUSH))])
|
|
110
|
+
|
|
111
|
+
me = new Handle({
|
|
112
|
+
storage,
|
|
113
|
+
store,
|
|
114
|
+
local,
|
|
115
|
+
discovery,
|
|
116
|
+
identity,
|
|
117
|
+
network,
|
|
118
|
+
spec,
|
|
119
|
+
opts,
|
|
120
|
+
dir,
|
|
121
|
+
routes: opts.routes,
|
|
122
|
+
key: opts.key,
|
|
123
|
+
encryptionKey: opts.encryptionKey,
|
|
124
|
+
keyPair: writer ? { publicKey: writer.publicKey, secretKey: writer.secretKey } : undefined,
|
|
125
|
+
pair: false
|
|
121
126
|
})
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
if (!opts.recovery) {
|
|
129
|
-
const ts = Date.now()
|
|
130
|
-
await me.store.call('add-member', {
|
|
131
|
-
id: identity.id,
|
|
132
|
-
key: me.store.writerKey,
|
|
133
|
-
role: 'owner',
|
|
127
|
+
await me.ready()
|
|
128
|
+
|
|
129
|
+
if (!writer && me.store.length === 0 && (!opts.key || opts.recovery)) {
|
|
130
|
+
const result = await me.bootstrap({
|
|
134
131
|
name: opts.name || null,
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
isMobile: opts.isMobile === true,
|
|
133
|
+
recovering: opts.recovery === true
|
|
137
134
|
})
|
|
135
|
+
if (local && result?.writer) {
|
|
136
|
+
await local.store.set('keypair', {
|
|
137
|
+
publicKey: result.writer.publicKey,
|
|
138
|
+
secretKey: result.writer.secretKey
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
if (!opts.recovery) {
|
|
142
|
+
const ts = Date.now()
|
|
143
|
+
await me.store.call('add-member', {
|
|
144
|
+
id: identity.id,
|
|
145
|
+
key: me.store.writerKey,
|
|
146
|
+
role: 'owner',
|
|
147
|
+
name: opts.name || null,
|
|
148
|
+
createdAt: ts,
|
|
149
|
+
updatedAt: ts
|
|
150
|
+
})
|
|
151
|
+
}
|
|
138
152
|
}
|
|
139
|
-
|
|
140
|
-
if (opts.recovery) await me.recover({ timeout: opts.recoveryTimeout })
|
|
153
|
+
if (opts.recovery) await me.recover({ timeout: opts.recoveryTimeout })
|
|
141
154
|
|
|
142
|
-
try {
|
|
143
155
|
for (const ext of internal.extensions) {
|
|
144
156
|
const off = await ext.setup?.(me)
|
|
145
157
|
if (typeof off === 'function') me.once('close', off)
|
|
146
158
|
}
|
|
159
|
+
|
|
160
|
+
bind(me, null)
|
|
161
|
+
return me
|
|
147
162
|
} catch (err) {
|
|
148
|
-
|
|
163
|
+
// once the root Handle exists it owns (and closes) everything; before that,
|
|
164
|
+
// tear the raw resources down in reverse order.
|
|
165
|
+
if (me) await me.close().catch(safetyCatch)
|
|
166
|
+
else {
|
|
167
|
+
await discovery?.destroy().catch(safetyCatch)
|
|
168
|
+
await network?.close().catch(safetyCatch)
|
|
169
|
+
await local?.close().catch(safetyCatch)
|
|
170
|
+
await store?.close().catch(safetyCatch)
|
|
171
|
+
await storage.close().catch(safetyCatch)
|
|
172
|
+
}
|
|
149
173
|
throw err
|
|
150
174
|
}
|
|
151
|
-
|
|
152
|
-
bind(me, null)
|
|
153
|
-
return me
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
/**
|