@cero-base/core 0.2.0 → 0.4.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/LICENSE +201 -0
- package/README.md +180 -136
- package/package.json +123 -28
- package/src/blobs/index.js +297 -0
- package/src/database/CLAUDE.md +3 -0
- package/src/database/bootstrap.js +76 -0
- package/src/database/dispatch.js +156 -0
- package/src/database/index.js +572 -0
- package/src/identity/CLAUDE.md +3 -0
- package/src/identity/index.js +232 -0
- package/src/index.js +20 -1
- package/src/lib/CLAUDE.md +3 -0
- package/src/lib/constants.js +24 -4
- package/src/lib/errors.js +150 -0
- package/src/lib/schema.js +58 -440
- package/src/lib/spec/index.js +353 -0
- package/src/lib/spec/schema.json +284 -0
- package/src/lib/utils.js +54 -49
- package/src/network/discovery.js +80 -0
- package/src/network/index.js +231 -0
- package/src/pairing/index.js +482 -0
- package/src/pairing/invite.js +199 -0
- package/src/rpc/client.js +45 -0
- package/src/rpc/index.js +141 -0
- package/src/rpc/server.js +45 -0
- package/src/storage/index.js +261 -0
- package/types/blobs/index.d.ts +169 -0
- package/types/database/bootstrap.d.ts +17 -0
- package/types/database/dispatch.d.ts +8 -0
- package/types/database/index.d.ts +329 -0
- package/types/identity/index.d.ts +160 -0
- package/types/index.d.ts +11 -0
- package/types/lib/constants.d.ts +13 -0
- package/types/lib/errors.d.ts +110 -0
- package/types/lib/schema.d.ts +53 -0
- package/types/lib/spec/index.d.ts +95 -0
- package/types/lib/utils.d.ts +39 -0
- package/types/network/discovery.d.ts +44 -0
- package/types/network/index.d.ts +115 -0
- package/types/pairing/index.d.ts +194 -0
- package/types/pairing/invite.d.ts +157 -0
- package/types/rpc/client.d.ts +18 -0
- package/types/rpc/index.d.ts +67 -0
- package/types/rpc/server.d.ts +18 -0
- package/types/storage/index.d.ts +163 -0
- package/src/lib/base.js +0 -84
- package/src/lib/batch.js +0 -98
- package/src/lib/builder.js +0 -24
- package/src/lib/collection.js +0 -252
- package/src/lib/crypto.js +0 -6
- package/src/lib/index.js +0 -6
- package/src/lib/room.js +0 -145
package/package.json
CHANGED
|
@@ -1,48 +1,143 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
5
8
|
"files": [
|
|
6
9
|
"src",
|
|
7
|
-
"
|
|
10
|
+
"types",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
8
13
|
],
|
|
9
14
|
"publishConfig": {
|
|
10
15
|
"access": "public"
|
|
11
16
|
},
|
|
12
|
-
"type": "module",
|
|
13
|
-
"main": "src/index.js",
|
|
14
17
|
"exports": {
|
|
15
|
-
".":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./types/index.d.ts",
|
|
20
|
+
"default": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./identity": {
|
|
23
|
+
"types": "./types/identity/index.d.ts",
|
|
24
|
+
"default": "./src/identity/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./storage": {
|
|
27
|
+
"types": "./types/storage/index.d.ts",
|
|
28
|
+
"default": "./src/storage/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./network": {
|
|
31
|
+
"types": "./types/network/index.d.ts",
|
|
32
|
+
"default": "./src/network/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./database": {
|
|
35
|
+
"types": "./types/database/index.d.ts",
|
|
36
|
+
"default": "./src/database/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./blobs": {
|
|
39
|
+
"types": "./types/blobs/index.d.ts",
|
|
40
|
+
"default": "./src/blobs/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./rpc": {
|
|
43
|
+
"types": "./types/rpc/index.d.ts",
|
|
44
|
+
"default": "./src/rpc/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./pairing": {
|
|
47
|
+
"types": "./types/pairing/index.d.ts",
|
|
48
|
+
"default": "./src/pairing/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./invite": {
|
|
51
|
+
"types": "./types/pairing/invite.d.ts",
|
|
52
|
+
"default": "./src/pairing/invite.js"
|
|
53
|
+
},
|
|
54
|
+
"./schema": {
|
|
55
|
+
"types": "./types/lib/schema.d.ts",
|
|
56
|
+
"default": "./src/lib/schema.js"
|
|
57
|
+
},
|
|
58
|
+
"./utils": {
|
|
59
|
+
"types": "./types/lib/utils.d.ts",
|
|
60
|
+
"default": "./src/lib/utils.js"
|
|
61
|
+
},
|
|
62
|
+
"./errors": {
|
|
63
|
+
"types": "./types/lib/errors.d.ts",
|
|
64
|
+
"default": "./src/lib/errors.js"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"typesVersions": {
|
|
68
|
+
"*": {
|
|
69
|
+
"invite": [
|
|
70
|
+
"types/pairing/invite.d.ts"
|
|
71
|
+
],
|
|
72
|
+
"schema": [
|
|
73
|
+
"types/lib/schema.d.ts"
|
|
74
|
+
],
|
|
75
|
+
"utils": [
|
|
76
|
+
"types/lib/utils.d.ts"
|
|
77
|
+
],
|
|
78
|
+
"errors": [
|
|
79
|
+
"types/lib/errors.d.ts"
|
|
80
|
+
],
|
|
81
|
+
"*": [
|
|
82
|
+
"types/*/index.d.ts"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"imports": {
|
|
87
|
+
"fs": {
|
|
88
|
+
"bare": "bare-fs",
|
|
89
|
+
"default": "fs"
|
|
90
|
+
},
|
|
91
|
+
"path": {
|
|
92
|
+
"bare": "bare-path",
|
|
93
|
+
"default": "path"
|
|
94
|
+
},
|
|
95
|
+
"crypto": {
|
|
96
|
+
"bare": "bare-crypto",
|
|
97
|
+
"default": "crypto"
|
|
98
|
+
}
|
|
19
99
|
},
|
|
20
100
|
"scripts": {
|
|
101
|
+
"build": "node schema/build.js",
|
|
21
102
|
"build:test": "rm -rf test/fixture/spec && node test/fixture/build.js",
|
|
103
|
+
"build:types": "rm -rf types && tsc -p .",
|
|
22
104
|
"pretest": "npm run build:test",
|
|
23
|
-
"
|
|
105
|
+
"prepublishOnly": "npm run build:types",
|
|
106
|
+
"test": "brittle-node test/*.test.js"
|
|
24
107
|
},
|
|
25
108
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
109
|
+
"autobee": "^1.0.3",
|
|
110
|
+
"b4a": "^1.8.1",
|
|
111
|
+
"bare-crypto": "^1.13.7",
|
|
112
|
+
"bare-fs": "^4.7.1",
|
|
113
|
+
"bare-path": "^3.0.0",
|
|
114
|
+
"bip39-mnemonic": "^2.5.0",
|
|
115
|
+
"blind-pairing": "^2.3.1",
|
|
116
|
+
"compact-encoding": "^3.1.0",
|
|
117
|
+
"corestore": "^7.9.2",
|
|
118
|
+
"framed-stream": "^1.0.1",
|
|
119
|
+
"hrpc": "^4.3.0",
|
|
120
|
+
"hyperblobs": "^2.12.0",
|
|
121
|
+
"hypercore": "^11.30.2",
|
|
122
|
+
"hypercore-crypto": "^3.7.0",
|
|
123
|
+
"hypercore-id-encoding": "^1.3.0",
|
|
124
|
+
"hypercore-storage": "^3.0.2",
|
|
125
|
+
"hyperdb": "^6.7.0",
|
|
126
|
+
"hyperdispatch": "^1.6.0",
|
|
127
|
+
"hyperschema": "^1.21.0",
|
|
128
|
+
"hyperswarm": "^4.17.0",
|
|
129
|
+
"keet-identity-key": "^3.2.0",
|
|
130
|
+
"protomux-wakeup": "^2.9.0",
|
|
131
|
+
"ready-resource": "^1.2.0",
|
|
132
|
+
"safety-catch": "^1.0.3",
|
|
133
|
+
"sodium-universal": "^5.0.1",
|
|
134
|
+
"streamx": "^2.26.0",
|
|
135
|
+
"z32": "^1.1.0"
|
|
30
136
|
},
|
|
31
137
|
"devDependencies": {
|
|
32
138
|
"@hyperswarm/testnet": "^3.1.4",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"corestore": "^7.9.1",
|
|
36
|
-
"duplex-through": "^1.0.2",
|
|
37
|
-
"hrpc": "^4.3.0",
|
|
38
|
-
"hyperdb": "^6.3.0",
|
|
39
|
-
"hyperdispatch": "^1.5.1",
|
|
40
|
-
"hyperschema": "^1.20.1"
|
|
139
|
+
"brittle": "^4.0.0",
|
|
140
|
+
"typescript": "^5.7.0"
|
|
41
141
|
},
|
|
42
|
-
"license": "Apache-2.0"
|
|
43
|
-
"repository": {
|
|
44
|
-
"type": "git",
|
|
45
|
-
"url": "https://github.com/lekinox/cero-base.git",
|
|
46
|
-
"directory": "packages/core"
|
|
47
|
-
}
|
|
142
|
+
"license": "Apache-2.0"
|
|
48
143
|
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import Hyperblobs from 'hyperblobs'
|
|
2
|
+
import ReadyResource from 'ready-resource'
|
|
3
|
+
import sodium from 'sodium-universal'
|
|
4
|
+
import b4a from 'b4a'
|
|
5
|
+
import z32 from 'z32'
|
|
6
|
+
|
|
7
|
+
import { NAMESPACE } from '../lib/constants.js'
|
|
8
|
+
import { toId } from '../lib/utils.js'
|
|
9
|
+
import { CeroError } from '../lib/errors.js'
|
|
10
|
+
|
|
11
|
+
const NS = `${NAMESPACE}/blobs`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} BlobsOpts
|
|
15
|
+
* @property {any} store Corestore (or compatible) used to host the blob core.
|
|
16
|
+
* @property {import('../identity/index.js').Identity} [identity] Identity supplying the default encryption key.
|
|
17
|
+
* @property {import('../network/index.js').Network} [network] Optional network used to announce + replicate the core.
|
|
18
|
+
* @property {Uint8Array} [key] Pre-existing blob core key — joins an existing blob feed.
|
|
19
|
+
* @property {Uint8Array} [encryptionKey] Explicit encryption key, overrides `identity.encryptionKey`.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {object} BlobEntry
|
|
22
|
+
* @property {string} id Opaque z32 id used by `get`/`info`/`delete`.
|
|
23
|
+
* @property {string} hash Content hash (z32-encoded blake2b of the bytes).
|
|
24
|
+
* @property {number} size Logical byte length recorded at put-time.
|
|
25
|
+
* @property {any} meta Caller-provided metadata, stored verbatim.
|
|
26
|
+
* @property {number} createdAt Wall-clock timestamp of the put.
|
|
27
|
+
*
|
|
28
|
+
* @typedef {object} PutOpts
|
|
29
|
+
* @property {any} [meta] Arbitrary metadata persisted alongside the blob.
|
|
30
|
+
* @property {number} [size] Override the recorded size (defaults to byte length).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Content-addressable blob store backed by a single Hyperblobs core.
|
|
35
|
+
* Writes dedupe locally by content hash, reads/streams are positional
|
|
36
|
+
* descriptors, and the underlying core is attached to the supplied
|
|
37
|
+
* network for replication.
|
|
38
|
+
*/
|
|
39
|
+
export class Blobs extends ReadyResource {
|
|
40
|
+
/** @param {BlobsOpts} [opts] */
|
|
41
|
+
constructor({ store, identity, network, key, encryptionKey } = {}) {
|
|
42
|
+
super()
|
|
43
|
+
if (!store) throw CeroError.REQUIRED('store')
|
|
44
|
+
if (!identity && !encryptionKey) throw CeroError.REQUIRED('identity or encryptionKey')
|
|
45
|
+
|
|
46
|
+
this.store = store
|
|
47
|
+
this.identity = identity || null
|
|
48
|
+
this.network = network || null
|
|
49
|
+
this.encryptionKey = encryptionKey || (identity && identity.encryptionKey) || null
|
|
50
|
+
|
|
51
|
+
this._coreKey = key || null
|
|
52
|
+
this.core = null
|
|
53
|
+
this.hyperblobs = null
|
|
54
|
+
|
|
55
|
+
this._discovery = null
|
|
56
|
+
/** @type {Map<string, BlobEntry>} contentHash → entry */
|
|
57
|
+
this._index = new Map()
|
|
58
|
+
/** @type {Map<string, BlobEntry>} id → entry (so info/has/delete by id work) */
|
|
59
|
+
this._byId = new Map()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Canonical z32 id of the underlying core (null until ready).
|
|
64
|
+
*
|
|
65
|
+
* @returns {string | null}
|
|
66
|
+
*/
|
|
67
|
+
get id() {
|
|
68
|
+
return this.key ? toId(this.key) : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Public key of the underlying core.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Uint8Array | null}
|
|
75
|
+
*/
|
|
76
|
+
get key() {
|
|
77
|
+
return this.core ? this.core.key : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Discovery key derived from the core key, used for swarm topics.
|
|
82
|
+
*
|
|
83
|
+
* @returns {Uint8Array | null}
|
|
84
|
+
*/
|
|
85
|
+
get discoveryKey() {
|
|
86
|
+
return this.core ? this.core.discoveryKey : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async _open() {
|
|
90
|
+
await this.store.ready()
|
|
91
|
+
if (this.network) await this.network.ready()
|
|
92
|
+
|
|
93
|
+
const opts = { encryptionKey: this.encryptionKey }
|
|
94
|
+
if (this._coreKey) opts.key = this._coreKey
|
|
95
|
+
else opts.name = 'blobs'
|
|
96
|
+
|
|
97
|
+
this.core = this.store.namespace(NS).get(opts)
|
|
98
|
+
await this.core.ready()
|
|
99
|
+
|
|
100
|
+
this.hyperblobs = new Hyperblobs(this.core)
|
|
101
|
+
await this.hyperblobs.ready()
|
|
102
|
+
|
|
103
|
+
if (this.network) {
|
|
104
|
+
this.network.attach(this.core)
|
|
105
|
+
this._discovery = this.network.join(this.core.discoveryKey)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async _close() {
|
|
110
|
+
if (this._discovery) await this._discovery.destroy()
|
|
111
|
+
if (this.network && this.core) this.network.detach(this.core)
|
|
112
|
+
if (this.hyperblobs) await this.hyperblobs.close()
|
|
113
|
+
if (this.core) await this.core.close()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Store a buffer or readable stream, returning its opaque id. Identical
|
|
118
|
+
* content from the same writer is deduplicated locally.
|
|
119
|
+
*
|
|
120
|
+
* @param {Uint8Array | import('streamx').Readable} input
|
|
121
|
+
* @param {PutOpts} [opts]
|
|
122
|
+
* @returns {Promise<string>}
|
|
123
|
+
*/
|
|
124
|
+
async put(input, { meta = null, size = null } = {}) {
|
|
125
|
+
this._guard()
|
|
126
|
+
if (input == null) throw CeroError.REQUIRED('input')
|
|
127
|
+
|
|
128
|
+
let bytes
|
|
129
|
+
if (isBufferLike(input)) {
|
|
130
|
+
bytes = b4a.from(input)
|
|
131
|
+
} else if (isReadable(input)) {
|
|
132
|
+
bytes = await readAll(input)
|
|
133
|
+
} else {
|
|
134
|
+
throw CeroError.INVALID('input must be a buffer or a Readable stream')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hashBuf = contentHash(bytes)
|
|
138
|
+
const hash = toId(hashBuf)
|
|
139
|
+
|
|
140
|
+
// Local dedup: same writer putting same bytes returns the existing id.
|
|
141
|
+
const existing = this._index.get(hash)
|
|
142
|
+
if (existing) return existing.id
|
|
143
|
+
|
|
144
|
+
const raw = await this.hyperblobs.put(bytes)
|
|
145
|
+
const id = encodeId(raw)
|
|
146
|
+
const entry = {
|
|
147
|
+
id,
|
|
148
|
+
hash,
|
|
149
|
+
size: size ?? bytes.byteLength,
|
|
150
|
+
meta,
|
|
151
|
+
createdAt: Date.now()
|
|
152
|
+
}
|
|
153
|
+
this._index.set(hash, entry)
|
|
154
|
+
this._byId.set(id, entry)
|
|
155
|
+
return id
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Delete the blob with the given id and forget its local index entry.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} id
|
|
162
|
+
* @returns {Promise<void>}
|
|
163
|
+
*/
|
|
164
|
+
async delete(id) {
|
|
165
|
+
this._guard()
|
|
166
|
+
const raw = decodeId(id)
|
|
167
|
+
await this.hyperblobs.clear(raw)
|
|
168
|
+
const entry = this._byId.get(id)
|
|
169
|
+
if (entry) {
|
|
170
|
+
this._byId.delete(id)
|
|
171
|
+
this._index.delete(entry.hash)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve the blob bytes for an id. Fetches from peers if not local.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} id
|
|
179
|
+
* @param {any} [opts]
|
|
180
|
+
* @returns {Promise<Uint8Array>}
|
|
181
|
+
*/
|
|
182
|
+
async get(id, opts) {
|
|
183
|
+
this._guard()
|
|
184
|
+
const raw = decodeId(id)
|
|
185
|
+
return this.hyperblobs.get(raw, opts)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Streaming counterpart of `get` — returns a Readable over the blob bytes.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} id
|
|
192
|
+
* @param {any} [opts]
|
|
193
|
+
* @returns {import('streamx').Readable}
|
|
194
|
+
*/
|
|
195
|
+
read(id, opts) {
|
|
196
|
+
this._guard()
|
|
197
|
+
const raw = decodeId(id)
|
|
198
|
+
return this.hyperblobs.createReadStream(raw, opts)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Look up the locally-cached metadata for a blob id, or `null` if absent.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} id
|
|
205
|
+
* @returns {Promise<BlobEntry | null>}
|
|
206
|
+
*/
|
|
207
|
+
async info(id) {
|
|
208
|
+
this._guard()
|
|
209
|
+
decodeId(id) // validate id shape
|
|
210
|
+
return this._byId.get(id) || null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Whether the underlying blocks for the blob are present in the local core.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} id
|
|
217
|
+
* @returns {Promise<boolean>}
|
|
218
|
+
*/
|
|
219
|
+
async has(id) {
|
|
220
|
+
this._guard()
|
|
221
|
+
const raw = decodeId(id)
|
|
222
|
+
const len = raw.blockOffset + raw.blockLength
|
|
223
|
+
if (this.core.length < len) return false
|
|
224
|
+
return this.core.has(raw.blockOffset, len)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Pre-fetch the blocks for `id` from peers without reading the bytes.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} id
|
|
231
|
+
* @param {any} [opts]
|
|
232
|
+
* @returns {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
async fetch(id, opts) {
|
|
235
|
+
this._guard()
|
|
236
|
+
const raw = decodeId(id)
|
|
237
|
+
const start = raw.blockOffset
|
|
238
|
+
const end = raw.blockOffset + raw.blockLength
|
|
239
|
+
const range = this.core.download({ start, end, ...opts })
|
|
240
|
+
await range.done()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_guard() {
|
|
244
|
+
if (this.closed) throw CeroError.CLOSED('Blobs')
|
|
245
|
+
if (!this.opened) throw CeroError.NOT_READY('Blobs', 'blobs')
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function contentHash(bytes) {
|
|
250
|
+
const out = b4a.alloc(32)
|
|
251
|
+
sodium.crypto_generichash(out, bytes)
|
|
252
|
+
return out
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isBufferLike(x) {
|
|
256
|
+
return b4a.isBuffer(x) || x instanceof Uint8Array
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isReadable(x) {
|
|
260
|
+
return x && typeof x.pipe === 'function' && typeof x.on === 'function'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function readAll(stream) {
|
|
264
|
+
const chunks = []
|
|
265
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
266
|
+
return b4a.concat(chunks)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// id <-> raw hyperblobs descriptor (positional inside the core)
|
|
270
|
+
// 16-byte packed: 4 uint32-BE = blockOffset, blockLength, byteOffset, byteLength
|
|
271
|
+
|
|
272
|
+
function encodeId(raw) {
|
|
273
|
+
const buf = b4a.alloc(16)
|
|
274
|
+
buf.writeUInt32BE(raw.blockOffset, 0)
|
|
275
|
+
buf.writeUInt32BE(raw.blockLength, 4)
|
|
276
|
+
buf.writeUInt32BE(raw.byteOffset, 8)
|
|
277
|
+
buf.writeUInt32BE(raw.byteLength, 12)
|
|
278
|
+
return z32.encode(buf)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function decodeId(id) {
|
|
282
|
+
if (typeof id !== 'string') throw CeroError.INVALID('id must be a string')
|
|
283
|
+
if (id.length === 0) throw CeroError.INVALID('id must be a non-empty string')
|
|
284
|
+
let buf
|
|
285
|
+
try {
|
|
286
|
+
buf = z32.decode(id)
|
|
287
|
+
} catch {
|
|
288
|
+
throw CeroError.INVALID('id must be valid z32')
|
|
289
|
+
}
|
|
290
|
+
if (buf.length !== 16) throw CeroError.INVALID('id must be 16 bytes when decoded')
|
|
291
|
+
return {
|
|
292
|
+
blockOffset: buf.readUInt32BE(0),
|
|
293
|
+
blockLength: buf.readUInt32BE(4),
|
|
294
|
+
byteOffset: buf.readUInt32BE(8),
|
|
295
|
+
byteLength: buf.readUInt32BE(12)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Hypercore from 'hypercore'
|
|
2
|
+
import { Identity } from '../identity/index.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* First-run device provisioning: mint a device writer keypair, persist it as a
|
|
6
|
+
* writer, then swap the autobee's local core over to it. With `recovering`,
|
|
7
|
+
* waits for the first peer-replicated append before swapping.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('./index.js').Database} db
|
|
10
|
+
* @param {{ name?: string | null, isMobile?: boolean, recovering?: boolean }} [opts]
|
|
11
|
+
* @returns {Promise<{ id: Uint8Array, writer: import('../identity/index.js').KeyPair }>}
|
|
12
|
+
*/
|
|
13
|
+
export async function bootstrap(db, { name, isMobile, recovering = false } = {}) {
|
|
14
|
+
const keyPair = Identity.randomKeyPair()
|
|
15
|
+
const manifest = {
|
|
16
|
+
version: db.store.manifestVersion,
|
|
17
|
+
signers: [{ publicKey: keyPair.publicKey }]
|
|
18
|
+
}
|
|
19
|
+
const writerKey = Hypercore.key(manifest)
|
|
20
|
+
|
|
21
|
+
if (recovering) await waitForFirstPeerAppend(db)
|
|
22
|
+
await saveWriter(db, writerKey, { name, isMobile })
|
|
23
|
+
await swapWriter(db, keyPair, manifest)
|
|
24
|
+
|
|
25
|
+
return { id: writerKey, writer: keyPair }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function waitForFirstPeerAppend(db) {
|
|
29
|
+
if (db.bee.local.length > 0 || !db.network?.swarm) return
|
|
30
|
+
await new Promise((resolve) => {
|
|
31
|
+
const onAppend = () => {
|
|
32
|
+
if (db.bee.local.length === 0) return
|
|
33
|
+
db.bee.local.off('append', onAppend)
|
|
34
|
+
resolve()
|
|
35
|
+
}
|
|
36
|
+
db.bee.local.on('append', onAppend)
|
|
37
|
+
})
|
|
38
|
+
await db.bee.update()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function saveWriter(db, writerKey, { name, isMobile }) {
|
|
42
|
+
await db.write([
|
|
43
|
+
[
|
|
44
|
+
'add-writer',
|
|
45
|
+
{
|
|
46
|
+
master: db.identity.publicKey,
|
|
47
|
+
writer: writerKey,
|
|
48
|
+
sig: db.identity.sign(writerKey),
|
|
49
|
+
name: name || null,
|
|
50
|
+
isMobile: isMobile === true
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function swapWriter(db, keyPair, manifest) {
|
|
57
|
+
const head = await db.bee.local.getUserData('autobee/head')
|
|
58
|
+
const enc = await db.bee.local.getUserData('autobee/encryption')
|
|
59
|
+
|
|
60
|
+
if (db.network) db.network.detach(db.bee)
|
|
61
|
+
if (db.onLocalAppend && db.bee.local) {
|
|
62
|
+
db.bee.local.off('append', db.onLocalAppend)
|
|
63
|
+
db.onLocalAppend = null
|
|
64
|
+
}
|
|
65
|
+
await db.bee.close()
|
|
66
|
+
db.bee = null
|
|
67
|
+
|
|
68
|
+
const deviceCore = db.store.namespace(db.namespace).get({ keyPair, manifest })
|
|
69
|
+
await deviceCore.ready()
|
|
70
|
+
if (head) await deviceCore.setUserData('autobee/head', head)
|
|
71
|
+
if (enc) await deviceCore.setUserData('autobee/encryption', enc)
|
|
72
|
+
await deviceCore.close()
|
|
73
|
+
|
|
74
|
+
db.keyPair = keyPair
|
|
75
|
+
await db.openBee()
|
|
76
|
+
}
|