@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.
Files changed (37) hide show
  1. package/dist/data/files/file-read-stream.d.ts +2 -1
  2. package/dist/data/files/file-read-stream.js +9 -5
  3. package/dist/data/files/files.d.ts +4 -2
  4. package/dist/data/files/files.js +4 -4
  5. package/dist/data/groups.d.ts +2 -2
  6. package/dist/device/binary-peer-connection.d.ts +54 -0
  7. package/dist/device/binary-peer-connection.js +350 -0
  8. package/dist/device/binary-peer-connection.test.d.ts +1 -0
  9. package/dist/device/binary-peer-connection.test.js +204 -0
  10. package/dist/device/connection.d.ts +29 -0
  11. package/dist/device/connection.js +85 -2
  12. package/dist/device/connection.test.js +20 -4
  13. package/dist/device/socket-io-binary-peer.d.ts +27 -0
  14. package/dist/device/socket-io-binary-peer.js +97 -0
  15. package/dist/device/socket.type.d.ts +52 -0
  16. package/dist/device/streamed-socket.d.ts +4 -1
  17. package/dist/device/streamed-socket.js +45 -4
  18. package/dist/device/tx-encoding.js +22 -4
  19. package/dist/device/tx-encoding.test.js +28 -10
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/keys.js +17 -14
  23. package/dist/package-loader/package-loader.d.ts +2 -0
  24. package/dist/package-loader/package-loader.js +4 -2
  25. package/dist/rpc-types.d.ts +1 -0
  26. package/dist/rpc-types.js +5 -1
  27. package/dist/user-connect/connection-code.d.ts +67 -0
  28. package/dist/user-connect/connection-code.js +176 -0
  29. package/dist/user-connect/connection-code.test.d.ts +1 -0
  30. package/dist/user-connect/connection-code.test.js +213 -0
  31. package/dist/user-connect/index.d.ts +3 -0
  32. package/dist/user-connect/index.js +19 -0
  33. package/dist/user-connect/user-connect.pvars.d.ts +3 -0
  34. package/dist/user-connect/user-connect.pvars.js +33 -0
  35. package/dist/user-connect/user-connect.types.d.ts +58 -0
  36. package/dist/user-connect/user-connect.types.js +8 -0
  37. 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(bytes.buffer, bytes.byteOffset, bytes.byteLength);
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 = bytes.subarray(4, 4 + metadataLength);
63
+ const metadataBytes = normalizedBytes.subarray(4, 4 + metadataLength);
38
64
  const metadata = (0, msgpack_1.decode)(metadataBytes);
39
65
  // Extract data
40
- const data = bytes.subarray(4 + metadataLength);
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
- console.error(`Error handling chunked request:`, error);
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 > 1024) {
15
- // Compress big payloads
16
- body = (0, fflate_1.compressSync)(encoded);
17
- flag = 1; // compressed
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 1024 bytes when encoded
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(2000),
94
- array: Array.from({ length: 100 }, (_, i) => ({ id: i, value: `item ${i}` }))
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(500),
108
- values: Array.from({ length: 200 }, () => 'same string')
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
- // Compressed size should be significantly smaller than the raw data
112
- const rawDataSize = JSON.stringify(largeRepetitiveObj).length;
113
- expect(encoded.length).toBeLessThan(rawDataSize / 2);
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 keyPair = nacl.sign.keyPair();
76
- // the first 32 bytes of the secret key are the secret part. The second 32 bytes are the public part.
77
- const secretKeyPart = keyPair.secretKey.slice(0, 32);
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(keyPair.secretKey),
81
- publicKey: encodeBase64(keyPair.publicKey),
82
- publicBoxKey: encodeBase64(boxKeyPair.publicKey),
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
- let _secretKey = decodeBase64(secretKey);
87
- const boxKeyPair = nacl.box.keyPair.fromSecretKey(_secretKey.slice(0, 32));
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(_secretKey),
90
- publicKey: encodeBase64(_secretKey.slice(32)),
91
- publicBoxKey: encodeBase64(boxKeyPair.publicKey),
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 boxKeyPair = nacl.box.keyPair.fromSecretKey(_secretKey.slice(0, 32));
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 boxKeyPair = nacl.box.keyPair.fromSecretKey(_secretKey.slice(0, 32));
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.`);
@@ -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
- exports.isClient = typeof window !== 'undefined';
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 {};