@cero-base/core 1.14.1 → 1.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/core",
3
- "version": "1.14.1",
3
+ "version": "1.15.1",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -136,7 +136,7 @@
136
136
  },
137
137
  "dependencies": {
138
138
  "@hyperswarm/secret-stream": "^6.9.1",
139
- "autobee": "2.0.0-rc.25",
139
+ "autobee": "2.0.0-rc.26",
140
140
  "autobee-encryption": "0.1.3",
141
141
  "b4a": "^1.8.1",
142
142
  "bare-crypto": "^1.15.3",
@@ -186,6 +186,29 @@ export class Database extends ReadyResource {
186
186
  optimistic: true,
187
187
  wakeup: wakeup || undefined,
188
188
  open: (b) => HyperDB.bee2(b, this.spec.database, { autoUpdate: true }),
189
+ // without this hook autobee trusts EVERY fast-forward candidate, so a
190
+ // removed device could serve a stale or forked head and we would boot
191
+ // onto it. A head is only worth following if its writer is still a
192
+ // device — decided against the reference view autobee hands us (its own
193
+ // state at the point of the decision), never the live view. Untrusted
194
+ // heads are then skipped outright: mostRecentTrusted stays unset because
195
+ // no cero row records another writer's oplog length to fall back to.
196
+ isTrusted: async (writer, view) => {
197
+ try {
198
+ const row = await bounded(view.get(`@${this.ns}/devices`, { id: toId(writer) }))
199
+ if (!row) return false
200
+ if (row.v) return true
201
+ // only where the view PROVABLY holds no device yet (a boot's
202
+ // genesis-era reference) may the genesis writer vouch — its core IS
203
+ // the db key, and its signatures are unforgeable. Everyone else
204
+ // stays refused: an empty view is the window an ex-member targets.
205
+ const any = await bounded(view.find(`@${this.ns}/devices`, { limit: 1 }).toArray())
206
+ if (!any) return false
207
+ return any.v.length === 0 && !!this.key && b4a.equals(writer, this.key)
208
+ } catch {
209
+ return false
210
+ }
211
+ },
189
212
  apply: async (nodes, view, host) => {
190
213
  // envelope handling lives here, once: unwrap every node and skip ops
191
214
  // from a newer app version (deterministic — they stay in the log), so
@@ -218,6 +241,12 @@ export class Database extends ReadyResource {
218
241
  }
219
242
  })
220
243
 
244
+ // swarm BEFORE boot when the key is known: replication is live while the
245
+ // bee opens, so the boot (and a future fast-forward boot) has peers to
246
+ // pull from instead of opening cold and waiting for discovery
247
+ const known = !!this.key
248
+ if (this.network && known) this._joinSwarm(bee, crypto.discoveryKey(this.key))
249
+
221
250
  await bee.ready()
222
251
  // assigned before the first update: apply-time hooks (_onEpoch's save,
223
252
  // _onFuture) dereference this.bee and must never see it null mid-drain
@@ -252,17 +281,19 @@ export class Database extends ReadyResource {
252
281
  // without a listener autobee escalates apply/view errors to a process crash
253
282
  bee.on('error', this._onerror)
254
283
 
255
- if (this.network) {
256
- this.network.attach(bee)
257
- // a writer swap re-opens the bee on the SAME topic — join first so the
258
- // old session's destroy is a refcounted detach, not a DHT unannounce
259
- // round-trip (which blocked the swap for seconds)
260
- const prev = this._discovery
261
- this._discovery = this.network.join(bee.discoveryKey, {
262
- mode: this.passive ? PASSIVE : ACTIVE
263
- })
264
- if (prev) await prev.destroy()
265
- }
284
+ // a fresh db has no key until boot mints it — join once it exists
285
+ if (this.network && !known) this._joinSwarm(bee, bee.discoveryKey)
286
+ }
287
+
288
+ // a writer swap re-opens the bee on the SAME topic — join first so the old
289
+ // session's destroy is a refcounted detach, not a DHT unannounce round-trip
290
+ _joinSwarm(bee, discoveryKey) {
291
+ this.network.attach(bee)
292
+ const prev = this._discovery
293
+ this._discovery = this.network.join(discoveryKey, {
294
+ mode: this.passive ? PASSIVE : ACTIVE
295
+ })
296
+ if (prev) prev.destroy().catch(safetyCatch)
266
297
  }
267
298
 
268
299
  /**
@@ -1212,6 +1243,17 @@ export class Database extends ReadyResource {
1212
1243
  }
1213
1244
  }
1214
1245
 
1246
+ // A trust decision reads a historic checkout view, whose blocks may not be
1247
+ // servable yet — an unbounded read there wedges autobee's drain forever. The
1248
+ // value is wrapped so a timeout (null) is never mistaken for what the read
1249
+ // would have returned; undecidable in time means "not trusted", and the
1250
+ // caller retries.
1251
+ const bounded = (promise, ms = 2000) =>
1252
+ Promise.race([
1253
+ promise.then((v) => ({ v })),
1254
+ new Promise((resolve) => setTimeout(() => resolve(null), ms))
1255
+ ])
1256
+
1215
1257
  // Fields the write path stamps itself — always allowed even if not user-declared.
1216
1258
  const SYSTEM_FIELDS = new Set(['id', 'memberId', 'index', 'createdAt', 'updatedAt'])
1217
1259
 
@@ -24,6 +24,7 @@ export function channelTopic(topic, channel) {
24
24
  * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
25
25
  * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
26
26
  * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
27
+ * @property {number[]} [backoffs] Reconnect backoff tiers in ms; the default escalates to ~10min, far too slow for local nets.
27
28
  * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
28
29
  * @property {any} [store] Corestore; required for mirrors (blind peers replicate its cores).
29
30
  * @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
@@ -37,12 +38,22 @@ export function channelTopic(topic, channel) {
37
38
  */
38
39
  export class Network extends ReadyResource {
39
40
  /** @param {NetworkOpts} [opts] */
40
- constructor({ identity, bootstrap, firewall, relayThrough, channel, store, mirrors } = {}) {
41
+ constructor({
42
+ identity,
43
+ bootstrap,
44
+ firewall,
45
+ relayThrough,
46
+ backoffs,
47
+ channel,
48
+ store,
49
+ mirrors
50
+ } = {}) {
41
51
  super()
42
52
  this.identity = identity || null
43
53
  this.bootstrap = bootstrap || null
44
54
  this.firewall = firewall || null
45
55
  this.relayThrough = relayThrough || null
56
+ this.backoffs = backoffs || null
46
57
  this.channel = channel || null
47
58
  this.store = store || null
48
59
  this.mirrors = (mirrors || []).map((k) => (typeof k === 'string' ? decodeKey(k) : k))
@@ -168,6 +179,7 @@ export class Network extends ReadyResource {
168
179
  if (this.bootstrap) opts.bootstrap = this.bootstrap
169
180
  if (this.firewall) opts.firewall = this.firewall
170
181
  if (this.relayThrough) opts.relayThrough = this.relayThrough
182
+ if (this.backoffs) opts.backoffs = this.backoffs
171
183
 
172
184
  const swarm = (this._swarm = new Hyperswarm(opts))
173
185
  swarm.on('connection', (stream, info) => {
@@ -94,6 +94,7 @@ export class Database extends ReadyResource {
94
94
  _discovery: import("../network/discovery.js").Discovery;
95
95
  /** Open the underlying autobee, wire dispatcher + apply, attach to network. */
96
96
  openBee(): Promise<void>;
97
+ _joinSwarm(bee: any, discoveryKey: any): void;
97
98
  /**
98
99
  * Flip announce mode at runtime — passive stays reachable (server) but
99
100
  * stops actively looking (client). Cheap; use it to demote idle rooms.
@@ -5,6 +5,7 @@ export function channelTopic(topic: any, channel: any): any;
5
5
  * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
6
6
  * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
7
7
  * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
8
+ * @property {number[]} [backoffs] Reconnect backoff tiers in ms; the default escalates to ~10min, far too slow for local nets.
8
9
  * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
9
10
  * @property {any} [store] Corestore; required for mirrors (blind peers replicate its cores).
10
11
  * @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
@@ -17,7 +18,7 @@ export function channelTopic(topic: any, channel: any): any;
17
18
  */
18
19
  export class Network extends ReadyResource {
19
20
  /** @param {NetworkOpts} [opts] */
20
- constructor({ identity, bootstrap, firewall, relayThrough, channel, store, mirrors }?: NetworkOpts);
21
+ constructor({ identity, bootstrap, firewall, relayThrough, backoffs, channel, store, mirrors }?: NetworkOpts);
21
22
  identity: import("../index.js").Identity;
22
23
  bootstrap: {
23
24
  host: string;
@@ -25,6 +26,7 @@ export class Network extends ReadyResource {
25
26
  }[];
26
27
  firewall: (remotePublicKey: Uint8Array, payload: any) => boolean;
27
28
  relayThrough: Uint8Array<ArrayBufferLike>[];
29
+ backoffs: number[];
28
30
  channel: string;
29
31
  store: any;
30
32
  mirrors: any[];
@@ -152,6 +154,10 @@ export type NetworkOpts = {
152
154
  * Relay public keys to tunnel through.
153
155
  */
154
156
  relayThrough?: Uint8Array[];
157
+ /**
158
+ * Reconnect backoff tiers in ms; the default escalates to ~10min, far too slow for local nets.
159
+ */
160
+ backoffs?: number[];
155
161
  /**
156
162
  * Optional network-isolation label; only same-channel peers meet.
157
163
  */