@cero-base/core 1.2.0 → 1.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.
@@ -0,0 +1,110 @@
1
+ import Hyperswarm from 'hyperswarm'
2
+ import b4a from 'b4a'
3
+ import { hash } from 'hypercore-crypto'
4
+ import safetyCatch from 'safety-catch'
5
+
6
+ /**
7
+ * @typedef {object} DHTTransportOpts
8
+ * @property {import('../../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
9
+ * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
10
+ * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
11
+ * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
12
+ * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
13
+ */
14
+
15
+ /**
16
+ * The internet transport: a Hyperswarm that finds peers over the DHT. Owns the
17
+ * swarm's whole lifecycle — construction (identity keyPair, bootstrap, firewall,
18
+ * relay), the channel-topic join/leave wrapping, flush, suspend/resume, and
19
+ * teardown. {@link Network} subscribes to `this.swarm`'s connection/peer events
20
+ * and drives topic joins; the swarm-specific wiring lives here.
21
+ */
22
+ export class DHTTransport {
23
+ /** @param {DHTTransportOpts} [opts] */
24
+ constructor({ identity, bootstrap, firewall, relayThrough, channel } = {}) {
25
+ const opts = {}
26
+ if (identity) opts.keyPair = { publicKey: identity.publicKey, secretKey: identity.secretKey }
27
+ if (bootstrap) opts.bootstrap = bootstrap
28
+ if (firewall) opts.firewall = firewall
29
+ if (relayThrough) opts.relayThrough = relayThrough
30
+
31
+ this.swarm = new Hyperswarm(opts)
32
+
33
+ if (channel) {
34
+ const join = this.swarm.join.bind(this.swarm)
35
+ const leave = this.swarm.leave.bind(this.swarm)
36
+ this.swarm.join = (topic, opts) => join(channelTopic(topic, channel), opts)
37
+ this.swarm.leave = (topic) => leave(channelTopic(topic, channel))
38
+ }
39
+ }
40
+
41
+ /** @returns {boolean} */
42
+ get suspended() {
43
+ return this.swarm?.suspended === true
44
+ }
45
+
46
+ /**
47
+ * Wait for pending DHT announces and lookups to settle, bounded by timeout.
48
+ *
49
+ * @param {{ timeout?: number }} [opts]
50
+ * @returns {Promise<void>}
51
+ */
52
+ async flush({ timeout = 500 } = {}) {
53
+ if (!this.swarm) return
54
+ await Promise.race([this.swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
55
+ }
56
+
57
+ /**
58
+ * Pause the swarm — keeps state, drops sockets. Idempotent.
59
+ *
60
+ * @returns {Promise<void>}
61
+ */
62
+ async suspend() {
63
+ if (!this.swarm || this.swarm.suspended) return
64
+ try {
65
+ await this.swarm.suspend()
66
+ } catch (err) {
67
+ safetyCatch(err)
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Resume a suspended swarm. Idempotent.
73
+ *
74
+ * @returns {Promise<void>}
75
+ */
76
+ async resume() {
77
+ if (!this.swarm || !this.swarm.suspended) return
78
+ try {
79
+ await this.swarm.resume()
80
+ } catch (err) {
81
+ safetyCatch(err)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Flush pending discovery, then tear down the swarm. Idempotent.
87
+ *
88
+ * @returns {Promise<void>}
89
+ */
90
+ async destroy() {
91
+ if (!this.swarm) return
92
+ try {
93
+ await this.flush()
94
+ } catch (err) {
95
+ safetyCatch(err)
96
+ }
97
+ try {
98
+ await this.swarm.destroy()
99
+ } catch (err) {
100
+ safetyCatch(err)
101
+ }
102
+ this.swarm = null
103
+ }
104
+ }
105
+
106
+ // A channel re-namespaces every swarm topic so only same-channel peers meet.
107
+ // No channel → identity (unchanged, back-compat).
108
+ export function channelTopic(topic, channel) {
109
+ return channel ? hash([topic, b4a.from(channel)]) : topic
110
+ }
@@ -0,0 +1,57 @@
1
+ import { Duplex } from 'streamx'
2
+
3
+ // A single GATT write/notify caps at ATT_MTU − 3 ≈ 182 bytes; 150 stays under
4
+ // that without negotiating an MTU.
5
+ const PAYLOAD = 150
6
+
7
+ /**
8
+ * A dumb byte-carrying duplex for the GATT transport. Framing and session logic
9
+ * live in BLETransport; this only fragments outbound writes to fit a GATT
10
+ * write and pushes inbound payload bytes.
11
+ *
12
+ * @extends Duplex
13
+ */
14
+ export class GattStream extends Duplex {
15
+ /**
16
+ * @param {object} opts
17
+ * @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
18
+ * @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
19
+ */
20
+ constructor({ send, onclose } = {}) {
21
+ super()
22
+ this._send = send
23
+ this._onclose = onclose || null
24
+ }
25
+
26
+ async _write(chunk, cb) {
27
+ try {
28
+ for (let offset = 0; offset < chunk.byteLength; offset += PAYLOAD) {
29
+ await this._send(chunk.subarray(offset, offset + PAYLOAD))
30
+ }
31
+ cb(null)
32
+ } catch (err) {
33
+ cb(err)
34
+ }
35
+ }
36
+
37
+ receive(buffer) {
38
+ this.push(buffer)
39
+ }
40
+
41
+ remoteEnd() {
42
+ this.push(null)
43
+ }
44
+
45
+ _destroy(cb) {
46
+ const onclose = this._onclose
47
+ this._onclose = null
48
+ if (onclose) {
49
+ try {
50
+ onclose()
51
+ } catch {
52
+ // teardown is best-effort
53
+ }
54
+ }
55
+ cb(null)
56
+ }
57
+ }
@@ -1,4 +1,4 @@
1
- export function channelTopic(topic: any, channel: any): any;
1
+ export { channelTopic };
2
2
  /**
3
3
  * @typedef {object} NetworkOpts
4
4
  * @property {import('../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
@@ -24,12 +24,14 @@ export class Network extends ReadyResource {
24
24
  firewall: (remotePublicKey: Uint8Array, payload: any) => boolean;
25
25
  relayThrough: Uint8Array<ArrayBufferLike>[];
26
26
  channel: string;
27
- swarm: any;
27
+ _dht: DHTTransport;
28
28
  wakeup: any;
29
29
  _replicateables: Set<any>;
30
30
  _discoveries: Set<any>;
31
31
  _injected: Set<any>;
32
32
  _blind: any;
33
+ /** @returns {any} The underlying hyperswarm, or null before ready / after close. */
34
+ get swarm(): any;
33
35
  /**
34
36
  * Feed an externally-established connection — a Bluetooth L2CAP channel, a
35
37
  * serial link, an in-process pair, any duplex — into the network. A raw
@@ -75,7 +77,7 @@ export class Network extends ReadyResource {
75
77
  * @param {{ timeout?: number }} [opts]
76
78
  * @returns {Promise<void>}
77
79
  */
78
- flush({ timeout }?: {
80
+ flush(opts?: {
79
81
  timeout?: number;
80
82
  }): Promise<void>;
81
83
  /**
@@ -153,5 +155,7 @@ export type NetworkOpts = {
153
155
  export type Replicable = {
154
156
  replicate: (stream: any) => any;
155
157
  };
158
+ import { channelTopic } from './transports/dht.js';
156
159
  import ReadyResource from 'ready-resource';
160
+ import { DHTTransport } from './transports/dht.js';
157
161
  import { Discovery } from './discovery.js';
@@ -8,35 +8,34 @@
8
8
  */
9
9
  export function toServiceUUID(topic: Uint8Array, tag?: string): string;
10
10
  /**
11
- * Dual-role BLE transport: advertises + scans one service UUID, opens an L2CAP
12
- * channel to each discovered peer, and feeds it into `network.inject`. From
11
+ * Dual-role BLE transport: advertises + scans one service UUID, opens a GATT
12
+ * byte-stream to each discovered peer, and feeds it into `network.inject`. From
13
13
  * there replication and pairing are transport-agnostic (see Network.inject).
14
14
  *
15
- * Choreography mirrors the proven bare-mobile-doctor worklet: server adds a
16
- * readable PSM characteristic at startup and answers reads dynamically; the
17
- * central connects, reads "<psm>:<nodeId>", tie-breaks, then opens the channel.
18
- * `backend` is bare-bluetooth in production and a mock in tests.
19
- *
20
- * ponytail: capability-handshake DoS link-scoring is deferred — it needs a
21
- * replication-progress signal (design §4b). v1 caps links + times out dials.
15
+ * The server adds one data characteristic (write + notify) and advertises. The
16
+ * central connects, discovers the characteristic, subscribes, then framed bytes
17
+ * flow both ways central→server as GATT writes, server→central as
18
+ * notifications each tagged with an 8-byte session id. `backend` is
19
+ * bare-bluetooth in production and a mock in tests.
22
20
  *
23
21
  * @extends ReadyResource
24
22
  */
25
- export class BluetoothTransport extends ReadyResource {
23
+ export class BLETransport extends ReadyResource {
26
24
  /**
27
25
  * @param {object} opts
28
26
  * @param {any} opts.backend bare-bluetooth-shaped module (Central, Server, Service, Characteristic).
29
- * @param {import('./index.js').Network} opts.network
27
+ * @param {import('../index.js').Network} opts.network
30
28
  * @param {Uint8Array} opts.uuid The 32-byte topic the service UUID derives from.
31
29
  * @param {Uint8Array} opts.nodeId Stable local id (identity/device key) for the initiate tie-break.
32
30
  * @param {string} [opts.tag] UUID namespace (channel mesh vs invite mesh).
33
31
  * @param {number} [opts.cap] Max concurrent links; gossip covers the rest.
34
32
  * @param {{ scanMode?: any }} [opts.scanOptions] Platform scan options (e.g. Android low-power).
35
33
  * @param {boolean} [opts.keepLinks] On close, stop the radio but leave established links alive (invite rendezvous: the link outlives the QR and carries the initial replication).
34
+ * @param {string} [opts.name] Local app-user display name, sent to peers over a hello frame.
36
35
  */
37
- constructor({ backend, network, uuid, nodeId, tag, cap, scanOptions, keepLinks }: {
36
+ constructor({ backend, network, uuid, nodeId, tag, cap, scanOptions, keepLinks, name }: {
38
37
  backend: any;
39
- network: import("./index.js").Network;
38
+ network: import("../index.js").Network;
40
39
  uuid: Uint8Array;
41
40
  nodeId: Uint8Array;
42
41
  tag?: string;
@@ -45,9 +44,11 @@ export class BluetoothTransport extends ReadyResource {
45
44
  scanMode?: any;
46
45
  };
47
46
  keepLinks?: boolean;
47
+ name?: string;
48
48
  });
49
49
  backend: any;
50
- network: import("./index.js").Network;
50
+ network: import("../index.js").Network;
51
+ name: string;
51
52
  nodeId: Uint8Array<ArrayBufferLike>;
52
53
  nodeHex: any;
53
54
  serviceUUID: string;
@@ -59,12 +60,20 @@ export class BluetoothTransport extends ReadyResource {
59
60
  state: string;
60
61
  central: any;
61
62
  server: any;
62
- psm: any;
63
+ _dataChar: any;
64
+ /** sessionId hex → { stream, sid } for server-side (peripheral) sessions */
65
+ _sessions: Map<any, any>;
66
+ /** serialized server notify queue: { frame, resolve, reject } */
67
+ _notifyQueue: any[];
63
68
  _scanning: boolean;
64
69
  _advertising: boolean;
65
70
  _serviceAdded: boolean;
66
- /** peripheral id being dialed its connect-timeout timer */
67
- _dialing: Map<any, any>;
71
+ /** peripheral id per-peer dial state { timer, linked, coolUntil, failures, peerKey, peripheral } */
72
+ _devices: Map<any, any>;
73
+ /** last central.connect timestamp — global inter-dial rate limit */
74
+ _lastDial: number;
75
+ _scanTimer: any;
76
+ _suspended: boolean;
68
77
  /** live injected links keyed by remote node id hex */
69
78
  peers: Map<any, any>;
70
79
  /**
@@ -77,17 +86,48 @@ export class BluetoothTransport extends ReadyResource {
77
86
  */
78
87
  shouldInitiate(peerNodeId: Uint8Array): boolean;
79
88
  get linkCount(): number;
89
+ _device(id: any): any;
90
+ _prune(id: any): void;
80
91
  _startServer(Service: any, Characteristic: any): void;
81
92
  _maybeAdvertise(): void;
93
+ _onWriteRequests(requests: any): void;
94
+ _onServerFrame(data: any): void;
95
+ _closeServerSession(sidHex: any, sid: any): void;
96
+ _helloPayload(): any;
97
+ _parseHello(payload: any): string;
98
+ _applyPeerName(session: any, payload: any): void;
99
+ _enqueueNotify(f: any): Promise<any>;
100
+ _drainNotify(): void;
82
101
  _startScan(): void;
102
+ _armScanRestart(): void;
103
+ _stopScan(): void;
83
104
  _onState(raw: any): void;
84
105
  _onDiscover(peripheral: any): void;
85
106
  _onConnect(peripheral: any): void;
107
+ _startCentralSession(peripheral: any, char: any): void;
108
+ _onCentralNotify(peripheral: any, data: any): void;
109
+ _centralSend(peripheral: any, char: any, f: any): any;
110
+ _writeOnce(peripheral: any, char: any, f: any): Promise<any>;
86
111
  _abortDial(peripheral: any, _reason: any): void;
87
112
  _clearDial(id: any): void;
113
+ _isDialing(): boolean;
88
114
  _onCentralError(err: any): void;
89
- _onChannel(l2cap: any, isInitiator: any, peripheralId: any): void;
90
- _track(conn: any, peripheralId: any): void;
115
+ _onChannel(stream: any, isInitiator: any, peripheralId: any): any;
116
+ _track(conn: any, peripheralId: any, isInitiator: any): void;
91
117
  _untrack(conn: any): void;
118
+ _sayGoodbye(): Promise<void>;
119
+ /**
120
+ * Pause radio activity but KEEP the Server/Central instances and the
121
+ * registered GATT service alive — the toggle-friendly counterpart to _close.
122
+ * CoreBluetooth managers can't be destroy()ed (native double-free), so one
123
+ * transport is reused across toggles rather than recreated. Idempotent.
124
+ */
125
+ suspend(): Promise<void>;
126
+ /**
127
+ * Restart advertising + scanning on the SAME Server/Central. `_serviceAdded`
128
+ * is still true (the service was never removed) so advertising resumes
129
+ * immediately. Safe to call repeatedly; no-op once closing/closed.
130
+ */
131
+ resume(): void;
92
132
  }
93
133
  import ReadyResource from 'ready-resource';
@@ -0,0 +1,75 @@
1
+ export function channelTopic(topic: any, channel: any): any;
2
+ /**
3
+ * @typedef {object} DHTTransportOpts
4
+ * @property {import('../../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
5
+ * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
6
+ * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
7
+ * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
8
+ * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
9
+ */
10
+ /**
11
+ * The internet transport: a Hyperswarm that finds peers over the DHT. Owns the
12
+ * swarm's whole lifecycle — construction (identity keyPair, bootstrap, firewall,
13
+ * relay), the channel-topic join/leave wrapping, flush, suspend/resume, and
14
+ * teardown. {@link Network} subscribes to `this.swarm`'s connection/peer events
15
+ * and drives topic joins; the swarm-specific wiring lives here.
16
+ */
17
+ export class DHTTransport {
18
+ /** @param {DHTTransportOpts} [opts] */
19
+ constructor({ identity, bootstrap, firewall, relayThrough, channel }?: DHTTransportOpts);
20
+ swarm: any;
21
+ /** @returns {boolean} */
22
+ get suspended(): boolean;
23
+ /**
24
+ * Wait for pending DHT announces and lookups to settle, bounded by timeout.
25
+ *
26
+ * @param {{ timeout?: number }} [opts]
27
+ * @returns {Promise<void>}
28
+ */
29
+ flush({ timeout }?: {
30
+ timeout?: number;
31
+ }): Promise<void>;
32
+ /**
33
+ * Pause the swarm — keeps state, drops sockets. Idempotent.
34
+ *
35
+ * @returns {Promise<void>}
36
+ */
37
+ suspend(): Promise<void>;
38
+ /**
39
+ * Resume a suspended swarm. Idempotent.
40
+ *
41
+ * @returns {Promise<void>}
42
+ */
43
+ resume(): Promise<void>;
44
+ /**
45
+ * Flush pending discovery, then tear down the swarm. Idempotent.
46
+ *
47
+ * @returns {Promise<void>}
48
+ */
49
+ destroy(): Promise<void>;
50
+ }
51
+ export type DHTTransportOpts = {
52
+ /**
53
+ * Long-lived keypair used as the swarm identity.
54
+ */
55
+ identity?: import("../../identity/index.js").Identity;
56
+ /**
57
+ * Custom DHT bootstrap nodes.
58
+ */
59
+ bootstrap?: Array<{
60
+ host: string;
61
+ port: number;
62
+ }>;
63
+ /**
64
+ * Incoming-connection filter.
65
+ */
66
+ firewall?: (remotePublicKey: Uint8Array, payload: any) => boolean;
67
+ /**
68
+ * Relay public keys to tunnel through.
69
+ */
70
+ relayThrough?: Uint8Array[];
71
+ /**
72
+ * Optional network-isolation label; only same-channel peers meet.
73
+ */
74
+ channel?: string;
75
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A dumb byte-carrying duplex for the GATT transport. Framing and session logic
3
+ * live in BLETransport; this only fragments outbound writes to fit a GATT
4
+ * write and pushes inbound payload bytes.
5
+ *
6
+ * @extends Duplex
7
+ */
8
+ export class GattStream extends Duplex<import("streamx").DuplexEvents> {
9
+ /**
10
+ * @param {object} opts
11
+ * @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
12
+ * @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
13
+ */
14
+ constructor({ send, onclose }?: {
15
+ send: (buffer: Uint8Array) => Promise<void>;
16
+ onclose?: () => void;
17
+ });
18
+ _send: (buffer: Uint8Array) => Promise<void>;
19
+ _onclose: () => void;
20
+ _write(chunk: any, cb: any): Promise<void>;
21
+ receive(buffer: any): void;
22
+ remoteEnd(): void;
23
+ _destroy(cb: any): void;
24
+ }
25
+ import { Duplex } from 'streamx';