@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.
- package/README.md +14 -12
- package/package.json +13 -4
- package/src/blobs/index.js +1 -1
- package/src/database/bootstrap.js +2 -2
- package/src/database/dispatch.js +41 -6
- package/src/database/index.js +177 -17
- package/src/identity/index.js +15 -1
- package/src/lib/errors.js +8 -0
- package/src/lib/utils.js +33 -12
- package/src/network/bluetooth.js +804 -0
- package/src/network/gatt-stream.js +59 -0
- package/src/network/index.js +121 -8
- package/src/pairing/index.js +6 -6
- package/src/rpc/index.js +2 -2
- package/src/storage/index.js +30 -8
- package/types/database/dispatch.d.ts +2 -1
- package/types/database/index.d.ts +34 -8
- package/types/identity/index.d.ts +9 -0
- package/types/lib/errors.d.ts +6 -0
- package/types/lib/utils.d.ts +4 -6
- package/types/network/bluetooth.d.ts +162 -0
- package/types/network/gatt-stream.d.ts +26 -0
- package/types/network/index.d.ts +36 -1
- package/types/storage/index.d.ts +7 -1
- package/src/CLAUDE.md +0 -3
- package/src/database/CLAUDE.md +0 -3
- package/src/identity/CLAUDE.md +0 -3
- package/src/lib/CLAUDE.md +0 -3
- package/src/network/CLAUDE.md +0 -3
- package/src/rpc/CLAUDE.md +0 -3
- package/src/storage/CLAUDE.md +0 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a stable 128-bit BLE service UUID from a topic. Only devices that
|
|
3
|
+
* compute the same UUID (same channel / same invite) ever discover each other.
|
|
4
|
+
*
|
|
5
|
+
* @param {Uint8Array} topic
|
|
6
|
+
* @param {string} [tag] Namespace so channel and invite meshes never collide.
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function toServiceUUID(topic: Uint8Array, tag?: string): string;
|
|
10
|
+
/**
|
|
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
|
+
* there replication and pairing are transport-agnostic (see Network.inject).
|
|
14
|
+
*
|
|
15
|
+
* Choreography: the server adds one data characteristic (write + notify) and
|
|
16
|
+
* advertises. The central connects, discovers the characteristic, subscribes,
|
|
17
|
+
* then framed bytes flow both ways — central→server as GATT writes, server→
|
|
18
|
+
* central as notifications — each tagged with an 8-byte session id (bitchat
|
|
19
|
+
* model). `backend` is bare-bluetooth in production and a mock in tests.
|
|
20
|
+
*
|
|
21
|
+
* ponytail: capability-handshake DoS link-scoring is deferred — it needs a
|
|
22
|
+
* replication-progress signal (design §4b). v1 caps links + times out dials.
|
|
23
|
+
*
|
|
24
|
+
* @extends ReadyResource
|
|
25
|
+
*/
|
|
26
|
+
export class BluetoothTransport extends ReadyResource {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {any} opts.backend bare-bluetooth-shaped module (Central, Server, Service, Characteristic).
|
|
30
|
+
* @param {import('./index.js').Network} opts.network
|
|
31
|
+
* @param {Uint8Array} opts.uuid The 32-byte topic the service UUID derives from.
|
|
32
|
+
* @param {Uint8Array} opts.nodeId Stable local id (identity/device key) for the initiate tie-break.
|
|
33
|
+
* @param {string} [opts.tag] UUID namespace (channel mesh vs invite mesh).
|
|
34
|
+
* @param {number} [opts.cap] Max concurrent links; gossip covers the rest.
|
|
35
|
+
* @param {{ scanMode?: any }} [opts.scanOptions] Platform scan options (e.g. Android low-power).
|
|
36
|
+
* @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).
|
|
37
|
+
* @param {string} [opts.name] Local app-user display name, sent to peers over a hello frame.
|
|
38
|
+
*/
|
|
39
|
+
constructor({ backend, network, uuid, nodeId, tag, cap, scanOptions, keepLinks, name }: {
|
|
40
|
+
backend: any;
|
|
41
|
+
network: import("./index.js").Network;
|
|
42
|
+
uuid: Uint8Array;
|
|
43
|
+
nodeId: Uint8Array;
|
|
44
|
+
tag?: string;
|
|
45
|
+
cap?: number;
|
|
46
|
+
scanOptions?: {
|
|
47
|
+
scanMode?: any;
|
|
48
|
+
};
|
|
49
|
+
keepLinks?: boolean;
|
|
50
|
+
name?: string;
|
|
51
|
+
});
|
|
52
|
+
backend: any;
|
|
53
|
+
network: import("./index.js").Network;
|
|
54
|
+
name: string;
|
|
55
|
+
nodeId: Uint8Array<ArrayBufferLike>;
|
|
56
|
+
nodeHex: any;
|
|
57
|
+
serviceUUID: string;
|
|
58
|
+
cap: number;
|
|
59
|
+
scanOptions: {
|
|
60
|
+
scanMode?: any;
|
|
61
|
+
};
|
|
62
|
+
keepLinks: boolean;
|
|
63
|
+
state: string;
|
|
64
|
+
central: any;
|
|
65
|
+
server: any;
|
|
66
|
+
_dataChar: any;
|
|
67
|
+
/** sessionId hex → { stream, sid } for server-side (peripheral) sessions */
|
|
68
|
+
_sessions: Map<any, any>;
|
|
69
|
+
/** serialized server notify queue: { frame, resolve, reject } */
|
|
70
|
+
_notifyQueue: any[];
|
|
71
|
+
_scanning: boolean;
|
|
72
|
+
_advertising: boolean;
|
|
73
|
+
_serviceAdded: boolean;
|
|
74
|
+
/** peripheral id being dialed → its connect-timeout timer */
|
|
75
|
+
_dialing: Map<any, any>;
|
|
76
|
+
/** peripheral ids that carry a live channel — never re-dialed (a second
|
|
77
|
+
* dial's failure would disconnect the peripheral and kill the good link) */
|
|
78
|
+
_linked: Set<any>;
|
|
79
|
+
/** peripheral id → retry-after timestamp; failed dials back off */
|
|
80
|
+
_coolUntil: Map<any, any>;
|
|
81
|
+
/** peripheral id → consecutive failure count; drives exponential backoff */
|
|
82
|
+
_failures: Map<any, any>;
|
|
83
|
+
/** peripheral id → remote peer key, learned at handshake — dial guard */
|
|
84
|
+
_peerByPeripheral: Map<any, any>;
|
|
85
|
+
/** live central-side peripheral wrappers — for goodbye + physical hang-up on suspend */
|
|
86
|
+
_connectedPeripherals: Set<any>;
|
|
87
|
+
/** last central.connect timestamp — global inter-dial rate limit */
|
|
88
|
+
_lastDial: number;
|
|
89
|
+
_scanTimer: any;
|
|
90
|
+
_suspended: boolean;
|
|
91
|
+
/** live injected links keyed by remote node id hex */
|
|
92
|
+
peers: Map<any, any>;
|
|
93
|
+
/**
|
|
94
|
+
* Whether we should be the one to open the connection to `peerNodeId`. The
|
|
95
|
+
* lexicographically smaller id initiates; the larger waits — so a pair
|
|
96
|
+
* connects once, not twice. Equal (our own reflection) → false.
|
|
97
|
+
*
|
|
98
|
+
* @param {Uint8Array} peerNodeId
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
shouldInitiate(peerNodeId: Uint8Array): boolean;
|
|
102
|
+
get linkCount(): number;
|
|
103
|
+
_startServer(Service: any, Characteristic: any): void;
|
|
104
|
+
_maybeAdvertise(): void;
|
|
105
|
+
_onWriteRequests(requests: any): void;
|
|
106
|
+
_onServerFrame(data: any): void;
|
|
107
|
+
_closeServerSession(sidHex: any, sid: any): void;
|
|
108
|
+
/** Our hello payload: the local app-user name the peer labels this link with. */
|
|
109
|
+
_helloPayload(): any;
|
|
110
|
+
/**
|
|
111
|
+
* Parse a hello payload. Malformed → null (the caller ignores it).
|
|
112
|
+
*
|
|
113
|
+
* @param {Uint8Array} payload
|
|
114
|
+
* @returns {string | null}
|
|
115
|
+
*/
|
|
116
|
+
_parseHello(payload: Uint8Array): string | null;
|
|
117
|
+
/** Stash a peer's name onto a server session + its conn, then refresh mirrors. */
|
|
118
|
+
_applyPeerName(session: any, payload: any): void;
|
|
119
|
+
_enqueueNotify(f: any): Promise<any>;
|
|
120
|
+
_drainNotify(): void;
|
|
121
|
+
_startScan(): void;
|
|
122
|
+
_armScanRestart(): void;
|
|
123
|
+
_stopScan(): void;
|
|
124
|
+
_onState(raw: any): void;
|
|
125
|
+
_onDiscover(peripheral: any): void;
|
|
126
|
+
_onConnect(peripheral: any): void;
|
|
127
|
+
_startCentralSession(peripheral: any, char: any): void;
|
|
128
|
+
_onCentralNotify(peripheral: any, data: any): void;
|
|
129
|
+
_centralSend(peripheral: any, char: any, f: any): any;
|
|
130
|
+
_writeOnce(peripheral: any, char: any, f: any): Promise<any>;
|
|
131
|
+
_abortDial(peripheral: any, _reason: any): void;
|
|
132
|
+
_clearDial(id: any): void;
|
|
133
|
+
_onCentralError(err: any): void;
|
|
134
|
+
_onChannel(l2cap: any, isInitiator: any, peripheralId: any): any;
|
|
135
|
+
_track(conn: any, peripheralId: any, isInitiator: any): void;
|
|
136
|
+
_untrack(conn: any): void;
|
|
137
|
+
/**
|
|
138
|
+
* Best-effort TYPE_CLOSE to every live session — server sessions over the
|
|
139
|
+
* notify path, central sessions over the write path — reusing the same helpers
|
|
140
|
+
* a normal stream close uses. Waits up to DRAIN_MS for the frames to flush,
|
|
141
|
+
* then resolves regardless: suspend must never hang on a wedged radio.
|
|
142
|
+
*
|
|
143
|
+
* @returns {Promise<void>}
|
|
144
|
+
*/
|
|
145
|
+
_sayGoodbye(): Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* Pause radio activity but KEEP the Server/Central instances and the
|
|
148
|
+
* registered GATT service alive — the toggle-friendly counterpart to _close.
|
|
149
|
+
* iOS CoreBluetooth managers can't be destroy()ed (native double-free), so a
|
|
150
|
+
* fresh transport per toggle leaks a manager whose stale peripheral-manager
|
|
151
|
+
* keeps a duplicate GATT service registered; remote centrals then subscribe to
|
|
152
|
+
* the dead service and hear silence. Reuse one instance instead. Idempotent.
|
|
153
|
+
*/
|
|
154
|
+
suspend(): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Restart advertising + scanning on the SAME Server/Central. `_serviceAdded`
|
|
157
|
+
* is still true (the service was never removed) so advertising resumes
|
|
158
|
+
* immediately. Safe to call repeatedly; no-op once closing/closed.
|
|
159
|
+
*/
|
|
160
|
+
resume(): void;
|
|
161
|
+
}
|
|
162
|
+
import ReadyResource from 'ready-resource';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dumb byte-carrying duplex for the GATT transport. Framing and session logic
|
|
3
|
+
* live in BluetoothTransport; this only fragments outbound writes to fit a GATT
|
|
4
|
+
* write and pushes inbound payload bytes. NoiseSecretStream wraps it as a raw
|
|
5
|
+
* duplex, exactly like the old L2CAP channel.
|
|
6
|
+
*
|
|
7
|
+
* @extends Duplex
|
|
8
|
+
*/
|
|
9
|
+
export class GattStream extends Duplex<import("streamx").DuplexEvents> {
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {(buffer: Uint8Array) => Promise<void>} opts.send Transmit one payload piece (transport frames it).
|
|
13
|
+
* @param {() => void} [opts.onclose] Called once on teardown (send a close frame, disconnect).
|
|
14
|
+
*/
|
|
15
|
+
constructor({ send, onclose }?: {
|
|
16
|
+
send: (buffer: Uint8Array) => Promise<void>;
|
|
17
|
+
onclose?: () => void;
|
|
18
|
+
});
|
|
19
|
+
_send: (buffer: Uint8Array) => Promise<void>;
|
|
20
|
+
_onclose: () => void;
|
|
21
|
+
_write(chunk: any, cb: any): Promise<void>;
|
|
22
|
+
receive(buffer: any): void;
|
|
23
|
+
remoteEnd(): void;
|
|
24
|
+
_destroy(cb: any): void;
|
|
25
|
+
}
|
|
26
|
+
import { Duplex } from 'streamx';
|
package/types/network/index.d.ts
CHANGED
|
@@ -28,9 +28,44 @@ export class Network extends ReadyResource {
|
|
|
28
28
|
wakeup: any;
|
|
29
29
|
_replicateables: Set<any>;
|
|
30
30
|
_discoveries: Set<any>;
|
|
31
|
+
_injected: Set<any>;
|
|
32
|
+
_blind: any;
|
|
33
|
+
/**
|
|
34
|
+
* Feed an externally-established connection — a Bluetooth L2CAP channel, a
|
|
35
|
+
* serial link, an in-process pair, any duplex — into the network. A raw
|
|
36
|
+
* duplex is wrapped in NoiseSecretStream (pass `isInitiator`); a stream
|
|
37
|
+
* that already IS one is used as-is. From here it gets the exact same
|
|
38
|
+
* treatment as a swarm connection: wakeup, replication of every attached
|
|
39
|
+
* core, pairing, and the 'connection' event.
|
|
40
|
+
*
|
|
41
|
+
* @param {any} stream Duplex transport, or a ready NoiseSecretStream.
|
|
42
|
+
* @param {{ isInitiator?: boolean }} [opts] Which side initiates the noise handshake (raw duplexes only).
|
|
43
|
+
* @returns {any} The encrypted connection stream.
|
|
44
|
+
*/
|
|
45
|
+
inject(stream: any, { isInitiator }?: {
|
|
46
|
+
isInitiator?: boolean;
|
|
47
|
+
}): any;
|
|
48
|
+
/**
|
|
49
|
+
* Lazily create the network-shared BlindPairing. One instance serves every
|
|
50
|
+
* handle's pairing member — per-handle instances each added their own swarm
|
|
51
|
+
* and DHT listeners plus a protomux channel per connection.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<any>}
|
|
54
|
+
*/
|
|
55
|
+
blind(): Promise<any>;
|
|
56
|
+
/**
|
|
57
|
+
* Re-attach pairing channels on injected connections. blind-pairing only
|
|
58
|
+
* auto-attaches refs that existed when a connection arrived — swarm peers
|
|
59
|
+
* meet again over topic joins, injected links (Bluetooth, pipes) don't, so
|
|
60
|
+
* a member/candidate added later must re-run the attach. Idempotent:
|
|
61
|
+
* protomux refuses duplicate channels.
|
|
62
|
+
*
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
refreshInjected(): Promise<void>;
|
|
31
66
|
/** @returns {Map<string, any>} Known peers keyed by public-key string. */
|
|
32
67
|
get peers(): Map<string, any>;
|
|
33
|
-
/** @returns {Set<any>} Live connection streams. */
|
|
68
|
+
/** @returns {Set<any>} Live connection streams — swarm and injected. */
|
|
34
69
|
get connections(): Set<any>;
|
|
35
70
|
/** @returns {boolean} */
|
|
36
71
|
get suspended(): boolean;
|
package/types/storage/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @property {'rocks' | 'bee'} backend
|
|
5
5
|
* @property {any} [root] Pre-existing HypercoreStorage to reuse.
|
|
6
6
|
* @property {any} [store] Pre-existing Corestore to reuse.
|
|
7
|
+
* @property {Uint8Array} [storageKey] 32-byte key encrypting the backing core at rest (bee backend only).
|
|
7
8
|
*
|
|
8
9
|
* @typedef {{ name: string, kind: string }} Ref
|
|
9
10
|
* @typedef {{ id?: string, createdAt: number, updatedAt: number, [k: string]: any }} StoredRow
|
|
@@ -35,7 +36,7 @@ export class Storage extends ReadyResource {
|
|
|
35
36
|
* @param {string} dir
|
|
36
37
|
* @param {StorageOpts} [opts]
|
|
37
38
|
*/
|
|
38
|
-
constructor(dir: string, { spec, backend, root, store }?: StorageOpts);
|
|
39
|
+
constructor(dir: string, { spec, backend, root, store, storageKey }?: StorageOpts);
|
|
39
40
|
dir: string;
|
|
40
41
|
spec: {
|
|
41
42
|
database: any;
|
|
@@ -47,6 +48,7 @@ export class Storage extends ReadyResource {
|
|
|
47
48
|
};
|
|
48
49
|
};
|
|
49
50
|
backend: "rocks" | "bee";
|
|
51
|
+
storageKey: Uint8Array<ArrayBufferLike>;
|
|
50
52
|
ns: string;
|
|
51
53
|
refs: Record<string, {
|
|
52
54
|
kind?: string;
|
|
@@ -143,6 +145,10 @@ export type StorageOpts = {
|
|
|
143
145
|
* Pre-existing Corestore to reuse.
|
|
144
146
|
*/
|
|
145
147
|
store?: any;
|
|
148
|
+
/**
|
|
149
|
+
* 32-byte key encrypting the backing core at rest (bee backend only).
|
|
150
|
+
*/
|
|
151
|
+
storageKey?: Uint8Array;
|
|
146
152
|
};
|
|
147
153
|
export type Ref = {
|
|
148
154
|
name: string;
|
package/src/CLAUDE.md
DELETED
package/src/database/CLAUDE.md
DELETED
package/src/identity/CLAUDE.md
DELETED
package/src/lib/CLAUDE.md
DELETED
package/src/network/CLAUDE.md
DELETED
package/src/rpc/CLAUDE.md
DELETED
package/src/storage/CLAUDE.md
DELETED