@cero-base/core 1.1.1 → 1.3.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.
@@ -0,0 +1,59 @@
1
+ import { Duplex } from 'streamx'
2
+
3
+ // iOS caps a single GATT write (and a notify payload) at ATT_MTU − 3 ≈ 182
4
+ // bytes. 150 stays safely under that without negotiating an MTU. Raise later
5
+ // via the peripheral's maximumWriteValueLength + write-without-response.
6
+ const PAYLOAD = 150
7
+
8
+ /**
9
+ * A dumb byte-carrying duplex for the GATT transport. Framing and session logic
10
+ * live in BluetoothTransport; this only fragments outbound writes to fit a GATT
11
+ * write and pushes inbound payload bytes. NoiseSecretStream wraps it as a raw
12
+ * duplex, exactly like the old L2CAP channel.
13
+ *
14
+ * @extends Duplex
15
+ */
16
+ export class GattStream extends Duplex {
17
+ /**
18
+ * @param {object} opts
19
+ * @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
20
+ * @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
21
+ */
22
+ constructor({ send, onclose } = {}) {
23
+ super()
24
+ this._send = send
25
+ this._onclose = onclose || null
26
+ }
27
+
28
+ async _write(chunk, cb) {
29
+ try {
30
+ for (let offset = 0; offset < chunk.byteLength; offset += PAYLOAD) {
31
+ await this._send(chunk.subarray(offset, offset + PAYLOAD))
32
+ }
33
+ cb(null)
34
+ } catch (err) {
35
+ cb(err)
36
+ }
37
+ }
38
+
39
+ receive(buffer) {
40
+ this.push(buffer)
41
+ }
42
+
43
+ remoteEnd() {
44
+ this.push(null)
45
+ }
46
+
47
+ _destroy(cb) {
48
+ const onclose = this._onclose
49
+ this._onclose = null
50
+ if (onclose) {
51
+ try {
52
+ onclose()
53
+ } catch {
54
+ // teardown is best-effort
55
+ }
56
+ }
57
+ cb(null)
58
+ }
59
+ }
@@ -1,4 +1,6 @@
1
1
  import Hyperswarm from 'hyperswarm'
2
+ import NoiseSecretStream from '@hyperswarm/secret-stream'
3
+ import BlindPairing from 'blind-pairing'
2
4
  import ProtomuxWakeup from 'protomux-wakeup'
3
5
  import ReadyResource from 'ready-resource'
4
6
  import safetyCatch from 'safety-catch'
