@peers-app/peers-sdk 0.8.0 → 0.8.2

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.
@@ -4,6 +4,7 @@ export declare class FileReadStream {
4
4
  private fileRecord;
5
5
  private fileTable;
6
6
  private preloadChunksCount;
7
+ private skipVerification;
7
8
  private chunkHashes;
8
9
  private currentChunkIndex;
9
10
  private currentChunkBuffer;
@@ -11,7 +12,7 @@ export declare class FileReadStream {
11
12
  private positionInChunk;
12
13
  private totalPosition;
13
14
  private eof;
14
- constructor(fileRecord: IFile, fileTable: FilesTable, preloadChunksCount?: number);
15
+ constructor(fileRecord: IFile, fileTable: FilesTable, preloadChunksCount?: number, skipVerification?: boolean);
15
16
  private loadChunkHashes;
16
17
  private loadChunkByIndex;
17
18
  private preloadChunks;
@@ -7,6 +7,7 @@ class FileReadStream {
7
7
  fileRecord;
8
8
  fileTable;
9
9
  preloadChunksCount;
10
+ skipVerification;
10
11
  chunkHashes = null;
11
12
  currentChunkIndex = 0;
12
13
  currentChunkBuffer = null;
@@ -14,10 +15,11 @@ class FileReadStream {
14
15
  positionInChunk = 0;
15
16
  totalPosition = 0;
16
17
  eof = false;
17
- constructor(fileRecord, fileTable, preloadChunksCount = 2) {
18
+ constructor(fileRecord, fileTable, preloadChunksCount = 2, skipVerification = false) {
18
19
  this.fileRecord = fileRecord;
19
20
  this.fileTable = fileTable;
20
21
  this.preloadChunksCount = preloadChunksCount;
22
+ this.skipVerification = skipVerification;
21
23
  }
22
24
  async loadChunkHashes() {
23
25
  if (this.chunkHashes !== null) {
@@ -55,10 +57,12 @@ class FileReadStream {
55
57
  throw new Error(`Chunk ${chunkHash} not found in storage or peers`);
56
58
  }
57
59
  }
58
- // Verify chunk integrity
59
- const actualChunkHash = (0, keys_1.hashBytes)(chunk);
60
- if (actualChunkHash !== chunkHash) {
61
- throw new Error(`Chunk integrity check failed: expected ${chunkHash}, got ${actualChunkHash}`);
60
+ // Verify chunk integrity (skip for trusted local reads)
61
+ if (!this.skipVerification) {
62
+ const actualChunkHash = (0, keys_1.hashBytes)(chunk);
63
+ if (actualChunkHash !== chunkHash) {
64
+ throw new Error(`Chunk integrity check failed: expected ${chunkHash}, got ${actualChunkHash}`);
65
+ }
62
66
  }
63
67
  return chunk;
64
68
  }
@@ -20,9 +20,11 @@ export declare class FilesTable extends Table<IFile> {
20
20
  * @returns The inserted file record
21
21
  */
22
22
  saveFileRecord(fileRecord: IFile): Promise<IFile>;
23
- openReadStream(fileId: string, preloadChunksCount?: number): Promise<FileReadStream | null>;
23
+ openReadStream(fileId: string, preloadChunksCount?: number, skipVerification?: boolean): Promise<FileReadStream | null>;
24
24
  saveFile(metaData: IFileInput, data: Uint8Array | string): Promise<IFile>;
25
- getFileContents(fileId: string): Promise<Uint8Array | null>;
25
+ getFileContents(fileId: string, opts?: {
26
+ skipVerification?: boolean;
27
+ }): Promise<Uint8Array | null>;
26
28
  deleteFile(fileId: string): Promise<void>;
27
29
  createIndexFileRecursively(chunkHashes: string[]): Promise<string>;
28
30
  loadChunkHashesRecursively(indexFileId: string): Promise<string[]>;
@@ -39,12 +39,12 @@ class FilesTable extends orm_1.Table {
39
39
  async saveFileRecord(fileRecord) {
40
40
  return await super.save(fileRecord);
41
41
  }
42
- async openReadStream(fileId, preloadChunksCount) {
42
+ async openReadStream(fileId, preloadChunksCount, skipVerification) {
43
43
  const fileRecord = await this.get(fileId);
44
44
  if (!fileRecord) {
45
45
  return null;
46
46
  }
47
- return new file_read_stream_1.FileReadStream(fileRecord, this, preloadChunksCount);
47
+ return new file_read_stream_1.FileReadStream(fileRecord, this, preloadChunksCount, skipVerification);
48
48
  }
49
49
  async saveFile(metaData, data) {
50
50
  // Use FileWriteStream internally to ensure consistent chunking logic
@@ -60,9 +60,9 @@ class FilesTable extends orm_1.Table {
60
60
  // Finalize and return the result
61
61
  return await writeStream.finalize();
62
62
  }
63
- async getFileContents(fileId) {
63
+ async getFileContents(fileId, opts) {
64
64
  // Use FileReadStream internally to ensure consistent chunk reading logic
65
- const readStream = await this.openReadStream(fileId);
65
+ const readStream = await this.openReadStream(fileId, undefined, opts?.skipVerification);
66
66
  if (!readStream) {
67
67
  return null;
68
68
  }
@@ -0,0 +1,54 @@
1
+ import { Connection } from "./connection";
2
+ import { Device } from "./device";
3
+ import { ISocket } from "./socket.type";
4
+ type ITrustLevelFn = Connection['getTrustLevel'];
5
+ /**
6
+ * Generic binary peer interface supporting both WebRTC and libp2p transports.
7
+ * Provides a unified abstraction for binary data channel communication.
8
+ */
9
+ export interface IBinaryPeer {
10
+ on(event: 'data', handler: (data: Buffer | Uint8Array) => void): void;
11
+ on(event: 'close', handler: () => void): void;
12
+ on(event: 'drain', handler: () => void): void;
13
+ send(data: string | Uint8Array | Buffer): void;
14
+ destroy(): void;
15
+ /**
16
+ * Returns the number of bytes currently buffered waiting to be sent.
17
+ * Used for backpressure detection. Returns 0 if not supported.
18
+ */
19
+ getBufferedAmount?(): number;
20
+ }
21
+ export type IWebRTCPeer = IBinaryPeer;
22
+ export type ILibp2pPeer = IBinaryPeer;
23
+ export interface WrapBinaryPeerOptions {
24
+ /** Protocol identifier used for server address (e.g., 'wrtc', 'libp2p', or 'ws') */
25
+ protocol: 'wrtc' | 'libp2p' | 'ws';
26
+ /**
27
+ * Mark the transport as already secure (encrypted at transport layer).
28
+ * When true, sets secureLocal and secureRemote to skip application-level encryption.
29
+ */
30
+ markTransportSecure?: boolean;
31
+ }
32
+ /**
33
+ * Creates an ISocket from a binary peer without creating a full Connection.
34
+ * Useful when you need to pass custom localServerAddresses to the Connection.
35
+ */
36
+ export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string): ISocket;
37
+ /**
38
+ * Wraps a binary peer (WebRTC, libp2p, or WebSocket) into a Connection.
39
+ * This is the main implementation that handles all binary protocols.
40
+ */
41
+ export declare function wrapBinaryPeer(connectionId: string, peer: IBinaryPeer, localDevice: Device, initiator: boolean, options: WrapBinaryPeerOptions, getTrustLevel?: ITrustLevelFn): Connection;
42
+ /**
43
+ * Wraps a WebRTC peer into a Connection.
44
+ * Backwards-compatible wrapper around wrapBinaryPeer.
45
+ * WebRTC is encrypted at the transport layer (DTLS-SRTP), so we skip application-level encryption.
46
+ */
47
+ export declare function wrapWrtc(connectionId: string, peer: IWebRTCPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn): Connection;
48
+ /**
49
+ * Wraps a libp2p peer into a Connection.
50
+ * Backwards-compatible wrapper around wrapBinaryPeer.
51
+ * libp2p is encrypted at the transport layer (noise protocol), so we skip application-level encryption.
52
+ */
53
+ export declare function wrapLibp2p(connectionId: string, peer: ILibp2pPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn): Connection;
54
+ export {};
@@ -0,0 +1,350 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBinaryPeerSocket = createBinaryPeerSocket;
4
+ exports.wrapBinaryPeer = wrapBinaryPeer;
5
+ exports.wrapWrtc = wrapWrtc;
6
+ exports.wrapLibp2p = wrapLibp2p;
7
+ const msgpack_1 = require("@msgpack/msgpack");
8
+ const connection_1 = require("./connection");
9
+ const get_trust_level_fn_1 = require("./get-trust-level-fn");
10
+ const streamed_socket_1 = require("./streamed-socket");
11
+ const utils_1 = require("../utils");
12
+ const tx_encoding_1 = require("./tx-encoding");
13
+ // Protocol discriminator bytes for multiplexing RPC and raw byte streams
14
+ const PROTOCOL_RPC = 0x00;
15
+ const PROTOCOL_RAW_BYTES = 0x01;
16
+ // Threshold for routing large RPC messages through raw bytes streaming
17
+ // Messages larger than this will be automatically chunked with backpressure handling
18
+ const LARGE_MESSAGE_THRESHOLD = 64 * 1024; // 64KB
19
+ // Reserved stream ID for RPC messages sent via raw bytes
20
+ const RPC_STREAM_ID = '__rpc';
21
+ // Encode message metadata + data into a single Uint8Array
22
+ function encodeMessage(message) {
23
+ // Check if args/result is a Uint8Array - if so, flag it to keep it as raw bytes
24
+ let isUint8ArrayData = false;
25
+ if (message.type === 'call' && message.args instanceof Uint8Array) {
26
+ isUint8ArrayData = true;
27
+ }
28
+ else if (message.type === 'callback' && message.result instanceof Uint8Array) {
29
+ isUint8ArrayData = true;
30
+ }
31
+ // Encode metadata using msgpack (without args/result/error - those go in data)
32
+ const metadata = {
33
+ type: message.type,
34
+ callbackId: message.callbackId,
35
+ ...(message.eventName !== undefined && { eventName: message.eventName }),
36
+ ...(isUint8ArrayData && { isUint8ArrayData: true }),
37
+ };
38
+ const metadataBytes = (0, msgpack_1.encode)(metadata);
39
+ const metadataLength = metadataBytes.length;
40
+ // Encode the data payload (args, result, or error)
41
+ let dataBytes;
42
+ if (message.type === 'call' && message.args !== undefined) {
43
+ // If args is already a Uint8Array, use it directly without txEncode
44
+ if (isUint8ArrayData) {
45
+ dataBytes = message.args;
46
+ }
47
+ else {
48
+ dataBytes = (0, tx_encoding_1.txEncode)(message.args);
49
+ }
50
+ }
51
+ else if (message.type === 'callback') {
52
+ if (message.error !== undefined) {
53
+ dataBytes = (0, tx_encoding_1.txEncode)({ error: message.error });
54
+ }
55
+ else if (isUint8ArrayData) {
56
+ // If result is a Uint8Array, use it directly
57
+ dataBytes = message.result;
58
+ }
59
+ else {
60
+ dataBytes = (0, tx_encoding_1.txEncode)({ result: message.result });
61
+ }
62
+ }
63
+ else {
64
+ dataBytes = new Uint8Array(0);
65
+ }
66
+ // Create combined buffer: [4 bytes length][metadata][data]
67
+ const combined = new Uint8Array(4 + metadataLength + dataBytes.length);
68
+ // Write metadata length as 32-bit unsigned integer (big-endian)
69
+ const view = new DataView(combined.buffer, combined.byteOffset, combined.byteLength);
70
+ view.setUint32(0, metadataLength, false); // big-endian
71
+ // Write metadata
72
+ combined.set(metadataBytes, 4);
73
+ // Write data
74
+ combined.set(dataBytes, 4 + metadataLength);
75
+ return combined;
76
+ }
77
+ // Decode binary message back into IMessage
78
+ function decodeMessage(bytes) {
79
+ // Read metadata length
80
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
81
+ const metadataLength = view.getUint32(0, false); // big-endian
82
+ // Extract metadata
83
+ const metadataBytes = bytes.subarray(4, 4 + metadataLength);
84
+ const metadata = (0, msgpack_1.decode)(metadataBytes);
85
+ // Extract data
86
+ const dataBytes = bytes.subarray(4 + metadataLength);
87
+ const message = {
88
+ type: metadata.type,
89
+ callbackId: metadata.callbackId,
90
+ eventName: metadata.eventName,
91
+ };
92
+ // Decode the data payload
93
+ if (dataBytes.length > 0) {
94
+ if (message.type === 'call') {
95
+ // If it was a Uint8Array, use it directly without txDecode
96
+ if (metadata.isUint8ArrayData) {
97
+ message.args = dataBytes;
98
+ }
99
+ else {
100
+ message.args = (0, tx_encoding_1.txDecode)(dataBytes);
101
+ }
102
+ }
103
+ else if (message.type === 'callback') {
104
+ if (metadata.isUint8ArrayData) {
105
+ // If it was a Uint8Array result, use it directly
106
+ message.result = dataBytes;
107
+ }
108
+ else {
109
+ const decoded = (0, tx_encoding_1.txDecode)(dataBytes);
110
+ if (decoded.error !== undefined) {
111
+ message.error = decoded.error;
112
+ }
113
+ else {
114
+ message.result = decoded.result;
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return message;
120
+ }
121
+ /**
122
+ * Creates an ISocket from a binary peer without creating a full Connection.
123
+ * Useful when you need to pass custom localServerAddresses to the Connection.
124
+ */
125
+ function createBinaryPeerSocket(connectionId, peer, protocol = 'binary') {
126
+ const handlers = {};
127
+ const callbacks = {};
128
+ const rawBytesHandlers = {};
129
+ let isClosed = false;
130
+ const stats = (0, streamed_socket_1.createSocketStats)();
131
+ // Map protocol string to TransportType (needed early for threshold calculations)
132
+ const transportType = protocol === 'wrtc' ? 'wrtc' :
133
+ protocol === 'libp2p' ? 'libp2p' :
134
+ protocol === 'ws' ? 'ws' : 'unknown';
135
+ // Core sendRawBytes implementation - defined early so sendRPCData can use it
136
+ const sendRawBytesImpl = async (streamId, data) => {
137
+ const streamIdBytes = new TextEncoder().encode(streamId);
138
+ const packet = new Uint8Array(1 + 2 + streamIdBytes.length + data.length);
139
+ packet[0] = PROTOCOL_RAW_BYTES;
140
+ const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
141
+ view.setUint16(1, streamIdBytes.length, false);
142
+ packet.set(streamIdBytes, 3);
143
+ packet.set(data, 3 + streamIdBytes.length);
144
+ // Use transport-specific thresholds for backpressure
145
+ const HIGH_WATER_MARK = transportType === 'wrtc' ? 128 * 1024 : 4 * 1024 * 1024;
146
+ const LOW_WATER_MARK = transportType === 'wrtc' ? 32 * 1024 : 2 * 1024 * 1024;
147
+ if (peer.getBufferedAmount && peer.getBufferedAmount() > HIGH_WATER_MARK) {
148
+ await new Promise(resolve => {
149
+ const checkDrain = () => {
150
+ if (!peer.getBufferedAmount || peer.getBufferedAmount() < LOW_WATER_MARK) {
151
+ resolve();
152
+ }
153
+ else {
154
+ setTimeout(checkDrain, 5);
155
+ }
156
+ };
157
+ peer.on('drain', resolve);
158
+ checkDrain();
159
+ });
160
+ }
161
+ stats.bytesSent += packet.length;
162
+ stats.messagesSent++;
163
+ peer.send(packet);
164
+ };
165
+ // Helper to send RPC messages - routes large messages through raw bytes streaming
166
+ const sendRPCData = async (encoded) => {
167
+ if (encoded.length > LARGE_MESSAGE_THRESHOLD) {
168
+ // Large message: use raw bytes streaming with backpressure
169
+ await sendRawBytesImpl(RPC_STREAM_ID, encoded);
170
+ }
171
+ else {
172
+ // Small message: send directly
173
+ const withProtocol = new Uint8Array(1 + encoded.length);
174
+ withProtocol[0] = PROTOCOL_RPC;
175
+ withProtocol.set(encoded, 1);
176
+ stats.bytesSent += withProtocol.length;
177
+ stats.messagesSent++;
178
+ peer.send(withProtocol);
179
+ }
180
+ };
181
+ // Handle incoming data
182
+ peer.on('data', async (data) => {
183
+ try {
184
+ const bytes = new Uint8Array(data);
185
+ stats.bytesReceived += bytes.length;
186
+ stats.messagesReceived++;
187
+ if (bytes.length === 0)
188
+ return;
189
+ const protocolType = bytes[0];
190
+ // Handle raw bytes stream
191
+ if (protocolType === PROTOCOL_RAW_BYTES) {
192
+ if (bytes.length < 3)
193
+ return;
194
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
195
+ const streamIdLength = view.getUint16(1, false);
196
+ if (bytes.length < 3 + streamIdLength)
197
+ return;
198
+ const streamIdBytes = bytes.subarray(3, 3 + streamIdLength);
199
+ const streamId = new TextDecoder().decode(streamIdBytes);
200
+ const payload = bytes.subarray(3 + streamIdLength);
201
+ // Check if this is an RPC message sent via raw bytes (for large payloads)
202
+ if (streamId === RPC_STREAM_ID) {
203
+ handleRPCMessage(payload, handlers, callbacks, sendRPCData);
204
+ return;
205
+ }
206
+ const handler = rawBytesHandlers[streamId];
207
+ if (handler)
208
+ handler(payload);
209
+ return;
210
+ }
211
+ // Handle RPC messages
212
+ if (protocolType === PROTOCOL_RPC) {
213
+ const rpcBytes = bytes.subarray(1);
214
+ handleRPCMessage(rpcBytes, handlers, callbacks, sendRPCData);
215
+ return;
216
+ }
217
+ // Legacy format (no discriminator) for backwards compatibility
218
+ handleRPCMessage(bytes, handlers, callbacks, sendRPCData);
219
+ }
220
+ catch (err) {
221
+ console.error(`Error parsing ${protocol} message:`, err);
222
+ }
223
+ });
224
+ // Handle closure
225
+ peer.on('close', () => {
226
+ if (isClosed)
227
+ return;
228
+ isClosed = true;
229
+ socket.connected = false;
230
+ });
231
+ const socket = {
232
+ id: connectionId,
233
+ connected: true,
234
+ handlesOwnEncoding: true,
235
+ transportType,
236
+ stats,
237
+ emit(eventName, args, callback) {
238
+ const callbackId = (0, utils_1.newid)();
239
+ callbacks[callbackId] = callback;
240
+ const message = { type: 'call', eventName, args, callbackId };
241
+ const encoded = encodeMessage(message);
242
+ sendRPCData(encoded);
243
+ },
244
+ on(eventName, handler) {
245
+ handlers[eventName] = handler;
246
+ },
247
+ removeAllListeners(eventName) {
248
+ if (eventName) {
249
+ delete handlers[eventName];
250
+ }
251
+ else {
252
+ Object.keys(handlers).forEach(key => delete handlers[key]);
253
+ }
254
+ },
255
+ disconnect() {
256
+ peer.destroy();
257
+ },
258
+ sendRawBytes: sendRawBytesImpl,
259
+ onRawBytes(streamId, handler) {
260
+ rawBytesHandlers[streamId] = handler;
261
+ },
262
+ removeRawBytesHandler(streamId) {
263
+ delete rawBytesHandlers[streamId];
264
+ }
265
+ };
266
+ return socket;
267
+ }
268
+ // Helper to handle RPC message parsing
269
+ // sendRPCData automatically routes large messages through raw bytes streaming
270
+ function handleRPCMessage(bytes, handlers, callbacks, sendRPCData) {
271
+ const message = decodeMessage(bytes);
272
+ if (message.type === 'callback') {
273
+ const callback = callbacks[message.callbackId];
274
+ if (callback) {
275
+ delete callbacks[message.callbackId];
276
+ callback(message.error, message.result);
277
+ }
278
+ return;
279
+ }
280
+ if (message.type === 'call') {
281
+ const handler = handlers[message.eventName];
282
+ if (!handler) {
283
+ console.error(`No handler for event: ${message.eventName}`);
284
+ return;
285
+ }
286
+ try {
287
+ const callback = (err, result) => {
288
+ const responseMessage = {
289
+ type: 'callback',
290
+ callbackId: message.callbackId,
291
+ };
292
+ if (err)
293
+ responseMessage.error = err;
294
+ else
295
+ responseMessage.result = result;
296
+ const encoded = encodeMessage(responseMessage);
297
+ // Use sendRPCData which auto-routes large responses through raw bytes
298
+ sendRPCData(encoded);
299
+ };
300
+ handler(message.args, callback);
301
+ }
302
+ catch (error) {
303
+ console.error("Error in handler", { eventName: message.eventName, error });
304
+ const errorMessage = {
305
+ type: 'callback',
306
+ callbackId: message.callbackId,
307
+ error: error.message
308
+ };
309
+ const encoded = encodeMessage(errorMessage);
310
+ sendRPCData(encoded);
311
+ }
312
+ }
313
+ }
314
+ /**
315
+ * Wraps a binary peer (WebRTC, libp2p, or WebSocket) into a Connection.
316
+ * This is the main implementation that handles all binary protocols.
317
+ */
318
+ function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, getTrustLevel) {
319
+ const { protocol, markTransportSecure } = options;
320
+ // Create the socket wrapper using shared implementation
321
+ const socket = createBinaryPeerSocket(connectionId, peer, protocol);
322
+ const serverAddress = `${protocol}://${connectionId}`;
323
+ const localServerAddresses = initiator ? undefined : [serverAddress];
324
+ if (process.env.NODE_ENV !== 'test') {
325
+ getTrustLevel ??= (0, get_trust_level_fn_1.getTrustLevelFn)(localDevice, serverAddress);
326
+ }
327
+ const connection = new connection_1.Connection(socket, localDevice, localServerAddresses, getTrustLevel);
328
+ // Mark transport as secure if applicable (WebRTC DTLS-SRTP, libp2p noise, wss)
329
+ if (markTransportSecure) {
330
+ connection.secureLocal = true;
331
+ connection.secureRemote = true;
332
+ }
333
+ return connection;
334
+ }
335
+ /**
336
+ * Wraps a WebRTC peer into a Connection.
337
+ * Backwards-compatible wrapper around wrapBinaryPeer.
338
+ * WebRTC is encrypted at the transport layer (DTLS-SRTP), so we skip application-level encryption.
339
+ */
340
+ function wrapWrtc(connectionId, peer, localDevice, initiator, getTrustLevel) {
341
+ return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: 'wrtc', markTransportSecure: true }, getTrustLevel);
342
+ }
343
+ /**
344
+ * Wraps a libp2p peer into a Connection.
345
+ * Backwards-compatible wrapper around wrapBinaryPeer.
346
+ * libp2p is encrypted at the transport layer (noise protocol), so we skip application-level encryption.
347
+ */
348
+ function wrapLibp2p(connectionId, peer, localDevice, initiator, getTrustLevel) {
349
+ return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: 'libp2p', markTransportSecure: true }, getTrustLevel);
350
+ }
@@ -0,0 +1 @@
1
+ export {};