@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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +180 -136
  3. package/package.json +123 -28
  4. package/src/blobs/index.js +297 -0
  5. package/src/database/CLAUDE.md +3 -0
  6. package/src/database/bootstrap.js +76 -0
  7. package/src/database/dispatch.js +156 -0
  8. package/src/database/index.js +572 -0
  9. package/src/identity/CLAUDE.md +3 -0
  10. package/src/identity/index.js +232 -0
  11. package/src/index.js +20 -1
  12. package/src/lib/CLAUDE.md +3 -0
  13. package/src/lib/constants.js +24 -4
  14. package/src/lib/errors.js +150 -0
  15. package/src/lib/schema.js +58 -440
  16. package/src/lib/spec/index.js +353 -0
  17. package/src/lib/spec/schema.json +284 -0
  18. package/src/lib/utils.js +54 -49
  19. package/src/network/discovery.js +80 -0
  20. package/src/network/index.js +231 -0
  21. package/src/pairing/index.js +482 -0
  22. package/src/pairing/invite.js +199 -0
  23. package/src/rpc/client.js +45 -0
  24. package/src/rpc/index.js +141 -0
  25. package/src/rpc/server.js +45 -0
  26. package/src/storage/index.js +261 -0
  27. package/types/blobs/index.d.ts +169 -0
  28. package/types/database/bootstrap.d.ts +17 -0
  29. package/types/database/dispatch.d.ts +8 -0
  30. package/types/database/index.d.ts +329 -0
  31. package/types/identity/index.d.ts +160 -0
  32. package/types/index.d.ts +11 -0
  33. package/types/lib/constants.d.ts +13 -0
  34. package/types/lib/errors.d.ts +110 -0
  35. package/types/lib/schema.d.ts +53 -0
  36. package/types/lib/spec/index.d.ts +95 -0
  37. package/types/lib/utils.d.ts +39 -0
  38. package/types/network/discovery.d.ts +44 -0
  39. package/types/network/index.d.ts +115 -0
  40. package/types/pairing/index.d.ts +194 -0
  41. package/types/pairing/invite.d.ts +157 -0
  42. package/types/rpc/client.d.ts +18 -0
  43. package/types/rpc/index.d.ts +67 -0
  44. package/types/rpc/server.d.ts +18 -0
  45. package/types/storage/index.d.ts +163 -0
  46. package/src/lib/base.js +0 -84
  47. package/src/lib/batch.js +0 -98
  48. package/src/lib/builder.js +0 -24
  49. package/src/lib/collection.js +0 -252
  50. package/src/lib/crypto.js +0 -6
  51. package/src/lib/index.js +0 -6
  52. 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.2.0",
4
- "description": "P2P database with collection-based CRUD and RPC",
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
- "README.md"
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
- ".": "./src/index.js",
16
- "./schema": "./src/lib/schema.js",
17
- "./builder": "./src/lib/builder.js",
18
- "./constants": "./src/lib/constants.js"
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
- "test": "brittle test/*.js"
105
+ "prepublishOnly": "npm run build:types",
106
+ "test": "brittle-node test/*.test.js"
24
107
  },
25
108
  "dependencies": {
26
- "@lekinox/cero": "^1.3.1",
27
- "compact-encoding": "^2.19.1",
28
- "ready-resource": "^1.0.2",
29
- "safety-catch": "^1.0.2"
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
- "autobase-test-helpers": "^3.1.0",
34
- "brittle": "^3.7.0",
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,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>
@@ -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
+ }