@cero-base/cero 1.8.1 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/cero",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -95,11 +95,11 @@
95
95
  "test:node": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
96
96
  },
97
97
  "dependencies": {
98
- "@cero-base/core": "^1.8.1",
98
+ "@cero-base/core": "^1.10.0",
99
99
  "b4a": "^1.8.1",
100
100
  "bare-abort-controller": "^1.1.2",
101
101
  "bare-crypto": "^1.15.3",
102
- "bare-fs": "^4.7.4",
102
+ "bare-fs": "^4.8.0",
103
103
  "bare-path": "^3.1.1",
104
104
  "compact-encoding": "^3.3.0",
105
105
  "corestore": "^7.12.0",
@@ -120,7 +120,7 @@
120
120
  "bare-events": "^2.9.1",
121
121
  "bare-fetch": "^3.2.0",
122
122
  "bare-process": "^4.5.1",
123
- "bare-url": "^2.4.6",
123
+ "bare-url": "^2.5.2",
124
124
  "brittle": "^4.1.0",
125
125
  "typescript": "^5.9.3"
126
126
  },
package/src/bluetooth.js CHANGED
@@ -43,8 +43,9 @@ export class Bluetooth extends ReadyResource {
43
43
  * @param {boolean} [opts.autoStart] Start on handle open (from `cero({ bluetooth: true })`).
44
44
  * @param {number} [opts.maxOutbound] Max concurrent outbound links; gossip covers the rest.
45
45
  * @param {number} [opts.maxInbound] Max concurrent inbound sessions; newcomers past this are refused.
46
+ * @param {'l2cap' | 'gatt'} [opts.pipe] Data pipe — 'l2cap' (default, faster) or 'gatt'. Both peers must match.
46
47
  */
47
- constructor(handle, { backend, autoStart, maxOutbound, maxInbound } = {}) {
48
+ constructor(handle, { backend, autoStart, maxOutbound, maxInbound, pipe } = {}) {
48
49
  super()
49
50
  this._handle = handle
50
51
  // explicit null/false disables bluetooth; only an omitted backend lazy-loads
@@ -53,6 +54,7 @@ export class Bluetooth extends ReadyResource {
53
54
  this._autoStart = autoStart === true
54
55
  this._maxOutbound = maxOutbound
55
56
  this._maxInbound = maxInbound
57
+ this._pipe = pipe
56
58
  this._transport = null
57
59
  this._name = null
58
60
  this._announces = new Set()
@@ -83,6 +85,7 @@ export class Bluetooth extends ReadyResource {
83
85
  uuid: topic,
84
86
  nodeId: this._handle.identity.publicKey,
85
87
  tag: 'cero-ble-invite',
88
+ pipe: this._pipe,
86
89
  // the QR closing stops the rendezvous, not the just-established link —
87
90
  // that link carries the joiner's initial replication
88
91
  keepLinks: true
@@ -145,6 +148,7 @@ export class Bluetooth extends ReadyResource {
145
148
  : handle.identity.publicKey,
146
149
  nodeId: handle.identity.publicKey,
147
150
  name: this._name || '',
151
+ pipe: this._pipe,
148
152
  maxOutbound: this._maxOutbound,
149
153
  maxInbound: this._maxInbound,
150
154
  // Android scans in a battery-saver mode by default — too slow for a mesh
@@ -48,7 +48,9 @@ export const main = {
48
48
  role: required(string),
49
49
  expires: int,
50
50
  createdAt: int,
51
- index: uint
51
+ index: uint,
52
+ seed: bytes,
53
+ reuse: bool
52
54
  },
53
55
  handle: {
54
56
  id: required(string),
@@ -154,9 +154,18 @@ export class Handle extends ReadyResource {
154
154
  network: this.network,
155
155
  identity: this.identity,
156
156
  topic: this.store.key,
157
- onerror: this._onerror
157
+ onerror: this._onerror,
158
+ onconsume: (id) => {
159
+ this.store.call('del-invite', { id }).catch(safetyCatch)
160
+ }
158
161
  })
159
162
  await this.pair.ready()
163
+ // Serve invites persisted by any member — and keep the served set in
164
+ // step as rows replicate in (minted elsewhere) or disappear (revoked
165
+ // or consumed elsewhere).
166
+ await this._syncInvites().catch(safetyCatch)
167
+ this._invitesSync = () => this._syncInvites().catch(safetyCatch)
168
+ this.store.on('update', this._invitesSync)
160
169
  }
161
170
  attachRefs(this, this.store.refs)
162
171
  this.root._coreKeys.set(b4a.toString(this.store.key, 'hex'), this.store.encryptionKey)
@@ -179,6 +188,10 @@ export class Handle extends ReadyResource {
179
188
  for (const c of [...this.children]) await c.close()
180
189
  this.children.clear()
181
190
  }
191
+ if (this._invitesSync) {
192
+ this.store.off('update', this._invitesSync)
193
+ this._invitesSync = null
194
+ }
182
195
  if (this.pair) await this.pair.close()
183
196
 
184
197
  if (this.parent) {
@@ -455,12 +468,37 @@ export class Handle extends ReadyResource {
455
468
  * @param {{ role?: string, expiresIn?: number, data?: any }} [opts]
456
469
  * @returns {Promise<string>} Z32-encoded invite string.
457
470
  */
458
- invite(opts) {
471
+ async invite(opts) {
459
472
  if (!this.pair)
460
473
  throw CeroError.INVALID(
461
474
  'invite() is not available on the root handle — open a child handle first'
462
475
  )
463
- return this.pair.createInvite(opts)
476
+ const str = await this.pair.createInvite(opts)
477
+ const record = this.pair.recordOf(str)
478
+ if (record) {
479
+ // Persist so every member replica serves this invite across restarts —
480
+ // the in-memory record keeps working this session even if the write
481
+ // fails.
482
+ await this.store
483
+ .call('add-invite', {
484
+ id: b4a.toString(record.id, 'hex'),
485
+ invite: b4a.from(str),
486
+ publicKey: record.publicKey,
487
+ seed: record.seed,
488
+ role: record.invite.role || '',
489
+ expires: record.invite.expires || 0,
490
+ createdAt: Date.now(),
491
+ reuse: !!record.reuse
492
+ })
493
+ .catch(safetyCatch)
494
+ }
495
+ return str
496
+ }
497
+
498
+ async _syncInvites() {
499
+ if (!this.pair) return
500
+ const { data } = await this.store.get('invites')
501
+ this.pair.syncRows(data || [])
464
502
  }
465
503
 
466
504
  /**
@@ -471,7 +509,13 @@ export class Handle extends ReadyResource {
471
509
  */
472
510
  revoke(invite) {
473
511
  if (!this.pair) throw CeroError.INVALID('revoke() is not available on the root handle')
474
- return this.pair.revoke(invite)
512
+ const record = this.pair.recordOf(invite)
513
+ const revoked = this.pair.revoke(invite)
514
+ if (revoked && record) {
515
+ // drop the persisted row so every other member stops serving it too
516
+ this.store.call('del-invite', { id: b4a.toString(record.id, 'hex') }).catch(safetyCatch)
517
+ }
518
+ return revoked
475
519
  }
476
520
 
477
521
  /**
@@ -21,12 +21,14 @@ export class Bluetooth extends ReadyResource {
21
21
  * @param {boolean} [opts.autoStart] Start on handle open (from `cero({ bluetooth: true })`).
22
22
  * @param {number} [opts.maxOutbound] Max concurrent outbound links; gossip covers the rest.
23
23
  * @param {number} [opts.maxInbound] Max concurrent inbound sessions; newcomers past this are refused.
24
+ * @param {'l2cap' | 'gatt'} [opts.pipe] Data pipe — 'l2cap' (default, faster) or 'gatt'. Both peers must match.
24
25
  */
25
- constructor(handle: object, { backend, autoStart, maxOutbound, maxInbound }?: {
26
+ constructor(handle: object, { backend, autoStart, maxOutbound, maxInbound, pipe }?: {
26
27
  backend?: any;
27
28
  autoStart?: boolean;
28
29
  maxOutbound?: number;
29
30
  maxInbound?: number;
31
+ pipe?: "l2cap" | "gatt";
30
32
  });
31
33
  _handle: any;
32
34
  _backend: any;
@@ -34,6 +36,7 @@ export class Bluetooth extends ReadyResource {
34
36
  _autoStart: boolean;
35
37
  _maxOutbound: number;
36
38
  _maxInbound: number;
39
+ _pipe: "l2cap" | "gatt";
37
40
  _transport: BLETransport;
38
41
  _name: any;
39
42
  _announces: Set<any>;
@@ -42,6 +42,8 @@ export const main: {
42
42
  expires: import("@cero-base/core").Prim;
43
43
  createdAt: import("@cero-base/core").Prim;
44
44
  index: import("@cero-base/core").Prim;
45
+ seed: import("@cero-base/core").Prim;
46
+ reuse: import("@cero-base/core").Prim;
45
47
  };
46
48
  handle: {
47
49
  id: import("@cero-base/core").Prim;
@@ -100,6 +100,7 @@ export class Handle extends ReadyResource {
100
100
  store: Database;
101
101
  pair: Pairing;
102
102
  _wantsPair: boolean;
103
+ _invitesSync: () => Promise<void>;
103
104
  /**
104
105
  * Tie a destroyable resource (a `watch` stream, a timer, any `{ destroy }`)
105
106
  * to this handle's lifecycle — it's destroyed automatically on close, so
@@ -235,6 +236,7 @@ export class Handle extends ReadyResource {
235
236
  expiresIn?: number;
236
237
  data?: any;
237
238
  }): Promise<string>;
239
+ _syncInvites(): Promise<void>;
238
240
  /**
239
241
  * Revoke a previously-minted invite by its string form.
240
242
  *