@cryptforge/key-exchange 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/dist/electron-main.d.mts +41 -0
- package/dist/electron-main.d.ts +41 -0
- package/dist/electron-main.js +806 -0
- package/dist/electron-main.mjs +768 -0
- package/dist/electron-preload.d.mts +3 -0
- package/dist/electron-preload.d.ts +3 -0
- package/dist/electron-preload.js +114 -0
- package/dist/electron-preload.mjs +87 -0
- package/dist/electron-renderer.d.mts +74 -0
- package/dist/electron-renderer.d.ts +74 -0
- package/dist/electron-renderer.js +108 -0
- package/dist/electron-renderer.mjs +80 -0
- package/dist/index.d.mts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +586 -0
- package/dist/index.mjs +572 -0
- package/dist/server.d.mts +46 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +940 -0
- package/dist/server.mjs +902 -0
- package/package.json +93 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/server.ts
|
|
31
|
+
var server_exports = {};
|
|
32
|
+
__export(server_exports, {
|
|
33
|
+
KeyTransportServer: () => KeyTransportServer,
|
|
34
|
+
useKeyExchangeServer: () => useKeyExchangeServer
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(server_exports);
|
|
37
|
+
|
|
38
|
+
// src/server/useKeyExchangeServer.ts
|
|
39
|
+
var import_hyperdht = __toESM(require("hyperdht"));
|
|
40
|
+
var import_b4a2 = __toESM(require("b4a"));
|
|
41
|
+
|
|
42
|
+
// src/server/messages.ts
|
|
43
|
+
var import_b4a = __toESM(require("b4a"));
|
|
44
|
+
|
|
45
|
+
// src/types/messages.ts
|
|
46
|
+
var MESSAGES = {
|
|
47
|
+
enableBroadcast: "admin:broadcast:enable",
|
|
48
|
+
connect: "client:topic:connect",
|
|
49
|
+
onClientConnection: "admin:topic:connected",
|
|
50
|
+
requestKeystore: "client:keystore:request",
|
|
51
|
+
onKeystoreRequestInvalidPIN: "admin:pin:invalid",
|
|
52
|
+
onKeystoreRequestValidPIN: "admin:pin:valid",
|
|
53
|
+
onKeystoreRequest: "admin:request:received",
|
|
54
|
+
approveRequest: "admin:request:approve",
|
|
55
|
+
onClientApproval: "client:request:approved",
|
|
56
|
+
denyRequest: "admin:request:deny",
|
|
57
|
+
onClientDenial: "client:request:denied",
|
|
58
|
+
onCompletion: "server:link:complete",
|
|
59
|
+
disableBroadcast: "admin:broadcast:disable",
|
|
60
|
+
onBroadcastDisable: "client:broadcast:disabled",
|
|
61
|
+
getDeviceInfo: "client:request:deviceInfo",
|
|
62
|
+
getHostname: "client:request:hostname",
|
|
63
|
+
// Presence/Network messages
|
|
64
|
+
connectPresence: "network:connect",
|
|
65
|
+
broadcastClientState: "network:broadcast",
|
|
66
|
+
onClientStateRequest: "network:request",
|
|
67
|
+
onClientStateUpdate: "network:update"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/server/messages.ts
|
|
71
|
+
var syncSetupEventRoutes = {
|
|
72
|
+
onClientConnection: MESSAGES.onClientConnection,
|
|
73
|
+
onKeystoreRequest: MESSAGES.onKeystoreRequest,
|
|
74
|
+
onClientApproval: MESSAGES.onClientApproval,
|
|
75
|
+
onClientDenial: MESSAGES.onClientDenial,
|
|
76
|
+
onCompletion: MESSAGES.onCompletion,
|
|
77
|
+
onBroadcastDisable: MESSAGES.onBroadcastDisable,
|
|
78
|
+
onKeystoreRequestInvalidPIN: MESSAGES.onKeystoreRequestInvalidPIN,
|
|
79
|
+
onKeystoreRequestValidPIN: MESSAGES.onKeystoreRequestValidPIN
|
|
80
|
+
};
|
|
81
|
+
var requestHeaders = [
|
|
82
|
+
{
|
|
83
|
+
code: 1e3,
|
|
84
|
+
name: "Request PIN",
|
|
85
|
+
description: "The Client is asking the Server to show UI of a PIN screen for the user to enter on the Client"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: 2e3,
|
|
89
|
+
name: "Request Keystore",
|
|
90
|
+
description: "The Client is asking the Server to show UI for the user to approve or reject the request"
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
var sendRequest = async (options) => {
|
|
94
|
+
const index = requestHeaders.findIndex(
|
|
95
|
+
(response) => response.name === options.header
|
|
96
|
+
);
|
|
97
|
+
if (index !== -1) {
|
|
98
|
+
const header = requestHeaders[index];
|
|
99
|
+
const body = options.body();
|
|
100
|
+
const request = {
|
|
101
|
+
header,
|
|
102
|
+
body
|
|
103
|
+
};
|
|
104
|
+
const payload = JSON.stringify(request);
|
|
105
|
+
for (const socket of options.to) {
|
|
106
|
+
console.log(
|
|
107
|
+
`hyperswarm: sending ${header.name} to connection ${import_b4a.default.toString(
|
|
108
|
+
socket.remotePublicKey,
|
|
109
|
+
"hex"
|
|
110
|
+
)}`
|
|
111
|
+
);
|
|
112
|
+
socket.write(payload);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log(`Request with name \`${options.header}\` not found`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var responseHeaders = [
|
|
119
|
+
{
|
|
120
|
+
code: 1501,
|
|
121
|
+
name: "Connection Failure: Broadcast disabled",
|
|
122
|
+
type: "Failure"
|
|
123
|
+
},
|
|
124
|
+
{ code: 2200, name: "Authentication Succeeded", type: "Success" },
|
|
125
|
+
{ code: 2400, name: "Authentication Error: Invalid PIN", type: "Error" },
|
|
126
|
+
{ code: 3200, name: "Transmition Succeeded: Approved", type: "Success" },
|
|
127
|
+
{ code: 3201, name: "Transmition Succeeded: Rejected", type: "Success" },
|
|
128
|
+
{ code: 4200, name: "Setup Succeeded", type: "Success" }
|
|
129
|
+
];
|
|
130
|
+
var sendResponse = (options) => {
|
|
131
|
+
const index = responseHeaders.findIndex(
|
|
132
|
+
(response) => response.name === options.header
|
|
133
|
+
);
|
|
134
|
+
if (index !== -1) {
|
|
135
|
+
const header = responseHeaders[index];
|
|
136
|
+
const data = options.body();
|
|
137
|
+
const response = {
|
|
138
|
+
header,
|
|
139
|
+
body: data
|
|
140
|
+
};
|
|
141
|
+
const payload = JSON.stringify(response);
|
|
142
|
+
for (const socket of options.to) {
|
|
143
|
+
console.log(
|
|
144
|
+
`hyperswarm: sending ${header.name} to connection ${import_b4a.default.toString(
|
|
145
|
+
socket.remotePublicKey,
|
|
146
|
+
"hex"
|
|
147
|
+
)}`
|
|
148
|
+
);
|
|
149
|
+
socket.write(payload);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`Response with name \`${options.header}\` not found`);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/server/useKeyExchangeServer.ts
|
|
157
|
+
function useKeyExchangeServer(onEvent) {
|
|
158
|
+
let client;
|
|
159
|
+
let dht;
|
|
160
|
+
const keyPair = import_hyperdht.default.keyPair();
|
|
161
|
+
const getKey = () => {
|
|
162
|
+
return import_b4a2.default.toString(keyPair.publicKey, "hex");
|
|
163
|
+
};
|
|
164
|
+
const generateRandomPIN = () => {
|
|
165
|
+
return Math.floor(1e5 + Math.random() * 9e5);
|
|
166
|
+
};
|
|
167
|
+
const pin = generateRandomPIN();
|
|
168
|
+
const getPIN = () => {
|
|
169
|
+
return `${pin}`;
|
|
170
|
+
};
|
|
171
|
+
let server;
|
|
172
|
+
const createServer = async () => {
|
|
173
|
+
dht = new import_hyperdht.default();
|
|
174
|
+
server = await dht.createServer((socket) => {
|
|
175
|
+
const name = import_b4a2.default.toString(socket.remotePublicKey, "hex");
|
|
176
|
+
console.log(`server: got a connection on \`${name}\``);
|
|
177
|
+
socket.on("data", (data) => {
|
|
178
|
+
parseRequest(data);
|
|
179
|
+
});
|
|
180
|
+
socket.on("error", (error) => {
|
|
181
|
+
onConnectionError(error);
|
|
182
|
+
});
|
|
183
|
+
client = socket;
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
const parseRequest = (data) => {
|
|
187
|
+
console.log(`server: got data: ${data}`);
|
|
188
|
+
const parsedData = JSON.parse(data);
|
|
189
|
+
const payload = parsedData;
|
|
190
|
+
console.log("payload");
|
|
191
|
+
console.log(payload);
|
|
192
|
+
switch (payload.header.name) {
|
|
193
|
+
case "Request PIN":
|
|
194
|
+
onRequestPin(payload.body);
|
|
195
|
+
break;
|
|
196
|
+
case "Request Keystore":
|
|
197
|
+
onRequestKeystore(payload.body);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const onConnectionError = (error) => {
|
|
202
|
+
console.log(`server: Connection error: ${error}`);
|
|
203
|
+
};
|
|
204
|
+
const onRequestPin = (body) => {
|
|
205
|
+
const route = syncSetupEventRoutes["onClientConnection"];
|
|
206
|
+
onEvent({
|
|
207
|
+
route,
|
|
208
|
+
data: {
|
|
209
|
+
pin: getPIN(),
|
|
210
|
+
name: body.deviceName,
|
|
211
|
+
app: body.appName
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
const onRequestKeystore = (body) => {
|
|
216
|
+
const pin2 = body.pin;
|
|
217
|
+
if (pin2 !== getPIN()) {
|
|
218
|
+
sendResponse({
|
|
219
|
+
to: [client],
|
|
220
|
+
header: "Authentication Error: Invalid PIN",
|
|
221
|
+
body: () => {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
} else {
|
|
227
|
+
sendResponse({
|
|
228
|
+
to: [client],
|
|
229
|
+
header: "Authentication Succeeded",
|
|
230
|
+
body: () => {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const route = syncSetupEventRoutes["onKeystoreRequest"];
|
|
236
|
+
onEvent({
|
|
237
|
+
route,
|
|
238
|
+
data: {}
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const onRequestDenied = () => {
|
|
242
|
+
const route = syncSetupEventRoutes["onClientDenial"];
|
|
243
|
+
onEvent({ route, data: {} });
|
|
244
|
+
};
|
|
245
|
+
const onSetupComplete = () => {
|
|
246
|
+
const route = syncSetupEventRoutes["onCompletion"];
|
|
247
|
+
onEvent({ route, data: {} });
|
|
248
|
+
};
|
|
249
|
+
const beginBroadcast = async () => {
|
|
250
|
+
await createServer();
|
|
251
|
+
await server.listen(keyPair);
|
|
252
|
+
await server.refresh();
|
|
253
|
+
console.log(`server: opened connection to ${getKey()}`);
|
|
254
|
+
};
|
|
255
|
+
const endBroadcast = async () => {
|
|
256
|
+
if (server) {
|
|
257
|
+
await server.close();
|
|
258
|
+
console.log(`server: closed connection to ${getKey()}`);
|
|
259
|
+
}
|
|
260
|
+
if (dht) {
|
|
261
|
+
await dht.destroy();
|
|
262
|
+
console.log(`server: destroyed dht`);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const approveRequest = async (identity, appId) => {
|
|
266
|
+
sendResponse({
|
|
267
|
+
to: [client],
|
|
268
|
+
header: "Transmition Succeeded: Approved",
|
|
269
|
+
body: () => {
|
|
270
|
+
return {
|
|
271
|
+
identity,
|
|
272
|
+
appId
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const completeSetupSuccess = async () => {
|
|
278
|
+
console.log("sending compltion message to client");
|
|
279
|
+
sendResponse({
|
|
280
|
+
to: [client],
|
|
281
|
+
header: "Setup Succeeded",
|
|
282
|
+
body: () => {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
onSetupComplete();
|
|
287
|
+
};
|
|
288
|
+
const rejectRequest = async () => {
|
|
289
|
+
sendResponse({
|
|
290
|
+
to: [client],
|
|
291
|
+
header: "Transmition Succeeded: Rejected",
|
|
292
|
+
body: () => {
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
onRequestDenied();
|
|
297
|
+
};
|
|
298
|
+
const broadcastDisabled = async () => {
|
|
299
|
+
if (client) {
|
|
300
|
+
sendResponse({
|
|
301
|
+
to: [client],
|
|
302
|
+
header: "Connection Failure: Broadcast disabled",
|
|
303
|
+
body: () => {
|
|
304
|
+
return {};
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
getKey,
|
|
311
|
+
getPIN,
|
|
312
|
+
beginBroadcast,
|
|
313
|
+
endBroadcast,
|
|
314
|
+
approveRequest,
|
|
315
|
+
completeSetupSuccess,
|
|
316
|
+
rejectRequest,
|
|
317
|
+
broadcastDisabled
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/server/keyTransportServer.ts
|
|
322
|
+
var import_ws = __toESM(require("ws"));
|
|
323
|
+
|
|
324
|
+
// src/useNetworkPresenceServer.ts
|
|
325
|
+
var import_hyperswarm = __toESM(require("hyperswarm"));
|
|
326
|
+
var import_b4a3 = __toESM(require("b4a"));
|
|
327
|
+
function useNetworkPresence(onUpdate, onRequest) {
|
|
328
|
+
const deviceIDs = /* @__PURE__ */ new Map();
|
|
329
|
+
const swarm = new import_hyperswarm.default();
|
|
330
|
+
swarm.on("connection", (connection) => {
|
|
331
|
+
const name = import_b4a3.default.toString(connection.remotePublicKey, "hex");
|
|
332
|
+
console.log("Presence: * got a connection from:", name, "*");
|
|
333
|
+
connections.push(connection);
|
|
334
|
+
sendStateRequestMessage(connection);
|
|
335
|
+
connection.once("close", () => {
|
|
336
|
+
console.log(
|
|
337
|
+
`Presence: Connection closed. Removing connection ${import_b4a3.default.toString(
|
|
338
|
+
connection.remotePublicKey,
|
|
339
|
+
"hex"
|
|
340
|
+
)}`
|
|
341
|
+
);
|
|
342
|
+
connections.splice(connections.indexOf(connection), 1);
|
|
343
|
+
const id = deviceIDs.get(import_b4a3.default.toString(connection.remotePublicKey, "hex"));
|
|
344
|
+
if (id) {
|
|
345
|
+
broadcastStatusMessage(id, "unknown");
|
|
346
|
+
}
|
|
347
|
+
deviceIDs.delete(import_b4a3.default.toString(connection.remotePublicKey, "hex"));
|
|
348
|
+
});
|
|
349
|
+
connection.on("data", (data) => {
|
|
350
|
+
let parsedData;
|
|
351
|
+
try {
|
|
352
|
+
parsedData = JSON.parse(data);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error("Error parsing data:", error);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const header = parsedData;
|
|
358
|
+
console.log(
|
|
359
|
+
"Presence: got data for ",
|
|
360
|
+
connection.publicKey.toString("hex")
|
|
361
|
+
);
|
|
362
|
+
if (header.type === "update") {
|
|
363
|
+
const payload = parsedData;
|
|
364
|
+
onUpdate({ id: payload.id, state: payload.state });
|
|
365
|
+
deviceIDs.set(
|
|
366
|
+
import_b4a3.default.toString(connection.remotePublicKey, "hex"),
|
|
367
|
+
payload.id
|
|
368
|
+
);
|
|
369
|
+
} else if (header.type === "request") {
|
|
370
|
+
onRequest();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
connection.on("error", (error) => {
|
|
374
|
+
console.error(
|
|
375
|
+
`Presence: Connection error on connection ${import_b4a3.default.toString(
|
|
376
|
+
connection.remotePublicKey,
|
|
377
|
+
"hex"
|
|
378
|
+
)}: ${error}`
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
const connections = [];
|
|
383
|
+
const connect = async (topic, id) => {
|
|
384
|
+
console.log("Presence: connecting to topic: ", topic);
|
|
385
|
+
const key = import_b4a3.default.from(topic, "hex");
|
|
386
|
+
console.log("key: ", key);
|
|
387
|
+
const discovery = swarm.join(key, { client: true, server: true });
|
|
388
|
+
console.log("got discovery: ");
|
|
389
|
+
const publicKey = import_b4a3.default.toString(
|
|
390
|
+
discovery.swarm.keyPair?.publicKey || discovery.swarm.publicKey || Buffer.alloc(32),
|
|
391
|
+
"hex"
|
|
392
|
+
);
|
|
393
|
+
console.log("Presence: public key is ", publicKey);
|
|
394
|
+
deviceIDs.set(publicKey, id);
|
|
395
|
+
discovery.flushed().then(() => {
|
|
396
|
+
console.log("Presence: joined topic:", import_b4a3.default.toString(key, "hex"));
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
const sendStateRequestMessage = async (connection) => {
|
|
400
|
+
const message = {
|
|
401
|
+
type: "request"
|
|
402
|
+
};
|
|
403
|
+
const payload = JSON.stringify(message);
|
|
404
|
+
console.log(
|
|
405
|
+
"sending state request message to: ",
|
|
406
|
+
connection.publicKey.toString("hex")
|
|
407
|
+
);
|
|
408
|
+
connection.write(payload);
|
|
409
|
+
};
|
|
410
|
+
const receiveStatusMessage = async (id, state) => {
|
|
411
|
+
return broadcastStatusMessage(id, state);
|
|
412
|
+
};
|
|
413
|
+
const broadcastStatusMessage = (id, state) => {
|
|
414
|
+
console.log(`server got connection state message for ${id}: `, state);
|
|
415
|
+
const message = {
|
|
416
|
+
type: "update",
|
|
417
|
+
id,
|
|
418
|
+
state
|
|
419
|
+
};
|
|
420
|
+
console.log("active connections: ", connections.length);
|
|
421
|
+
const payload = JSON.stringify(message);
|
|
422
|
+
for (const connection of connections) {
|
|
423
|
+
console.log("sending message to: ", connection.publicKey.toString("hex"));
|
|
424
|
+
connection.write(payload);
|
|
425
|
+
}
|
|
426
|
+
onUpdate(message);
|
|
427
|
+
};
|
|
428
|
+
return {
|
|
429
|
+
connect,
|
|
430
|
+
receiveStatusMessage
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/client/useKeyExchangeClient.ts
|
|
435
|
+
var import_hyperdht2 = __toESM(require("hyperdht"));
|
|
436
|
+
var import_b4a4 = __toESM(require("b4a"));
|
|
437
|
+
function useKeyExchangeClient(onEvent) {
|
|
438
|
+
let socket;
|
|
439
|
+
let deviceName;
|
|
440
|
+
let appName;
|
|
441
|
+
const connect = async (options) => {
|
|
442
|
+
deviceName = options.name;
|
|
443
|
+
appName = options.app;
|
|
444
|
+
return new Promise((resolve) => {
|
|
445
|
+
console.log(`client: connecting to server on ${options.key}`);
|
|
446
|
+
const publicKey = import_b4a4.default.from(options.key, "hex");
|
|
447
|
+
const dht = new import_hyperdht2.default();
|
|
448
|
+
socket = dht.connect(publicKey);
|
|
449
|
+
socket.once("open", () => {
|
|
450
|
+
const name = import_b4a4.default.toString(socket.remotePublicKey, "hex");
|
|
451
|
+
console.log(`client: got a connection on ${name}`);
|
|
452
|
+
resolve();
|
|
453
|
+
});
|
|
454
|
+
socket.on("data", (data) => {
|
|
455
|
+
parseResponse(data);
|
|
456
|
+
});
|
|
457
|
+
socket.on("error", (error) => {
|
|
458
|
+
onConnectionError(error);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
const parseResponse = async (data) => {
|
|
463
|
+
console.log(`client: got data: ${data}`);
|
|
464
|
+
const parsedData = JSON.parse(data);
|
|
465
|
+
const payload = parsedData;
|
|
466
|
+
console.log("payload");
|
|
467
|
+
console.log(payload);
|
|
468
|
+
switch (payload.header.name) {
|
|
469
|
+
case "Connection Failure: Broadcast disabled":
|
|
470
|
+
onEvent({
|
|
471
|
+
route: syncSetupEventRoutes["onBroadcastDisable"],
|
|
472
|
+
data: {}
|
|
473
|
+
});
|
|
474
|
+
break;
|
|
475
|
+
case "Transmition Succeeded: Approved":
|
|
476
|
+
const identity = payload.body.identity;
|
|
477
|
+
const appId = payload.body.appId;
|
|
478
|
+
onEvent({
|
|
479
|
+
route: syncSetupEventRoutes["onClientApproval"],
|
|
480
|
+
data: { identity, appId }
|
|
481
|
+
});
|
|
482
|
+
break;
|
|
483
|
+
case "Transmition Succeeded: Rejected":
|
|
484
|
+
onEvent({ route: syncSetupEventRoutes["onClientDenial"], data: {} });
|
|
485
|
+
break;
|
|
486
|
+
case "Setup Succeeded":
|
|
487
|
+
onEvent({ route: syncSetupEventRoutes["onCompletion"], data: {} });
|
|
488
|
+
break;
|
|
489
|
+
case "Authentication Error: Invalid PIN":
|
|
490
|
+
onEvent({
|
|
491
|
+
route: syncSetupEventRoutes["onKeystoreRequestInvalidPIN"],
|
|
492
|
+
data: {
|
|
493
|
+
reason: "Invalid PIN Entered"
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
break;
|
|
497
|
+
case "Authentication Succeeded":
|
|
498
|
+
onEvent({
|
|
499
|
+
route: syncSetupEventRoutes["onKeystoreRequestValidPIN"],
|
|
500
|
+
data: {}
|
|
501
|
+
});
|
|
502
|
+
break;
|
|
503
|
+
default:
|
|
504
|
+
console.error(`error: response ${payload} not handled in client`);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const onConnectionError = async (error) => {
|
|
509
|
+
console.log(`client: Connection error: ${error}`);
|
|
510
|
+
const route = syncSetupEventRoutes["onBroadcastDisable"];
|
|
511
|
+
switch (error.code) {
|
|
512
|
+
case "PEER_NOT_FOUND":
|
|
513
|
+
onEvent({
|
|
514
|
+
route,
|
|
515
|
+
data: {
|
|
516
|
+
title: "Peer Not Found",
|
|
517
|
+
description: "Another device with the specified broadcast key could not be located."
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
break;
|
|
521
|
+
default:
|
|
522
|
+
onEvent({
|
|
523
|
+
route,
|
|
524
|
+
data: {
|
|
525
|
+
title: "Client Disconnected",
|
|
526
|
+
description: "The connection to your other device was lost. Please ensure you have another device that is broadcasting"
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const requestPIN = async () => {
|
|
533
|
+
await sendRequest({
|
|
534
|
+
to: [socket],
|
|
535
|
+
header: "Request PIN",
|
|
536
|
+
body: () => {
|
|
537
|
+
return { deviceName, appName };
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
const requestKeystore = async (pin) => {
|
|
542
|
+
await sendRequest({
|
|
543
|
+
to: [socket],
|
|
544
|
+
header: "Request Keystore",
|
|
545
|
+
body: () => {
|
|
546
|
+
return {
|
|
547
|
+
pin
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
const route = syncSetupEventRoutes["onKeystoreRequest"];
|
|
552
|
+
onEvent({ route, data: {} });
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
connect,
|
|
556
|
+
requestPIN,
|
|
557
|
+
requestKeystore
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/CoreTransportLogic.ts
|
|
562
|
+
var import_crypto = __toESM(require("crypto"));
|
|
563
|
+
var import_os = __toESM(require("os"));
|
|
564
|
+
var import_systeminformation = __toESM(require("systeminformation"));
|
|
565
|
+
var CoreTransportLogic = class {
|
|
566
|
+
syncServer;
|
|
567
|
+
syncClient;
|
|
568
|
+
presence;
|
|
569
|
+
deviceInfo = null;
|
|
570
|
+
handleEvent;
|
|
571
|
+
presenceConnected = false;
|
|
572
|
+
constructor(handleEvent = () => {
|
|
573
|
+
}) {
|
|
574
|
+
this.handleEvent = handleEvent;
|
|
575
|
+
this.syncServer = useKeyExchangeServer(this.handleEvent);
|
|
576
|
+
this.syncClient = useKeyExchangeClient(this.handleEvent);
|
|
577
|
+
this.presence = useNetworkPresence(
|
|
578
|
+
this.onStateUpdate.bind(this),
|
|
579
|
+
this.onStateRequest.bind(this)
|
|
580
|
+
);
|
|
581
|
+
this.initializeDeviceInfo();
|
|
582
|
+
}
|
|
583
|
+
async initializeDeviceInfo() {
|
|
584
|
+
this.deviceInfo = await this.getDeviceInfo();
|
|
585
|
+
}
|
|
586
|
+
onStateUpdate(response) {
|
|
587
|
+
console.log("received state message for:", response.id);
|
|
588
|
+
this.handleEvent({ route: MESSAGES.onClientStateUpdate, data: response });
|
|
589
|
+
}
|
|
590
|
+
onStateRequest() {
|
|
591
|
+
console.log("received state request");
|
|
592
|
+
this.handleEvent({ route: MESSAGES.onClientStateRequest, data: {} });
|
|
593
|
+
}
|
|
594
|
+
delay(ms) {
|
|
595
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
596
|
+
}
|
|
597
|
+
// Sync methods
|
|
598
|
+
async enableBroadcast() {
|
|
599
|
+
console.log(`admin: enabled broadcast`);
|
|
600
|
+
try {
|
|
601
|
+
await this.syncServer.beginBroadcast();
|
|
602
|
+
return this.syncServer.getKey();
|
|
603
|
+
} catch (error) {
|
|
604
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : "UNKNOWN";
|
|
605
|
+
console.error("Error on broadcast enable during topic sync setup:", code);
|
|
606
|
+
if (code === "ALREADY_LISTENING") {
|
|
607
|
+
return this.syncServer.getKey();
|
|
608
|
+
}
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async connect(topic, name, app) {
|
|
613
|
+
console.log(`client: requested connection to ${topic}`);
|
|
614
|
+
try {
|
|
615
|
+
await this.syncClient.connect({ key: topic, name, app });
|
|
616
|
+
await this.syncClient.requestPIN();
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error("Error on topic connect during sync setup:", error);
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async requestKeystore(pin) {
|
|
623
|
+
console.log(`client: requested keystore using pin \`${pin}\``);
|
|
624
|
+
try {
|
|
625
|
+
await this.syncClient.requestKeystore(pin);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.error("Error on keystore request during sync setup:", error);
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async approveRequest(data) {
|
|
632
|
+
console.log(`admin: request approved`);
|
|
633
|
+
try {
|
|
634
|
+
await this.syncServer.approveRequest(data.identity, data.appId);
|
|
635
|
+
await this.delay(3e3);
|
|
636
|
+
await this.syncServer.completeSetupSuccess();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
console.error("Error on request approve during sync setup:", error);
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async denyRequest() {
|
|
643
|
+
console.log(`admin: request denied`);
|
|
644
|
+
try {
|
|
645
|
+
await this.syncServer.rejectRequest();
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error("Error on request deny during sync setup:", error);
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async disableBroadcast() {
|
|
652
|
+
try {
|
|
653
|
+
await this.syncServer.endBroadcast();
|
|
654
|
+
await this.syncServer.broadcastDisabled();
|
|
655
|
+
} catch (error) {
|
|
656
|
+
console.error("Error on broadcast disable during sync setup:", error);
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Device methods
|
|
661
|
+
async getDeviceInfo() {
|
|
662
|
+
if (this.deviceInfo) {
|
|
663
|
+
return this.deviceInfo;
|
|
664
|
+
}
|
|
665
|
+
const hostname = import_os.default.hostname();
|
|
666
|
+
const osInfo = await import_systeminformation.default.osInfo();
|
|
667
|
+
const uuid = await import_systeminformation.default.uuid();
|
|
668
|
+
const serial = osInfo.serial;
|
|
669
|
+
const distro = osInfo.distro;
|
|
670
|
+
const arch = osInfo.arch;
|
|
671
|
+
const hardwareUUID = uuid.hardware;
|
|
672
|
+
const key = serial + distro + arch + hardwareUUID;
|
|
673
|
+
const hash = import_crypto.default.createHash("sha256").update(key).digest("hex");
|
|
674
|
+
return { id: hash, name: hostname };
|
|
675
|
+
}
|
|
676
|
+
async getHostname() {
|
|
677
|
+
return import_os.default.hostname();
|
|
678
|
+
}
|
|
679
|
+
// Network methods
|
|
680
|
+
async connectPresence(topic) {
|
|
681
|
+
if (this.presenceConnected) {
|
|
682
|
+
console.log("Presence already connected, skipping");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const deviceInfo = await this.getDeviceInfo();
|
|
686
|
+
const topicHash = import_crypto.default.createHash("sha256").update(topic).digest("hex");
|
|
687
|
+
console.log(`Connecting presence network to topic: ${topicHash}`);
|
|
688
|
+
await this.presence.connect(topicHash, deviceInfo.id);
|
|
689
|
+
this.presenceConnected = true;
|
|
690
|
+
}
|
|
691
|
+
broadcastClientState(id, state) {
|
|
692
|
+
this.presence.receiveStatusMessage(id, state);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// src/server/keyTransportServer.ts
|
|
697
|
+
var KeyTransportServer = class {
|
|
698
|
+
wss;
|
|
699
|
+
activeConnections = /* @__PURE__ */ new Set();
|
|
700
|
+
core;
|
|
701
|
+
port;
|
|
702
|
+
logger;
|
|
703
|
+
constructor(config) {
|
|
704
|
+
this.port = config.port || 3001;
|
|
705
|
+
this.logger = config.logger || console;
|
|
706
|
+
this.wss = new import_ws.WebSocketServer({ port: this.port });
|
|
707
|
+
this.core = new CoreTransportLogic(this.handleEvent);
|
|
708
|
+
this.setupServerHandlers();
|
|
709
|
+
this.logger.log(
|
|
710
|
+
`\u{1F680} CryptForge WebSocket Server running on ws://localhost:${this.port}`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
// Event handler for CoreTransportLogic
|
|
714
|
+
handleEvent = (options) => {
|
|
715
|
+
this.logger?.log(
|
|
716
|
+
`Core event with route: \`${options.route}\` data:`,
|
|
717
|
+
options.data
|
|
718
|
+
);
|
|
719
|
+
this.broadcastEvent(options.route, options.data);
|
|
720
|
+
};
|
|
721
|
+
// Helper function to send a response
|
|
722
|
+
sendResponse = (ws, type, requestId, data, error) => {
|
|
723
|
+
const response = {
|
|
724
|
+
type,
|
|
725
|
+
requestId,
|
|
726
|
+
...data && { data },
|
|
727
|
+
...error && { error }
|
|
728
|
+
};
|
|
729
|
+
ws.send(JSON.stringify(response));
|
|
730
|
+
this.logger?.log("\u{1F4E4} Sent response:", response);
|
|
731
|
+
};
|
|
732
|
+
// Helper function to broadcast events to all connected clients
|
|
733
|
+
broadcastEvent = (route, data) => {
|
|
734
|
+
const message = JSON.stringify({
|
|
735
|
+
type: route,
|
|
736
|
+
data
|
|
737
|
+
});
|
|
738
|
+
this.activeConnections.forEach((ws) => {
|
|
739
|
+
if (ws.readyState === import_ws.default.OPEN) {
|
|
740
|
+
ws.send(message);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
this.logger?.log(`\u{1F4E1} Broadcast event: ${route}`, data);
|
|
744
|
+
};
|
|
745
|
+
setupServerHandlers = () => {
|
|
746
|
+
this.wss.on("connection", (ws) => {
|
|
747
|
+
this.logger?.log("\u2705 Client connected");
|
|
748
|
+
this.activeConnections.add(ws);
|
|
749
|
+
ws.on("message", async (data) => {
|
|
750
|
+
await this.handleMessage(ws, data);
|
|
751
|
+
});
|
|
752
|
+
ws.on("close", () => {
|
|
753
|
+
this.logger?.log("\u{1F44B} Client disconnected");
|
|
754
|
+
this.activeConnections.delete(ws);
|
|
755
|
+
});
|
|
756
|
+
ws.on("error", (error) => {
|
|
757
|
+
this.logger?.error("\u274C WebSocket error:", error);
|
|
758
|
+
this.activeConnections.delete(ws);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
this.wss.on("error", (error) => {
|
|
762
|
+
this.logger?.error("\u274C Server error:", error);
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
handleMessage = async (ws, data) => {
|
|
766
|
+
try {
|
|
767
|
+
const message = JSON.parse(data.toString());
|
|
768
|
+
this.logger?.log("\u{1F4E8} Received:", message);
|
|
769
|
+
const { type, requestId, data: requestData } = message;
|
|
770
|
+
switch (type) {
|
|
771
|
+
// Sync messages
|
|
772
|
+
case MESSAGES.enableBroadcast:
|
|
773
|
+
this.logger?.log("\u{1F4E1} Enabling broadcast...");
|
|
774
|
+
try {
|
|
775
|
+
const broadcastId = await this.core.enableBroadcast();
|
|
776
|
+
this.sendResponse(ws, type, requestId, { broadcastId });
|
|
777
|
+
} catch (error) {
|
|
778
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
779
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case MESSAGES.connect:
|
|
784
|
+
this.logger?.log("\u{1F50C} Connecting to topic...");
|
|
785
|
+
try {
|
|
786
|
+
await this.core.connect(
|
|
787
|
+
requestData.topic,
|
|
788
|
+
requestData.name,
|
|
789
|
+
requestData.app
|
|
790
|
+
);
|
|
791
|
+
this.sendResponse(ws, type, requestId, {
|
|
792
|
+
connected: true,
|
|
793
|
+
topic: requestData.topic,
|
|
794
|
+
name: requestData.name,
|
|
795
|
+
app: requestData.app
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
799
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
803
|
+
case MESSAGES.requestKeystore:
|
|
804
|
+
this.logger?.log("\u{1F4E8} Requesting keystore...");
|
|
805
|
+
try {
|
|
806
|
+
await this.core.requestKeystore(requestData.pin);
|
|
807
|
+
this.sendResponse(ws, type, requestId, {
|
|
808
|
+
requested: true,
|
|
809
|
+
pin: requestData.pin
|
|
810
|
+
});
|
|
811
|
+
} catch (error) {
|
|
812
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
813
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
break;
|
|
817
|
+
case MESSAGES.approveRequest:
|
|
818
|
+
this.logger?.log("\u2705 Approving request...");
|
|
819
|
+
try {
|
|
820
|
+
await this.core.approveRequest({
|
|
821
|
+
identity: requestData.identity,
|
|
822
|
+
appId: requestData.appId
|
|
823
|
+
});
|
|
824
|
+
this.sendResponse(ws, type, requestId, {
|
|
825
|
+
approved: true
|
|
826
|
+
});
|
|
827
|
+
} catch (error) {
|
|
828
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
829
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
break;
|
|
833
|
+
case MESSAGES.denyRequest:
|
|
834
|
+
this.logger?.log("\u274C Denying request...");
|
|
835
|
+
try {
|
|
836
|
+
await this.core.denyRequest();
|
|
837
|
+
this.sendResponse(ws, type, requestId, {
|
|
838
|
+
denied: true
|
|
839
|
+
});
|
|
840
|
+
} catch (error) {
|
|
841
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
842
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
case MESSAGES.disableBroadcast:
|
|
847
|
+
this.logger?.log("\u{1F4F4} Disabling broadcast...");
|
|
848
|
+
try {
|
|
849
|
+
await this.core.disableBroadcast();
|
|
850
|
+
this.sendResponse(ws, type, requestId, {
|
|
851
|
+
disabled: true
|
|
852
|
+
});
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
855
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
break;
|
|
859
|
+
case MESSAGES.getDeviceInfo:
|
|
860
|
+
this.logger?.log("\u{1F50D} Getting device info...");
|
|
861
|
+
try {
|
|
862
|
+
const deviceInfo = await this.core.getDeviceInfo();
|
|
863
|
+
this.sendResponse(ws, type, requestId, deviceInfo);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
866
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
break;
|
|
870
|
+
case MESSAGES.getHostname:
|
|
871
|
+
this.logger?.log("\u{1F50D} Getting hostname...");
|
|
872
|
+
try {
|
|
873
|
+
const hostname = await this.core.getHostname();
|
|
874
|
+
this.sendResponse(ws, type, requestId, { hostname });
|
|
875
|
+
} catch (error) {
|
|
876
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
877
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
break;
|
|
881
|
+
case MESSAGES.connectPresence:
|
|
882
|
+
this.logger?.log("\u{1F50C} Connecting presence network...");
|
|
883
|
+
try {
|
|
884
|
+
await this.core.connectPresence(requestData.topic);
|
|
885
|
+
this.sendResponse(ws, type, requestId, { success: true });
|
|
886
|
+
} catch (error) {
|
|
887
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
888
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
break;
|
|
892
|
+
case MESSAGES.broadcastClientState:
|
|
893
|
+
this.logger?.log("\u{1F4E1} Broadcasting client state...");
|
|
894
|
+
try {
|
|
895
|
+
this.core.broadcastClientState(requestData.id, requestData.state);
|
|
896
|
+
this.sendResponse(ws, type, requestId, { success: true });
|
|
897
|
+
} catch (error) {
|
|
898
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
899
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
break;
|
|
903
|
+
default:
|
|
904
|
+
this.logger?.log(`\u26A0\uFE0F Unknown message type: ${type}`);
|
|
905
|
+
if (requestId) {
|
|
906
|
+
this.sendResponse(ws, type, requestId, null, {
|
|
907
|
+
message: `Unknown message type: ${type}`
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
} catch (error) {
|
|
912
|
+
this.logger?.error("\u274C Error processing message:", error);
|
|
913
|
+
try {
|
|
914
|
+
const parsed = JSON.parse(data.toString());
|
|
915
|
+
if (parsed.requestId) {
|
|
916
|
+
this.sendResponse(ws, parsed.type, parsed.requestId, null, {
|
|
917
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
} catch (e) {
|
|
921
|
+
this.logger?.error("\u274C Could not send error response:", e);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
// Public methods for managing the server
|
|
926
|
+
close = (callback) => {
|
|
927
|
+
this.wss.close(callback);
|
|
928
|
+
};
|
|
929
|
+
getActiveConnections = () => {
|
|
930
|
+
return new Set(this.activeConnections);
|
|
931
|
+
};
|
|
932
|
+
getPort = () => {
|
|
933
|
+
return this.port;
|
|
934
|
+
};
|
|
935
|
+
};
|
|
936
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
937
|
+
0 && (module.exports = {
|
|
938
|
+
KeyTransportServer,
|
|
939
|
+
useKeyExchangeServer
|
|
940
|
+
});
|