@cero-base/cero 0.4.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.
- package/LICENSE +201 -0
- package/README.md +287 -0
- package/package.json +79 -0
- package/src/CLAUDE.md +3 -0
- package/src/builder.js +276 -0
- package/src/handle/CLAUDE.md +3 -0
- package/src/handle/index.js +600 -0
- package/src/index.js +170 -0
- package/src/lib/CLAUDE.md +3 -0
- package/src/lib/builtins.js +410 -0
- package/src/lib/operators.js +128 -0
- package/src/lib/peek.js +36 -0
- package/src/lib/spec.js +6 -0
- package/src/lib/utils.js +41 -0
- package/src/local/index.js +49 -0
- package/src/rpc/CLAUDE.md +3 -0
- package/src/rpc/client.js +337 -0
- package/src/rpc/index.js +8 -0
- package/src/rpc/server.js +290 -0
- package/types/builder.d.ts +32 -0
- package/types/handle/index.d.ts +353 -0
- package/types/index.d.ts +102 -0
- package/types/lib/builtins.d.ts +141 -0
- package/types/lib/operators.d.ts +29 -0
- package/types/lib/peek.d.ts +10 -0
- package/types/lib/spec.d.ts +1 -0
- package/types/lib/utils.d.ts +44 -0
- package/types/local/index.d.ts +33 -0
- package/types/rpc/client.d.ts +125 -0
- package/types/rpc/index.d.ts +2 -0
- package/types/rpc/server.d.ts +154 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import b4a from 'b4a'
|
|
2
|
+
import Hypercore from 'hypercore'
|
|
3
|
+
import ReadyResource from 'ready-resource'
|
|
4
|
+
import safetyCatch from 'safety-catch'
|
|
5
|
+
import z32 from 'z32'
|
|
6
|
+
|
|
7
|
+
import { Identity } from '@cero-base/core/identity'
|
|
8
|
+
import { Database } from '@cero-base/core/database'
|
|
9
|
+
import { Pairing } from '@cero-base/core/pairing'
|
|
10
|
+
import { toId } from '@cero-base/core/utils'
|
|
11
|
+
import { CeroError } from '@cero-base/core/errors'
|
|
12
|
+
|
|
13
|
+
import { attachRefs } from '../lib/utils.js'
|
|
14
|
+
|
|
15
|
+
export { Ref } from '../lib/utils.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {import('@cero-base/core/network').Network} Network
|
|
19
|
+
* @typedef {import('../local/index.js').Local} Local
|
|
20
|
+
* @typedef {import('@cero-base/core/identity').KeyPair} KeyPair
|
|
21
|
+
*
|
|
22
|
+
* @typedef {object} HandleOpts
|
|
23
|
+
* @property {Handle} [parent] Parent handle when this is a child slot.
|
|
24
|
+
* @property {Identity} [identity] Long-lived user identity. Inherited from `parent` if omitted.
|
|
25
|
+
* @property {Network} [network] Shared swarm. Inherited from `parent` if omitted.
|
|
26
|
+
* @property {any} [store] Pre-existing Corestore. Falls back to `parent.store.store`.
|
|
27
|
+
* @property {any} [spec] Built cero spec.
|
|
28
|
+
* @property {Local} [local] Local store for per-handle keypairs.
|
|
29
|
+
* @property {any} [storage] Owned HypercoreStorage to close on shutdown.
|
|
30
|
+
* @property {any} [discovery] Owned identity Discovery to destroy on shutdown.
|
|
31
|
+
* @property {string} [dir] Data directory (root handles only).
|
|
32
|
+
* @property {any} [opts] Pass-through cero(...) options.
|
|
33
|
+
* @property {Record<string, Function>} [routes]
|
|
34
|
+
* @property {Uint8Array} [key] Existing database key.
|
|
35
|
+
* @property {Uint8Array} [encryptionKey] Existing encryption key.
|
|
36
|
+
* @property {string} [namespace] Corestore namespace.
|
|
37
|
+
* @property {KeyPair} [keyPair] Writer keypair.
|
|
38
|
+
* @property {boolean} [pair] When `false`, skips creating a `Pairing` session.
|
|
39
|
+
*
|
|
40
|
+
* @typedef {object} CreateChildOpts
|
|
41
|
+
* @property {string | null} [name]
|
|
42
|
+
* @property {Record<string, Function>} [routes]
|
|
43
|
+
* @property {string} [role]
|
|
44
|
+
* @property {boolean} [accept]
|
|
45
|
+
*
|
|
46
|
+
* @typedef {object} JoinChildOpts
|
|
47
|
+
* @property {Record<string, Function>} [routes]
|
|
48
|
+
* @property {number} [timeout]
|
|
49
|
+
*
|
|
50
|
+
* @typedef {object} StaticJoinOpts
|
|
51
|
+
* @property {Handle} [parent]
|
|
52
|
+
* @property {Network} [network]
|
|
53
|
+
* @property {Identity} [identity]
|
|
54
|
+
* @property {any} [store]
|
|
55
|
+
* @property {any} [spec]
|
|
56
|
+
* @property {string} [namespace]
|
|
57
|
+
* @property {Record<string, Function>} [routes]
|
|
58
|
+
* @property {number} [timeout]
|
|
59
|
+
*
|
|
60
|
+
* @typedef {object} AcceptOpts
|
|
61
|
+
* @property {string} [role] Role to grant the joining peer. Falls back to the invite's role, then `'write'`.
|
|
62
|
+
* @property {string | null} [name]
|
|
63
|
+
*
|
|
64
|
+
* @typedef {object} RecoverOpts
|
|
65
|
+
* @property {number} [timeout]
|
|
66
|
+
* @property {string | null} [name]
|
|
67
|
+
* @property {boolean} [isMobile]
|
|
68
|
+
*
|
|
69
|
+
* @typedef {object} HandleExtra
|
|
70
|
+
* @property {string | null} [name] Display name; set on child handles by the owner flow.
|
|
71
|
+
* @property {import('../lib/utils.js').Ref} [profile] `profile` ref, attached dynamically when the schema declares one.
|
|
72
|
+
* @property {import('../lib/utils.js').Ref} [members] `members` ref, attached dynamically when the schema declares one.
|
|
73
|
+
*
|
|
74
|
+
* @typedef {Handle & HandleExtra} Child A child handle plus its dynamically-attached refs.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A cero handle — a single writable database session attached to a swarm.
|
|
79
|
+
* The "root" handle is the user's facade; child handles (created via
|
|
80
|
+
* `_create`/`_join`/`_load`) live under it and share the same identity,
|
|
81
|
+
* network and corestore. Ref properties (`profile`, `members`, ...) are
|
|
82
|
+
* attached dynamically per the schema; child handles also carry a `name`.
|
|
83
|
+
*/
|
|
84
|
+
export class Handle extends ReadyResource {
|
|
85
|
+
/** @param {HandleOpts} [opts] */
|
|
86
|
+
constructor(opts = {}) {
|
|
87
|
+
super()
|
|
88
|
+
|
|
89
|
+
const { parent = null } = opts
|
|
90
|
+
const identity = opts.identity || parent?.identity
|
|
91
|
+
const network = opts.network || parent?.network
|
|
92
|
+
const store = opts.store || parent?.store?.store
|
|
93
|
+
const spec = opts.spec
|
|
94
|
+
|
|
95
|
+
if (!identity) throw CeroError.REQUIRED('identity')
|
|
96
|
+
if (!network) throw CeroError.REQUIRED('network')
|
|
97
|
+
if (!store) throw CeroError.REQUIRED('store')
|
|
98
|
+
if (!spec) throw CeroError.REQUIRED('spec')
|
|
99
|
+
|
|
100
|
+
this.identity = identity
|
|
101
|
+
this.network = network
|
|
102
|
+
this.spec = spec
|
|
103
|
+
this.parent = parent
|
|
104
|
+
|
|
105
|
+
this.local = opts.local || null
|
|
106
|
+
this._storage = opts.storage || null
|
|
107
|
+
this._discovery = opts.discovery || null
|
|
108
|
+
this._dir = opts.dir || null
|
|
109
|
+
this._opts = opts.opts || {}
|
|
110
|
+
this._onerror = this._opts.onerror || safetyCatch
|
|
111
|
+
this.children = parent ? null : new Set()
|
|
112
|
+
|
|
113
|
+
this.store = new Database({
|
|
114
|
+
store: store,
|
|
115
|
+
identity,
|
|
116
|
+
network,
|
|
117
|
+
spec,
|
|
118
|
+
routes: opts.routes,
|
|
119
|
+
key: opts.key,
|
|
120
|
+
encryptionKey: opts.encryptionKey,
|
|
121
|
+
namespace: opts.namespace,
|
|
122
|
+
keyPair: opts.keyPair
|
|
123
|
+
})
|
|
124
|
+
this.pair = null
|
|
125
|
+
this._wantsPair = opts.pair !== false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async _open() {
|
|
129
|
+
await this.store.ready()
|
|
130
|
+
if (this._wantsPair) {
|
|
131
|
+
this.pair = new Pairing({
|
|
132
|
+
network: this.network,
|
|
133
|
+
identity: this.identity,
|
|
134
|
+
topic: this.store.key
|
|
135
|
+
})
|
|
136
|
+
await this.pair.ready()
|
|
137
|
+
}
|
|
138
|
+
attachRefs(this, this.store.refs)
|
|
139
|
+
if (!this.children) return
|
|
140
|
+
|
|
141
|
+
this._offUpdate = this.store.onUpdate(() => {
|
|
142
|
+
if (this.closing || this.closed) return
|
|
143
|
+
for (const c of this.children) this._syncMember(c).catch(this._onerror)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async _close() {
|
|
148
|
+
this._offUpdate?.()
|
|
149
|
+
if (this.children) {
|
|
150
|
+
for (const c of [...this.children]) await c.close()
|
|
151
|
+
this.children.clear()
|
|
152
|
+
}
|
|
153
|
+
if (this.pair) await this.pair.close()
|
|
154
|
+
|
|
155
|
+
if (this.parent) {
|
|
156
|
+
this.parent.children?.delete(this)
|
|
157
|
+
await this.store.close()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.local) await this.local.close()
|
|
162
|
+
const store = this.store.store
|
|
163
|
+
await this.store.close()
|
|
164
|
+
if (this._discovery) await this._discovery.destroy()
|
|
165
|
+
await this.network.close()
|
|
166
|
+
await store.close()
|
|
167
|
+
if (this._storage) await this._storage.close()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Canonical id — identity id for the root handle, store key for children. */
|
|
171
|
+
get id() {
|
|
172
|
+
if (!this.parent) return this.identity.id
|
|
173
|
+
return this.store?.key ? toId(this.store.key) : null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** This device's id + name. `null` on child handles. */
|
|
177
|
+
get device() {
|
|
178
|
+
if (this.parent) return null
|
|
179
|
+
const k = this.store?.writerKey
|
|
180
|
+
return k ? { id: toId(k), name: this._opts.name || null } : null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get suspended() {
|
|
184
|
+
return this._suspended === true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Initialise a fresh database: write the genesis claim, derive the writer.
|
|
189
|
+
* Forwards to `Database.bootstrap`.
|
|
190
|
+
*
|
|
191
|
+
* @param {any} [opts]
|
|
192
|
+
* @returns {Promise<any>}
|
|
193
|
+
*/
|
|
194
|
+
bootstrap(opts) {
|
|
195
|
+
return this.store.bootstrap(opts)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Claim writer capability on an existing database (paired-device flow).
|
|
200
|
+
* Forwards to `Database.claim`.
|
|
201
|
+
*
|
|
202
|
+
* @param {{ name?: string | null, isMobile?: boolean }} [opts]
|
|
203
|
+
* @returns {Promise<void>}
|
|
204
|
+
*/
|
|
205
|
+
claim(opts) {
|
|
206
|
+
return this.store.claim(opts)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Claim + wait until this peer becomes a writer + bring the bee up to date.
|
|
211
|
+
* Used by `restore()` after wiping local state.
|
|
212
|
+
*
|
|
213
|
+
* @param {RecoverOpts} [opts]
|
|
214
|
+
* @returns {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
async recover({ timeout = 30000, name = null, isMobile = false } = {}) {
|
|
217
|
+
if (this.store.writable) return
|
|
218
|
+
await this.store.claim({ name, isMobile })
|
|
219
|
+
await this.store.whenWritable({ timeout })
|
|
220
|
+
await this.store.bee.update()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Mint a pairing invite for this handle.
|
|
225
|
+
*
|
|
226
|
+
* @param {{ role?: string, expiresIn?: number, data?: any }} [opts]
|
|
227
|
+
* @returns {Promise<string>} Z32-encoded invite string.
|
|
228
|
+
*/
|
|
229
|
+
invite(opts) {
|
|
230
|
+
return this.pair.createInvite(opts)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Revoke a previously-minted invite by its string form.
|
|
235
|
+
*
|
|
236
|
+
* @param {string} invite
|
|
237
|
+
* @returns {boolean} `true` if the invite was found and removed.
|
|
238
|
+
*/
|
|
239
|
+
revoke(invite) {
|
|
240
|
+
return this.pair.revoke(invite)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Accept a paired candidate — adds them as a writer (or read-only member)
|
|
245
|
+
* and confirms the pairing so they receive this handle's keys.
|
|
246
|
+
*
|
|
247
|
+
* @param {any} candidate
|
|
248
|
+
* @param {AcceptOpts} [opts]
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async accept(candidate, { role, name } = {}) {
|
|
252
|
+
const data = candidate.userData
|
|
253
|
+
if (!b4a.isBuffer(data) || data.length !== 64) {
|
|
254
|
+
throw CeroError.INVALID(
|
|
255
|
+
'candidate userData must be a 64-byte buffer (identity + writer pubkey)'
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
role = role || candidate.invite.role || 'write'
|
|
259
|
+
|
|
260
|
+
await candidate.confirm({ key: this.store.key, encryptionKey: this.store.encryptionKey })
|
|
261
|
+
|
|
262
|
+
const ts = Date.now()
|
|
263
|
+
const writerKey = Hypercore.key({ version: 2, signers: [{ publicKey: data.subarray(32, 64) }] })
|
|
264
|
+
const member = {
|
|
265
|
+
id: toId(data.subarray(0, 32)),
|
|
266
|
+
key: writerKey,
|
|
267
|
+
role,
|
|
268
|
+
name: name || null,
|
|
269
|
+
createdAt: ts,
|
|
270
|
+
updatedAt: ts
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (role === 'read') {
|
|
274
|
+
await this.store.call('add-member', member)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const sig = this.identity.sign(writerKey)
|
|
279
|
+
await this.store.tx(async () => {
|
|
280
|
+
await this.store.call('add-writer', {
|
|
281
|
+
sig,
|
|
282
|
+
master: this.identity.publicKey,
|
|
283
|
+
writer: writerKey,
|
|
284
|
+
name: name || null,
|
|
285
|
+
isMobile: false
|
|
286
|
+
})
|
|
287
|
+
await this.store.call('add-member', member)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Leave a child handle — removes it from the parent's `handles` collection
|
|
293
|
+
* and closes the session. No-op on root handles.
|
|
294
|
+
*
|
|
295
|
+
* @returns {Promise<void>}
|
|
296
|
+
*/
|
|
297
|
+
async leave() {
|
|
298
|
+
if (!this.parent) return
|
|
299
|
+
await this.parent.store.call('del-handle', { id: toId(this.store.key) })
|
|
300
|
+
await this.close()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Create a new child handle of `type`. Owner-flow — generates a fresh
|
|
305
|
+
* writer, adds it as a writer + member, and registers the child on the
|
|
306
|
+
* parent's `handles` collection.
|
|
307
|
+
*
|
|
308
|
+
* @param {string} type
|
|
309
|
+
* @param {CreateChildOpts} [opts]
|
|
310
|
+
* @returns {Promise<Handle>}
|
|
311
|
+
*/
|
|
312
|
+
async _create(type, { name = null, routes, role, accept } = {}) {
|
|
313
|
+
const writer = Identity.randomKeyPair()
|
|
314
|
+
const child = /** @type {Child} */ (
|
|
315
|
+
new Handle({
|
|
316
|
+
parent: this,
|
|
317
|
+
spec: pickHandle(this.spec, type),
|
|
318
|
+
namespace: `cero/handle/${type}/${writer.id}`,
|
|
319
|
+
routes,
|
|
320
|
+
keyPair: writer
|
|
321
|
+
})
|
|
322
|
+
)
|
|
323
|
+
await child.ready()
|
|
324
|
+
child.name = name
|
|
325
|
+
|
|
326
|
+
const id = toId(child.store.key)
|
|
327
|
+
await this._saveKeyPair(id, writer)
|
|
328
|
+
|
|
329
|
+
const ts = Date.now()
|
|
330
|
+
const writerKey = child.store.writerKey
|
|
331
|
+
await child.store.call('add-writer', {
|
|
332
|
+
master: this.identity.publicKey,
|
|
333
|
+
writer: writerKey,
|
|
334
|
+
sig: this.identity.sign(writerKey),
|
|
335
|
+
name,
|
|
336
|
+
isMobile: false
|
|
337
|
+
})
|
|
338
|
+
await child.store.call('add-member', {
|
|
339
|
+
id: this.identity.id,
|
|
340
|
+
key: writerKey,
|
|
341
|
+
role: 'owner',
|
|
342
|
+
name: null,
|
|
343
|
+
createdAt: ts,
|
|
344
|
+
updatedAt: ts
|
|
345
|
+
})
|
|
346
|
+
if (name && child.profile) await child.store.set('profile', { name })
|
|
347
|
+
await this.store.call('add-handle', {
|
|
348
|
+
id,
|
|
349
|
+
type,
|
|
350
|
+
key: child.store.key,
|
|
351
|
+
encryptionKey: child.store.encryptionKey,
|
|
352
|
+
name,
|
|
353
|
+
createdAt: ts,
|
|
354
|
+
updatedAt: ts
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
if (accept !== false) this._wireAccept(child, { role })
|
|
358
|
+
this.children.add(child)
|
|
359
|
+
await this._syncMember(child)
|
|
360
|
+
return child
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Join a child handle by invite (joiner-flow). Waits for writer
|
|
365
|
+
* capability and registers the child on the parent's `handles`
|
|
366
|
+
* collection.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} invite
|
|
369
|
+
* @param {string} type
|
|
370
|
+
* @param {JoinChildOpts} [opts]
|
|
371
|
+
* @returns {Promise<Handle>}
|
|
372
|
+
*/
|
|
373
|
+
async _join(invite, type, { routes, timeout } = {}) {
|
|
374
|
+
const deadline = timeout || 30000
|
|
375
|
+
const child = /** @type {Child} */ (
|
|
376
|
+
await Handle.join(invite, {
|
|
377
|
+
parent: this,
|
|
378
|
+
spec: pickHandle(this.spec, type),
|
|
379
|
+
namespace: `cero/handle/${type}/${randomNs()}`,
|
|
380
|
+
routes,
|
|
381
|
+
timeout
|
|
382
|
+
})
|
|
383
|
+
)
|
|
384
|
+
await child.ready()
|
|
385
|
+
if (!child.store.writable) await child.store.whenWritable({ timeout: deadline })
|
|
386
|
+
|
|
387
|
+
const id = toId(child.store.key)
|
|
388
|
+
await this._saveKeyPair(id, child.store.keyPair)
|
|
389
|
+
|
|
390
|
+
const name = child.profile ? await waitForProfileName(child, deadline) : null
|
|
391
|
+
const ts = Date.now()
|
|
392
|
+
await this.store.call('add-handle', {
|
|
393
|
+
id,
|
|
394
|
+
type,
|
|
395
|
+
key: child.store.key,
|
|
396
|
+
encryptionKey: child.store.encryptionKey,
|
|
397
|
+
name,
|
|
398
|
+
createdAt: ts,
|
|
399
|
+
updatedAt: ts
|
|
400
|
+
})
|
|
401
|
+
this._wireAccept(child)
|
|
402
|
+
this.children.add(child)
|
|
403
|
+
await this._syncMember(child)
|
|
404
|
+
return child
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Re-open an existing child handle by id. Reuses the stored writer
|
|
409
|
+
* keypair if available; otherwise generates a fresh one and claims
|
|
410
|
+
* writer capability.
|
|
411
|
+
*
|
|
412
|
+
* @param {string} type
|
|
413
|
+
* @param {string} id
|
|
414
|
+
* @returns {Promise<Handle>}
|
|
415
|
+
*/
|
|
416
|
+
async _load(type, id) {
|
|
417
|
+
for (const c of this.children) if (c.id === id) return c
|
|
418
|
+
const { data } = await this.store.get('handles', id)
|
|
419
|
+
if (!data) throw CeroError.UNKNOWN('handle', id)
|
|
420
|
+
if (data.type !== type) throw new Error(`handle ${id} is type ${data.type}, not ${type}`)
|
|
421
|
+
let writer = await this._loadKeyPair(id)
|
|
422
|
+
const firstTime = !writer
|
|
423
|
+
if (firstTime) {
|
|
424
|
+
writer = Identity.randomKeyPair()
|
|
425
|
+
}
|
|
426
|
+
const child = new Handle({
|
|
427
|
+
parent: this,
|
|
428
|
+
spec: pickHandle(this.spec, type),
|
|
429
|
+
namespace: `cero/handle/${type}/${id}`,
|
|
430
|
+
key: data.key,
|
|
431
|
+
encryptionKey: data.encryptionKey,
|
|
432
|
+
keyPair: /** @type {KeyPair} */ (writer)
|
|
433
|
+
})
|
|
434
|
+
await child.ready()
|
|
435
|
+
if (firstTime && !child.store.writable) {
|
|
436
|
+
await child.store.claim({
|
|
437
|
+
name: this._opts.name || null,
|
|
438
|
+
isMobile: this._opts.isMobile === true
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
if (firstTime) {
|
|
442
|
+
await this._saveKeyPair(id, writer)
|
|
443
|
+
}
|
|
444
|
+
this._wireAccept(child)
|
|
445
|
+
this.children.add(child)
|
|
446
|
+
return child
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Pause networking + storage. Idempotent; no-op on child handles.
|
|
451
|
+
*
|
|
452
|
+
* @returns {Promise<void>}
|
|
453
|
+
*/
|
|
454
|
+
async suspend() {
|
|
455
|
+
if (this.parent || this.closing || this.closed || this._suspended) return
|
|
456
|
+
this._suspended = true
|
|
457
|
+
for (const c of this.children) if (c.pair) await c.pair.suspend()
|
|
458
|
+
await this.network.suspend()
|
|
459
|
+
try {
|
|
460
|
+
await this.store.store.suspend()
|
|
461
|
+
} catch (err) {
|
|
462
|
+
this._onerror(err)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Resume a suspended root handle. Idempotent; no-op on child handles.
|
|
468
|
+
*
|
|
469
|
+
* @returns {Promise<void>}
|
|
470
|
+
*/
|
|
471
|
+
async resume() {
|
|
472
|
+
if (this.parent || this.closing || this.closed || !this._suspended) return
|
|
473
|
+
this._suspended = false
|
|
474
|
+
try {
|
|
475
|
+
await this.store.store.resume()
|
|
476
|
+
} catch (err) {
|
|
477
|
+
this._onerror(err)
|
|
478
|
+
}
|
|
479
|
+
await this.network.resume()
|
|
480
|
+
await Promise.all([...this.children].map((child) => child.pair?.resume()))
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Pair into an existing handle via an invite, returning a brand-new
|
|
485
|
+
* `Handle` already configured with the resolved key + encryption key.
|
|
486
|
+
*
|
|
487
|
+
* @param {string} invite
|
|
488
|
+
* @param {StaticJoinOpts} [opts]
|
|
489
|
+
* @returns {Promise<Handle>}
|
|
490
|
+
*/
|
|
491
|
+
static async join(
|
|
492
|
+
invite,
|
|
493
|
+
{ parent, network, identity, store, spec, namespace, routes, timeout = 30000 } = {}
|
|
494
|
+
) {
|
|
495
|
+
const net = network || parent?.network
|
|
496
|
+
const id = identity || parent?.identity
|
|
497
|
+
if (!net) throw new TypeError('network is required')
|
|
498
|
+
if (!id) throw new TypeError('identity is required')
|
|
499
|
+
if (!store && !parent) throw new TypeError('store is required')
|
|
500
|
+
if (!spec) throw new TypeError('spec is required')
|
|
501
|
+
|
|
502
|
+
const writer = Identity.randomKeyPair()
|
|
503
|
+
const pair = new Pairing({ network: net, identity: id })
|
|
504
|
+
await pair.ready()
|
|
505
|
+
|
|
506
|
+
let key, encryptionKey
|
|
507
|
+
try {
|
|
508
|
+
const userData = b4a.concat([id.publicKey, writer.publicKey])
|
|
509
|
+
;({ key, encryptionKey } = await pair.join(invite, { userData, timeout }))
|
|
510
|
+
} finally {
|
|
511
|
+
await pair.close()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return new Handle({
|
|
515
|
+
parent,
|
|
516
|
+
store,
|
|
517
|
+
identity: id,
|
|
518
|
+
network: net,
|
|
519
|
+
spec,
|
|
520
|
+
namespace,
|
|
521
|
+
routes,
|
|
522
|
+
key,
|
|
523
|
+
encryptionKey,
|
|
524
|
+
keyPair: writer
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async _syncMember(child) {
|
|
529
|
+
if (!child.store.writable) return
|
|
530
|
+
const { data: profile } = await this.store.get('profile')
|
|
531
|
+
if (!profile) return
|
|
532
|
+
const { data: existing } = await child.store.get('members', this.identity.id)
|
|
533
|
+
if (!existing) return
|
|
534
|
+
await child.store.call('set-member', {
|
|
535
|
+
...existing,
|
|
536
|
+
...profile,
|
|
537
|
+
id: existing.id,
|
|
538
|
+
updatedAt: Date.now()
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @param {Handle} child
|
|
544
|
+
* @param {{ role?: string }} [opts]
|
|
545
|
+
*/
|
|
546
|
+
_wireAccept(child, { role } = {}) {
|
|
547
|
+
child.pair.on('candidate', (cand) => {
|
|
548
|
+
if (this.closing || this.closed || child.closing || child.closed) return
|
|
549
|
+
child.accept(cand, { role }).catch(this._onerror)
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* @param {string} id
|
|
555
|
+
* @param {KeyPair | { publicKey: Uint8Array, secretKey: Uint8Array } | null} keyPair
|
|
556
|
+
* @returns {Promise<void>}
|
|
557
|
+
*/
|
|
558
|
+
async _saveKeyPair(id, keyPair) {
|
|
559
|
+
if (!this.local || !keyPair) return
|
|
560
|
+
await this.local.store.put('handle-keypairs', {
|
|
561
|
+
id,
|
|
562
|
+
publicKey: keyPair.publicKey,
|
|
563
|
+
secretKey: keyPair.secretKey
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* @param {string} id
|
|
569
|
+
* @returns {Promise<{ publicKey: Uint8Array, secretKey: Uint8Array } | null>}
|
|
570
|
+
*/
|
|
571
|
+
async _loadKeyPair(id) {
|
|
572
|
+
if (!this.local) return null
|
|
573
|
+
const { data } = await this.local.store.get('handle-keypairs', id)
|
|
574
|
+
if (!data) return null
|
|
575
|
+
return {
|
|
576
|
+
publicKey: data.publicKey,
|
|
577
|
+
secretKey: data.secretKey
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function pickHandle(spec, type) {
|
|
583
|
+
const h = spec.handles?.[type]
|
|
584
|
+
if (!h) throw CeroError.UNKNOWN('handle type', type)
|
|
585
|
+
return h
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function randomNs() {
|
|
589
|
+
return z32.encode(Identity.randomBytes(8))
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function waitForProfileName(child, timeout) {
|
|
593
|
+
const deadline = Date.now() + timeout
|
|
594
|
+
while (Date.now() < deadline) {
|
|
595
|
+
const { data } = await child.store.get('profile')
|
|
596
|
+
if (data?.name) return data.name
|
|
597
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
598
|
+
}
|
|
599
|
+
return null
|
|
600
|
+
}
|