@peers-app/peers-sdk 0.7.39 → 0.8.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/dist/data/groups.d.ts +2 -2
- package/dist/device/device-election.test.d.ts +1 -0
- package/dist/device/device-election.test.js +444 -0
- package/dist/device/device.d.ts +2 -1
- package/dist/device/device.js +9 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.d.ts +1 -0
- package/dist/keys.js +21 -14
- package/dist/types/peer-device.d.ts +8 -8
- package/dist/types/peer-device.js +1 -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/dist/users.query.d.ts +2 -1
- package/dist/users.query.js +1 -1
- package/package.json +3 -1
package/dist/data/groups.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export declare const groupSchema: z.ZodObject<{
|
|
|
17
17
|
}, "strip", z.ZodTypeAny, {
|
|
18
18
|
name: string;
|
|
19
19
|
description: string;
|
|
20
|
-
signature: string;
|
|
21
20
|
publicKey: string;
|
|
21
|
+
signature: string;
|
|
22
22
|
publicBoxKey: string;
|
|
23
23
|
groupId: string;
|
|
24
24
|
founderUserId: string;
|
|
@@ -28,8 +28,8 @@ export declare const groupSchema: z.ZodObject<{
|
|
|
28
28
|
}, {
|
|
29
29
|
name: string;
|
|
30
30
|
description: string;
|
|
31
|
-
signature: string;
|
|
32
31
|
publicKey: string;
|
|
32
|
+
signature: string;
|
|
33
33
|
publicBoxKey: string;
|
|
34
34
|
groupId: string;
|
|
35
35
|
founderUserId: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const device_election_1 = require("./device-election");
|
|
4
|
+
const utils_1 = require("../utils");
|
|
5
|
+
const lodash_1 = require("lodash");
|
|
6
|
+
describe("device-election", () => {
|
|
7
|
+
const createConnection = (deviceId, latencyMs = 10, errorRate = 0.001) => ({
|
|
8
|
+
deviceId,
|
|
9
|
+
latencyMs,
|
|
10
|
+
errorRate,
|
|
11
|
+
timestampLastApplied: Date.now(),
|
|
12
|
+
});
|
|
13
|
+
const createNetworkInfo = (deviceId, connections = [], preferredDeviceIds = [], connectionSlotsAvailable = 10) => ({
|
|
14
|
+
deviceId,
|
|
15
|
+
timestampLastApplied: Date.now(),
|
|
16
|
+
connections,
|
|
17
|
+
preferredDeviceIds,
|
|
18
|
+
cpuPercent: 0,
|
|
19
|
+
memPercent: 0,
|
|
20
|
+
connectionSlotsAvailable,
|
|
21
|
+
});
|
|
22
|
+
describe("electDevices", () => {
|
|
23
|
+
it("should return empty arrays when there are no connections", () => {
|
|
24
|
+
const deviceId = (0, utils_1.newid)();
|
|
25
|
+
const result = (0, device_election_1.electDevices)({
|
|
26
|
+
deviceId,
|
|
27
|
+
myConnections: [],
|
|
28
|
+
allNetworkInfo: [],
|
|
29
|
+
});
|
|
30
|
+
expect(result.preferredDeviceIds).toEqual([]);
|
|
31
|
+
expect(result.preferredByDeviceIds).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
it("should elect devices that provide connections to unconnected devices", () => {
|
|
34
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
35
|
+
const device1 = (0, utils_1.newid)();
|
|
36
|
+
const device2 = (0, utils_1.newid)();
|
|
37
|
+
const device3 = (0, utils_1.newid)();
|
|
38
|
+
const myConnection1 = createConnection(device1);
|
|
39
|
+
const myConnection2 = createConnection(device2);
|
|
40
|
+
const networkInfo1 = createNetworkInfo(device1, [
|
|
41
|
+
createConnection(device2),
|
|
42
|
+
createConnection(device3),
|
|
43
|
+
]);
|
|
44
|
+
const networkInfo2 = createNetworkInfo(device2, [
|
|
45
|
+
createConnection(device1),
|
|
46
|
+
]);
|
|
47
|
+
const result = (0, device_election_1.electDevices)({
|
|
48
|
+
deviceId: myDeviceId,
|
|
49
|
+
myConnections: [myConnection1, myConnection2],
|
|
50
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
51
|
+
});
|
|
52
|
+
// device1 should be preferred because it connects to device3 (which we're not connected to)
|
|
53
|
+
// device2 should be preferred because it connects to device1 (which we're connected to, but still useful)
|
|
54
|
+
expect(result.preferredDeviceIds.length).toBeGreaterThan(0);
|
|
55
|
+
expect(result.preferredDeviceIds).toContain(device1);
|
|
56
|
+
});
|
|
57
|
+
it("should prefer devices that are preferred by other devices", () => {
|
|
58
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
59
|
+
const device1 = (0, utils_1.newid)();
|
|
60
|
+
const device2 = (0, utils_1.newid)();
|
|
61
|
+
const myConnection1 = createConnection(device1);
|
|
62
|
+
const myConnection2 = createConnection(device2);
|
|
63
|
+
// device1 is preferred by device2
|
|
64
|
+
const networkInfo1 = createNetworkInfo(device1, [], []);
|
|
65
|
+
const networkInfo2 = createNetworkInfo(device2, [], [device1]);
|
|
66
|
+
const result = (0, device_election_1.electDevices)({
|
|
67
|
+
deviceId: myDeviceId,
|
|
68
|
+
myConnections: [myConnection1, myConnection2],
|
|
69
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
70
|
+
});
|
|
71
|
+
expect(result.preferredDeviceIds).toContain(device1);
|
|
72
|
+
});
|
|
73
|
+
it("should track which devices prefer me", () => {
|
|
74
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
75
|
+
const device1 = (0, utils_1.newid)();
|
|
76
|
+
const device2 = (0, utils_1.newid)();
|
|
77
|
+
const myConnection1 = createConnection(device1);
|
|
78
|
+
const myConnection2 = createConnection(device2);
|
|
79
|
+
// device1 and device2 both prefer myDeviceId
|
|
80
|
+
const networkInfo1 = createNetworkInfo(device1, [], [myDeviceId]);
|
|
81
|
+
const networkInfo2 = createNetworkInfo(device2, [], [myDeviceId]);
|
|
82
|
+
const result = (0, device_election_1.electDevices)({
|
|
83
|
+
deviceId: myDeviceId,
|
|
84
|
+
myConnections: [myConnection1, myConnection2],
|
|
85
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
86
|
+
});
|
|
87
|
+
expect(result.preferredByDeviceIds).toContain(device1);
|
|
88
|
+
expect(result.preferredByDeviceIds).toContain(device2);
|
|
89
|
+
});
|
|
90
|
+
it("should prefer connections with lower latency and error rate", () => {
|
|
91
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
92
|
+
const device1 = (0, utils_1.newid)();
|
|
93
|
+
const device2 = (0, utils_1.newid)();
|
|
94
|
+
const device3 = (0, utils_1.newid)();
|
|
95
|
+
// device1 has better connection quality
|
|
96
|
+
const myConnection1 = createConnection(device1, 5, 0.0001);
|
|
97
|
+
// device2 has worse connection quality
|
|
98
|
+
const myConnection2 = createConnection(device2, 50, 0.01);
|
|
99
|
+
const networkInfo1 = createNetworkInfo(device1, [createConnection(device3)]);
|
|
100
|
+
const networkInfo2 = createNetworkInfo(device2, [createConnection(device3)]);
|
|
101
|
+
const result = (0, device_election_1.electDevices)({
|
|
102
|
+
deviceId: myDeviceId,
|
|
103
|
+
myConnections: [myConnection1, myConnection2],
|
|
104
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
105
|
+
});
|
|
106
|
+
// device1 should be preferred due to better connection quality
|
|
107
|
+
expect(result.preferredDeviceIds[0]).toBe(device1);
|
|
108
|
+
});
|
|
109
|
+
it("should prefer devices that connect to more unconnected devices", () => {
|
|
110
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
111
|
+
const device1 = (0, utils_1.newid)();
|
|
112
|
+
const device2 = (0, utils_1.newid)();
|
|
113
|
+
const device3 = (0, utils_1.newid)();
|
|
114
|
+
const device4 = (0, utils_1.newid)();
|
|
115
|
+
const myConnection1 = createConnection(device1);
|
|
116
|
+
const myConnection2 = createConnection(device2);
|
|
117
|
+
// device1 connects to device3 and device4
|
|
118
|
+
const networkInfo1 = createNetworkInfo(device1, [
|
|
119
|
+
createConnection(device3),
|
|
120
|
+
createConnection(device4),
|
|
121
|
+
]);
|
|
122
|
+
// device2 only connects to device3
|
|
123
|
+
const networkInfo2 = createNetworkInfo(device2, [
|
|
124
|
+
createConnection(device3),
|
|
125
|
+
]);
|
|
126
|
+
const result = (0, device_election_1.electDevices)({
|
|
127
|
+
deviceId: myDeviceId,
|
|
128
|
+
myConnections: [myConnection1, myConnection2],
|
|
129
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
130
|
+
});
|
|
131
|
+
// device1 should be preferred because it connects to more devices
|
|
132
|
+
expect(result.preferredDeviceIds[0]).toBe(device1);
|
|
133
|
+
});
|
|
134
|
+
it("should exclude my own deviceId from preferred devices", () => {
|
|
135
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
136
|
+
const device1 = (0, utils_1.newid)();
|
|
137
|
+
const myConnection1 = createConnection(device1);
|
|
138
|
+
const networkInfo1 = createNetworkInfo(device1, []);
|
|
139
|
+
// Include network info for myself (shouldn't happen in practice, but test edge case)
|
|
140
|
+
const myNetworkInfo = createNetworkInfo(myDeviceId, []);
|
|
141
|
+
const result = (0, device_election_1.electDevices)({
|
|
142
|
+
deviceId: myDeviceId,
|
|
143
|
+
myConnections: [myConnection1],
|
|
144
|
+
allNetworkInfo: [networkInfo1, myNetworkInfo],
|
|
145
|
+
});
|
|
146
|
+
expect(result.preferredDeviceIds).not.toContain(myDeviceId);
|
|
147
|
+
});
|
|
148
|
+
it("should handle devices with limited connection slots", () => {
|
|
149
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
150
|
+
const device1 = (0, utils_1.newid)();
|
|
151
|
+
const device2 = (0, utils_1.newid)();
|
|
152
|
+
const myConnection1 = createConnection(device1);
|
|
153
|
+
const myConnection2 = createConnection(device2);
|
|
154
|
+
// device1 is nearing max connections
|
|
155
|
+
const networkInfo1 = createNetworkInfo(device1, [], [], 2);
|
|
156
|
+
// device2 has plenty of slots
|
|
157
|
+
const networkInfo2 = createNetworkInfo(device2, [], [], 10);
|
|
158
|
+
const result = (0, device_election_1.electDevices)({
|
|
159
|
+
deviceId: myDeviceId,
|
|
160
|
+
myConnections: [myConnection1, myConnection2],
|
|
161
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
162
|
+
});
|
|
163
|
+
// device2 should be preferred due to more available slots
|
|
164
|
+
expect(result.preferredDeviceIds[0]).toBe(device2);
|
|
165
|
+
});
|
|
166
|
+
it("should handle bidirectional connections preference", () => {
|
|
167
|
+
const myDeviceId = (0, utils_1.newid)();
|
|
168
|
+
const device1 = (0, utils_1.newid)();
|
|
169
|
+
const device2 = (0, utils_1.newid)();
|
|
170
|
+
const myConnection1 = createConnection(device1);
|
|
171
|
+
const myConnection2 = createConnection(device2);
|
|
172
|
+
// device1 has a connection back to me
|
|
173
|
+
const networkInfo1 = createNetworkInfo(device1, [
|
|
174
|
+
createConnection(myDeviceId),
|
|
175
|
+
]);
|
|
176
|
+
// device2 doesn't have a connection back to me
|
|
177
|
+
const networkInfo2 = createNetworkInfo(device2, []);
|
|
178
|
+
const result = (0, device_election_1.electDevices)({
|
|
179
|
+
deviceId: myDeviceId,
|
|
180
|
+
myConnections: [myConnection1, myConnection2],
|
|
181
|
+
allNetworkInfo: [networkInfo1, networkInfo2],
|
|
182
|
+
});
|
|
183
|
+
// device1 should be preferred due to bidirectional connection
|
|
184
|
+
expect(result.preferredDeviceIds[0]).toBe(device1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("getLeastPreferredConnection", () => {
|
|
188
|
+
it("should return undefined when there are no connections", () => {
|
|
189
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
190
|
+
connections: [],
|
|
191
|
+
preferredDeviceIds: [],
|
|
192
|
+
preferredByDeviceIds: [],
|
|
193
|
+
});
|
|
194
|
+
expect(result).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
it("should prefer non-critical connections first", () => {
|
|
197
|
+
const device1 = (0, utils_1.newid)();
|
|
198
|
+
const device2 = (0, utils_1.newid)();
|
|
199
|
+
const device3 = (0, utils_1.newid)();
|
|
200
|
+
const connections = [
|
|
201
|
+
{ ...createConnection(device1), groups: ["group1"] },
|
|
202
|
+
{ ...createConnection(device2), groups: ["group1", "group2"] },
|
|
203
|
+
{ ...createConnection(device3), groups: ["group1"] },
|
|
204
|
+
];
|
|
205
|
+
// device1 is critical, device2 and device3 are not
|
|
206
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
207
|
+
connections,
|
|
208
|
+
preferredDeviceIds: [device1],
|
|
209
|
+
preferredByDeviceIds: [],
|
|
210
|
+
});
|
|
211
|
+
// Should return one of the non-critical connections (device2 or device3)
|
|
212
|
+
expect(result).toBeDefined();
|
|
213
|
+
expect(result?.deviceId).not.toBe(device1);
|
|
214
|
+
});
|
|
215
|
+
it("should prefer connections that are not critical to me and I'm not critical to them", () => {
|
|
216
|
+
const device1 = (0, utils_1.newid)();
|
|
217
|
+
const device2 = (0, utils_1.newid)();
|
|
218
|
+
const device3 = (0, utils_1.newid)();
|
|
219
|
+
const connections = [
|
|
220
|
+
{ ...createConnection(device1), groups: ["group1"] },
|
|
221
|
+
{ ...createConnection(device2), groups: ["group1"] },
|
|
222
|
+
{ ...createConnection(device3), groups: ["group1"] },
|
|
223
|
+
];
|
|
224
|
+
// device1 is critical to me, device2 is critical to them, device3 is neither
|
|
225
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
226
|
+
connections,
|
|
227
|
+
preferredDeviceIds: [device1],
|
|
228
|
+
preferredByDeviceIds: [device2],
|
|
229
|
+
});
|
|
230
|
+
// Should prefer device3
|
|
231
|
+
expect(result?.deviceId).toBe(device3);
|
|
232
|
+
});
|
|
233
|
+
it("should fall back to non-critical-to-me connections if no ideal candidates", () => {
|
|
234
|
+
const device1 = (0, utils_1.newid)();
|
|
235
|
+
const device2 = (0, utils_1.newid)();
|
|
236
|
+
const connections = [
|
|
237
|
+
{ ...createConnection(device1), groups: ["group1"] },
|
|
238
|
+
{ ...createConnection(device2), groups: ["group1"] },
|
|
239
|
+
];
|
|
240
|
+
// device1 is critical to me, device2 is critical to them
|
|
241
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
242
|
+
connections,
|
|
243
|
+
preferredDeviceIds: [device1],
|
|
244
|
+
preferredByDeviceIds: [device2],
|
|
245
|
+
});
|
|
246
|
+
// Should prefer device2 (not critical to me) over device1
|
|
247
|
+
expect(result?.deviceId).toBe(device2);
|
|
248
|
+
});
|
|
249
|
+
it("should prefer connections with higher latency and error rate", () => {
|
|
250
|
+
const device1 = (0, utils_1.newid)();
|
|
251
|
+
const device2 = (0, utils_1.newid)();
|
|
252
|
+
const device3 = (0, utils_1.newid)();
|
|
253
|
+
const connections = [
|
|
254
|
+
{ ...createConnection(device1, 10, 0.001), groups: [] },
|
|
255
|
+
{ ...createConnection(device2, 50, 0.01), groups: [] },
|
|
256
|
+
{ ...createConnection(device3, 100, 0.1), groups: [] },
|
|
257
|
+
];
|
|
258
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
259
|
+
connections,
|
|
260
|
+
preferredDeviceIds: [],
|
|
261
|
+
preferredByDeviceIds: [],
|
|
262
|
+
});
|
|
263
|
+
// Should prefer device3 (worst connection quality)
|
|
264
|
+
expect(result?.deviceId).toBe(device3);
|
|
265
|
+
});
|
|
266
|
+
it("should prefer connections with fewer groups", () => {
|
|
267
|
+
const device1 = (0, utils_1.newid)();
|
|
268
|
+
const device2 = (0, utils_1.newid)();
|
|
269
|
+
const device3 = (0, utils_1.newid)();
|
|
270
|
+
const connections = [
|
|
271
|
+
{ ...createConnection(device1), groups: ["group1", "group2", "group3"] },
|
|
272
|
+
{ ...createConnection(device2), groups: ["group1", "group2"] },
|
|
273
|
+
{ ...createConnection(device3), groups: ["group1"] },
|
|
274
|
+
];
|
|
275
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
276
|
+
connections,
|
|
277
|
+
preferredDeviceIds: [],
|
|
278
|
+
preferredByDeviceIds: [],
|
|
279
|
+
});
|
|
280
|
+
// Should prefer device3 (fewest groups)
|
|
281
|
+
expect(result?.deviceId).toBe(device3);
|
|
282
|
+
});
|
|
283
|
+
it("should consider all connections if all are critical", () => {
|
|
284
|
+
const device1 = (0, utils_1.newid)();
|
|
285
|
+
const device2 = (0, utils_1.newid)();
|
|
286
|
+
const connections = [
|
|
287
|
+
{ ...createConnection(device1, 10, 0.001), groups: ["group1"] },
|
|
288
|
+
{ ...createConnection(device2, 100, 0.1), groups: [] },
|
|
289
|
+
];
|
|
290
|
+
// Both are critical
|
|
291
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
292
|
+
connections,
|
|
293
|
+
preferredDeviceIds: [device1, device2],
|
|
294
|
+
preferredByDeviceIds: [device1, device2],
|
|
295
|
+
});
|
|
296
|
+
// Should still return one (device2 has worse quality and fewer groups)
|
|
297
|
+
expect(result?.deviceId).toBe(device2);
|
|
298
|
+
});
|
|
299
|
+
it("should randomize order before sorting to avoid bias", () => {
|
|
300
|
+
const device1 = (0, utils_1.newid)();
|
|
301
|
+
const device2 = (0, utils_1.newid)();
|
|
302
|
+
const device3 = (0, utils_1.newid)();
|
|
303
|
+
const connections = [
|
|
304
|
+
{ ...createConnection(device1), groups: [] },
|
|
305
|
+
{ ...createConnection(device2), groups: [] },
|
|
306
|
+
{ ...createConnection(device3), groups: [] },
|
|
307
|
+
];
|
|
308
|
+
// All have same characteristics, so result should be one of them
|
|
309
|
+
const result = (0, device_election_1.getLeastPreferredConnection)({
|
|
310
|
+
connections,
|
|
311
|
+
preferredDeviceIds: [],
|
|
312
|
+
preferredByDeviceIds: [],
|
|
313
|
+
});
|
|
314
|
+
expect(result).toBeDefined();
|
|
315
|
+
expect([device1, device2, device3]).toContain(result?.deviceId);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
describe("Network simulations", () => {
|
|
319
|
+
it.skip('should work as connections grow', () => {
|
|
320
|
+
const MAX_CONNECTIONS = 100;
|
|
321
|
+
const groupId = (0, utils_1.newid)();
|
|
322
|
+
const deviceInfos = [];
|
|
323
|
+
function addDevice() {
|
|
324
|
+
const newDevice = {
|
|
325
|
+
deviceId: (0, utils_1.newid)(),
|
|
326
|
+
connections: [],
|
|
327
|
+
preferredDeviceIds: [],
|
|
328
|
+
preferredByDeviceIds: [],
|
|
329
|
+
};
|
|
330
|
+
deviceInfos.push(newDevice);
|
|
331
|
+
}
|
|
332
|
+
function removeDevice(d) {
|
|
333
|
+
deviceInfos.splice(deviceInfos.indexOf(d), 1);
|
|
334
|
+
d.connections.forEach(c => {
|
|
335
|
+
const d2 = deviceInfos.find(d2 => d2.deviceId === c.deviceId);
|
|
336
|
+
if (d2) {
|
|
337
|
+
disconnectDevices(d, d2);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function connectDevices(d1, d2) {
|
|
342
|
+
// same device
|
|
343
|
+
if (d1.deviceId === d2.deviceId) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// already connected
|
|
347
|
+
if (d1.connections.some(c => c.deviceId === d2.deviceId)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
d1.connections.push({
|
|
351
|
+
deviceId: d2.deviceId,
|
|
352
|
+
errorRate: 0,
|
|
353
|
+
latencyMs: 30,
|
|
354
|
+
timestampLastApplied: 0,
|
|
355
|
+
});
|
|
356
|
+
d2.connections.push({
|
|
357
|
+
deviceId: d1.deviceId,
|
|
358
|
+
errorRate: 0,
|
|
359
|
+
latencyMs: 30,
|
|
360
|
+
timestampLastApplied: 0,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function disconnectDevices(d1, d2) {
|
|
364
|
+
d1.connections = d1.connections.filter(c => c.deviceId !== d2.deviceId);
|
|
365
|
+
d2.connections = d2.connections.filter(c => c.deviceId !== d1.deviceId);
|
|
366
|
+
d1.preferredByDeviceIds = d1.preferredByDeviceIds.filter(id => id !== d2.deviceId);
|
|
367
|
+
d2.preferredByDeviceIds = d2.preferredByDeviceIds.filter(id => id !== d1.deviceId);
|
|
368
|
+
d1.preferredDeviceIds = d1.preferredDeviceIds.filter(id => id !== d2.deviceId);
|
|
369
|
+
d2.preferredDeviceIds = d2.preferredDeviceIds.filter(id => id !== d1.deviceId);
|
|
370
|
+
}
|
|
371
|
+
// pre-populate with devices
|
|
372
|
+
Array(25).fill(undefined).forEach(() => addDevice());
|
|
373
|
+
const totalTicks = 200;
|
|
374
|
+
for (let tick = 0; tick < totalTicks; tick++) {
|
|
375
|
+
addDevice();
|
|
376
|
+
for (const device of [...deviceInfos]) {
|
|
377
|
+
// // randomly remove
|
|
378
|
+
// if (Math.random() < 0.01) {
|
|
379
|
+
// removeDevice(device);
|
|
380
|
+
// continue;
|
|
381
|
+
// }
|
|
382
|
+
// // randomly disconnect
|
|
383
|
+
// if (Math.random() < 0.1) {
|
|
384
|
+
// const disconnect =
|
|
385
|
+
// disconnectDevices()
|
|
386
|
+
// }
|
|
387
|
+
// simulate a device wanting to maintain a "well connected" network
|
|
388
|
+
while (device.connections.length < 25) {
|
|
389
|
+
const otherPreferredIds = device.connections.map(c => deviceInfos.find(d => d.deviceId === c.deviceId)?.preferredDeviceIds ?? []).flat();
|
|
390
|
+
const newDeviceId = otherPreferredIds.find(deviceId => !device.connections.some(c => c.deviceId == deviceId) && deviceId !== device.deviceId);
|
|
391
|
+
const newDevice = deviceInfos.find(d => d.deviceId === newDeviceId);
|
|
392
|
+
if (newDevice) {
|
|
393
|
+
connectDevices(device, newDevice);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const unconnectedDevices = deviceInfos.filter(d => !device.connections.some(c => c.deviceId === d.deviceId));
|
|
397
|
+
connectDevices(device, (0, lodash_1.shuffle)(unconnectedDevices)[0]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// do elections
|
|
401
|
+
const networkInfo = device.connections.map(c => {
|
|
402
|
+
const remoteDevice = deviceInfos.find(d => d.deviceId === c.deviceId);
|
|
403
|
+
return {
|
|
404
|
+
deviceId: remoteDevice.deviceId,
|
|
405
|
+
connections: [...remoteDevice?.connections],
|
|
406
|
+
connectionSlotsAvailable: MAX_CONNECTIONS - remoteDevice.connections.length,
|
|
407
|
+
cpuPercent: 0.1,
|
|
408
|
+
memPercent: 0.2,
|
|
409
|
+
preferredDeviceIds: [...remoteDevice.preferredDeviceIds],
|
|
410
|
+
timestampLastApplied: 0,
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
const result = (0, device_election_1.electDevices)({
|
|
414
|
+
deviceId: device.deviceId,
|
|
415
|
+
myConnections: [...device.connections],
|
|
416
|
+
allNetworkInfo: networkInfo
|
|
417
|
+
});
|
|
418
|
+
device.preferredDeviceIds = result.preferredDeviceIds;
|
|
419
|
+
device.preferredByDeviceIds = result.preferredByDeviceIds;
|
|
420
|
+
// now that we've done elections, remove connections if we have too many (note this is slightly unrealistic as this usually happens before elections in the real code)
|
|
421
|
+
while (device.connections.length >= MAX_CONNECTIONS) {
|
|
422
|
+
const removeConn = (0, device_election_1.getLeastPreferredConnection)({
|
|
423
|
+
connections: device.connections.map(c => ({ ...c, groups: [groupId] })),
|
|
424
|
+
preferredDeviceIds: [...device.preferredDeviceIds],
|
|
425
|
+
preferredByDeviceIds: [...device.preferredByDeviceIds],
|
|
426
|
+
});
|
|
427
|
+
const disconnectDevice = deviceInfos.find(d => d.deviceId === removeConn?.deviceId);
|
|
428
|
+
disconnectDevices(device, disconnectDevice);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const output = (0, lodash_1.sortBy)(deviceInfos, d => -d.preferredByDeviceIds.length).map(d => {
|
|
433
|
+
return {
|
|
434
|
+
preferredBy: d.preferredByDeviceIds.length,
|
|
435
|
+
preferred: d.preferredDeviceIds.length,
|
|
436
|
+
connCnt: d.connections.length,
|
|
437
|
+
passiveCnt: d.connections.length - d.preferredDeviceIds.length,
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
// TODO - detect if every device is able to reach every other device or not
|
|
441
|
+
console.table(output);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
package/dist/device/device.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export declare class Device implements IDeviceInfo {
|
|
|
13
13
|
boxDataForDevice(data: any, device: IDeviceInfo): IDataBox;
|
|
14
14
|
openBoxWithSecretKey(data: IDataBox): any;
|
|
15
15
|
signAndBoxDataForDevice<T>(data: T, device: IDeviceInfo): IDataBox;
|
|
16
|
+
signAndBoxDataForKey<T>(data: T, toPublicBoxKey: string): IDataBox;
|
|
16
17
|
unwrapResponse<T>(response: T | ISignedObject<T> | IDataBox): T;
|
|
17
|
-
openBoxedAndSignedData<T>(
|
|
18
|
+
openBoxedAndSignedData<T>(data: IDataBox): T;
|
|
18
19
|
getHandshake(connectionId: string, serverAddress: string): ISignedObject<IDeviceHandshake>;
|
|
19
20
|
handshakeResponse(remoteHandshake: ISignedObject<IDeviceHandshake>, connectionId: string, thisServerAddress: string): IDeviceHandshake;
|
|
20
21
|
private deviceInfoSigned;
|
package/dist/device/device.js
CHANGED
|
@@ -37,6 +37,10 @@ class Device {
|
|
|
37
37
|
const signedData = this.signObjectWithSecretKey(data);
|
|
38
38
|
return this.boxDataForDevice(signedData, device);
|
|
39
39
|
}
|
|
40
|
+
signAndBoxDataForKey(data, toPublicBoxKey) {
|
|
41
|
+
const signedData = this.signObjectWithSecretKey(data);
|
|
42
|
+
return this.boxDataWithKeys(signedData, toPublicBoxKey);
|
|
43
|
+
}
|
|
40
44
|
unwrapResponse(response) {
|
|
41
45
|
if (!response) {
|
|
42
46
|
return response;
|
|
@@ -51,9 +55,9 @@ class Device {
|
|
|
51
55
|
}
|
|
52
56
|
return response;
|
|
53
57
|
}
|
|
54
|
-
openBoxedAndSignedData(
|
|
55
|
-
const
|
|
56
|
-
return (0, keys_1.openSignedObject)(
|
|
58
|
+
openBoxedAndSignedData(data) {
|
|
59
|
+
const signedData = this.openBoxWithSecretKey(data);
|
|
60
|
+
return (0, keys_1.openSignedObject)(signedData);
|
|
57
61
|
}
|
|
58
62
|
getHandshake(connectionId, serverAddress) {
|
|
59
63
|
try {
|
|
@@ -70,7 +74,7 @@ class Device {
|
|
|
70
74
|
return localDeviceInfoSigned;
|
|
71
75
|
}
|
|
72
76
|
catch (e) {
|
|
73
|
-
throw new Error(`Failed to handshake: ${e.message}
|
|
77
|
+
throw new Error(`Failed to handshake: ${e.message}`, { cause: e });
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
handshakeResponse(remoteHandshake, connectionId, thisServerAddress) {
|
|
@@ -91,7 +95,7 @@ class Device {
|
|
|
91
95
|
return handshakeResponse;
|
|
92
96
|
}
|
|
93
97
|
catch (e) {
|
|
94
|
-
throw new Error(`Failed to handshake: ${e.message}
|
|
98
|
+
throw new Error(`Failed to handshake: ${e.message}`, { cause: e });
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
deviceInfoSigned;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/keys.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare function openMessageWithPublicKey(msg: string, publicKey: string)
|
|
|
32
32
|
export declare function signObjectWithSecretKey<T>(obj: T, secretKey: string): ISignedObject<T>;
|
|
33
33
|
export declare function openSignedObject<T>(signedObj: ISignedObject<T>): T;
|
|
34
34
|
export declare function boxDataWithKeys(data: any, toPublicBoxKey: string, mySecretKey: string): IDataBox;
|
|
35
|
+
export declare function isBoxedData(data: any): boolean;
|
|
35
36
|
export declare function openBoxWithSecretKey(box: IDataBox, mySecretKey: string): any;
|
|
36
37
|
export declare function encryptData<T>(data: T, secretKey: string): string;
|
|
37
38
|
export declare function decryptData<T>(encryptedData: string, secretKey: string): (T | null);
|
package/dist/keys.js
CHANGED
|
@@ -15,6 +15,7 @@ exports.openMessageWithPublicKey = openMessageWithPublicKey;
|
|
|
15
15
|
exports.signObjectWithSecretKey = signObjectWithSecretKey;
|
|
16
16
|
exports.openSignedObject = openSignedObject;
|
|
17
17
|
exports.boxDataWithKeys = boxDataWithKeys;
|
|
18
|
+
exports.isBoxedData = isBoxedData;
|
|
18
19
|
exports.openBoxWithSecretKey = openBoxWithSecretKey;
|
|
19
20
|
exports.encryptData = encryptData;
|
|
20
21
|
exports.decryptData = decryptData;
|
|
@@ -27,6 +28,7 @@ const buffer_1 = require("buffer");
|
|
|
27
28
|
const nacl = require("tweetnacl");
|
|
28
29
|
const utils = require("tweetnacl-util");
|
|
29
30
|
const tweetnacl_util_1 = require("tweetnacl-util");
|
|
31
|
+
const ed2curve = require("ed2curve");
|
|
30
32
|
const tx_encoding_1 = require("./device/tx-encoding");
|
|
31
33
|
const serial_json_1 = require("./serial-json");
|
|
32
34
|
globalThis.Buffer = buffer_1.Buffer; // shim for browsers/RN
|
|
@@ -71,23 +73,23 @@ function newToken(size = 32) {
|
|
|
71
73
|
return encodeBase64(nacl.randomBytes(size));
|
|
72
74
|
}
|
|
73
75
|
function newKeys() {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
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
|
|
78
79
|
return {
|
|
79
|
-
secretKey: encodeBase64(
|
|
80
|
-
publicKey: encodeBase64(
|
|
81
|
-
publicBoxKey: encodeBase64(
|
|
80
|
+
secretKey: encodeBase64(sign.secretKey),
|
|
81
|
+
publicKey: encodeBase64(sign.publicKey),
|
|
82
|
+
publicBoxKey: encodeBase64(box.publicKey),
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
function hydrateKeys(secretKey) {
|
|
85
|
-
|
|
86
|
-
const
|
|
86
|
+
const sk64 = decodeBase64(secretKey); // Ed25519 64-byte secretKey
|
|
87
|
+
const curveSecret = ed2curve.convertSecretKey(sk64);
|
|
88
|
+
const box = nacl.box.keyPair.fromSecretKey(curveSecret);
|
|
87
89
|
return {
|
|
88
|
-
secretKey: encodeBase64(
|
|
89
|
-
publicKey: encodeBase64(
|
|
90
|
-
publicBoxKey: encodeBase64(
|
|
90
|
+
secretKey: encodeBase64(sk64),
|
|
91
|
+
publicKey: encodeBase64(sk64.slice(32)), // last 32 bytes = Ed25519 pk
|
|
92
|
+
publicBoxKey: encodeBase64(box.publicKey),
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
function signMessageWithSecretKey(msg, secretKey) {
|
|
@@ -144,7 +146,8 @@ function openSignedObject(signedObj) {
|
|
|
144
146
|
}
|
|
145
147
|
function boxDataWithKeys(data, toPublicBoxKey, mySecretKey) {
|
|
146
148
|
let _secretKey = decodeBase64(mySecretKey);
|
|
147
|
-
const
|
|
149
|
+
const curveSecret = ed2curve.convertSecretKey(_secretKey);
|
|
150
|
+
const boxKeyPair = nacl.box.keyPair.fromSecretKey(curveSecret);
|
|
148
151
|
const _toPublicBoxKey = decodeBase64(toPublicBoxKey);
|
|
149
152
|
const nonce = nacl.randomBytes(24);
|
|
150
153
|
const dataByteArray = (0, tx_encoding_1.txEncode)(data);
|
|
@@ -155,9 +158,13 @@ function boxDataWithKeys(data, toPublicBoxKey, mySecretKey) {
|
|
|
155
158
|
fromPublicKey: encodeBase64(boxKeyPair.publicKey),
|
|
156
159
|
};
|
|
157
160
|
}
|
|
161
|
+
function isBoxedData(data) {
|
|
162
|
+
return typeof data === 'object' && typeof data.contents === 'string' && typeof data.nonce === 'string' && typeof data.fromPublicKey === 'string';
|
|
163
|
+
}
|
|
158
164
|
function openBoxWithSecretKey(box, mySecretKey) {
|
|
159
165
|
let _secretKey = decodeBase64(mySecretKey);
|
|
160
|
-
const
|
|
166
|
+
const curveSecret = ed2curve.convertSecretKey(_secretKey);
|
|
167
|
+
const boxKeyPair = nacl.box.keyPair.fromSecretKey(curveSecret);
|
|
161
168
|
const boxedData = decodeBase64(box.contents);
|
|
162
169
|
const nonce = decodeBase64(box.nonce);
|
|
163
170
|
const _fromPublicBoxKey = decodeBase64(box.fromPublicKey);
|
|
@@ -37,14 +37,14 @@ export interface IDeviceConnection {
|
|
|
37
37
|
closed?: boolean;
|
|
38
38
|
}
|
|
39
39
|
export declare const PeerDeviceConsts: Readonly<{
|
|
40
|
-
MAX_CONNECTIONS: 30;
|
|
41
|
-
RESYNC_INTERVAL:
|
|
42
|
-
NOTIFY_CHANGE_DELAY: 100;
|
|
43
|
-
CHANGES_PAGE_SIZE: 100;
|
|
44
|
-
RETRY_COUNT: 2;
|
|
45
|
-
TIMEOUT_MIN: 3000;
|
|
46
|
-
TIMEOUT_MAX: 30000;
|
|
47
|
-
NETWORK_INFO_CACHE_TIME: 1000;
|
|
40
|
+
readonly MAX_CONNECTIONS: 30;
|
|
41
|
+
readonly RESYNC_INTERVAL: 300000;
|
|
42
|
+
readonly NOTIFY_CHANGE_DELAY: 100;
|
|
43
|
+
readonly CHANGES_PAGE_SIZE: 100;
|
|
44
|
+
readonly RETRY_COUNT: 2;
|
|
45
|
+
readonly TIMEOUT_MIN: 3000;
|
|
46
|
+
readonly TIMEOUT_MAX: 30000;
|
|
47
|
+
readonly NETWORK_INFO_CACHE_TIME: 1000;
|
|
48
48
|
}>;
|
|
49
49
|
export type IFileChunkInfo = {
|
|
50
50
|
hasChunk: false;
|
|
@@ -4,7 +4,7 @@ exports.PeerDeviceConsts = void 0;
|
|
|
4
4
|
exports.PeerDeviceConsts = Object.freeze({
|
|
5
5
|
// TODO set this based on device type (desktops and servers should be able to handle 100 connections)
|
|
6
6
|
MAX_CONNECTIONS: 30,
|
|
7
|
-
RESYNC_INTERVAL:
|
|
7
|
+
RESYNC_INTERVAL: 300_000, // play with this to find the right balance
|
|
8
8
|
NOTIFY_CHANGE_DELAY: 100,
|
|
9
9
|
CHANGES_PAGE_SIZE: 100,
|
|
10
10
|
RETRY_COUNT: 2,
|
|
@@ -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 {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const connection_code_1 = require("./connection-code");
|
|
4
|
+
describe('connection-code', () => {
|
|
5
|
+
describe('generateConnectionCode', () => {
|
|
6
|
+
it('should generate a code with 12 characters', () => {
|
|
7
|
+
const result = (0, connection_code_1.generateConnectionCode)();
|
|
8
|
+
expect(result.code).toHaveLength(12);
|
|
9
|
+
});
|
|
10
|
+
it('should have alias of 4 characters', () => {
|
|
11
|
+
const result = (0, connection_code_1.generateConnectionCode)();
|
|
12
|
+
expect(result.alias).toHaveLength(4);
|
|
13
|
+
});
|
|
14
|
+
it('should have secret of 8 characters', () => {
|
|
15
|
+
const result = (0, connection_code_1.generateConnectionCode)();
|
|
16
|
+
expect(result.secret).toHaveLength(8);
|
|
17
|
+
});
|
|
18
|
+
it('should have code = alias + secret', () => {
|
|
19
|
+
const result = (0, connection_code_1.generateConnectionCode)();
|
|
20
|
+
expect(result.code).toBe(result.alias + result.secret);
|
|
21
|
+
});
|
|
22
|
+
it('should only contain valid Crockford Base32 characters', () => {
|
|
23
|
+
const validChars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
24
|
+
for (let i = 0; i < 10; i++) {
|
|
25
|
+
const result = (0, connection_code_1.generateConnectionCode)();
|
|
26
|
+
for (const char of result.code) {
|
|
27
|
+
expect(validChars).toContain(char);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('should generate different codes each time', () => {
|
|
32
|
+
const codes = new Set();
|
|
33
|
+
for (let i = 0; i < 100; i++) {
|
|
34
|
+
codes.add((0, connection_code_1.generateConnectionCode)().code);
|
|
35
|
+
}
|
|
36
|
+
expect(codes.size).toBe(100);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('formatConnectionCode', () => {
|
|
40
|
+
it('should format a 12-character code as XXXX-YYYY-ZZZZ', () => {
|
|
41
|
+
expect((0, connection_code_1.formatConnectionCode)('ABCD1234EFGH')).toBe('ABCD-1234-EFGH');
|
|
42
|
+
});
|
|
43
|
+
it('should handle lowercase input', () => {
|
|
44
|
+
expect((0, connection_code_1.formatConnectionCode)('abcd1234efgh')).toBe('ABCD-1234-EFGH');
|
|
45
|
+
});
|
|
46
|
+
it('should handle input with existing dashes', () => {
|
|
47
|
+
expect((0, connection_code_1.formatConnectionCode)('ABCD-1234-EFGH')).toBe('ABCD-1234-EFGH');
|
|
48
|
+
});
|
|
49
|
+
it('should handle input with spaces', () => {
|
|
50
|
+
expect((0, connection_code_1.formatConnectionCode)('ABCD 1234 EFGH')).toBe('ABCD-1234-EFGH');
|
|
51
|
+
});
|
|
52
|
+
it('should throw for codes that are too short', () => {
|
|
53
|
+
expect(() => (0, connection_code_1.formatConnectionCode)('ABCD1234')).toThrow('must be 12 characters');
|
|
54
|
+
});
|
|
55
|
+
it('should throw for codes that are too long', () => {
|
|
56
|
+
expect(() => (0, connection_code_1.formatConnectionCode)('ABCD1234EFGH5678')).toThrow('must be 12 characters');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('parseConnectionCode', () => {
|
|
60
|
+
it('should parse a plain 12-character code', () => {
|
|
61
|
+
const result = (0, connection_code_1.parseConnectionCode)('ABCD12345678');
|
|
62
|
+
expect(result.alias).toBe('ABCD');
|
|
63
|
+
expect(result.secret).toBe('12345678');
|
|
64
|
+
});
|
|
65
|
+
it('should parse a formatted code with dashes', () => {
|
|
66
|
+
const result = (0, connection_code_1.parseConnectionCode)('ABCD-1234-5678');
|
|
67
|
+
expect(result.alias).toBe('ABCD');
|
|
68
|
+
expect(result.secret).toBe('12345678');
|
|
69
|
+
});
|
|
70
|
+
it('should handle lowercase input', () => {
|
|
71
|
+
const result = (0, connection_code_1.parseConnectionCode)('abcd-1234-5678');
|
|
72
|
+
expect(result.alias).toBe('ABCD');
|
|
73
|
+
expect(result.secret).toBe('12345678');
|
|
74
|
+
});
|
|
75
|
+
it('should handle mixed case and spaces', () => {
|
|
76
|
+
const result = (0, connection_code_1.parseConnectionCode)('AbCd 1234 5678');
|
|
77
|
+
expect(result.alias).toBe('ABCD');
|
|
78
|
+
expect(result.secret).toBe('12345678');
|
|
79
|
+
});
|
|
80
|
+
it('should throw for invalid length', () => {
|
|
81
|
+
expect(() => (0, connection_code_1.parseConnectionCode)('ABCD1234')).toThrow('must be 12 characters');
|
|
82
|
+
});
|
|
83
|
+
it('should be the inverse of formatConnectionCode', () => {
|
|
84
|
+
const original = (0, connection_code_1.generateConnectionCode)();
|
|
85
|
+
const formatted = (0, connection_code_1.formatConnectionCode)(original.code);
|
|
86
|
+
const parsed = (0, connection_code_1.parseConnectionCode)(formatted);
|
|
87
|
+
expect(parsed.alias).toBe(original.alias);
|
|
88
|
+
expect(parsed.secret).toBe(original.secret);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('encryptWithSecret and decryptWithSecret', () => {
|
|
92
|
+
it('should round-trip a simple string', () => {
|
|
93
|
+
const secret = 'ABCD1234';
|
|
94
|
+
const data = 'Hello, World!';
|
|
95
|
+
const encrypted = (0, connection_code_1.encryptWithSecret)(data, secret);
|
|
96
|
+
const decrypted = (0, connection_code_1.decryptWithSecret)(encrypted, secret);
|
|
97
|
+
expect(decrypted).toBe(data);
|
|
98
|
+
});
|
|
99
|
+
it('should round-trip an object', () => {
|
|
100
|
+
const secret = 'ZYXW9876';
|
|
101
|
+
const data = { userId: 'user123', name: 'Test User', count: 42 };
|
|
102
|
+
const encrypted = (0, connection_code_1.encryptWithSecret)(data, secret);
|
|
103
|
+
const decrypted = (0, connection_code_1.decryptWithSecret)(encrypted, secret);
|
|
104
|
+
expect(decrypted).toEqual(data);
|
|
105
|
+
});
|
|
106
|
+
it('should round-trip user connect info', () => {
|
|
107
|
+
const secret = 'TESTCODE';
|
|
108
|
+
const userInfo = {
|
|
109
|
+
userId: 'user123',
|
|
110
|
+
publicKey: 'pk_abc123',
|
|
111
|
+
publicBoxKey: 'pbk_xyz789',
|
|
112
|
+
deviceId: 'device456',
|
|
113
|
+
};
|
|
114
|
+
const encrypted = (0, connection_code_1.encryptWithSecret)(userInfo, secret);
|
|
115
|
+
const decrypted = (0, connection_code_1.decryptWithSecret)(encrypted, secret);
|
|
116
|
+
expect(decrypted).toEqual(userInfo);
|
|
117
|
+
});
|
|
118
|
+
it('should produce different ciphertext for same data (due to random nonce)', () => {
|
|
119
|
+
const secret = 'ABCD1234';
|
|
120
|
+
const data = 'Same data';
|
|
121
|
+
const encrypted1 = (0, connection_code_1.encryptWithSecret)(data, secret);
|
|
122
|
+
const encrypted2 = (0, connection_code_1.encryptWithSecret)(data, secret);
|
|
123
|
+
expect(encrypted1).not.toBe(encrypted2);
|
|
124
|
+
});
|
|
125
|
+
it('should fail decryption with wrong secret', () => {
|
|
126
|
+
const data = 'Secret message';
|
|
127
|
+
const encrypted = (0, connection_code_1.encryptWithSecret)(data, 'CORRECT1');
|
|
128
|
+
expect(() => (0, connection_code_1.decryptWithSecret)(encrypted, 'WRONGKEY')).toThrow('Decryption failed');
|
|
129
|
+
});
|
|
130
|
+
it('should fail decryption with corrupted data', () => {
|
|
131
|
+
const secret = 'ABCD1234';
|
|
132
|
+
const data = 'Hello';
|
|
133
|
+
const encrypted = (0, connection_code_1.encryptWithSecret)(data, secret);
|
|
134
|
+
const corrupted = encrypted.slice(0, -5) + 'XXXXX';
|
|
135
|
+
expect(() => (0, connection_code_1.decryptWithSecret)(corrupted, secret)).toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('generateConfirmationHash', () => {
|
|
139
|
+
const userA = {
|
|
140
|
+
userId: 'userA123',
|
|
141
|
+
publicKey: 'pkA',
|
|
142
|
+
publicBoxKey: 'pbkA',
|
|
143
|
+
deviceId: 'deviceA',
|
|
144
|
+
};
|
|
145
|
+
const userB = {
|
|
146
|
+
userId: 'userB456',
|
|
147
|
+
publicKey: 'pkB',
|
|
148
|
+
publicBoxKey: 'pbkB',
|
|
149
|
+
deviceId: 'deviceB',
|
|
150
|
+
};
|
|
151
|
+
it('should return a 4-character hash', () => {
|
|
152
|
+
const hash = (0, connection_code_1.generateConfirmationHash)(userA, userB);
|
|
153
|
+
expect(hash).toHaveLength(4);
|
|
154
|
+
});
|
|
155
|
+
it('should only contain valid Crockford Base32 characters', () => {
|
|
156
|
+
const validChars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
157
|
+
const hash = (0, connection_code_1.generateConfirmationHash)(userA, userB);
|
|
158
|
+
for (const char of hash) {
|
|
159
|
+
expect(validChars).toContain(char);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
it('should produce same hash regardless of argument order', () => {
|
|
163
|
+
const hashAB = (0, connection_code_1.generateConfirmationHash)(userA, userB);
|
|
164
|
+
const hashBA = (0, connection_code_1.generateConfirmationHash)(userB, userA);
|
|
165
|
+
expect(hashAB).toBe(hashBA);
|
|
166
|
+
});
|
|
167
|
+
it('should produce different hash for different users', () => {
|
|
168
|
+
const userC = {
|
|
169
|
+
userId: 'userC789',
|
|
170
|
+
publicKey: 'pkC',
|
|
171
|
+
publicBoxKey: 'pbkC',
|
|
172
|
+
deviceId: 'deviceC',
|
|
173
|
+
};
|
|
174
|
+
const hashAB = (0, connection_code_1.generateConfirmationHash)(userA, userB);
|
|
175
|
+
const hashAC = (0, connection_code_1.generateConfirmationHash)(userA, userC);
|
|
176
|
+
expect(hashAB).not.toBe(hashAC);
|
|
177
|
+
});
|
|
178
|
+
it('should be deterministic', () => {
|
|
179
|
+
const hash1 = (0, connection_code_1.generateConfirmationHash)(userA, userB);
|
|
180
|
+
const hash2 = (0, connection_code_1.generateConfirmationHash)(userA, userB);
|
|
181
|
+
expect(hash1).toBe(hash2);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('isValidConnectionCode', () => {
|
|
185
|
+
it('should return true for valid 12-character codes', () => {
|
|
186
|
+
expect((0, connection_code_1.isValidConnectionCode)('ABCD1234EFGH')).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
it('should return true for codes with dashes', () => {
|
|
189
|
+
expect((0, connection_code_1.isValidConnectionCode)('ABCD-1234-EFGH')).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it('should return true for lowercase codes', () => {
|
|
192
|
+
expect((0, connection_code_1.isValidConnectionCode)('abcd1234efgh')).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
it('should return true for generated codes', () => {
|
|
195
|
+
const code = (0, connection_code_1.generateConnectionCode)();
|
|
196
|
+
expect((0, connection_code_1.isValidConnectionCode)(code.code)).toBe(true);
|
|
197
|
+
expect((0, connection_code_1.isValidConnectionCode)((0, connection_code_1.formatConnectionCode)(code.code))).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
it('should return false for codes that are too short', () => {
|
|
200
|
+
expect((0, connection_code_1.isValidConnectionCode)('ABCD1234')).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
it('should return false for codes that are too long', () => {
|
|
203
|
+
expect((0, connection_code_1.isValidConnectionCode)('ABCD1234EFGH5678')).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
it('should return false for codes with invalid characters', () => {
|
|
206
|
+
// I, L, O, U are not in Crockford Base32
|
|
207
|
+
expect((0, connection_code_1.isValidConnectionCode)('ABCDILOU1234')).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
it('should return false for empty string', () => {
|
|
210
|
+
expect((0, connection_code_1.isValidConnectionCode)('')).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./connection-code"), exports);
|
|
18
|
+
__exportStar(require("./user-connect.types"), exports);
|
|
19
|
+
__exportStar(require("./user-connect.pvars"), exports);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const userConnectStatus: import("../data/persistent-vars").PersistentVar<any>;
|
|
2
|
+
export declare const userConnectCodeOffer: import("../data/persistent-vars").PersistentVar<string>;
|
|
3
|
+
export declare const userConnectCodeAnswer: import("../data/persistent-vars").PersistentVar<string>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.userConnectCodeAnswer = exports.userConnectCodeOffer = exports.userConnectStatus = void 0;
|
|
4
|
+
const persistent_vars_1 = require("../data/persistent-vars");
|
|
5
|
+
exports.userConnectStatus = (0, persistent_vars_1.deviceVar)('userConnectStatus', { defaultValue: undefined });
|
|
6
|
+
exports.userConnectCodeOffer = (0, persistent_vars_1.deviceVar)('userConnectCodeOffer', { defaultValue: '' });
|
|
7
|
+
exports.userConnectCodeOffer.loadingPromise.then(() => {
|
|
8
|
+
let codeTimeout = undefined;
|
|
9
|
+
(0, exports.userConnectStatus)('');
|
|
10
|
+
exports.userConnectCodeOffer.subscribe(() => {
|
|
11
|
+
(0, exports.userConnectStatus)('');
|
|
12
|
+
clearTimeout(codeTimeout);
|
|
13
|
+
if ((0, exports.userConnectCodeOffer)()) {
|
|
14
|
+
codeTimeout = setTimeout(() => {
|
|
15
|
+
(0, exports.userConnectCodeOffer)('');
|
|
16
|
+
}, 600_000); // 10 minutes
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
exports.userConnectCodeAnswer = (0, persistent_vars_1.deviceVar)('userConnectCodeAnswer', { defaultValue: '' });
|
|
21
|
+
exports.userConnectCodeAnswer.loadingPromise.then(() => {
|
|
22
|
+
let codeTimeout = undefined;
|
|
23
|
+
(0, exports.userConnectStatus)('');
|
|
24
|
+
exports.userConnectCodeAnswer.subscribe(() => {
|
|
25
|
+
(0, exports.userConnectStatus)('');
|
|
26
|
+
clearTimeout(codeTimeout);
|
|
27
|
+
if ((0, exports.userConnectCodeAnswer)()) {
|
|
28
|
+
codeTimeout = setTimeout(() => {
|
|
29
|
+
(0, exports.userConnectCodeAnswer)('');
|
|
30
|
+
}, 600_000); // 10 minutes
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the user connection flow.
|
|
3
|
+
*
|
|
4
|
+
* This flow allows two users to securely exchange user information
|
|
5
|
+
* using a 12-character code (4-char rendezvous alias + 8-char shared secret).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* User information exchanged during the connection flow.
|
|
9
|
+
*/
|
|
10
|
+
export interface IUserConnectInfo {
|
|
11
|
+
userId: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
publicKey: string;
|
|
14
|
+
publicBoxKey: string;
|
|
15
|
+
deviceId: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Message sent by the responder to initiate the connection.
|
|
19
|
+
* The payload is encrypted with the shared secret from the connection code.
|
|
20
|
+
*/
|
|
21
|
+
export interface IUserConnectRequest {
|
|
22
|
+
type: 'user-connect-request';
|
|
23
|
+
/** User info encrypted with the shared secret */
|
|
24
|
+
encryptedUserInfo: string;
|
|
25
|
+
/** The responder's deviceId so the initiator can respond directly */
|
|
26
|
+
responseDeviceId: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Message sent by the initiator in response to a connection request.
|
|
30
|
+
* The payload is encrypted with the shared secret.
|
|
31
|
+
*/
|
|
32
|
+
export interface IUserConnectResponse {
|
|
33
|
+
type: 'user-connect-response';
|
|
34
|
+
/** User info encrypted with the shared secret */
|
|
35
|
+
encryptedUserInfo: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Message broadcast to register a short-lived device alias.
|
|
39
|
+
* Other devices store this mapping for routing purposes.
|
|
40
|
+
*/
|
|
41
|
+
export interface IDeviceAliasMessage {
|
|
42
|
+
type: 'device-alias';
|
|
43
|
+
/** The short alias (4 chars Crockford Base32) */
|
|
44
|
+
alias: string;
|
|
45
|
+
/** The actual deviceId this alias maps to */
|
|
46
|
+
deviceId: string;
|
|
47
|
+
/** TTL in milliseconds (typically 10 minutes) */
|
|
48
|
+
ttlMs: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Result of a successful user connection.
|
|
52
|
+
*/
|
|
53
|
+
export interface IUserConnectResult {
|
|
54
|
+
/** The remote user's information */
|
|
55
|
+
remoteUser: IUserConnectInfo;
|
|
56
|
+
/** Confirmation hash (first 4 chars) for visual verification */
|
|
57
|
+
confirmationHash: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Types for the user connection flow.
|
|
4
|
+
*
|
|
5
|
+
* This flow allows two users to securely exchange user information
|
|
6
|
+
* using a 12-character code (4-char rendezvous alias + 8-char shared secret).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/users.query.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DataContext, DataFilter, ICursorIterable, IUser, TrustLevel } from "./index";
|
|
1
|
+
import { DataContext, DataFilter, ICursorIterable, IUser, TrustLevel, UserContext } from "./index";
|
|
2
2
|
export interface IUsersQueryOpts {
|
|
3
|
+
userContext?: UserContext;
|
|
3
4
|
currentDataContext?: DataContext;
|
|
4
5
|
includeCurrentDataContext?: boolean;
|
|
5
6
|
includeUserDataContext?: boolean;
|
package/dist/users.query.js
CHANGED
|
@@ -106,7 +106,7 @@ async function usersCursor(filter, opts = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
async function getUserById(userId, opts = {}) {
|
|
108
108
|
const { currentDataContext, includeCurrentDataContext = true, includeUserDataContext = true, includeOtherDataContexts = true, includeTrustLevel = true } = opts;
|
|
109
|
-
const userContext = await (0, index_1.getUserContext)();
|
|
109
|
+
const userContext = opts.userContext || await (0, index_1.getUserContext)();
|
|
110
110
|
const contextToUse = currentDataContext || userContext.defaultDataContext();
|
|
111
111
|
// Try current data context first
|
|
112
112
|
if (includeCurrentDataContext) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-sdk.git"
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@msgpack/msgpack": "^3.1.2",
|
|
34
34
|
"@noble/hashes": "^1.8.0",
|
|
35
|
+
"ed2curve": "^0.3.0",
|
|
35
36
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
37
|
"fflate": "^0.8.2",
|
|
37
38
|
"lodash": "^4.17.21",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@types/better-sqlite3": "^7.6.12",
|
|
47
|
+
"@types/ed2curve": "^0.2.4",
|
|
46
48
|
"@types/jest": "^29.5.13",
|
|
47
49
|
"@types/lodash": "^4.17.7",
|
|
48
50
|
"@types/moment": "^2.13.0",
|