@peers-app/peers-sdk 0.7.40 → 0.8.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/dist/data/files/file-read-stream.d.ts +2 -1
- package/dist/data/files/file-read-stream.js +9 -5
- package/dist/data/files/files.d.ts +4 -2
- package/dist/data/files/files.js +4 -4
- package/dist/data/groups.d.ts +2 -2
- package/dist/device/binary-peer-connection.d.ts +54 -0
- package/dist/device/binary-peer-connection.js +350 -0
- package/dist/device/binary-peer-connection.test.d.ts +1 -0
- package/dist/device/binary-peer-connection.test.js +204 -0
- package/dist/device/connection.d.ts +29 -0
- package/dist/device/connection.js +85 -2
- package/dist/device/connection.test.js +20 -4
- package/dist/device/socket-io-binary-peer.d.ts +27 -0
- package/dist/device/socket-io-binary-peer.js +97 -0
- package/dist/device/socket.type.d.ts +52 -0
- package/dist/device/streamed-socket.d.ts +4 -1
- package/dist/device/streamed-socket.js +45 -4
- package/dist/device/tx-encoding.js +22 -4
- package/dist/device/tx-encoding.test.js +28 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/keys.js +17 -14
- package/dist/package-loader/package-loader.d.ts +2 -0
- package/dist/package-loader/package-loader.js +4 -2
- package/dist/rpc-types.d.ts +1 -0
- package/dist/rpc-types.js +5 -1
- package/dist/user-connect/connection-code.d.ts +67 -0
- package/dist/user-connect/connection-code.js +176 -0
- package/dist/user-connect/connection-code.test.d.ts +1 -0
- package/dist/user-connect/connection-code.test.js +213 -0
- package/dist/user-connect/index.d.ts +3 -0
- package/dist/user-connect/index.js +19 -0
- package/dist/user-connect/user-connect.pvars.d.ts +3 -0
- package/dist/user-connect/user-connect.pvars.js +33 -0
- package/dist/user-connect/user-connect.types.d.ts +58 -0
- package/dist/user-connect/user-connect.types.js +8 -0
- package/package.json +3 -1
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.StreamedSocket = void 0;
|
|
4
|
+
exports.createSocketStats = createSocketStats;
|
|
4
5
|
const lodash_1 = require("lodash");
|
|
5
6
|
const utils_1 = require("../utils");
|
|
6
7
|
const socket_type_1 = require("./socket.type");
|
|
7
8
|
const tx_encoding_1 = require("./tx-encoding");
|
|
8
9
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
10
|
+
/** Creates a new stats object for tracking socket throughput */
|
|
11
|
+
function createSocketStats() {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
return {
|
|
14
|
+
bytesSent: 0,
|
|
15
|
+
bytesReceived: 0,
|
|
16
|
+
messagesSent: 0,
|
|
17
|
+
messagesReceived: 0,
|
|
18
|
+
startTime: now,
|
|
19
|
+
// For calculating recent rates
|
|
20
|
+
lastSampleTime: now,
|
|
21
|
+
lastSampleBytesSent: 0,
|
|
22
|
+
lastSampleBytesReceived: 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
9
25
|
// Encode chunk metadata + data into a single Uint8Array
|
|
10
26
|
function encodeChunk(chunk) {
|
|
11
27
|
// Encode metadata using msgpack
|
|
@@ -30,14 +46,24 @@ function encodeChunk(chunk) {
|
|
|
30
46
|
}
|
|
31
47
|
// Decode binary chunk back into IMessageChunk
|
|
32
48
|
function decodeChunk(bytes) {
|
|
49
|
+
// Ensure we have a proper Uint8Array with a valid ArrayBuffer
|
|
50
|
+
// Socket.io might send data that doesn't have a proper buffer property
|
|
51
|
+
let normalizedBytes;
|
|
52
|
+
if (bytes.buffer instanceof ArrayBuffer) {
|
|
53
|
+
normalizedBytes = bytes;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Create a new Uint8Array copy which will have a proper buffer
|
|
57
|
+
normalizedBytes = new Uint8Array(bytes);
|
|
58
|
+
}
|
|
33
59
|
// Read metadata length
|
|
34
|
-
const view = new DataView(
|
|
60
|
+
const view = new DataView(normalizedBytes.buffer, normalizedBytes.byteOffset, normalizedBytes.byteLength);
|
|
35
61
|
const metadataLength = view.getUint32(0, false); // big-endian
|
|
36
62
|
// Extract metadata
|
|
37
|
-
const metadataBytes =
|
|
63
|
+
const metadataBytes = normalizedBytes.subarray(4, 4 + metadataLength);
|
|
38
64
|
const metadata = (0, msgpack_1.decode)(metadataBytes);
|
|
39
65
|
// Extract data
|
|
40
|
-
const data =
|
|
66
|
+
const data = normalizedBytes.subarray(4 + metadataLength);
|
|
41
67
|
return {
|
|
42
68
|
messageId: metadata.messageId,
|
|
43
69
|
chunkIndex: metadata.chunkIndex,
|
|
@@ -51,10 +77,13 @@ class StreamedSocket {
|
|
|
51
77
|
maxChunkSize;
|
|
52
78
|
safeSocketChunkEventName = '__streamed-socket-chunk';
|
|
53
79
|
safeSocketResponseEventName = '__streamed-socket-response';
|
|
80
|
+
stats = createSocketStats();
|
|
54
81
|
constructor(socket, maxChunkSize = socket_type_1.DEFAULT_MAX_CHUNK_SIZE) {
|
|
55
82
|
this.socket = socket;
|
|
56
83
|
this.maxChunkSize = maxChunkSize;
|
|
57
84
|
socket.on(this.safeSocketChunkEventName, (encodedChunk) => {
|
|
85
|
+
// Track received bytes
|
|
86
|
+
this.stats.bytesReceived += encodedChunk.length;
|
|
58
87
|
const chunk = decodeChunk(encodedChunk);
|
|
59
88
|
this.handleChunk(chunk);
|
|
60
89
|
});
|
|
@@ -103,13 +132,18 @@ class StreamedSocket {
|
|
|
103
132
|
if (chunks.length > 1) {
|
|
104
133
|
console.debug(`Sending ${chunks.length} chunks for event ${eventName} with messageId ${messageId}`);
|
|
105
134
|
}
|
|
135
|
+
let totalBytesSent = 0;
|
|
106
136
|
for (const chunk of chunks) {
|
|
107
137
|
// Encode chunk with metadata into single Uint8Array
|
|
108
138
|
const encodedChunk = encodeChunk(chunk);
|
|
139
|
+
totalBytesSent += encodedChunk.length;
|
|
109
140
|
// TODO on error or timeout retry
|
|
110
141
|
// TODO respond to backpressure from socket
|
|
111
142
|
this.socket.emit(this.safeSocketChunkEventName, encodedChunk, lodash_1.noop);
|
|
112
143
|
}
|
|
144
|
+
// Track stats
|
|
145
|
+
this.stats.bytesSent += totalBytesSent;
|
|
146
|
+
this.stats.messagesSent++;
|
|
113
147
|
}
|
|
114
148
|
handlers = {};
|
|
115
149
|
on(eventName, handler) {
|
|
@@ -159,6 +193,8 @@ class StreamedSocket {
|
|
|
159
193
|
reassembled.set(chunk.data, offset);
|
|
160
194
|
offset += chunk.data.length;
|
|
161
195
|
}
|
|
196
|
+
// Track message received
|
|
197
|
+
this.stats.messagesReceived++;
|
|
162
198
|
delete this.chunkBuffers[chunkZero.messageId];
|
|
163
199
|
const eventName = chunkZero.eventName;
|
|
164
200
|
const args = (0, tx_encoding_1.txDecode)(reassembled);
|
|
@@ -190,7 +226,12 @@ class StreamedSocket {
|
|
|
190
226
|
}, lodash_1.noop);
|
|
191
227
|
}
|
|
192
228
|
catch (error) {
|
|
193
|
-
|
|
229
|
+
if (String(error).includes('No handler registered for event')) {
|
|
230
|
+
console.warn(String(error));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.error(`Error handling chunked request:`, error);
|
|
234
|
+
}
|
|
194
235
|
return;
|
|
195
236
|
}
|
|
196
237
|
}
|
|
@@ -5,16 +5,34 @@ exports.txDecode = txDecode;
|
|
|
5
5
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
6
6
|
const fflate_1 = require("fflate");
|
|
7
7
|
const serial_json_1 = require("../serial-json");
|
|
8
|
+
// Minimum size before attempting compression (bytes)
|
|
9
|
+
// Small payloads don't benefit much from compression
|
|
10
|
+
const COMPRESSION_THRESHOLD = 8 * 1024; // 8KB
|
|
11
|
+
// Minimum compression ratio to justify the CPU cost
|
|
12
|
+
// If compressed isn't at least 20% smaller, skip it
|
|
13
|
+
const MIN_COMPRESSION_RATIO = 0.8;
|
|
14
|
+
// Compression level: 1 (fastest) to 9 (best compression)
|
|
15
|
+
// Level 1 is ~5-10x faster than level 6 with reasonable compression
|
|
16
|
+
const COMPRESSION_LEVEL = 1;
|
|
8
17
|
function txEncode(data) {
|
|
9
18
|
const noCycles = (0, serial_json_1.toJSON)(data); // remove cycles and encode dates, etc.
|
|
10
19
|
// First encode to msgpack
|
|
11
20
|
const encoded = (0, msgpack_1.encode)(noCycles);
|
|
12
21
|
let body;
|
|
13
22
|
let flag;
|
|
14
|
-
if (encoded.length >
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
if (encoded.length > COMPRESSION_THRESHOLD) {
|
|
24
|
+
// Try to compress - use fast compression level
|
|
25
|
+
const compressed = (0, fflate_1.compressSync)(encoded, { level: COMPRESSION_LEVEL });
|
|
26
|
+
// Only use compression if it actually helps significantly
|
|
27
|
+
if (compressed.length < encoded.length * MIN_COMPRESSION_RATIO) {
|
|
28
|
+
body = compressed;
|
|
29
|
+
flag = 1; // compressed
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Compression didn't help much, skip it
|
|
33
|
+
body = encoded;
|
|
34
|
+
flag = 0; // not compressed
|
|
35
|
+
}
|
|
18
36
|
}
|
|
19
37
|
else {
|
|
20
38
|
body = encoded;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tx_encoding_1 = require("./tx-encoding");
|
|
4
|
+
const msgpack_1 = require("@msgpack/msgpack");
|
|
5
|
+
const serial_json_1 = require("../serial-json");
|
|
4
6
|
describe('tx-encode-decode', () => {
|
|
5
7
|
describe('txEncode and txDecode', () => {
|
|
6
8
|
it('should encode and decode simple objects', () => {
|
|
@@ -88,14 +90,22 @@ describe('tx-encode-decode', () => {
|
|
|
88
90
|
expect(encoded[0]).toBe(0); // flag = 0 means not compressed
|
|
89
91
|
});
|
|
90
92
|
it('should compress large payloads (> 1024 bytes)', () => {
|
|
91
|
-
// Create an object that will exceed
|
|
93
|
+
// Create an object that will exceed 8KB when msgpack-encoded
|
|
94
|
+
// msgpack is more compact than JSON, so we need significantly more data
|
|
95
|
+
// A string of 20,000 chars is roughly 20KB in JSON, but msgpack uses length prefix + bytes
|
|
96
|
+
// So we need even more to ensure msgpack encoding exceeds 8KB
|
|
92
97
|
const largeObj = {
|
|
93
|
-
data: 'x'.repeat(
|
|
94
|
-
array: Array.from({ length:
|
|
98
|
+
data: 'x'.repeat(30000),
|
|
99
|
+
array: Array.from({ length: 1000 }, (_, i) => ({
|
|
100
|
+
id: i,
|
|
101
|
+
value: `item ${i} with some extra text to make it larger and ensure we exceed the threshold`,
|
|
102
|
+
nested: { prop: `nested value ${i}` }
|
|
103
|
+
}))
|
|
95
104
|
};
|
|
96
|
-
const jsonStr = JSON.stringify(largeObj);
|
|
97
|
-
expect(jsonStr.length).toBeGreaterThan(1024);
|
|
98
105
|
const encoded = (0, tx_encoding_1.txEncode)(largeObj);
|
|
106
|
+
// Verify the encoded size (without flag byte) exceeds 8KB threshold
|
|
107
|
+
const encodedSize = encoded.length - 1; // subtract flag byte
|
|
108
|
+
expect(encodedSize).toBeGreaterThan(8 * 1024);
|
|
99
109
|
// First byte is the flag
|
|
100
110
|
expect(encoded[0]).toBe(1); // flag = 1 means compressed
|
|
101
111
|
// Verify it can still be decoded correctly
|
|
@@ -103,14 +113,22 @@ describe('tx-encode-decode', () => {
|
|
|
103
113
|
expect(decoded).toEqual(largeObj);
|
|
104
114
|
});
|
|
105
115
|
it('should have smaller encoded size for large compressible data', () => {
|
|
116
|
+
// Create data that exceeds 8KB when msgpack-encoded and compresses well
|
|
117
|
+
// Use data with some repetition but also enough variety to ensure msgpack size is large
|
|
106
118
|
const largeRepetitiveObj = {
|
|
107
|
-
data: 'repeat '.repeat(
|
|
108
|
-
values: Array.from({ length:
|
|
119
|
+
data: 'repeat '.repeat(10000), // Highly repetitive string - will compress well
|
|
120
|
+
values: Array.from({ length: 5000 }, (_, i) => `item ${i} with some text that repeats to create compressible data pattern`)
|
|
109
121
|
};
|
|
122
|
+
// First, verify the msgpack-encoded size exceeds 8KB threshold
|
|
123
|
+
const noCycles = (0, serial_json_1.toJSON)(largeRepetitiveObj);
|
|
124
|
+
const msgpackEncoded = (0, msgpack_1.encode)(noCycles);
|
|
125
|
+
expect(msgpackEncoded.length).toBeGreaterThan(8 * 1024);
|
|
110
126
|
const encoded = (0, tx_encoding_1.txEncode)(largeRepetitiveObj);
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
// Verify compression flag is set (means compression was applied)
|
|
128
|
+
expect(encoded[0]).toBe(1); // flag = 1 means compressed
|
|
129
|
+
// Compressed size should be significantly smaller than the raw msgpack data
|
|
130
|
+
const compressedSize = encoded.length - 1; // subtract flag byte
|
|
131
|
+
expect(compressedSize).toBeLessThan(msgpackEncoded.length * 0.8); // Should be at least 20% smaller
|
|
114
132
|
// Verify correctness
|
|
115
133
|
const decoded = (0, tx_encoding_1.txDecode)(encoded);
|
|
116
134
|
expect(decoded).toEqual(largeRepetitiveObj);
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export * from "./types/workflow";
|
|
|
12
12
|
export * from "./types/workflow-logger";
|
|
13
13
|
export * from "./types/workflow-run-context";
|
|
14
14
|
export * from "./types/zod-types";
|
|
15
|
+
export * from "./device/binary-peer-connection";
|
|
16
|
+
export * from "./device/socket-io-binary-peer";
|
|
15
17
|
export * from "./device/connection";
|
|
16
18
|
export * from "./device/device";
|
|
17
19
|
export * from "./device/device-election";
|
|
@@ -33,3 +35,4 @@ export * from "./rpc-types";
|
|
|
33
35
|
export * from "./serial-json";
|
|
34
36
|
export * from "./utils";
|
|
35
37
|
export * from "./logging";
|
|
38
|
+
export * from "./user-connect";
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ __exportStar(require("./types/workflow"), exports);
|
|
|
29
29
|
__exportStar(require("./types/workflow-logger"), exports);
|
|
30
30
|
__exportStar(require("./types/workflow-run-context"), exports);
|
|
31
31
|
__exportStar(require("./types/zod-types"), exports);
|
|
32
|
+
__exportStar(require("./device/binary-peer-connection"), exports);
|
|
33
|
+
__exportStar(require("./device/socket-io-binary-peer"), exports);
|
|
32
34
|
__exportStar(require("./device/connection"), exports);
|
|
33
35
|
__exportStar(require("./device/device"), exports);
|
|
34
36
|
__exportStar(require("./device/device-election"), exports);
|
|
@@ -50,3 +52,4 @@ __exportStar(require("./rpc-types"), exports);
|
|
|
50
52
|
__exportStar(require("./serial-json"), exports);
|
|
51
53
|
__exportStar(require("./utils"), exports);
|
|
52
54
|
__exportStar(require("./logging"), exports);
|
|
55
|
+
__exportStar(require("./user-connect"), exports);
|
package/dist/keys.js
CHANGED
|
@@ -28,6 +28,7 @@ const buffer_1 = require("buffer");
|
|
|
28
28
|
const nacl = require("tweetnacl");
|
|
29
29
|
const utils = require("tweetnacl-util");
|
|
30
30
|
const tweetnacl_util_1 = require("tweetnacl-util");
|
|
31
|
+
const ed2curve = require("ed2curve");
|
|
31
32
|
const tx_encoding_1 = require("./device/tx-encoding");
|
|
32
33
|
const serial_json_1 = require("./serial-json");
|
|
33
34
|
globalThis.Buffer = buffer_1.Buffer; // shim for browsers/RN
|
|
@@ -72,23 +73,23 @@ function newToken(size = 32) {
|
|
|
72
73
|
return encodeBase64(nacl.randomBytes(size));
|
|
73
74
|
}
|
|
74
75
|
function newKeys() {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const boxKeyPair = nacl.box.keyPair.fromSecretKey(secretKeyPart);
|
|
76
|
+
const sign = nacl.sign.keyPair(); // Ed25519
|
|
77
|
+
const curveSecret = ed2curve.convertSecretKey(sign.secretKey); // 32 bytes
|
|
78
|
+
const box = nacl.box.keyPair.fromSecretKey(curveSecret); // X25519
|
|
79
79
|
return {
|
|
80
|
-
secretKey: encodeBase64(
|
|
81
|
-
publicKey: encodeBase64(
|
|
82
|
-
publicBoxKey: encodeBase64(
|
|
80
|
+
secretKey: encodeBase64(sign.secretKey),
|
|
81
|
+
publicKey: encodeBase64(sign.publicKey),
|
|
82
|
+
publicBoxKey: encodeBase64(box.publicKey),
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
function hydrateKeys(secretKey) {
|
|
86
|
-
|
|
87
|
-
const
|
|
86
|
+
const sk64 = decodeBase64(secretKey); // Ed25519 64-byte secretKey
|
|
87
|
+
const curveSecret = ed2curve.convertSecretKey(sk64);
|
|
88
|
+
const box = nacl.box.keyPair.fromSecretKey(curveSecret);
|
|
88
89
|
return {
|
|
89
|
-
secretKey: encodeBase64(
|
|
90
|
-
publicKey: encodeBase64(
|
|
91
|
-
publicBoxKey: encodeBase64(
|
|
90
|
+
secretKey: encodeBase64(sk64),
|
|
91
|
+
publicKey: encodeBase64(sk64.slice(32)), // last 32 bytes = Ed25519 pk
|
|
92
|
+
publicBoxKey: encodeBase64(box.publicKey),
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
function signMessageWithSecretKey(msg, secretKey) {
|
|
@@ -145,7 +146,8 @@ function openSignedObject(signedObj) {
|
|
|
145
146
|
}
|
|
146
147
|
function boxDataWithKeys(data, toPublicBoxKey, mySecretKey) {
|
|
147
148
|
let _secretKey = decodeBase64(mySecretKey);
|
|
148
|
-
const
|
|
149
|
+
const curveSecret = ed2curve.convertSecretKey(_secretKey);
|
|
150
|
+
const boxKeyPair = nacl.box.keyPair.fromSecretKey(curveSecret);
|
|
149
151
|
const _toPublicBoxKey = decodeBase64(toPublicBoxKey);
|
|
150
152
|
const nonce = nacl.randomBytes(24);
|
|
151
153
|
const dataByteArray = (0, tx_encoding_1.txEncode)(data);
|
|
@@ -161,7 +163,8 @@ function isBoxedData(data) {
|
|
|
161
163
|
}
|
|
162
164
|
function openBoxWithSecretKey(box, mySecretKey) {
|
|
163
165
|
let _secretKey = decodeBase64(mySecretKey);
|
|
164
|
-
const
|
|
166
|
+
const curveSecret = ed2curve.convertSecretKey(_secretKey);
|
|
167
|
+
const boxKeyPair = nacl.box.keyPair.fromSecretKey(curveSecret);
|
|
165
168
|
const boxedData = decodeBase64(box.contents);
|
|
166
169
|
const nonce = decodeBase64(box.nonce);
|
|
167
170
|
const _fromPublicBoxKey = decodeBase64(box.fromPublicKey);
|
|
@@ -3,6 +3,8 @@ import type { IPeersPackage } from "../types/peers-package";
|
|
|
3
3
|
import { IPackage } from "../data/packages";
|
|
4
4
|
export declare class PackageLoader {
|
|
5
5
|
readonly dataContext: DataContext;
|
|
6
|
+
static PeersSDK: any;
|
|
7
|
+
static Zod: any;
|
|
6
8
|
private packageInstances;
|
|
7
9
|
require: (<T>(module: string) => T) | undefined;
|
|
8
10
|
constructor(dataContext: DataContext);
|
|
@@ -8,6 +8,8 @@ const packages_1 = require("../data/packages");
|
|
|
8
8
|
const tools_2 = require("../data/tools");
|
|
9
9
|
class PackageLoader {
|
|
10
10
|
dataContext;
|
|
11
|
+
static PeersSDK;
|
|
12
|
+
static Zod;
|
|
11
13
|
packageInstances = {};
|
|
12
14
|
require = undefined;
|
|
13
15
|
constructor(dataContext) {
|
|
@@ -59,9 +61,9 @@ class PackageLoader {
|
|
|
59
61
|
case 'PeersSDK':
|
|
60
62
|
// Import all peers-sdk exports
|
|
61
63
|
// return _require('peers-sdk');
|
|
62
|
-
return _require('../index');
|
|
64
|
+
return PackageLoader.PeersSDK || _require('../index');
|
|
63
65
|
case 'zod':
|
|
64
|
-
return _require('zod');
|
|
66
|
+
return PackageLoader.Zod || _require('zod');
|
|
65
67
|
default:
|
|
66
68
|
// For other modules, use the standard require
|
|
67
69
|
console.warn(`Package ${pkg.name} is requiring a module ${moduleId}, which is not provided by the package loader.`);
|
package/dist/rpc-types.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare const rpcServerCalls: {
|
|
|
18
18
|
encryptData: ((value: string, groupId?: string) => Promise<string>);
|
|
19
19
|
tableMethodCall: ((dataContextId: string, tableName: string, methodName: string, ...args: any[]) => Promise<any>);
|
|
20
20
|
getFileContents: ((fileId: string, encoding?: BufferEncoding) => Promise<string>);
|
|
21
|
+
injectUIBundle: ((uiBundleFileId: string) => Promise<void>);
|
|
21
22
|
resetAllDeviceSyncInfo: (() => Promise<void>);
|
|
22
23
|
importGroupShare: ((groupShareJson: string) => Promise<string>);
|
|
23
24
|
};
|
package/dist/rpc-types.js
CHANGED
|
@@ -19,6 +19,8 @@ exports.rpcServerCalls = {
|
|
|
19
19
|
tableMethodCall: rpcStub('tableMethodCall'),
|
|
20
20
|
// TODO lock this down so not all code can get any file contents
|
|
21
21
|
getFileContents: rpcStub('getFileContents'),
|
|
22
|
+
// Inject UI bundle directly into WebView (bypasses slow postMessage for large strings)
|
|
23
|
+
injectUIBundle: rpcStub('injectUIBundle'),
|
|
22
24
|
resetAllDeviceSyncInfo: rpcStub('resetAllDeviceSyncInfo'),
|
|
23
25
|
importGroupShare: rpcStub('importGroupShare'),
|
|
24
26
|
// TODO try to get rid of this and rely on the client-side table and server-side table individually emitting events
|
|
@@ -33,7 +35,9 @@ exports.rpcClientCalls = {
|
|
|
33
35
|
setClientPath: rpcStub('setClientPath'),
|
|
34
36
|
openThread: rpcStub('openThread'),
|
|
35
37
|
};
|
|
36
|
-
|
|
38
|
+
// Check if we're in a web browser or webview (not React Native or Node.js)
|
|
39
|
+
// React Native has window but not document, so we check for both
|
|
40
|
+
exports.isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
37
41
|
if (exports.isClient) {
|
|
38
42
|
// @ts-ignore
|
|
39
43
|
const _window = window;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for generating and handling connection codes.
|
|
3
|
+
*
|
|
4
|
+
* Connection codes are 12 characters of Crockford Base32:
|
|
5
|
+
* - First 4 chars: device alias (rendezvous point)
|
|
6
|
+
* - Last 8 chars: shared secret for encryption
|
|
7
|
+
*
|
|
8
|
+
* Displayed format: XXXX-YYYY-ZZZZ
|
|
9
|
+
*/
|
|
10
|
+
import type { IUserConnectInfo } from './user-connect.types';
|
|
11
|
+
export interface IConnectionCode {
|
|
12
|
+
/** Full 12-character code */
|
|
13
|
+
code: string;
|
|
14
|
+
/** First 4 characters - device alias for rendezvous */
|
|
15
|
+
alias: string;
|
|
16
|
+
/** Last 8 characters - shared secret for encryption */
|
|
17
|
+
secret: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a new connection code.
|
|
21
|
+
* @returns Object with full code, alias, and secret
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateConnectionCode(): IConnectionCode;
|
|
24
|
+
/**
|
|
25
|
+
* Format a connection code for display.
|
|
26
|
+
* @param code 12-character code
|
|
27
|
+
* @returns Formatted as "XXXX-YYYY-ZZZZ"
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatConnectionCode(code: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Parse a formatted connection code.
|
|
32
|
+
* @param formatted Code in any format (with or without dashes)
|
|
33
|
+
* @returns Object with alias and secret
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseConnectionCode(formatted: string): {
|
|
36
|
+
code: string;
|
|
37
|
+
alias: string;
|
|
38
|
+
secret: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Encrypt data using the shared secret.
|
|
42
|
+
* @param data Data to encrypt
|
|
43
|
+
* @param secret 8-character shared secret
|
|
44
|
+
* @returns Base64-encoded encrypted data with nonce
|
|
45
|
+
*/
|
|
46
|
+
export declare function encryptWithSecret(data: any, secret: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Decrypt data using the shared secret.
|
|
49
|
+
* @param encrypted Base64-encoded encrypted data
|
|
50
|
+
* @param secret 8-character shared secret
|
|
51
|
+
* @returns Decrypted data
|
|
52
|
+
*/
|
|
53
|
+
export declare function decryptWithSecret(encrypted: string, secret: string): any;
|
|
54
|
+
/**
|
|
55
|
+
* Generate a confirmation hash from both users' information.
|
|
56
|
+
* Both parties should see the same hash if the exchange was successful.
|
|
57
|
+
* @param userA First user's info
|
|
58
|
+
* @param userB Second user's info
|
|
59
|
+
* @returns 4-character confirmation code
|
|
60
|
+
*/
|
|
61
|
+
export declare function generateConfirmationHash(userA: IUserConnectInfo, userB: IUserConnectInfo): string;
|
|
62
|
+
/**
|
|
63
|
+
* Validate that a string is a valid connection code format.
|
|
64
|
+
* @param code Code to validate (with or without dashes)
|
|
65
|
+
* @returns true if valid
|
|
66
|
+
*/
|
|
67
|
+
export declare function isValidConnectionCode(code: string): boolean;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for generating and handling connection codes.
|
|
4
|
+
*
|
|
5
|
+
* Connection codes are 12 characters of Crockford Base32:
|
|
6
|
+
* - First 4 chars: device alias (rendezvous point)
|
|
7
|
+
* - Last 8 chars: shared secret for encryption
|
|
8
|
+
*
|
|
9
|
+
* Displayed format: XXXX-YYYY-ZZZZ
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.generateConnectionCode = generateConnectionCode;
|
|
13
|
+
exports.formatConnectionCode = formatConnectionCode;
|
|
14
|
+
exports.parseConnectionCode = parseConnectionCode;
|
|
15
|
+
exports.encryptWithSecret = encryptWithSecret;
|
|
16
|
+
exports.decryptWithSecret = decryptWithSecret;
|
|
17
|
+
exports.generateConfirmationHash = generateConfirmationHash;
|
|
18
|
+
exports.isValidConnectionCode = isValidConnectionCode;
|
|
19
|
+
const nacl = require("tweetnacl");
|
|
20
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
21
|
+
const tx_encoding_1 = require("../device/tx-encoding");
|
|
22
|
+
const keys_1 = require("../keys");
|
|
23
|
+
/**
|
|
24
|
+
* Crockford Base32 alphabet.
|
|
25
|
+
* Excludes I, L, O, U to avoid confusion with 1, 1, 0, V respectively.
|
|
26
|
+
*/
|
|
27
|
+
const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
28
|
+
/**
|
|
29
|
+
* Generate random bytes using nacl.
|
|
30
|
+
*/
|
|
31
|
+
function randomBytes(length) {
|
|
32
|
+
return nacl.randomBytes(length);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Encode bytes to Crockford Base32 string.
|
|
36
|
+
*/
|
|
37
|
+
function toCrockfordBase32(bytes, length) {
|
|
38
|
+
let result = '';
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (result.length < length) {
|
|
41
|
+
if (i >= bytes.length) {
|
|
42
|
+
// Need more bytes
|
|
43
|
+
const moreBytes = randomBytes(length - result.length);
|
|
44
|
+
for (const b of moreBytes) {
|
|
45
|
+
result += CROCKFORD_ALPHABET[b % 32];
|
|
46
|
+
if (result.length >= length)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
// Use 5 bits at a time for base32
|
|
52
|
+
result += CROCKFORD_ALPHABET[bytes[i] % 32];
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
return result.substring(0, length);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Derive a 32-byte encryption key from the shared secret.
|
|
59
|
+
*/
|
|
60
|
+
function deriveKeyFromSecret(secret) {
|
|
61
|
+
// Use SHA-256 to derive a proper 32-byte key from the secret
|
|
62
|
+
const secretBytes = new TextEncoder().encode(secret);
|
|
63
|
+
return (0, sha2_1.sha256)(secretBytes);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate a new connection code.
|
|
67
|
+
* @returns Object with full code, alias, and secret
|
|
68
|
+
*/
|
|
69
|
+
function generateConnectionCode() {
|
|
70
|
+
const bytes = randomBytes(16);
|
|
71
|
+
const alias = toCrockfordBase32(bytes.slice(0, 4), 4);
|
|
72
|
+
const secret = toCrockfordBase32(bytes.slice(4), 8);
|
|
73
|
+
return {
|
|
74
|
+
code: alias + secret,
|
|
75
|
+
alias,
|
|
76
|
+
secret,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format a connection code for display.
|
|
81
|
+
* @param code 12-character code
|
|
82
|
+
* @returns Formatted as "XXXX-YYYY-ZZZZ"
|
|
83
|
+
*/
|
|
84
|
+
function formatConnectionCode(code) {
|
|
85
|
+
const normalized = code.toUpperCase().replace(/[^0-9A-Z]/g, '');
|
|
86
|
+
if (normalized.length !== 12) {
|
|
87
|
+
throw new Error(`Connection code must be 12 characters, got ${normalized.length}`);
|
|
88
|
+
}
|
|
89
|
+
return `${normalized.slice(0, 4)}-${normalized.slice(4, 8)}-${normalized.slice(8, 12)}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse a formatted connection code.
|
|
93
|
+
* @param formatted Code in any format (with or without dashes)
|
|
94
|
+
* @returns Object with alias and secret
|
|
95
|
+
*/
|
|
96
|
+
function parseConnectionCode(formatted) {
|
|
97
|
+
const normalized = formatted.toUpperCase().replace(/[^0-9A-Z]/g, '');
|
|
98
|
+
if (normalized.length !== 12) {
|
|
99
|
+
throw new Error(`Connection code must be 12 characters, got ${normalized.length}`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
code: normalized,
|
|
103
|
+
alias: normalized.slice(0, 4),
|
|
104
|
+
secret: normalized.slice(4, 12),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Encrypt data using the shared secret.
|
|
109
|
+
* @param data Data to encrypt
|
|
110
|
+
* @param secret 8-character shared secret
|
|
111
|
+
* @returns Base64-encoded encrypted data with nonce
|
|
112
|
+
*/
|
|
113
|
+
function encryptWithSecret(data, secret) {
|
|
114
|
+
const key = deriveKeyFromSecret(secret);
|
|
115
|
+
const nonce = nacl.randomBytes(24);
|
|
116
|
+
const dataBytes = (0, tx_encoding_1.txEncode)(data);
|
|
117
|
+
const encrypted = nacl.secretbox(dataBytes, nonce, key);
|
|
118
|
+
// Combine nonce + encrypted data
|
|
119
|
+
const combined = new Uint8Array(nonce.length + encrypted.length);
|
|
120
|
+
combined.set(nonce);
|
|
121
|
+
combined.set(encrypted, nonce.length);
|
|
122
|
+
return (0, keys_1.encodeBase64)(combined);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Decrypt data using the shared secret.
|
|
126
|
+
* @param encrypted Base64-encoded encrypted data
|
|
127
|
+
* @param secret 8-character shared secret
|
|
128
|
+
* @returns Decrypted data
|
|
129
|
+
*/
|
|
130
|
+
function decryptWithSecret(encrypted, secret) {
|
|
131
|
+
const key = deriveKeyFromSecret(secret);
|
|
132
|
+
const combined = (0, keys_1.decodeBase64)(encrypted);
|
|
133
|
+
const nonce = combined.slice(0, 24);
|
|
134
|
+
const ciphertext = combined.slice(24);
|
|
135
|
+
const decrypted = nacl.secretbox.open(ciphertext, nonce, key);
|
|
136
|
+
if (!decrypted) {
|
|
137
|
+
throw new Error('Decryption failed - invalid secret or corrupted data');
|
|
138
|
+
}
|
|
139
|
+
return (0, tx_encoding_1.txDecode)(decrypted);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Generate a confirmation hash from both users' information.
|
|
143
|
+
* Both parties should see the same hash if the exchange was successful.
|
|
144
|
+
* @param userA First user's info
|
|
145
|
+
* @param userB Second user's info
|
|
146
|
+
* @returns 4-character confirmation code
|
|
147
|
+
*/
|
|
148
|
+
function generateConfirmationHash(userA, userB) {
|
|
149
|
+
// Sort by userId to ensure consistent ordering regardless of who is A or B
|
|
150
|
+
const sorted = [userA, userB].sort((a, b) => a.userId.localeCompare(b.userId));
|
|
151
|
+
const combined = JSON.stringify(sorted);
|
|
152
|
+
const hash = (0, sha2_1.sha256)(new TextEncoder().encode(combined));
|
|
153
|
+
// Take first 4 characters of the hash encoded in Crockford Base32
|
|
154
|
+
return toCrockfordBase32(hash.slice(0, 4), 4);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Validate that a string is a valid connection code format.
|
|
158
|
+
* @param code Code to validate (with or without dashes)
|
|
159
|
+
* @returns true if valid
|
|
160
|
+
*/
|
|
161
|
+
function isValidConnectionCode(code) {
|
|
162
|
+
try {
|
|
163
|
+
const normalized = code.toUpperCase().replace(/[^0-9A-Z]/g, '');
|
|
164
|
+
if (normalized.length !== 12)
|
|
165
|
+
return false;
|
|
166
|
+
// Check all characters are valid Crockford Base32
|
|
167
|
+
for (const char of normalized) {
|
|
168
|
+
if (!CROCKFORD_ALPHABET.includes(char))
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|