@cero-base/cero 1.18.1 → 1.19.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/README.md +5 -6
- package/package.json +10 -9
- package/src/handle/index.js +17 -37
- package/src/index.js +55 -71
- package/src/lib/operators.js +2 -2
- package/src/lib/refs.js +49 -0
- package/src/local/index.js +2 -2
- package/src/rpc/client.js +7 -5
- package/src/rpc/server.js +6 -5
- package/types/build/builtins.d.ts +46 -50
- package/types/build/index.d.ts +27 -7
- package/types/build/schemas.d.ts +3 -3
- package/types/extensions/handle-sync.d.ts +2 -2
- package/types/extensions/index.d.ts +2 -2
- package/types/extensions/profile-sync.d.ts +2 -2
- package/types/handle/index.d.ts +147 -165
- package/types/index.d.ts +82 -102
- package/types/lib/bluetooth.d.ts +17 -15
- package/types/lib/constants.d.ts +16 -16
- package/types/lib/internal.d.ts +5 -5
- package/types/lib/operators.d.ts +31 -31
- package/types/lib/peek.d.ts +1 -1
- package/types/lib/refs.d.ts +38 -0
- package/types/lib/spec.d.ts +6 -1
- package/types/local/index.d.ts +22 -20
- package/types/rpc/client.d.ts +111 -123
- package/types/rpc/index.d.ts +7 -2
- package/types/rpc/server.d.ts +56 -67
- package/src/lib/utils.js +0 -67
- package/types/lib/utils.d.ts +0 -55
package/README.md
CHANGED
|
@@ -52,9 +52,8 @@ Ships with TypeScript declarations (`.d.ts`) generated from JSDoc.
|
|
|
52
52
|
| `channel` | Optional network-isolation label. Peers connect only to peers on the **same** channel (it salts every swarm topic); omit it for the global network. A storage remembers its channel and refuses to reopen under a different one. Any string works. |
|
|
53
53
|
| `name` / `isMobile` | Stamped on the device's `add-writer` event. |
|
|
54
54
|
| `seed` / `phrase` | Restore from explicit 16-/32-byte entropy or a BIP-39 mnemonic. Without either, a stored identity is loaded if present, else a fresh one is generated. |
|
|
55
|
-
| `key` |
|
|
56
|
-
| `
|
|
57
|
-
| `recoveryTimeout` | Bound on the recovery wait. |
|
|
55
|
+
| `key` | Existing database key to recover into, skipping the pointer lookup. |
|
|
56
|
+
| `recoveryTimeout` | Max wait to find another device and be admitted, in ms. Defaults to 30000. A supplied `seed`/`phrase` on a device with no writer recovers — there is no flag. |
|
|
58
57
|
| `routes` | Custom action handlers keyed by route name. |
|
|
59
58
|
| `encryptionKey` | Override the per-identity encryption key. |
|
|
60
59
|
| `storageKey` | 32-byte key encrypting local key material (master seed, device keypairs) at rest. Source it from the OS keychain — cero never stores it, and the same key must be passed on every open. Without it, key material sits plaintext on disk: rely on full-disk encryption. |
|
|
@@ -496,7 +495,7 @@ import { spec } from './spec/index.js'
|
|
|
496
495
|
|
|
497
496
|
// serve builds and owns the root cero — pass storage + the built spec, not a handle.
|
|
498
497
|
// Returns a Server instance (call server.close() to shut down).
|
|
499
|
-
const server = await serve(Bare.IPC, { storage: './data',
|
|
498
|
+
const server = await serve(Bare.IPC, spec, { storage: './data', phrase: '…' })
|
|
500
499
|
```
|
|
501
500
|
|
|
502
501
|
### Client (the UI process)
|
|
@@ -566,7 +565,7 @@ function streamPair() {
|
|
|
566
565
|
return [a, b]
|
|
567
566
|
}
|
|
568
567
|
const [s, c] = streamPair()
|
|
569
|
-
await serve(s, { storage: './data'
|
|
568
|
+
await serve(s, spec, { storage: './data' })
|
|
570
569
|
const remote = await connect(c, spec)
|
|
571
570
|
```
|
|
572
571
|
|
|
@@ -661,7 +660,7 @@ Point your app's `mirrors` at that key. Storage grows with the rooms it holds; `
|
|
|
661
660
|
| ---------------------------- | ------------------------------------------------------------------------ |
|
|
662
661
|
| `@cero-base/cero` | factory + operators + schema DSL + `define`/`bind` |
|
|
663
662
|
| `@cero-base/cero/client` | `connect(ipc, spec)` — talk to a cero running in another process |
|
|
664
|
-
| `@cero-base/cero/server` | `serve(ipc, { storage
|
|
663
|
+
| `@cero-base/cero/server` | `serve(ipc, spec, { storage })` — run + expose a cero over an IPC stream |
|
|
665
664
|
| `@cero-base/cero/build` | `build(specDir, schema)` — generate the on-disk spec |
|
|
666
665
|
| `@cero-base/cero/extensions` | bundled extensions (`profileSync`, …) |
|
|
667
666
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -85,28 +85,29 @@
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"scripts": {
|
|
88
|
-
"build:test": "rm -rf test/
|
|
88
|
+
"build:test": "rm -rf test/fixtures/spec && node test/fixtures/build.js",
|
|
89
89
|
"build:types": "rm -rf types && tsc -p .",
|
|
90
90
|
"pretest": "npm run build:test",
|
|
91
91
|
"prepublishOnly": "npm run build:types",
|
|
92
92
|
"test": "npm run test:bare",
|
|
93
93
|
"pretest:bare": "npm run build:test",
|
|
94
|
-
"test:bare": "
|
|
95
|
-
"test:node": "
|
|
94
|
+
"test:bare": "find test -name '*.test.js' | sort | xargs -P1 -n1 brittle-bare test/bare.js",
|
|
95
|
+
"test:node": "find test -name '*.test.js' | sort | xargs -P1 -n1 brittle-node"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@cero-base/core": "^1.
|
|
98
|
+
"@cero-base/core": "^1.19.0",
|
|
99
99
|
"b4a": "^1.8.1",
|
|
100
100
|
"bare-abort-controller": "^1.1.2",
|
|
101
101
|
"bare-crypto": "^1.15.3",
|
|
102
102
|
"bare-fs": "^4.8.1",
|
|
103
103
|
"bare-path": "^3.1.2",
|
|
104
104
|
"ble-swarm": "^2.3.0",
|
|
105
|
-
"compact-encoding": "^3.
|
|
106
|
-
"corestore": "^7.12.
|
|
105
|
+
"compact-encoding": "^3.4.0",
|
|
106
|
+
"corestore": "^7.12.3",
|
|
107
107
|
"hrpc": "^4.3.1",
|
|
108
|
-
"hypercore": "^11.35.
|
|
108
|
+
"hypercore": "^11.35.4",
|
|
109
109
|
"hypercore-crypto": "^3.7.0",
|
|
110
|
+
"hypercore-id-encoding": "^1.3.0",
|
|
110
111
|
"hypercore-storage": "^3.2.1",
|
|
111
112
|
"hyperdb": "^6.9.0",
|
|
112
113
|
"hyperdispatch": "^1.6.0",
|
|
@@ -124,7 +125,7 @@
|
|
|
124
125
|
"bare-process": "^4.5.1",
|
|
125
126
|
"bare-url": "^2.5.4",
|
|
126
127
|
"brittle": "^4.1.1",
|
|
127
|
-
"typescript": "^
|
|
128
|
+
"typescript": "^7.0.2"
|
|
128
129
|
},
|
|
129
130
|
"license": "Apache-2.0"
|
|
130
131
|
}
|
package/src/handle/index.js
CHANGED
|
@@ -12,7 +12,8 @@ import { Identity } from '@cero-base/core/identity'
|
|
|
12
12
|
import { Database } from '@cero-base/core/database'
|
|
13
13
|
import { epochEntries, blobEpochKey } from '@cero-base/core/database/encryption'
|
|
14
14
|
import { Pairing } from '@cero-base/core/pairing'
|
|
15
|
-
import
|
|
15
|
+
import hid from 'hypercore-id-encoding'
|
|
16
|
+
import { grants, can, isRank, admission, REMOVE, onAbort } from '@cero-base/core/utils'
|
|
16
17
|
import { CeroError } from '@cero-base/core/errors'
|
|
17
18
|
import { Blobs } from '@cero-base/core/blobs'
|
|
18
19
|
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
@@ -20,10 +21,10 @@ import { FileServer } from '@cero-base/core/blobs/server'
|
|
|
20
21
|
|
|
21
22
|
import { NS, TIMEOUT } from '../lib/constants.js'
|
|
22
23
|
|
|
23
|
-
import {
|
|
24
|
+
import { Ref } from '../lib/refs.js'
|
|
24
25
|
import { bind } from '../lib/operators.js'
|
|
25
26
|
|
|
26
|
-
export { Ref } from '../lib/
|
|
27
|
+
export { Ref } from '../lib/refs.js'
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* @typedef {import('@cero-base/core/network').Network} Network
|
|
@@ -74,17 +75,14 @@ export { Ref } from '../lib/utils.js'
|
|
|
74
75
|
* @property {string} [role] Role to grant the joining peer. Falls back to the invite's role, then `'member'`.
|
|
75
76
|
* @property {string | null} [name]
|
|
76
77
|
*
|
|
77
|
-
* @typedef {object} RecoverOpts
|
|
78
|
-
* @property {number} [timeout]
|
|
79
|
-
*
|
|
80
78
|
* @typedef {object} HandleExtra
|
|
81
79
|
* @property {string | null} [name] Display name; set on child handles by the owner flow.
|
|
82
|
-
* @property {import('../lib/
|
|
83
|
-
* @property {import('../lib/
|
|
80
|
+
* @property {import('../lib/refs.js').Ref} [profile] `profile` ref, attached dynamically when the schema declares one.
|
|
81
|
+
* @property {import('../lib/refs.js').Ref} [members] `members` ref, attached dynamically when the schema declares one.
|
|
84
82
|
*
|
|
85
83
|
* @typedef {Handle & HandleExtra} Child A child handle plus its dynamically-attached refs.
|
|
86
84
|
*
|
|
87
|
-
* @typedef {Handle & Record<string, import('../lib/
|
|
85
|
+
* @typedef {Handle & Record<string, import('../lib/refs.js').Ref>} CeroHandle A handle with every schema ref reachable as a `Ref` property (e.g. `me.profile`, `room.messages`).
|
|
88
86
|
*/
|
|
89
87
|
|
|
90
88
|
/**
|
|
@@ -171,7 +169,7 @@ export class Handle extends ReadyResource {
|
|
|
171
169
|
this._invitesSync = () => this._syncInvites().catch(safetyCatch)
|
|
172
170
|
this.store.on('update', this._invitesSync)
|
|
173
171
|
}
|
|
174
|
-
|
|
172
|
+
Ref.attach(this, this.store.refs)
|
|
175
173
|
this.root._coreKeys.set(b4a.toString(this.store.key, 'hex'), this.store.encryptionKey)
|
|
176
174
|
if (!this.parent) await this.fileServer.listen()
|
|
177
175
|
}
|
|
@@ -406,14 +404,14 @@ export class Handle extends ReadyResource {
|
|
|
406
404
|
/** Canonical id — identity id for the root handle, store key for children. */
|
|
407
405
|
get id() {
|
|
408
406
|
if (!this.parent) return this.identity.id
|
|
409
|
-
return this.store?.key ?
|
|
407
|
+
return this.store?.key ? hid.encode(this.store.key) : null
|
|
410
408
|
}
|
|
411
409
|
|
|
412
410
|
/** This device's id + name. `null` on child handles. */
|
|
413
411
|
get device() {
|
|
414
412
|
if (this.parent) return null
|
|
415
413
|
const k = this.store?.writerKey
|
|
416
|
-
return k ? { id:
|
|
414
|
+
return k ? { id: hid.encode(k), name: this._opts.name || null } : null
|
|
417
415
|
}
|
|
418
416
|
|
|
419
417
|
get suspended() {
|
|
@@ -441,20 +439,6 @@ export class Handle extends ReadyResource {
|
|
|
441
439
|
return this.store.claim()
|
|
442
440
|
}
|
|
443
441
|
|
|
444
|
-
/**
|
|
445
|
-
* Claim + wait until this peer becomes a writer + bring the bee up to date.
|
|
446
|
-
* Used by `restore()` after wiping local state.
|
|
447
|
-
*
|
|
448
|
-
* @param {RecoverOpts} [opts]
|
|
449
|
-
* @returns {Promise<void>}
|
|
450
|
-
*/
|
|
451
|
-
async recover({ timeout = TIMEOUT } = {}) {
|
|
452
|
-
if (this.store.writable) return
|
|
453
|
-
await this.store.claim()
|
|
454
|
-
await this.store.whenWritable({ timeout })
|
|
455
|
-
await this.store.bee.update()
|
|
456
|
-
}
|
|
457
|
-
|
|
458
442
|
/**
|
|
459
443
|
* Flip this handle's swarm announce mode — `setActive(false)` demotes an
|
|
460
444
|
* idle/background room to server-only (still reachable, stops searching);
|
|
@@ -568,7 +552,7 @@ export class Handle extends ReadyResource {
|
|
|
568
552
|
const ts = Date.now()
|
|
569
553
|
const writerKey = Hypercore.key({ version: 2, signers: [{ publicKey: data.subarray(32, 64) }] })
|
|
570
554
|
const member = {
|
|
571
|
-
id:
|
|
555
|
+
id: hid.encode(data.subarray(0, 32)),
|
|
572
556
|
key: writerKey,
|
|
573
557
|
role,
|
|
574
558
|
name: name || null,
|
|
@@ -581,9 +565,7 @@ export class Handle extends ReadyResource {
|
|
|
581
565
|
return
|
|
582
566
|
}
|
|
583
567
|
|
|
584
|
-
const sig = this.identity.sign(
|
|
585
|
-
addWriterPayload(this.store.key, writerKey, this.store.writerKey)
|
|
586
|
-
)
|
|
568
|
+
const sig = this.identity.sign(admission(this.store.key, writerKey, this.store.writerKey))
|
|
587
569
|
await this.store.tx(async (tx) => {
|
|
588
570
|
await tx.call('add-writer', {
|
|
589
571
|
sig,
|
|
@@ -618,7 +600,7 @@ export class Handle extends ReadyResource {
|
|
|
618
600
|
*/
|
|
619
601
|
async leave() {
|
|
620
602
|
if (!this.parent) return
|
|
621
|
-
await this.parent.store.call('del-handle', { id:
|
|
603
|
+
await this.parent.store.call('del-handle', { id: hid.encode(this.store.key) })
|
|
622
604
|
await this.close()
|
|
623
605
|
}
|
|
624
606
|
|
|
@@ -652,7 +634,7 @@ export class Handle extends ReadyResource {
|
|
|
652
634
|
await child.ready()
|
|
653
635
|
child.name = name
|
|
654
636
|
|
|
655
|
-
const id =
|
|
637
|
+
const id = hid.encode(child.store.key)
|
|
656
638
|
// add-handle makes the row visible before this create finishes, and a
|
|
657
639
|
// duplicate Handle over the same core deadlocks in ready()
|
|
658
640
|
const inflight = new Promise((resolve, reject) => {
|
|
@@ -671,9 +653,7 @@ export class Handle extends ReadyResource {
|
|
|
671
653
|
await tx.call('add-writer', {
|
|
672
654
|
master: this.identity.publicKey,
|
|
673
655
|
writer: writerKey,
|
|
674
|
-
sig: this.identity.sign(
|
|
675
|
-
addWriterPayload(child.store.key, writerKey, child.store.writerKey)
|
|
676
|
-
),
|
|
656
|
+
sig: this.identity.sign(admission(child.store.key, writerKey, child.store.writerKey)),
|
|
677
657
|
ts
|
|
678
658
|
})
|
|
679
659
|
await tx.call('add-member', {
|
|
@@ -758,7 +738,7 @@ export class Handle extends ReadyResource {
|
|
|
758
738
|
if (existing) {
|
|
759
739
|
const known = await this._load(type, existing.id)
|
|
760
740
|
const { data: me } = await known.store.get('members', this.identity.id)
|
|
761
|
-
const { data: device } = await known.store.get('devices',
|
|
741
|
+
const { data: device } = await known.store.get('devices', hid.encode(known.store.writerKey))
|
|
762
742
|
if (me && device) return known
|
|
763
743
|
await known.close().catch(safetyCatch)
|
|
764
744
|
}
|
|
@@ -784,7 +764,7 @@ export class Handle extends ReadyResource {
|
|
|
784
764
|
await child.ready()
|
|
785
765
|
if (!child.store.writable) await child.store.whenWritable({ timeout: deadline })
|
|
786
766
|
|
|
787
|
-
const id =
|
|
767
|
+
const id = hid.encode(child.store.key)
|
|
788
768
|
// same create/open race as _create, a concurrent _load must share this child
|
|
789
769
|
const inflight = new Promise((resolve, reject) => {
|
|
790
770
|
publish = resolve
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import Hypercore from 'hypercore'
|
|
|
2
2
|
import HypercoreStorage from 'hypercore-storage'
|
|
3
3
|
import Corestore from 'corestore'
|
|
4
4
|
import safetyCatch from 'safety-catch'
|
|
5
|
+
import c from 'compact-encoding'
|
|
5
6
|
import fs from 'fs'
|
|
6
7
|
|
|
7
8
|
import { Identity } from '@cero-base/core/identity'
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
} from './lib/operators.js'
|
|
30
31
|
import { peek } from './lib/peek.js'
|
|
31
32
|
import { t, schema } from './lib/spec.js'
|
|
32
|
-
import { FLUSH } from './lib/constants.js'
|
|
33
|
+
import { FLUSH, TIMEOUT } from './lib/constants.js'
|
|
33
34
|
import { internal } from './lib/internal.js'
|
|
34
35
|
|
|
35
36
|
export { Handle, Ref, Local }
|
|
@@ -68,12 +69,11 @@ export { t, schema } from './lib/spec.js'
|
|
|
68
69
|
* @property {number[]} [backoffs] Swarm reconnect backoff tiers in ms (testing/tuning).
|
|
69
70
|
* @property {string} [channel] Optional network-isolation label; only same-channel peers connect.
|
|
70
71
|
* @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys. Rooms and files are mirrored through them so peers sync even when never online at the same time. Mirrors hold only encrypted blocks — they never read your data.
|
|
71
|
-
* @property {Uint8Array} [key]
|
|
72
|
+
* @property {Uint8Array} [key] Existing database key to recover into, skipping the pointer lookup.
|
|
72
73
|
* @property {Uint8Array} [encryptionKey] Pre-existing encryption key.
|
|
73
74
|
* @property {Record<string, Function>} [routes] Custom RPC routes for the database dispatcher.
|
|
74
75
|
* @property {(err: any) => void} [onerror] Background-task error handler.
|
|
75
|
-
* @property {
|
|
76
|
-
* @property {number} [recoveryTimeout] Max wait for peer data + writer capability during recovery.
|
|
76
|
+
* @property {number} [recoveryTimeout] Max wait to find another device and be admitted, in ms. Defaults to 30000.
|
|
77
77
|
* @property {Uint8Array} [storageKey] 32-byte key encrypting local key material (master seed, device keypairs) at rest. Source it from the OS keychain — cero never stores it.
|
|
78
78
|
* @property {boolean} [extensions] `false` disables the bundled extensions (profileSync, handleSync) for this instance. Build with `{ extensions: false }` too so the spec matches.
|
|
79
79
|
* @property {boolean | { autoStart?: boolean, backend?: any, maxOutbound?: number, maxInbound?: number, pipe?: 'l2cap' | 'gatt' }} [bluetooth] `true` enables nearby (Bluetooth) sync via `me.bluetooth` (auto-started). `{ autoStart: false }` creates the facade without starting the radio — the app calls `me.bluetooth.start()`/`stop()` (user toggle). `backend` injects a bare-bluetooth-shaped backend (tests). `maxOutbound`/`maxInbound` cap concurrent outbound links and inbound sessions. `pipe` picks the data pipe — `'l2cap'` (default, faster) or `'gatt'`; both peers must match. Absent backend on an unsupported host → `me.bluetooth.state === 'unsupported'`.
|
|
@@ -115,6 +115,11 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
115
115
|
|
|
116
116
|
const { identity, fresh } = await resolveIdentity(opts, local)
|
|
117
117
|
const writer = local ? (await local.store.get('keypair')).data : null
|
|
118
|
+
// a supplied identity on a device with no writer is recovering: it never
|
|
119
|
+
// authors the identity's pointer core or any core that exists elsewhere.
|
|
120
|
+
// Only an identity cero generated here creates a database
|
|
121
|
+
const recovering = !writer && (!!opts.key || !fresh)
|
|
122
|
+
const timeout = opts.recoveryTimeout || TIMEOUT
|
|
118
123
|
|
|
119
124
|
// Channel stamp: a storage remembers its channel; reopening it under a different channel —
|
|
120
125
|
// including no channel at all, which would silently rejoin the global network — is a
|
|
@@ -140,6 +145,20 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
140
145
|
// before proceeding only delays first onboarding
|
|
141
146
|
if (!fresh) await Promise.race([discovery.flush(), new Promise((r) => setTimeout(r, FLUSH))])
|
|
142
147
|
|
|
148
|
+
// the pointer core: signed by the identity, written once by the device that
|
|
149
|
+
// created the identity, read by every device that recovers it. It holds the
|
|
150
|
+
// root database key, so a phrase alone finds the data
|
|
151
|
+
const manifest = pointerManifest(store, identity)
|
|
152
|
+
const pointer = store.get(
|
|
153
|
+
!writer && !recovering
|
|
154
|
+
? { keyPair: { publicKey: identity.publicKey, secretKey: identity.secretKey }, manifest }
|
|
155
|
+
: { key: Hypercore.key(manifest) }
|
|
156
|
+
)
|
|
157
|
+
await pointer.ready()
|
|
158
|
+
network.attach(pointer)
|
|
159
|
+
|
|
160
|
+
const key = opts.key || (recovering ? await readPointer(pointer, timeout) : undefined)
|
|
161
|
+
|
|
143
162
|
me = new Handle({
|
|
144
163
|
storage,
|
|
145
164
|
store,
|
|
@@ -151,57 +170,31 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
151
170
|
opts,
|
|
152
171
|
dir,
|
|
153
172
|
routes: opts.routes,
|
|
154
|
-
key
|
|
173
|
+
key,
|
|
155
174
|
encryptionKey: opts.encryptionKey,
|
|
156
175
|
keyPair: writer ? { publicKey: writer.publicKey, secretKey: writer.secretKey } : undefined,
|
|
157
176
|
pair: false
|
|
158
177
|
})
|
|
159
178
|
await me.ready()
|
|
179
|
+
me.once('close', () => {
|
|
180
|
+
network.detach(pointer)
|
|
181
|
+
pointer.close().catch(safetyCatch)
|
|
182
|
+
})
|
|
160
183
|
|
|
161
|
-
|
|
162
|
-
// authoring genesis twice forks the seed-derived writer core (a writable
|
|
163
|
-
// core has one author, ever). Ask the peers directly with a plain session
|
|
164
|
-
// on the genesis core: the base never replicates an empty local core.
|
|
165
|
-
// Offline reuse is undetectable — this catches the reachable-peer case.
|
|
166
|
-
if (!writer && !opts.key && !opts.recovery && !fresh) {
|
|
167
|
-
const genesis = store.get({
|
|
168
|
-
key: Hypercore.key({
|
|
169
|
-
version: store.manifestVersion,
|
|
170
|
-
signers: [{ publicKey: identity.publicKey }]
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
await genesis.ready()
|
|
174
|
-
network.attach(genesis)
|
|
175
|
-
try {
|
|
176
|
-
const until = Date.now() + 1500
|
|
177
|
-
while (Date.now() < until) {
|
|
178
|
-
if (genesis.length > 0 || genesis.peers.some((p) => p.remoteLength > 0)) {
|
|
179
|
-
throw CeroError.CONFLICT('identity already has history — open with { recovery: true }')
|
|
180
|
-
}
|
|
181
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
182
|
-
}
|
|
183
|
-
} finally {
|
|
184
|
-
network.detach(genesis)
|
|
185
|
-
await genesis.close()
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// decided by the stored writer and the caller's intent, never by the local
|
|
189
|
-
// core's length: a same-identity device opens on the genesis core, which
|
|
190
|
-
// fills with the first device's blocks as soon as a peer connects
|
|
191
|
-
if (!writer && (!opts.key || opts.recovery)) {
|
|
184
|
+
if (!writer) {
|
|
192
185
|
const result = await me.bootstrap({
|
|
193
186
|
name: opts.name || null,
|
|
194
187
|
isMobile: opts.isMobile === true,
|
|
195
|
-
recovering
|
|
196
|
-
|
|
188
|
+
recovering,
|
|
189
|
+
timeout
|
|
197
190
|
})
|
|
198
|
-
if (local
|
|
191
|
+
if (local) {
|
|
199
192
|
await local.store.set('keypair', {
|
|
200
193
|
publicKey: result.writer.publicKey,
|
|
201
194
|
secretKey: result.writer.secretKey
|
|
202
195
|
})
|
|
203
196
|
}
|
|
204
|
-
if (!
|
|
197
|
+
if (!recovering) {
|
|
205
198
|
const ts = Date.now()
|
|
206
199
|
await me.store.call('add-member', {
|
|
207
200
|
id: identity.id,
|
|
@@ -211,9 +204,9 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
211
204
|
createdAt: ts,
|
|
212
205
|
updatedAt: ts
|
|
213
206
|
})
|
|
207
|
+
if (pointer.length === 0) await pointer.append(c.encode(c.fixed32, me.store.key))
|
|
214
208
|
}
|
|
215
209
|
}
|
|
216
|
-
if (opts.recovery) await me.recover({ timeout: opts.recoveryTimeout })
|
|
217
210
|
|
|
218
211
|
for (const ext of internal.extensions) {
|
|
219
212
|
if (ext.bundled && opts.extensions === false) continue
|
|
@@ -263,8 +256,8 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
263
256
|
|
|
264
257
|
/**
|
|
265
258
|
* Restore a cero instance from a mnemonic phrase. Closes the running
|
|
266
|
-
* instance, wipes the on-disk `main/` tree and re-opens with
|
|
267
|
-
* so the writer slot is re-claimed.
|
|
259
|
+
* instance, wipes the on-disk `main/` tree and re-opens with the phrase, which
|
|
260
|
+
* recovers so the writer slot is re-claimed.
|
|
268
261
|
*
|
|
269
262
|
* @param {Handle} me Existing root handle to restore.
|
|
270
263
|
* @param {string} phrase BIP-39 mnemonic phrase.
|
|
@@ -278,39 +271,18 @@ export async function restore(me, phrase) {
|
|
|
278
271
|
const current = await Identity.fromSeed(Identity.toSeed(phrase))
|
|
279
272
|
if (current.id === me.identity.id) return me
|
|
280
273
|
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
// global identity topic and never meets its channeled peers (recovery timeout).
|
|
274
|
+
// everything carries over except the old identity — the channel in
|
|
275
|
+
// particular, or the recovered instance never meets its peers
|
|
284
276
|
const {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
routes,
|
|
290
|
-
recoveryTimeout,
|
|
291
|
-
channel,
|
|
292
|
-
mirrors,
|
|
293
|
-
storageKey,
|
|
294
|
-
extensions
|
|
295
|
-
} = opts
|
|
277
|
+
_dir: dir,
|
|
278
|
+
spec,
|
|
279
|
+
_opts: { seed, identity, key, keyPair, ...opts }
|
|
280
|
+
} = me
|
|
296
281
|
|
|
297
282
|
await me.close()
|
|
298
283
|
await fs.promises.rm(`${dir}/main`, { recursive: true, force: true })
|
|
299
284
|
|
|
300
|
-
return cero(dir, spec, {
|
|
301
|
-
name,
|
|
302
|
-
bootstrap,
|
|
303
|
-
isMobile,
|
|
304
|
-
onerror,
|
|
305
|
-
routes,
|
|
306
|
-
recoveryTimeout,
|
|
307
|
-
channel,
|
|
308
|
-
mirrors,
|
|
309
|
-
storageKey,
|
|
310
|
-
extensions,
|
|
311
|
-
phrase,
|
|
312
|
-
recovery: true
|
|
313
|
-
})
|
|
285
|
+
return cero(dir, spec, { ...opts, phrase })
|
|
314
286
|
}
|
|
315
287
|
|
|
316
288
|
// Facade: expose the operators + helpers as properties on `cero` too, so both
|
|
@@ -349,6 +321,18 @@ cero.use = (...exts) => {
|
|
|
349
321
|
}
|
|
350
322
|
}
|
|
351
323
|
|
|
324
|
+
function pointerManifest(store, identity) {
|
|
325
|
+
return { version: store.manifestVersion, signers: [{ publicKey: identity.publicKey }] }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function readPointer(pointer, timeout) {
|
|
329
|
+
try {
|
|
330
|
+
return c.decode(c.fixed32, await pointer.get(0, { timeout }))
|
|
331
|
+
} catch {
|
|
332
|
+
throw CeroError.TIMED_OUT('recovery: finding a device of this identity')
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
352
336
|
async function resolveIdentity(opts, local) {
|
|
353
337
|
if (opts.identity) return { identity: opts.identity, fresh: false }
|
|
354
338
|
|
package/src/lib/operators.js
CHANGED
|
@@ -3,10 +3,10 @@ import b4a from 'b4a'
|
|
|
3
3
|
|
|
4
4
|
import { encodeId, decodeId } from '@cero-base/core/blobs/codec'
|
|
5
5
|
import { CeroError } from '@cero-base/core/errors'
|
|
6
|
-
import { onAbort } from '
|
|
6
|
+
import { onAbort } from '@cero-base/core/utils'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @typedef {import('./
|
|
9
|
+
* @typedef {import('./refs.js').Ref} Ref
|
|
10
10
|
* @typedef {import('../handle/index.js').CeroHandle} CeroHandle
|
|
11
11
|
* @typedef {{ data: any }} SingleResult
|
|
12
12
|
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
package/src/lib/refs.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CeroError } from '@cero-base/core/errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'collection' | 'single' | 'action' | 'handle'} RefKind
|
|
5
|
+
* @typedef {{ kind?: string, schema?: string }} RefInfo
|
|
6
|
+
* Shape of the entries in `meta.refs` — describes a single ref name.
|
|
7
|
+
* `kind` is one of {@link RefKind}, kept as `string` since it originates
|
|
8
|
+
* from a generated spec.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Typed pointer to a single ref (table or handle slot) on a `Handle` or
|
|
13
|
+
* `Local`. Operators (`put`/`get`/`open`/...) take a `Ref` as their first
|
|
14
|
+
* argument and dispatch through the owning handle's store.
|
|
15
|
+
*/
|
|
16
|
+
export class Ref {
|
|
17
|
+
/**
|
|
18
|
+
* @param {any} handle Owner — a `Handle` (or `Local`) the ref lives on.
|
|
19
|
+
* @param {string} name Ref name as declared in the schema.
|
|
20
|
+
* @param {string} kind Ref kind: `'collection'`, `'single'`, `'action'`, or `'handle'`.
|
|
21
|
+
* @param {string | null} [schema] Fully-qualified schema id, if any.
|
|
22
|
+
*/
|
|
23
|
+
constructor(handle, name, kind, schema = null) {
|
|
24
|
+
this.handle = handle
|
|
25
|
+
this.name = name
|
|
26
|
+
this.kind = kind
|
|
27
|
+
this.schema = schema
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Attach a `Ref` property to `target` for every entry in `refs`, so callers
|
|
32
|
+
* write `handle.someRef` instead of looking refs up by name.
|
|
33
|
+
*
|
|
34
|
+
* @param {any} target
|
|
35
|
+
* @param {Record<string, RefInfo>} refs
|
|
36
|
+
*/
|
|
37
|
+
static attach(target, refs) {
|
|
38
|
+
for (const [name, info] of Object.entries(refs || {})) {
|
|
39
|
+
// fail loud rather than silently overwrite a method or property (close,
|
|
40
|
+
// on, store) when a schema declares a ref named like a reserved member
|
|
41
|
+
if (name in target) {
|
|
42
|
+
throw CeroError.INVALID(
|
|
43
|
+
`schema ref '${name}' collides with a reserved ${target.constructor?.name || 'handle'} member — rename it`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
target[name] = new Ref(target, name, info.kind, info.schema)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/local/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import ReadyResource from 'ready-resource'
|
|
|
3
3
|
import { Storage } from '@cero-base/core/storage'
|
|
4
4
|
import { CeroError } from '@cero-base/core/errors'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Ref } from '../lib/refs.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {object} LocalOpts
|
|
@@ -43,7 +43,7 @@ export class Local extends ReadyResource {
|
|
|
43
43
|
|
|
44
44
|
async _open() {
|
|
45
45
|
await this.store.ready()
|
|
46
|
-
|
|
46
|
+
Ref.attach(this, this.store.refs)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async _close() {
|
package/src/rpc/client.js
CHANGED
|
@@ -4,7 +4,7 @@ import c from 'compact-encoding'
|
|
|
4
4
|
import z32 from 'z32'
|
|
5
5
|
import { decodeId } from '@cero-base/core/blobs/codec'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Ref } from '../lib/refs.js'
|
|
8
8
|
import {
|
|
9
9
|
put,
|
|
10
10
|
set,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
del,
|
|
13
13
|
count,
|
|
14
14
|
watch,
|
|
15
|
+
changes,
|
|
15
16
|
call,
|
|
16
17
|
open,
|
|
17
18
|
rotate,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
} from '../lib/operators.js'
|
|
21
22
|
import { t, schema } from '../lib/spec.js'
|
|
22
23
|
|
|
23
|
-
export { put, set, get, del, count, watch, call, open, rotate, bind, define, t, schema }
|
|
24
|
+
export { put, set, get, del, count, watch, changes, call, open, rotate, bind, define, t, schema }
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* @typedef {import('@cero-base/core/rpc').RPCClient} BaseRPCClient
|
|
@@ -449,7 +450,7 @@ class LocalRefs {
|
|
|
449
450
|
this._local = true
|
|
450
451
|
const refs = client.spec.meta.local?.refs || {}
|
|
451
452
|
const exposed = Object.fromEntries(Object.entries(refs).filter(([, info]) => !info.builtin))
|
|
452
|
-
|
|
453
|
+
Ref.attach(this, exposed)
|
|
453
454
|
}
|
|
454
455
|
|
|
455
456
|
/** Underlying RPC channel borrowed from the parent. */
|
|
@@ -491,7 +492,7 @@ export class Client extends RPCClient {
|
|
|
491
492
|
this._fileBase = fileBase || ''
|
|
492
493
|
this._fileToken = fileToken || ''
|
|
493
494
|
this.identity = { id, toPhrase: async () => (await this.rpc.seed({})).phrase || null }
|
|
494
|
-
|
|
495
|
+
Ref.attach(this, /** @type {Spec} */ (this.spec).meta.refs)
|
|
495
496
|
bind(this, null)
|
|
496
497
|
if (/** @type {Spec} */ (this.spec).meta.local?.refs) this.local = new LocalRefs(this)
|
|
497
498
|
}
|
|
@@ -560,7 +561,7 @@ class Handle {
|
|
|
560
561
|
this.spec = /** @type {Spec} */ (parent.spec).handles[type]
|
|
561
562
|
if (!this.spec.codec) bindCodec(this.spec)
|
|
562
563
|
this.store = this
|
|
563
|
-
|
|
564
|
+
Ref.attach(this, this.spec.meta.refs)
|
|
564
565
|
bind(this, this.type)
|
|
565
566
|
}
|
|
566
567
|
|
|
@@ -618,6 +619,7 @@ cero.get = get
|
|
|
618
619
|
cero.del = del
|
|
619
620
|
cero.count = count
|
|
620
621
|
cero.watch = watch
|
|
622
|
+
cero.changes = changes
|
|
621
623
|
cero.call = call
|
|
622
624
|
cero.open = open
|
|
623
625
|
cero.rotate = rotate
|
package/src/rpc/server.js
CHANGED
|
@@ -12,7 +12,6 @@ import { put, set, get, del, count, watch, changes, call } from '../lib/operator
|
|
|
12
12
|
*
|
|
13
13
|
* @typedef {object} ServerOpts
|
|
14
14
|
* @property {string} storage Directory passed to `cero()` for the local store.
|
|
15
|
-
* @property {object} spec Compiled cero spec (schema + rpc + handles).
|
|
16
15
|
* @property {string} [name] Optional display name forwarded to `cero()`.
|
|
17
16
|
* @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap.
|
|
18
17
|
* @property {boolean} [isMobile]
|
|
@@ -36,11 +35,12 @@ import { put, set, get, del, count, watch, changes, call } from '../lib/operator
|
|
|
36
35
|
export class Server extends RPCServer {
|
|
37
36
|
/**
|
|
38
37
|
* @param {any} ipc Framed IPC stream (must be writable).
|
|
38
|
+
* @param {object} spec
|
|
39
39
|
* @param {Partial<ServerOpts>} [opts]
|
|
40
40
|
*/
|
|
41
|
-
constructor(ipc, { storage,
|
|
42
|
-
if (!storage) throw CeroError.REQUIRED('storage')
|
|
41
|
+
constructor(ipc, spec, { storage, ...opts } = {}) {
|
|
43
42
|
if (!spec) throw CeroError.REQUIRED('spec')
|
|
43
|
+
if (!storage) throw CeroError.REQUIRED('storage')
|
|
44
44
|
super(ipc, spec)
|
|
45
45
|
if (spec.local?.schema && !spec.local.codec) bindCodec(spec.local)
|
|
46
46
|
this.storage = storage
|
|
@@ -409,11 +409,12 @@ export class Server extends RPCServer {
|
|
|
409
409
|
* Construct a `Server`, wait for it to be ready, and return it.
|
|
410
410
|
*
|
|
411
411
|
* @param {any} ipc
|
|
412
|
+
* @param {object} spec
|
|
412
413
|
* @param {ServerOpts} opts
|
|
413
414
|
* @returns {Promise<Server>}
|
|
414
415
|
*/
|
|
415
|
-
export async function serve(ipc, opts) {
|
|
416
|
-
const server = new Server(ipc, opts)
|
|
416
|
+
export async function serve(ipc, spec, opts) {
|
|
417
|
+
const server = new Server(ipc, spec, opts)
|
|
417
418
|
await server.ready()
|
|
418
419
|
return server
|
|
419
420
|
}
|