@@ -39,6 +41,89 @@ export class Network extends ReadyResource {
39
41
 
40
42
  this._replicateables = new Set()
41
43
  this._discoveries = new Set()
44
+ this._injected = new Set()
45
+ this._blind = null
46
+ }
47
+
48
+ /**
49
+ * Feed an externally-established connection — a Bluetooth L2CAP channel, a
50
+ * serial link, an in-process pair, any duplex — into the network. A raw
51
+ * duplex is wrapped in NoiseSecretStream (pass `isInitiator`); a stream
52
+ * that already IS one is used as-is. From here it gets the exact same
53
+ * treatment as a swarm connection: wakeup, replication of every attached
54
+ * core, pairing, and the 'connection' event.
55
+ *
56
+ * @param {any} stream Duplex transport, or a ready NoiseSecretStream.
57
+ * @param {{ isInitiator?: boolean }} [opts] Which side initiates the noise handshake (raw duplexes only).
58
+ * @returns {any} The encrypted connection stream.
59
+ */
60
+ inject(stream, { isInitiator } = {}) {
61
+ if (this.closing || this.closed) throw CeroError.CLOSED('Network')
62
+ if (!stream) throw CeroError.REQUIRED('stream')
63
+ // Injected links authenticate with the same long-lived identity as the
64
+ // swarm — an ephemeral key per link would defeat duplicate-peer detection
65
+ // (two radios between one pair would look like two different peers).
66
+ const keyPair = this.identity
67
+ ? { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
68
+ : (this.swarm?.keyPair ?? undefined)
69
+ const conn =
70
+ stream.noiseStream === stream
71
+ ? stream
72
+ : new NoiseSecretStream(isInitiator === true, stream, keyPair ? { keyPair } : undefined)
73
+
74
+ // blind-pairing picks the lowest-`rtt` unvisited channel to send on; that
75
+ // field only exists on real udx sockets. A raw injected duplex has none,
76
+ // so `rtt < Infinity` is always false and pairing never sends a request.
77
+ // Zero is also the semantically correct RTT for a direct injected link.
78
+ if (conn.rawStream && conn.rawStream.rtt === undefined) conn.rawStream.rtt = 0
79
+
80
+ this._injected.add(conn)
81
+ conn.on('close', () => this._injected.delete(conn))
82
+ conn.on('error', safetyCatch) // a dropped radio link must not crash the host
83
+
84
+ this.wakeup.addStream(conn)
85
+ for (const r of this._replicateables) replicateInto(r, conn)
86
+ this.emit('connection', conn, { injected: true })
87
+ return conn
88
+ }
89
+
90
+ /**
91
+ * Lazily create the network-shared BlindPairing. One instance serves every
92
+ * handle's pairing member — per-handle instances each added their own swarm
93
+ * and DHT listeners plus a protomux channel per connection.
94
+ *
95
+ * @returns {Promise<any>}
96
+ */
97
+ async blind() {
98
+ if (!this._blind) {
99
+ const blind = new BlindPairing(this.swarm)
100
+ this._blind = blind.ready().then(() => {
101
+ // blind-pairing only watches the swarm — injected connections must
102
+ // reach it too, or offline (e.g. Bluetooth) pairing never completes.
103
+ // _onconnection is upstream-private; the offline-pairing test pins it.
104
+ this.on('connection', (conn, info) => {
105
+ if (info?.injected) blind._onconnection(conn)
106
+ })
107
+ for (const conn of this._injected) blind._onconnection(conn)
108
+ return blind
109
+ })
110
+ }
111
+ return this._blind
112
+ }
113
+
114
+ /**
115
+ * Re-attach pairing channels on injected connections. blind-pairing only
116
+ * auto-attaches refs that existed when a connection arrived — swarm peers
117
+ * meet again over topic joins, injected links (Bluetooth, pipes) don't, so
118
+ * a member/candidate added later must re-run the attach. Idempotent:
119
+ * protomux refuses duplicate channels.
120
+ *
121
+ * @returns {Promise<void>}
122
+ */
123
+ async refreshInjected() {
124
+ if (!this._blind || !this._injected.size) return
125
+ const blind = await this._blind
126
+ for (const conn of this._injected) blind._onconnection(conn)
42
127
  }
43
128
 
44
129
  /** @returns {Map<string, any>} Known peers keyed by public-key string. */
@@ -46,9 +131,10 @@ export class Network extends ReadyResource {
46
131
  return this.swarm ? this.swarm.peers : new Map()
47
132
  }
48
133
 
49
- /** @returns {Set<any>} Live connection streams. */
134
+ /** @returns {Set<any>} Live connection streams — swarm and injected. */
50
135
  get connections() {
51
- return this.swarm ? this.swarm.connections : new Set()
136
+ if (!this._injected.size) return this.swarm ? this.swarm.connections : new Set()
137
+ return new Set([...(this.swarm ? this.swarm.connections : []), ...this._injected])
52
138
  }
53
139
 
54
140
  /** @returns {boolean} */
@@ -129,6 +215,15 @@ export class Network extends ReadyResource {
129
215
  }
130
216
 
131
217
  async _close() {
218
+ for (const conn of [...this._injected]) {
219
+ try {
220
+ conn.destroy()
221
+ } catch (err) {
222
+ safetyCatch(err)
223
+ }
224
+ }
225
+ this._injected.clear()
226
+
132
227
  for (const d of [...this._discoveries]) {
133
228
  try {
134
229
  await d.destroy()
@@ -137,6 +232,15 @@ export class Network extends ReadyResource {
137
232
  }
138
233
  }
139
234
 
235
+ if (this._blind) {
236
+ try {
237
+ await (await this._blind).close()
238
+ } catch (err) {
239
+ safetyCatch(err)
240
+ }
241
+ this._blind = null
242
+ }
243
+
140
244
  if (this.swarm) {
141
245
  try {
142
246
  await this.flush()
@@ -195,9 +299,7 @@ export class Network extends ReadyResource {
195
299
  attach(core) {
196
300
  if (!core) throw CeroError.REQUIRED('core')
197
301
  this._replicateables.add(core)
198
- if (this.swarm) {
199
- for (const stream of this.swarm.connections) replicateInto(core, stream)
200
- }
302
+ for (const stream of this.connections) replicateInto(core, stream)
201
303
  }
202
304
 
203
305
  /**
@@ -223,13 +325,24 @@ export class Network extends ReadyResource {
223
325
  if (!target || typeof target.replicate !== 'function') {
224
326
  throw CeroError.INVALID('target must be an object with a replicate(stream) method')
225
327
  }
226
- if (this.swarm) {
227
- for (const stream of this.swarm.connections) replicateInto(target, stream)
228
- }
328
+ for (const stream of this.connections) replicateInto(target, stream)
229
329
  }
230
330
  }
231
331
 
332
+ // Corestore replication is store-wide: N attached bees on one store would
333
+ // re-attach every core AND add N duplicate StreamTracker records per
334
+ // connection. Replicate each root store once per stream; wakeup streams are
335
+ // added at the network level, so skipped bees lose nothing.
336
+ const replicatedRoots = new WeakMap()
337
+
232
338
  function replicateInto(core, stream) {
339
+ const root = core.store ? core.store.root || core.store : null
340
+ if (root) {
341
+ let seen = replicatedRoots.get(stream)
342
+ if (!seen) replicatedRoots.set(stream, (seen = new WeakSet()))
343
+ if (seen.has(root)) return
344
+ seen.add(root)
345
+ }
233
346
  try {
234
347
  core.replicate(stream)
235
348
  } catch (err) {
@@ -88,13 +88,14 @@ export class Pairing extends ReadyResource {
88
88
 
89
89
  async _open() {
90
90
  await this.network.ready()
91
- this._blind = new BlindPairing(this.network.swarm)
92
- await this._blind.ready()
91
+ // shared instance — one set of swarm/DHT listeners for every handle
92
+ this._blind = await this.network.blind()
93
93
 
94
94
  this._member = this._blind.addMember({
95
95
  discoveryKey: discoveryKey(this.topic),
96
96
  onadd: (req) => this._onCandidate(req).catch(this._onerror)
97
97
  })
98
+ await this.network.refreshInjected()
98
99
  }
99
100
 
100
101
  async _close() {
@@ -105,10 +106,8 @@ export class Pairing extends ReadyResource {
105
106
  await this._member.close()
106
107
  this._member = null
107
108
  }
108
- if (this._blind) {
109
- await this._blind.close()
110
- this._blind = null
111
- }
109
+ // the BlindPairing is network-owned and shared — never close it here
110
+ this._blind = null
112
111
  this._invites.clear()
113
112
  }
114
113
 
@@ -415,6 +414,7 @@ class Candidate {
415
414
  onadd: (result) => this._done(result)
416
415
  })
417
416
  this._candidate.request.on('rejected', (err) => this._fail(fromBlindError(err)))
417
+ this.pairing.network.refreshInjected().catch(this._onerror ?? (() => {}))
418
418
  } catch (err) {
419
419
  this._fail(err instanceof CeroError ? err : CeroError.NETWORK_ERROR(err.message))
420
420
  }
package/src/rpc/index.js CHANGED
@@ -108,13 +108,13 @@ export function bindCodec(spec) {
108
108
  },
109
109
  encodeAction(handle, op, data) {
110
110
  const ref = handle?.[op]
111
- if (!ref || !ref.schema) throw new Error(`unknown action: ${op}`)
111
+ if (!ref || !ref.schema) throw CeroError.UNKNOWN('action', op)
112
112
  if (data == null) return EMPTY
113
113
  return schema.encode(ref.schema, data)
114
114
  },
115
115
  decodeAction(handle, op, buf) {
116
116
  const ref = handle?.[op]
117
- if (!ref || !ref.schema) throw new Error(`unknown action: ${op}`)
117
+ if (!ref || !ref.schema) throw CeroError.UNKNOWN('action', op)
118
118
  if (!buf || buf.length === 0) return undefined
119
119
  return schema.decode(ref.schema, buf)
120
120
  }
@@ -1,3 +1,5 @@
1
+ import fs from 'fs'
2
+
1
3
  import HypercoreStorage from 'hypercore-storage'
2
4
  import Corestore from 'corestore'
3
5
  import HyperDB from 'hyperdb'
@@ -7,12 +9,15 @@ import { ROCKS, BEE, SINGLE, COLLECTION } from '../lib/constants.js'
7
9
  import { genId, subscribe } from '../lib/utils.js'
8
10
  import { CeroError } from '../lib/errors.js'
9
11
 
12
+ const RANGE_OPS = new Set(['gt', 'gte', 'lt', 'lte', 'reverse', 'limit'])
13
+
10
14
  /**
11
15
  * @typedef {object} StorageOpts
12
16
  * @property {{ database: any, meta?: { ns?: string, refs?: Record<string, { kind?: string }> } }} spec
13
17
  * @property {'rocks' | 'bee'} backend
14
18
  * @property {any} [root] Pre-existing HypercoreStorage to reuse.
15
19
  * @property {any} [store] Pre-existing Corestore to reuse.
20
+ * @property {Uint8Array} [storageKey] 32-byte key encrypting the backing core at rest (bee backend only).
16
21
  *
17
22
  * @typedef {{ name: string, kind: string }} Ref
18
23
  * @typedef {{ id?: string, createdAt: number, updatedAt: number, [k: string]: any }} StoredRow
@@ -31,17 +36,22 @@ export class Storage extends ReadyResource {
31
36
  * @param {string} dir
32
37
  * @param {StorageOpts} [opts]
33
38
  */
34
- constructor(dir, { spec, backend, root, store } = {}) {
39
+ constructor(dir, { spec, backend, root, store, storageKey } = {}) {
35
40
  super()
36
41
  if (!root && !store && (typeof dir !== 'string' || !dir))
37
42
  throw CeroError.REQUIRED('dir, root, or store')
38
43
  if (!spec || !spec.database) throw CeroError.REQUIRED('spec.database')
39
44
  if (backend !== ROCKS && backend !== BEE)
40
45
  throw CeroError.INVALID('backend must be "rocks" or "bee"')
46
+ if (storageKey && backend === ROCKS)
47
+ throw CeroError.INVALID('storageKey requires the bee backend')
48
+ if (storageKey && storageKey.byteLength !== 32)
49
+ throw CeroError.INVALID('storageKey must be 32 bytes')
41
50
 
42
51
  this.dir = dir
43
52
  this.spec = spec
44
53
  this.backend = backend
54
+ this.storageKey = storageKey || null
45
55
  this.ns = spec.meta?.ns || 'cero'
46
56
  this.refs = spec.meta?.refs || {}
47
57
 
@@ -77,6 +87,7 @@ export class Storage extends ReadyResource {
77
87
  if (this._ownsRoot) {
78
88
  this.root = new HypercoreStorage(this.dir, { columnFamilies: [this._cf] })
79
89
  await this.root.ready()
90
+ await fs.promises.chmod(this.dir, 0o700)
80
91
  }
81
92
 
82
93
  if (this._ownsStore) {
@@ -90,7 +101,7 @@ export class Storage extends ReadyResource {
90
101
  const cf = this.root.rocks.columnFamily(this._cf)
91
102
  this.db = HyperDB.rocks(cf, this.spec.database)
92
103
  } else {
93
- const core = this.store.get({ name: 'local' })
104
+ const core = this.store.get({ name: 'local', encryptionKey: this.storageKey })
94
105
  await core.ready()
95
106
  this.db = HyperDB.bee(core, this.spec.database, { extension: false, autoUpdate: true })
96
107
  }
@@ -193,9 +204,19 @@ export class Storage extends ReadyResource {
193
204
  return { data: (await this.db.get(col, { id: query })) ?? null }
194
205
  }
195
206
 
207
+ // hyperdb only honors range/limit operators — equality fields must be
208
+ // filtered here or they are silently ignored and every row comes back
209
+ const eq = Object.keys(query || {}).filter((k) => !RANGE_OPS.has(k))
210
+ if (eq.length) {
211
+ const rows = await this.db.find(col, {}).toArray()
212
+ const matched = rows.filter((r) => eq.every((k) => r[k] === query[k]))
213
+ const data = query.limit != null ? matched.slice(0, query.limit) : matched
214
+ return { data, total: matched.length, size: data.length }
215
+ }
216
+
196
217
  const data = await this.db.find(col, query || {}).toArray()
197
- // a filter (or limit) caps `data`, so `total` needs a full count; with no
198
- // query `data` is already everything — skip the second scan.
218
+ // a limit caps `data`, so `total` needs a full count; with no query
219
+ // `data` is already everything — skip the second scan.
199
220
  const filtered = query && Object.keys(query).length > 0
200
221
  const total = filtered ? (await this.db.find(col, {}).toArray()).length : data.length
201
222
  return { data, total, size: data.length }
@@ -212,8 +233,10 @@ export class Storage extends ReadyResource {
212
233
  this._guard()
213
234
  const ref = this._ref(name)
214
235
  const col = this._col(ref)
215
- const rows = await this.db.find(col, query || {}).toArray()
216
- return { data: rows.length }
236
+ const eq = Object.keys(query || {}).filter((k) => !RANGE_OPS.has(k))
237
+ const rows = await this.db.find(col, eq.length ? {} : query || {}).toArray()
238
+ const matched = eq.length ? rows.filter((r) => eq.every((k) => r[k] === query[k])) : rows
239
+ return { data: matched.length }
217
240
  }
218
241
 
219
242
  /**
@@ -259,8 +282,7 @@ export class Storage extends ReadyResource {
259
282
  async _read(ref, id) {
260
283
  if (ref.kind === SINGLE) return this.db.findOne(this._col(ref), {})
261
284
  if (id == null) return null
262
- const rows = await this.db.find(this._col(ref), {}).toArray()
263
- return rows.find((r) => r.id === id) ?? null
285
+ return (await this.db.get(this._col(ref), { id })) ?? null
264
286
  }
265
287
 
266
288
  /** @param {Ref} ref @param {Record<string, any>} row @returns {Promise<void>} */
@@ -5,6 +5,7 @@
5
5
  * @param {{ dispatch: { Router: Function }, meta?: { refs?: Record<string, { kind?: string, builtin?: boolean, verb?: string }> } }} spec Generated hyperdispatch spec.
6
6
  * @param {string} ns Namespace prefix for collection and op names.
7
7
  * @param {Record<string, Function>} routes Custom action handlers keyed by route name.
8
+ * @param {(err: Error) => void} [onerror] Called when a malformed node is skipped.
8
9
  * @returns {{ dispatcher: object, apply: (nodes: Array<{ value: Buffer, key: Buffer }>, view: object, host: object) => Promise<void> }}
9
10
  */
10
11
  export function makeDispatcher(spec: {
@@ -18,7 +19,7 @@ export function makeDispatcher(spec: {
18
19
  verb?: string;
19
20
  }>;
20
21
  };
21
- }, ns: string, routes: Record<string, Function>): {
22
+ }, ns: string, routes: Record<string, Function>, onerror?: (err: Error) => void, getDbKey?: () => any): {
22
23
  dispatcher: object;
23
24
  apply: (nodes: Array<{
24
25
  value: Buffer;
@@ -9,12 +9,13 @@
9
9
  * @property {Uint8Array | null} [encryptionKey] Optional encryption key; falls back to identity's key.
10
10
  * @property {(nodes: any, view: any, host: any) => Promise<void>} [apply] Override the default apply function.
11
11
  * @property {Uint8Array | null} [key] Existing autobee key to reopen.
12
+ * @property {boolean} [passive] Join discovery server-only (reachable but not searching). Flip at runtime with `setActive`.
12
13
  * @property {import('../identity/index.js').KeyPair} [keyPair] Device writer keypair; defaults to identity's keypair.
13
- * @property {(err: Error) => void} [onerror] Called when a background after-hook or onApply callback fails.
14
+ * @property {(err: Error) => void} [onerror] Called when a background after-hook or onApply callback fails, a malformed node is skipped, or the bee errors.
14
15
  *
15
16
  * @typedef {{ data: any | null }} SingleResult
16
- * @typedef {{ data: any[], total: number, size: number }} ListResult
17
- * @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number, search?: string, fields?: string[] }} Query
17
+ * @typedef {{ data: any[], total: number | null, size: number }} ListResult `total` is null when a limited read skipped the full count — pass `{ total: true }` to force it.
18
+ * @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number, search?: string, fields?: string[], total?: boolean }} Query
18
19
  * @typedef {{ kind: string, verb: string, name: string }} Ref
19
20
  * @typedef {(ctx: any) => any | Promise<any>} HookFn
20
21
  */
@@ -58,6 +59,7 @@ export class Database extends ReadyResource {
58
59
  encryptionKey: Uint8Array<ArrayBufferLike>;
59
60
  applyOverride: (nodes: any, view: any, host: any) => Promise<void>;
60
61
  key: Uint8Array<ArrayBufferLike>;
62
+ passive: boolean;
61
63
  keyPair: import("../index.js").KeyPair | {
62
64
  publicKey: Uint8Array<ArrayBufferLike>;
63
65
  secretKey: Uint8Array<ArrayBufferLike>;
@@ -73,13 +75,22 @@ export class Database extends ReadyResource {
73
75
  };
74
76
  beforeHooks: Map<any, any>;
75
77
  afterHooks: Map<any, any>;
76
- updaters: Set<any>;
78
+ updaters: Map<any, any>;
77
79
  onApplyHooks: Set<any>;
78
80
  _applySeq: number;
81
+ _touched: Set<any>;
79
82
  txQueue: any[];
80
83
  _discovery: import("../network/discovery.js").Discovery;
81
84
  /** Open the underlying autobee, wire dispatcher + apply, attach to network. */
82
85
  openBee(): Promise<void>;
86
+ /**
87
+ * Flip announce mode at runtime — passive stays reachable (server) but
88
+ * stops actively looking (client). Cheap; use it to demote idle rooms.
89
+ *
90
+ * @param {boolean} active
91
+ * @returns {Promise<void>}
92
+ */
93
+ setActive(active: boolean): Promise<void>;
83
94
  /** @returns {Uint8Array | null} discovery key of the underlying bee */
84
95
  get discoveryKey(): Uint8Array | null;
85
96
  /** @returns {Uint8Array | null} this device's local writer key */
@@ -107,12 +118,16 @@ export class Database extends ReadyResource {
107
118
  */
108
119
  after(op: string, fn: HookFn): () => void;
109
120
  /**
110
- * Subscribe to local apply notifications. Fires whenever the view updates.
121
+ * Subscribe to local apply notifications. Fires whenever the view updates;
122
+ * pass `scope` (a ref name) to fire only when that ref was touched.
111
123
  *
112
124
  * @param {() => void} fn
125
+ * @param {string} [scope]
113
126
  * @returns {() => void} disposer
114
127
  */
115
- onUpdate(fn: () => void): () => void;
128
+ onUpdate(fn: () => void, scope?: string): () => void;
129
+ _touch(nodes: any): void;
130
+ _byVerb: Map<any, any>;
116
131
  /**
117
132
  * Observe every applied op — local AND replicated (apply processes the merged
118
133
  * log). The callback receives `{ op, name, row, writerKey, seq }` and runs
@@ -210,6 +225,9 @@ export class Database extends ReadyResource {
210
225
  * @returns {Promise<SingleResult | ListResult>}
211
226
  */
212
227
  get(name: string, query?: string | Query): Promise<SingleResult | ListResult>;
228
+ _orderReady(name: any, col: any): Promise<any>;
229
+ _orderOk: Map<any, any>;
230
+ _total(col: any, query: any, rows: any): Promise<any>;
213
231
  /**
214
232
  * Number of rows that match `query` (or total if omitted).
215
233
  *
@@ -359,21 +377,28 @@ export type DatabaseOpts = {
359
377
  * Existing autobee key to reopen.
360
378
  */
361
379
  key?: Uint8Array | null;
380
+ /**
381
+ * Join discovery server-only (reachable but not searching). Flip at runtime with `setActive`.
382
+ */
383
+ passive?: boolean;
362
384
  /**
363
385
  * Device writer keypair; defaults to identity's keypair.
364
386
  */
365
387
  keyPair?: import("../identity/index.js").KeyPair;
366
388
  /**
367
- * Called when a background after-hook or onApply callback fails.
389
+ * Called when a background after-hook or onApply callback fails, a malformed node is skipped, or the bee errors.
368
390
  */
369
391
  onerror?: (err: Error) => void;
370
392
  };
371
393
  export type SingleResult = {
372
394
  data: any | null;
373
395
  };
396
+ /**
397
+ * `total` is null when a limited read skipped the full count — pass `{ total: true }` to force it.
398
+ */
374
399
  export type ListResult = {
375
400
  data: any[];
376
- total: number;
401
+ total: number | null;
377
402
  size: number;
378
403
  };
379
404
  export type Query = {
@@ -385,6 +410,7 @@ export type Query = {
385
410
  limit?: number;
386
411
  search?: string;
387
412
  fields?: string[];
413
+ total?: boolean;
388
414
  };
389
415
  export type Ref = {
390
416
  kind: string;
@@ -125,6 +125,15 @@ export class Identity {
125
125
  * @type {Uint8Array}
126
126
  */
127
127
  seed: Uint8Array;
128
+ /**
129
+ * Redacted — secretKey/encryptionKey/seed must never reach JSON.stringify
130
+ * or a structured logger.
131
+ *
132
+ * @returns {{ id: string }}
133
+ */
134
+ toJSON(): {
135
+ id: string;
136
+ };
128
137
  /**
129
138
  * Detached Ed25519 signature over `message`.
130
139
  *
@@ -42,6 +42,12 @@ export class CeroError extends Error {
42
42
  * @param {string} name
43
43
  */
44
44
  static NOT_READY(resource: string, name: string): CeroError;
45
+ /**
46
+ * Operation raced an existing state (e.g. double init).
47
+ *
48
+ * @param {string} msg
49
+ */
50
+ static CONFLICT(msg: string): CeroError;
45
51
  /**
46
52
  * Resource has been destroyed.
47
53
  *
@@ -39,11 +39,9 @@ export const toId: (key: Uint8Array) => string;
39
39
  * @type {(id: string) => Uint8Array}
40
40
  */
41
41
  export const toKey: (id: string) => Uint8Array;
42
- /**
43
- * Test whether a string is a well-formed canonical id.
44
- *
45
- * @type {(id: string) => boolean}
46
- */
47
- export const isKeyId: (id: string) => boolean;
42
+ /** @type {(dbKey: Uint8Array, writer: Uint8Array, appender: Uint8Array) => Uint8Array} */
43
+ export const addWriterPayload: (dbKey: Uint8Array, writer: Uint8Array, appender: Uint8Array) => Uint8Array;
44
+ /** @type {(dbKey: Uint8Array, writer: Uint8Array) => Uint8Array} */
45
+ export const claimWriterPayload: (dbKey: Uint8Array, writer: Uint8Array) => Uint8Array;
48
46
  export function grants(a: any, b: any): boolean;
49
47
  export function outranks(a: any, b: any): boolean;