@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/src/index.js ADDED
@@ -0,0 +1,170 @@
1
+ import HypercoreStorage from 'hypercore-storage'
2
+ import Corestore from 'corestore'
3
+ import fs from 'fs'
4
+
5
+ import { Identity } from '@cero-base/core/identity'
6
+ import { Network } from '@cero-base/core/network'
7
+ import { CeroError } from '@cero-base/core/errors'
8
+
9
+ import { Handle, Ref } from './handle/index.js'
10
+ import { Local } from './local/index.js'
11
+ import { put, set, get, del, count, watch, call, open } from './lib/operators.js'
12
+ import { peek } from './lib/peek.js'
13
+ import { t, schema } from './lib/spec.js'
14
+
15
+ export { Handle, Ref, Local }
16
+ export { put, set, get, del, count, watch, call, open } from './lib/operators.js'
17
+ export { peek } from './lib/peek.js'
18
+ export { t, schema } from './lib/spec.js'
19
+
20
+ /**
21
+ * @typedef {object} CeroOpts
22
+ * @property {Identity} [identity] Pre-resolved identity. If absent, derived from `seed`/`phrase` or generated.
23
+ * @property {Uint8Array} [seed] 16- or 32-byte seed entropy.
24
+ * @property {string} [phrase] BIP-39 mnemonic — alternative to `seed`.
25
+ * @property {12 | 24} [words] Mnemonic length when generating a fresh identity.
26
+ * @property {string | null} [name] Friendly device name persisted on the identity claim.
27
+ * @property {boolean} [isMobile] Marks this device as mobile.
28
+ * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
29
+ * @property {Uint8Array} [key] Pre-existing database key (skip bootstrap).
30
+ * @property {Uint8Array} [encryptionKey] Pre-existing encryption key.
31
+ * @property {Record<string, Function>} [routes] Custom RPC routes for the database dispatcher.
32
+ * @property {(err: any) => void} [onerror] Background-task error handler.
33
+ * @property {boolean} [recovery] Recovery flow — wipe local state and re-claim a writer slot.
34
+ * @property {number} [recoveryTimeout] Max wait for writer capability during recovery.
35
+ */
36
+
37
+ /**
38
+ * Open (or create) a cero handle at `dir`. Sets up storage, network and
39
+ * identity, then returns a ready root `Handle` with all schema refs
40
+ * attached as properties.
41
+ *
42
+ * @param {string} dir Data directory.
43
+ * @param {any} spec Built spec — output of `cero/build`.
44
+ * @param {CeroOpts} [opts]
45
+ * @returns {Promise<Handle>}
46
+ */
47
+ export async function cero(dir, spec, opts = {}) {
48
+ if (typeof dir !== 'string' || !dir) throw CeroError.INVALID('dir must be a non-empty string')
49
+ if (!spec) throw CeroError.REQUIRED('spec')
50
+
51
+ const storage = new HypercoreStorage(`${dir}/main`)
52
+ await storage.ready()
53
+ const store = new Corestore(storage, { manifestVersion: 2 })
54
+ await store.ready()
55
+
56
+ let local = null
57
+ if (spec.local && spec.meta?.local) {
58
+ local = new Local(null, spec, { store })
59
+ await local.ready()
60
+ }
61
+
62
+ const identity = await resolveIdentity(opts, local)
63
+ const writer = local ? (await local.store.get('keypair')).data : null
64
+
65
+ const network = new Network({ bootstrap: opts.bootstrap })
66
+ await network.ready()
67
+ const discovery = network.join(identity.topic)
68
+ await Promise.race([discovery.flush(), new Promise((r) => setTimeout(r, 500))])
69
+
70
+ const me = new Handle({
71
+ storage,
72
+ store,
73
+ local,
74
+ discovery,
75
+ identity,
76
+ network,
77
+ spec,
78
+ opts,
79
+ dir,
80
+ routes: opts.routes,
81
+ key: opts.key,
82
+ encryptionKey: opts.encryptionKey,
83
+ keyPair: writer ? { publicKey: writer.publicKey, secretKey: writer.secretKey } : undefined,
84
+ pair: false
85
+ })
86
+ await me.ready()
87
+
88
+ if (!writer && me.store.length === 0 && (!opts.key || opts.recovery)) {
89
+ const result = await me.bootstrap({
90
+ name: opts.name || null,
91
+ isMobile: opts.isMobile === true,
92
+ recovering: opts.recovery === true
93
+ })
94
+ if (local && result?.writer) {
95
+ await local.store.set('keypair', {
96
+ publicKey: result.writer.publicKey,
97
+ secretKey: result.writer.secretKey
98
+ })
99
+ }
100
+ if (!opts.recovery) {
101
+ const ts = Date.now()
102
+ await me.store.call('add-member', {
103
+ id: identity.id,
104
+ key: me.store.writerKey,
105
+ role: 'owner',
106
+ name: opts.name || null,
107
+ createdAt: ts,
108
+ updatedAt: ts
109
+ })
110
+ }
111
+ }
112
+ if (opts.recovery)
113
+ await me.recover({
114
+ timeout: opts.recoveryTimeout,
115
+ name: opts.name || null,
116
+ isMobile: opts.isMobile === true
117
+ })
118
+
119
+ return me
120
+ }
121
+
122
+ /**
123
+ * Restore a cero instance from a mnemonic phrase. Closes the running
124
+ * instance, wipes the on-disk `main/` tree and re-opens with `recovery: true`
125
+ * so the writer slot is re-claimed.
126
+ *
127
+ * @param {Handle} me Existing root handle to restore.
128
+ * @param {string} phrase BIP-39 mnemonic phrase.
129
+ * @returns {Promise<Handle>} Freshly restored root handle.
130
+ */
131
+ export async function restore(me, phrase) {
132
+ if (!me?._dir) throw CeroError.INVALID('me must be a cero instance')
133
+ if (!phrase || typeof phrase !== 'string') throw CeroError.INVALID('phrase must be a string')
134
+
135
+ const { _dir: dir, spec, _opts: opts } = me
136
+ const { name, bootstrap, isMobile, onerror, routes, recoveryTimeout } = opts
137
+
138
+ await me.close()
139
+ await fs.promises.rm(`${dir}/main`, { recursive: true, force: true })
140
+
141
+ return cero(dir, spec, {
142
+ name,
143
+ bootstrap,
144
+ isMobile,
145
+ onerror,
146
+ routes,
147
+ recoveryTimeout,
148
+ phrase,
149
+ recovery: true
150
+ })
151
+ }
152
+
153
+ Object.assign(cero, { put, set, get, del, count, watch, call, open, peek, restore, t, schema })
154
+
155
+ async function resolveIdentity(opts, local) {
156
+ if (opts.identity) return opts.identity
157
+
158
+ const provided = opts.seed || (opts.phrase && Identity.toSeed(opts.phrase))
159
+ if (provided) {
160
+ if (local) await local.store.set('master', { seed: provided })
161
+ return Identity.fromSeed(provided)
162
+ }
163
+
164
+ const stored = local && (await local.store.get('master')).data?.seed
165
+ if (stored) return Identity.fromSeed(stored)
166
+
167
+ const fresh = await Identity.generate({ words: opts.words })
168
+ if (local) await local.store.set('master', { seed: fresh.seed })
169
+ return fresh
170
+ }
@@ -0,0 +1,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Builtin type/collection/dispatch/rpc definitions used by the builder.
3
+ * Every cero schema includes these; user-defined refs are added on top.
4
+ *
5
+ * @typedef {{ name: string, type: string, verb?: string, kind?: string }} BuiltinRef
6
+ * @typedef {{ name: string, type: string, required: boolean }} FieldDesc
7
+ * @typedef {{ name: string, compact: boolean, fields: FieldDesc[] }} TypeDesc
8
+ * @typedef {{ name: string, schema: string, key: string[] }} CollectionDesc
9
+ * @typedef {{ name: string, requestType: string }} DispatchDesc
10
+ * @typedef {{
11
+ * name: string,
12
+ * request: { name: string },
13
+ * response: { name: string, stream?: boolean }
14
+ * }} CommandDesc
15
+ */
16
+
17
+ /**
18
+ * Refs that exist on every main (Handle) schema.
19
+ *
20
+ * @type {BuiltinRef[]}
21
+ */
22
+ export const BUILTINS = [
23
+ { name: 'members', type: 'member', verb: 'member' },
24
+ { name: 'devices', type: 'device', verb: 'device' },
25
+ { name: 'invites', type: 'invite', verb: 'invite' },
26
+ { name: 'handles', type: 'handle', verb: 'handle' }
27
+ ]
28
+
29
+ /**
30
+ * Refs that exist on every local schema.
31
+ *
32
+ * @type {BuiltinRef[]}
33
+ */
34
+ export const LOCAL_BUILTINS = [
35
+ { name: 'master', type: 'master', kind: 'single' },
36
+ { name: 'keypair', type: 'keypair', kind: 'single' },
37
+ { name: 'handle-keypairs', type: 'handle-keypair', kind: 'collection' }
38
+ ]
39
+
40
+ const HYPERDB_TYPE = {
41
+ string: 'string',
42
+ uint: 'uint',
43
+ int: 'int',
44
+ bool: 'bool',
45
+ bytes: 'buffer',
46
+ json: 'json',
47
+ fixed32: 'fixed32',
48
+ fixed64: 'fixed64'
49
+ }
50
+
51
+ /**
52
+ * Map a schema-DSL primitive name to its HyperDB type name.
53
+ *
54
+ * @param {string} prim
55
+ * @returns {string}
56
+ */
57
+ export function hyperdbType(prim) {
58
+ return HYPERDB_TYPE[prim] || 'string'
59
+ }
60
+
61
+ /**
62
+ * Type descriptors registered into every main schema.
63
+ *
64
+ * @returns {TypeDesc[]}
65
+ */
66
+ export function builtinTypes() {
67
+ return [
68
+ { name: 'del-by-id', compact: false, fields: [{ name: 'id', type: 'string', required: true }] },
69
+ {
70
+ name: 'writer',
71
+ compact: false,
72
+ fields: [
73
+ { name: 'master', type: 'buffer', required: true },
74
+ { name: 'writer', type: 'buffer', required: true },
75
+ { name: 'sig', type: 'buffer', required: true },
76
+ { name: 'name', type: 'string', required: false },
77
+ { name: 'isMobile', type: 'bool', required: false },
78
+ { name: 'isIndexer', type: 'bool', required: false }
79
+ ]
80
+ },
81
+ {
82
+ name: 'counter',
83
+ compact: false,
84
+ fields: [
85
+ { name: 'name', type: 'string', required: true },
86
+ { name: 'value', type: 'uint', required: true }
87
+ ]
88
+ },
89
+ {
90
+ name: 'member',
91
+ compact: false,
92
+ fields: [
93
+ { name: 'id', type: 'string', required: true },
94
+ { name: 'key', type: 'buffer', required: true },
95
+ { name: 'role', type: 'string', required: true },
96
+ { name: 'name', type: 'string', required: false },
97
+ { name: 'createdAt', type: 'int', required: false },
98
+ { name: 'updatedAt', type: 'int', required: false },
99
+ { name: 'sig', type: 'buffer', required: false },
100
+ { name: 'index', type: 'uint', required: false }
101
+ ]
102
+ },
103
+ {
104
+ name: 'device',
105
+ compact: false,
106
+ fields: [
107
+ { name: 'id', type: 'string', required: true },
108
+ { name: 'memberId', type: 'string', required: false },
109
+ { name: 'name', type: 'string', required: false },
110
+ { name: 'isMobile', type: 'bool', required: false },
111
+ { name: 'createdAt', type: 'int', required: false },
112
+ { name: 'updatedAt', type: 'int', required: false },
113
+ { name: 'index', type: 'uint', required: false }
114
+ ]
115
+ },
116
+ {
117
+ name: 'invite',
118
+ compact: false,
119
+ fields: [
120
+ { name: 'id', type: 'string', required: true },
121
+ { name: 'invite', type: 'buffer', required: true },
122
+ { name: 'publicKey', type: 'buffer', required: true },
123
+ { name: 'data', type: 'buffer', required: false },
124
+ { name: 'sig', type: 'buffer', required: false },
125
+ { name: 'role', type: 'string', required: true },
126
+ { name: 'expires', type: 'int', required: false },
127
+ { name: 'createdAt', type: 'int', required: false },
128
+ { name: 'index', type: 'uint', required: false }
129
+ ]
130
+ },
131
+ {
132
+ name: 'handle',
133
+ compact: false,
134
+ fields: [
135
+ { name: 'id', type: 'string', required: true },
136
+ { name: 'type', type: 'string', required: true },
137
+ { name: 'key', type: 'buffer', required: true },
138
+ { name: 'encryptionKey', type: 'buffer', required: false },
139
+ { name: 'name', type: 'string', required: false },
140
+ { name: 'createdAt', type: 'int', required: false },
141
+ { name: 'updatedAt', type: 'int', required: false },
142
+ { name: 'index', type: 'uint', required: false }
143
+ ]
144
+ },
145
+ {
146
+ name: 'claim',
147
+ compact: false,
148
+ fields: [
149
+ { name: 'identity', type: 'buffer', required: true },
150
+ { name: 'writer', type: 'buffer', required: true },
151
+ { name: 'sig', type: 'buffer', required: true },
152
+ { name: 'name', type: 'string', required: false },
153
+ { name: 'isMobile', type: 'bool', required: false }
154
+ ]
155
+ }
156
+ ]
157
+ }
158
+
159
+ /** Collection name for the internal counters table. */
160
+ export const COUNTERS = 'counters'
161
+
162
+ /**
163
+ * Collection descriptors for every main schema, namespaced under `ns`.
164
+ *
165
+ * @param {string} ns
166
+ * @returns {CollectionDesc[]}
167
+ */
168
+ export function builtinCollections(ns) {
169
+ const out = BUILTINS.map((b) => ({ name: b.name, schema: `@${ns}/${b.type}`, key: ['id'] }))
170
+ out.push({ name: COUNTERS, schema: `@${ns}/counter`, key: ['name'] })
171
+ return out
172
+ }
173
+
174
+ /**
175
+ * Dispatch (mutation) descriptors for every main schema, namespaced under `ns`.
176
+ *
177
+ * @param {string} ns
178
+ * @returns {DispatchDesc[]}
179
+ */
180
+ export function builtinDispatches(ns) {
181
+ const at = (n) => `@${ns}/${n}`
182
+ const out = [
183
+ { name: 'add-writer', requestType: at('writer') },
184
+ { name: 'del-writer', requestType: at('writer') },
185
+ { name: 'claim-writer', requestType: at('claim') }
186
+ ]
187
+ for (const b of BUILTINS) {
188
+ out.push({ name: `add-${b.verb}`, requestType: at(b.type) })
189
+ out.push({ name: `set-${b.verb}`, requestType: at(b.type) })
190
+ out.push({ name: `del-${b.verb}`, requestType: at('del-by-id') })
191
+ }
192
+ return out
193
+ }
194
+
195
+ /**
196
+ * Type descriptors registered into every local schema.
197
+ *
198
+ * @returns {TypeDesc[]}
199
+ */
200
+ export function localBuiltinTypes() {
201
+ return [
202
+ { name: 'master', compact: false, fields: [{ name: 'seed', type: 'buffer', required: true }] },
203
+ {
204
+ name: 'keypair',
205
+ compact: false,
206
+ fields: [
207
+ { name: 'publicKey', type: 'buffer', required: true },
208
+ { name: 'secretKey', type: 'buffer', required: true }
209
+ ]
210
+ },
211
+ {
212
+ name: 'handle-keypair',
213
+ compact: false,
214
+ fields: [
215
+ { name: 'id', type: 'string', required: true },
216
+ { name: 'publicKey', type: 'buffer', required: true },
217
+ { name: 'secretKey', type: 'buffer', required: true },
218
+ { name: 'encryptionKey', type: 'buffer', required: false }
219
+ ]
220
+ }
221
+ ]
222
+ }
223
+
224
+ /**
225
+ * Collection descriptors for every local schema, namespaced under `ns`.
226
+ *
227
+ * @param {string} ns
228
+ * @returns {CollectionDesc[]}
229
+ */
230
+ export function localBuiltinCollections(ns) {
231
+ return [
232
+ { name: 'master', schema: `@${ns}/master`, key: [] },
233
+ { name: 'keypair', schema: `@${ns}/keypair`, key: [] },
234
+ { name: 'handle-keypairs', schema: `@${ns}/handle-keypair`, key: ['id'] }
235
+ ]
236
+ }
237
+
238
+ /**
239
+ * Request/response type descriptors for the RPC facade.
240
+ *
241
+ * @returns {TypeDesc[]}
242
+ */
243
+ export function rpcTypes() {
244
+ return [
245
+ { name: 'req-empty', compact: false, fields: [{ name: 'ok', type: 'bool', required: false }] },
246
+ {
247
+ name: 'req-restore',
248
+ compact: false,
249
+ fields: [{ name: 'phrase', type: 'string', required: true }]
250
+ },
251
+ {
252
+ name: 'req-row',
253
+ compact: false,
254
+ fields: [
255
+ { name: 'handle', type: 'string', required: true },
256
+ { name: 'ref', type: 'string', required: true },
257
+ { name: 'data', type: 'buffer', required: true }
258
+ ]
259
+ },
260
+ {
261
+ name: 'req-id',
262
+ compact: false,
263
+ fields: [
264
+ { name: 'handle', type: 'string', required: true },
265
+ { name: 'ref', type: 'string', required: true },
266
+ { name: 'id', type: 'string', required: true }
267
+ ]
268
+ },
269
+ {
270
+ name: 'req-query',
271
+ compact: false,
272
+ fields: [
273
+ { name: 'handle', type: 'string', required: true },
274
+ { name: 'ref', type: 'string', required: true },
275
+ { name: 'query', type: 'buffer', required: false }
276
+ ]
277
+ },
278
+ {
279
+ name: 'req-call',
280
+ compact: false,
281
+ fields: [
282
+ { name: 'handle', type: 'string', required: true },
283
+ { name: 'op', type: 'string', required: true },
284
+ { name: 'data', type: 'buffer', required: false }
285
+ ]
286
+ },
287
+ {
288
+ name: 'req-invite',
289
+ compact: false,
290
+ fields: [
291
+ { name: 'handle', type: 'string', required: true },
292
+ { name: 'role', type: 'string', required: false }
293
+ ]
294
+ },
295
+ {
296
+ name: 'req-revoke',
297
+ compact: false,
298
+ fields: [
299
+ { name: 'handle', type: 'string', required: true },
300
+ { name: 'invite', type: 'string', required: true }
301
+ ]
302
+ },
303
+ {
304
+ name: 'req-join',
305
+ compact: false,
306
+ fields: [
307
+ { name: 'parent', type: 'string', required: true },
308
+ { name: 'ref', type: 'string', required: true },
309
+ { name: 'invite', type: 'string', required: true }
310
+ ]
311
+ },
312
+ {
313
+ name: 'req-open',
314
+ compact: false,
315
+ fields: [
316
+ { name: 'parent', type: 'string', required: true },
317
+ { name: 'row', type: 'string', required: true }
318
+ ]
319
+ },
320
+ {
321
+ name: 'req-handle',
322
+ compact: false,
323
+ fields: [{ name: 'handle', type: 'string', required: true }]
324
+ },
325
+ {
326
+ name: 'res-data',
327
+ compact: false,
328
+ fields: [{ name: 'data', type: 'buffer', required: false }]
329
+ },
330
+ {
331
+ name: 'res-rows',
332
+ compact: false,
333
+ fields: [
334
+ { name: 'data', type: 'buffer', required: true },
335
+ { name: 'total', type: 'int', required: true },
336
+ { name: 'size', type: 'int', required: true }
337
+ ]
338
+ },
339
+ { name: 'res-count', compact: false, fields: [{ name: 'count', type: 'int', required: true }] },
340
+ {
341
+ name: 'res-invite',
342
+ compact: false,
343
+ fields: [{ name: 'invite', type: 'string', required: true }]
344
+ },
345
+ {
346
+ name: 'res-handle',
347
+ compact: false,
348
+ fields: [
349
+ { name: 'id', type: 'string', required: true },
350
+ { name: 'type', type: 'string', required: true },
351
+ { name: 'name', type: 'string', required: false }
352
+ ]
353
+ },
354
+ {
355
+ name: 'res-identity',
356
+ compact: false,
357
+ fields: [
358
+ { name: 'id', type: 'string', required: true },
359
+ { name: 'deviceId', type: 'string', required: false },
360
+ { name: 'phrase', type: 'string', required: false }
361
+ ]
362
+ },
363
+ { name: 'res-ok', compact: false, fields: [{ name: 'ok', type: 'bool', required: false }] }
364
+ ]
365
+ }
366
+
367
+ /**
368
+ * RPC command (verb) descriptors for the cero server, namespaced under `ns`.
369
+ *
370
+ * @param {string} ns
371
+ * @returns {CommandDesc[]}
372
+ */
373
+ export function rpcCommands(ns) {
374
+ const at = (n) => `@${ns}/${n}`
375
+ return [
376
+ {
377
+ name: 'init',
378
+ request: { name: at('req-empty') },
379
+ response: { name: at('res-identity') }
380
+ },
381
+ {
382
+ name: 'restore',
383
+ request: { name: at('req-restore') },
384
+ response: { name: at('res-identity') }
385
+ },
386
+ { name: 'add-row', request: { name: at('req-row') }, response: { name: at('res-data') } },
387
+ { name: 'add-handle', request: { name: at('req-row') }, response: { name: at('res-handle') } },
388
+ { name: 'set', request: { name: at('req-row') }, response: { name: at('res-data') } },
389
+ { name: 'get', request: { name: at('req-query') }, response: { name: at('res-rows') } },
390
+ { name: 'get-one', request: { name: at('req-id') }, response: { name: at('res-data') } },
391
+ { name: 'del', request: { name: at('req-id') }, response: { name: at('res-ok') } },
392
+ { name: 'count', request: { name: at('req-query') }, response: { name: at('res-count') } },
393
+ {
394
+ name: 'watch',
395
+ request: { name: at('req-query') },
396
+ response: { name: at('res-rows'), stream: true }
397
+ },
398
+ { name: 'call', request: { name: at('req-call') }, response: { name: at('res-data') } },
399
+ { name: 'invite', request: { name: at('req-invite') }, response: { name: at('res-invite') } },
400
+ { name: 'revoke', request: { name: at('req-revoke') }, response: { name: at('res-ok') } },
401
+ { name: 'join', request: { name: at('req-join') }, response: { name: at('res-handle') } },
402
+ {
403
+ name: 'open-handle',
404
+ request: { name: at('req-open') },
405
+ response: { name: at('res-handle') }
406
+ },
407
+ { name: 'close-handle', request: { name: at('req-handle') }, response: { name: at('res-ok') } },
408
+ { name: 'leave', request: { name: at('req-handle') }, response: { name: at('res-ok') } }
409
+ ]
410
+ }