pinomad 0.3.0 → 0.5.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 +4 -2
- package/dist/{chunk-3tf23vjq.js → chunk-1d9sppj0.js} +1 -1
- package/dist/chunk-26d04z02.js +126 -0
- package/dist/chunk-4by4x5c4.js +1376 -0
- package/dist/{chunk-kxws8b83.js → chunk-5ng86fmy.js} +4 -4
- package/dist/chunk-9d59jjmt.js +144 -0
- package/dist/{chunk-t9y2wfk5.js → chunk-aqj4wg33.js} +98 -6
- package/dist/{chunk-n60kkf1m.js → chunk-crv3dqzp.js} +4 -4
- package/dist/{chunk-tfqc1djy.js → chunk-d4wdt5qs.js} +976 -306
- package/dist/chunk-ebfaf3k2.js +190 -0
- package/dist/chunk-f0v3sht0.js +136 -0
- package/dist/{chunk-3qn8gbjb.js → chunk-gqr7ahv4.js} +1 -1
- package/dist/{chunk-c155p0t0.js → chunk-hsp12cjt.js} +111 -8
- package/dist/chunk-kmbh44dt.js +704 -0
- package/dist/{chunk-bggesvnj.js → chunk-m4q9wafw.js} +28 -92
- package/dist/{chunk-syf0z68f.js → chunk-zttf1hvh.js} +2 -2
- package/dist/pinomad.js +16 -11
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-BkF0-im5.css +1 -0
- package/web/assets/index-D4lgm3Y4.js +383 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-4dj2vb8t.js +0 -284
- package/dist/chunk-7fqn69ys.js +0 -33
- package/dist/chunk-fxaewke2.js +0 -113
- package/dist/chunk-z4x88s0q.js +0 -347
- package/web/assets/index-CBXdQLua.js +0 -383
- package/web/assets/index-CG33GBxM.css +0 -1
package/web/favicon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><style>.bg{fill:#F7F5F0}.body{fill:#FFF;stroke:#151515}.ink{fill:#151515}@media(prefers-color-scheme:dark){.bg,.body{fill:#181818}.body{stroke:#FFF}.ink{fill:#FFF}}</style><rect class="bg" width="1024" height="1024" rx="224"/><g transform="translate(128 170) scale(2.7) translate(-148 -148)"><path class="body" d="M 368 244 C 384 278 372 326 348 353 C 322 382 287 398 255 396 C 234 395 221 387 228 368 C 230 362 223 365 215 366 C 185 369 165 359 159 346 C 154 337 157 337 168 335 C 197 332 211 308 221 282 C 231 254 253 244 283 233 C 318 219 350 212 362 232 Z" stroke-width="13" stroke-linejoin="round"/><path class="ink" d="M 214 234 C 213 199 224 176 246 165 C 267 153 282 147 296 149 C 317 151 333 172 343 194 C 300 201 254 226 214 234 Z"/><path class="ink" d="M 195 239 C 229 241 262 226 306 209 C 347 193 385 177 410 185 C 437 191 438 210 425 229 C 414 244 397 257 377 267 C 373 246 365 227 350 223 C 331 217 311 226 285 236 C 255 246 238 251 227 271 C 216 291 215 305 200 312 C 172 311 153 302 149 287 C 142 264 164 244 195 239 Z"/><ellipse class="ink" cx="304" cy="272" rx="9.5" ry="15.5"/><ellipse class="ink" cx="343" cy="269" rx="9.5" ry="15.5"/></g></svg>
|
package/web/index.html
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
6
8
|
<title>PiNomad</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-D4lgm3Y4.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BkF0-im5.css">
|
|
9
11
|
</head>
|
|
10
12
|
<body>
|
|
11
13
|
<div id="root"></div>
|
package/dist/chunk-4dj2vb8t.js
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
serveWebClient
|
|
4
|
-
} from "./chunk-r6w5aqkw.js";
|
|
5
|
-
import {
|
|
6
|
-
commandHint,
|
|
7
|
-
webRoot
|
|
8
|
-
} from "./chunk-3tf23vjq.js";
|
|
9
|
-
import {
|
|
10
|
-
RELAY_CLOSE,
|
|
11
|
-
decodeHostToRelay,
|
|
12
|
-
encodeRelayFrame,
|
|
13
|
-
verifyRelayChallenge
|
|
14
|
-
} from "./chunk-z4x88s0q.js";
|
|
15
|
-
import"./chunk-df32avw6.js";
|
|
16
|
-
|
|
17
|
-
// apps/relay/src/main.ts
|
|
18
|
-
import { parseArgs } from "node:util";
|
|
19
|
-
|
|
20
|
-
// apps/relay/src/relay.ts
|
|
21
|
-
import { randomBytes } from "node:crypto";
|
|
22
|
-
import { once } from "node:events";
|
|
23
|
-
import { createServer } from "node:http";
|
|
24
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
25
|
-
var MAX_PAYLOAD = 128 * 1024;
|
|
26
|
-
var MAX_BUFFERED = 64 * 1024;
|
|
27
|
-
var HIGH_WATER = 1024 * 1024;
|
|
28
|
-
var LOW_WATER = 256 * 1024;
|
|
29
|
-
var HOST_PATH = "/host";
|
|
30
|
-
var DEVICE_PATH = /^\/c\/([A-Za-z0-9_-]{43})$/;
|
|
31
|
-
var ACCEPT_PATH = /^\/accept\/([A-Za-z0-9_-]{22})$/;
|
|
32
|
-
var isSendableCloseCode = (code) => code >= 1000 && code <= 1003 || code >= 1007 && code <= 1014 || code >= 3000 && code <= 4999;
|
|
33
|
-
var rawLength = (data) => Array.isArray(data) ? data.reduce((sum, part) => sum + part.length, 0) : data.byteLength;
|
|
34
|
-
async function startRelay(options) {
|
|
35
|
-
const publicOrigin = options.publicOrigin.replace(/\/+$/, "");
|
|
36
|
-
if (publicOrigin === "")
|
|
37
|
-
throw new Error("publicOrigin is required");
|
|
38
|
-
const allowedHosts = new Set(options.allowedHosts);
|
|
39
|
-
const acceptTimeoutMs = options.acceptTimeoutMs ?? 1e4;
|
|
40
|
-
const registerTimeoutMs = options.registerTimeoutMs ?? 1e4;
|
|
41
|
-
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
42
|
-
const listenHost = options.listenHost ?? "127.0.0.1";
|
|
43
|
-
const controls = new Map;
|
|
44
|
-
const pending = new Map;
|
|
45
|
-
const sockets = new Set;
|
|
46
|
-
const alive = new WeakMap;
|
|
47
|
-
function track(socket) {
|
|
48
|
-
sockets.add(socket);
|
|
49
|
-
alive.set(socket, true);
|
|
50
|
-
socket.on("pong", () => alive.set(socket, true));
|
|
51
|
-
socket.on("close", () => sockets.delete(socket));
|
|
52
|
-
socket.on("error", () => socket.terminate());
|
|
53
|
-
}
|
|
54
|
-
function splice(a, b) {
|
|
55
|
-
const forward = (from, to) => {
|
|
56
|
-
from.on("message", (data, isBinary) => {
|
|
57
|
-
if (to.readyState !== WebSocket.OPEN)
|
|
58
|
-
return;
|
|
59
|
-
to.send(data, { binary: isBinary }, () => {
|
|
60
|
-
if (from.isPaused && to.bufferedAmount <= LOW_WATER)
|
|
61
|
-
from.resume();
|
|
62
|
-
});
|
|
63
|
-
if (to.bufferedAmount > HIGH_WATER)
|
|
64
|
-
from.pause();
|
|
65
|
-
});
|
|
66
|
-
from.on("close", (code, reason) => {
|
|
67
|
-
if (to.readyState !== WebSocket.OPEN)
|
|
68
|
-
return;
|
|
69
|
-
to.close(isSendableCloseCode(code) ? code : 1001, reason);
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
forward(a, b);
|
|
73
|
-
forward(b, a);
|
|
74
|
-
}
|
|
75
|
-
function onControl(socket) {
|
|
76
|
-
track(socket);
|
|
77
|
-
const nonce = randomBytes(32).toString("base64url");
|
|
78
|
-
socket.send(encodeRelayFrame({ t: "challenge", nonce }));
|
|
79
|
-
let hostId;
|
|
80
|
-
const deadline = setTimeout(() => socket.close(1008, "register timeout"), registerTimeoutMs);
|
|
81
|
-
socket.on("message", (data, isBinary) => {
|
|
82
|
-
if (hostId !== undefined || isBinary) {
|
|
83
|
-
socket.close(1008, "unexpected frame");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
let frame;
|
|
87
|
-
try {
|
|
88
|
-
frame = decodeHostToRelay(data.toString());
|
|
89
|
-
} catch {
|
|
90
|
-
socket.close(1008, "malformed frame");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const verified = verifyRelayChallenge(frame.signature, { relayOrigin: publicOrigin, hostId: frame.hostId, nonce });
|
|
94
|
-
if (!verified) {
|
|
95
|
-
socket.close(RELAY_CLOSE.badSignature);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (!allowedHosts.has(frame.hostId)) {
|
|
99
|
-
socket.close(RELAY_CLOSE.hostNotAllowed);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
hostId = frame.hostId;
|
|
103
|
-
clearTimeout(deadline);
|
|
104
|
-
const previous = controls.get(hostId);
|
|
105
|
-
if (previous !== undefined)
|
|
106
|
-
previous.close(RELAY_CLOSE.replaced);
|
|
107
|
-
controls.set(hostId, socket);
|
|
108
|
-
socket.send(encodeRelayFrame({ t: "registered" }));
|
|
109
|
-
});
|
|
110
|
-
socket.once("close", () => {
|
|
111
|
-
clearTimeout(deadline);
|
|
112
|
-
if (hostId !== undefined && controls.get(hostId) === socket)
|
|
113
|
-
controls.delete(hostId);
|
|
114
|
-
for (const [connId, entry] of pending) {
|
|
115
|
-
if (entry.control === socket) {
|
|
116
|
-
pending.delete(connId);
|
|
117
|
-
clearTimeout(entry.timer);
|
|
118
|
-
entry.socket.close(RELAY_CLOSE.hostOffline);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
function onDevice(socket, hostId) {
|
|
124
|
-
track(socket);
|
|
125
|
-
const control = controls.get(hostId);
|
|
126
|
-
if (control === undefined) {
|
|
127
|
-
socket.close(RELAY_CLOSE.hostOffline);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const connId = randomBytes(16).toString("base64url");
|
|
131
|
-
const entry = {
|
|
132
|
-
control,
|
|
133
|
-
socket,
|
|
134
|
-
buffered: [],
|
|
135
|
-
bufferedBytes: 0,
|
|
136
|
-
timer: setTimeout(() => {
|
|
137
|
-
if (pending.delete(connId))
|
|
138
|
-
socket.close(RELAY_CLOSE.acceptTimeout);
|
|
139
|
-
}, acceptTimeoutMs),
|
|
140
|
-
onMessage(data, isBinary) {
|
|
141
|
-
entry.buffered.push({ data, isBinary });
|
|
142
|
-
entry.bufferedBytes += rawLength(data);
|
|
143
|
-
if (entry.bufferedBytes > MAX_BUFFERED && pending.delete(connId)) {
|
|
144
|
-
clearTimeout(entry.timer);
|
|
145
|
-
socket.close(1009);
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
onClose() {
|
|
149
|
-
if (pending.delete(connId))
|
|
150
|
-
clearTimeout(entry.timer);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
pending.set(connId, entry);
|
|
154
|
-
socket.on("message", entry.onMessage);
|
|
155
|
-
socket.once("close", entry.onClose);
|
|
156
|
-
control.send(encodeRelayFrame({ t: "incoming", connId }));
|
|
157
|
-
}
|
|
158
|
-
function onAccept(socket, connId) {
|
|
159
|
-
track(socket);
|
|
160
|
-
const entry = pending.get(connId);
|
|
161
|
-
if (entry === undefined) {
|
|
162
|
-
socket.close(RELAY_CLOSE.unknownConnection);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
pending.delete(connId);
|
|
166
|
-
clearTimeout(entry.timer);
|
|
167
|
-
const device = entry.socket;
|
|
168
|
-
device.removeListener("message", entry.onMessage);
|
|
169
|
-
device.removeListener("close", entry.onClose);
|
|
170
|
-
splice(socket, device);
|
|
171
|
-
for (const { data, isBinary } of entry.buffered)
|
|
172
|
-
socket.send(data, { binary: isBinary });
|
|
173
|
-
}
|
|
174
|
-
const httpServer = createServer((request, response) => {
|
|
175
|
-
if (options.webRoot !== undefined) {
|
|
176
|
-
serveWebClient(request, response, options.webRoot).catch(() => {
|
|
177
|
-
if (!response.headersSent)
|
|
178
|
-
response.writeHead(500);
|
|
179
|
-
response.end();
|
|
180
|
-
});
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
response.writeHead(404);
|
|
184
|
-
response.end();
|
|
185
|
-
});
|
|
186
|
-
const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_PAYLOAD });
|
|
187
|
-
httpServer.on("upgrade", (request, socket, head) => {
|
|
188
|
-
let pathname;
|
|
189
|
-
try {
|
|
190
|
-
pathname = new URL(request.url ?? "/", "http://relay.invalid").pathname;
|
|
191
|
-
} catch {
|
|
192
|
-
pathname = "";
|
|
193
|
-
}
|
|
194
|
-
const upgrade = (handle) => {
|
|
195
|
-
wss.handleUpgrade(request, socket, head, handle);
|
|
196
|
-
};
|
|
197
|
-
const device = DEVICE_PATH.exec(pathname);
|
|
198
|
-
const accept = ACCEPT_PATH.exec(pathname);
|
|
199
|
-
if (pathname === HOST_PATH)
|
|
200
|
-
upgrade(onControl);
|
|
201
|
-
else if (device !== null)
|
|
202
|
-
upgrade((ws) => onDevice(ws, device[1]));
|
|
203
|
-
else if (accept !== null)
|
|
204
|
-
upgrade((ws) => onAccept(ws, accept[1]));
|
|
205
|
-
else {
|
|
206
|
-
socket.write(`HTTP/1.1 404 Not Found\r
|
|
207
|
-
\r
|
|
208
|
-
`);
|
|
209
|
-
socket.destroy();
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
await new Promise((resolveListen, reject) => {
|
|
213
|
-
httpServer.once("listening", resolveListen);
|
|
214
|
-
httpServer.once("error", reject);
|
|
215
|
-
httpServer.listen(options.port, listenHost);
|
|
216
|
-
});
|
|
217
|
-
const { port } = httpServer.address();
|
|
218
|
-
const pingTimer = setInterval(() => {
|
|
219
|
-
for (const socket of sockets) {
|
|
220
|
-
if (alive.get(socket) === true) {
|
|
221
|
-
alive.set(socket, false);
|
|
222
|
-
socket.ping();
|
|
223
|
-
} else {
|
|
224
|
-
socket.terminate();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}, pingIntervalMs);
|
|
228
|
-
return {
|
|
229
|
-
port,
|
|
230
|
-
url: `ws://${listenHost.includes(":") ? `[${listenHost}]` : listenHost}:${port}`,
|
|
231
|
-
async close() {
|
|
232
|
-
clearInterval(pingTimer);
|
|
233
|
-
for (const entry of pending.values())
|
|
234
|
-
clearTimeout(entry.timer);
|
|
235
|
-
pending.clear();
|
|
236
|
-
for (const socket of sockets)
|
|
237
|
-
socket.terminate();
|
|
238
|
-
const closing = once(wss, "close");
|
|
239
|
-
wss.close();
|
|
240
|
-
await closing;
|
|
241
|
-
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// apps/relay/src/main.ts
|
|
247
|
-
async function main(argv) {
|
|
248
|
-
const { values } = parseArgs({
|
|
249
|
-
args: [...argv],
|
|
250
|
-
options: {
|
|
251
|
-
port: { type: "string", default: "7430" },
|
|
252
|
-
listen: { type: "string", default: "127.0.0.1" },
|
|
253
|
-
"public-origin": { type: "string" },
|
|
254
|
-
"allow-host": { type: "string", multiple: true }
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
const allowedHosts = values["allow-host"] ?? [];
|
|
258
|
-
if (values["public-origin"] === undefined || allowedHosts.length === 0) {
|
|
259
|
-
console.error(`usage: ${commandHint("relay")} --public-origin ORIGIN --allow-host HOST_ID [--allow-host ...] [--port 7430] [--listen 127.0.0.1]`);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
262
|
-
const relay = await startRelay({
|
|
263
|
-
port: Number(values.port),
|
|
264
|
-
listenHost: values.listen,
|
|
265
|
-
publicOrigin: values["public-origin"],
|
|
266
|
-
allowedHosts,
|
|
267
|
-
webRoot
|
|
268
|
-
});
|
|
269
|
-
console.log(JSON.stringify({ event: "ready", url: relay.url, publicOrigin: values["public-origin"], allowedHosts: allowedHosts.length }));
|
|
270
|
-
let stopping = false;
|
|
271
|
-
const stop = () => {
|
|
272
|
-
if (stopping)
|
|
273
|
-
return;
|
|
274
|
-
stopping = true;
|
|
275
|
-
relay.close().finally(() => process.exit(0));
|
|
276
|
-
};
|
|
277
|
-
process.on("SIGINT", stop);
|
|
278
|
-
process.on("SIGTERM", stop);
|
|
279
|
-
}
|
|
280
|
-
if (false)
|
|
281
|
-
;
|
|
282
|
-
export {
|
|
283
|
-
main
|
|
284
|
-
};
|
package/dist/chunk-7fqn69ys.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadRelayKey
|
|
3
|
-
} from "./chunk-fxaewke2.js";
|
|
4
|
-
import {
|
|
5
|
-
relayHostId
|
|
6
|
-
} from "./chunk-z4x88s0q.js";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_DATA_DIR
|
|
9
|
-
} from "./chunk-cjfmfyjb.js";
|
|
10
|
-
import"./chunk-df32avw6.js";
|
|
11
|
-
|
|
12
|
-
// apps/host/src/cli/relay-id.ts
|
|
13
|
-
import { mkdir } from "node:fs/promises";
|
|
14
|
-
import { resolve } from "node:path";
|
|
15
|
-
import { parseArgs } from "node:util";
|
|
16
|
-
async function relayId(argv) {
|
|
17
|
-
const { values } = parseArgs({
|
|
18
|
-
args: [...argv],
|
|
19
|
-
options: { "data-dir": { type: "string" } }
|
|
20
|
-
});
|
|
21
|
-
const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
22
|
-
await mkdir(dataDir, { recursive: true });
|
|
23
|
-
return relayHostId((await loadRelayKey(dataDir)).publicKey);
|
|
24
|
-
}
|
|
25
|
-
async function main(argv) {
|
|
26
|
-
console.log(await relayId(argv));
|
|
27
|
-
}
|
|
28
|
-
if (false)
|
|
29
|
-
;
|
|
30
|
-
export {
|
|
31
|
-
relayId,
|
|
32
|
-
main
|
|
33
|
-
};
|
package/dist/chunk-fxaewke2.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
fromBase64Url,
|
|
3
|
-
generateKeyPair,
|
|
4
|
-
keyPairFromPrivate,
|
|
5
|
-
toBase64Url
|
|
6
|
-
} from "./chunk-z4x88s0q.js";
|
|
7
|
-
|
|
8
|
-
// apps/host/src/devices.ts
|
|
9
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
12
|
-
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
13
|
-
import { defineDoc } from "@earendil-works/pi-durable";
|
|
14
|
-
var DevicesDoc = defineDoc({
|
|
15
|
-
kind: "pinomad.devices",
|
|
16
|
-
version: 1,
|
|
17
|
-
scope: "session",
|
|
18
|
-
initial: () => ({ devices: [] })
|
|
19
|
-
});
|
|
20
|
-
async function ensureDevices(harness, context) {
|
|
21
|
-
await harness.commit(async (tx) => void await tx.doc(DevicesDoc), context);
|
|
22
|
-
}
|
|
23
|
-
async function isRegistered(harness, publicKey, context) {
|
|
24
|
-
return await harness.commit(async (tx) => (await tx.doc(DevicesDoc)).devices.some((entry) => entry.publicKey === publicKey), context);
|
|
25
|
-
}
|
|
26
|
-
async function registerDevice(harness, device, context) {
|
|
27
|
-
return await harness.commit(async (tx) => {
|
|
28
|
-
const doc = await tx.doc(DevicesDoc);
|
|
29
|
-
const existing = doc.devices.find((entry2) => entry2.publicKey === device.publicKey);
|
|
30
|
-
if (existing !== undefined)
|
|
31
|
-
return { ...existing };
|
|
32
|
-
const name = device.name.trim().slice(0, 64) || "Unknown device";
|
|
33
|
-
const entry = { publicKey: device.publicKey, name, pairedAt: Date.now() };
|
|
34
|
-
doc.devices.push(entry);
|
|
35
|
-
return entry;
|
|
36
|
-
}, context);
|
|
37
|
-
}
|
|
38
|
-
async function revokeDevice(harness, publicKey, context) {
|
|
39
|
-
await harness.commit(async (tx) => {
|
|
40
|
-
const doc = await tx.doc(DevicesDoc);
|
|
41
|
-
doc.devices = doc.devices.filter((entry) => entry.publicKey !== publicKey);
|
|
42
|
-
}, context);
|
|
43
|
-
}
|
|
44
|
-
async function loadHostKey(dataDir) {
|
|
45
|
-
const path = join(dataDir, "host-key");
|
|
46
|
-
try {
|
|
47
|
-
const text = (await readFile(path, "utf8")).trim();
|
|
48
|
-
return keyPairFromPrivate(fromBase64Url(text));
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (error.code !== "ENOENT")
|
|
51
|
-
throw error;
|
|
52
|
-
const pair = generateKeyPair();
|
|
53
|
-
await writeFile(path, `${toBase64Url(pair.privateKey)}
|
|
54
|
-
`, { mode: 384 });
|
|
55
|
-
return pair;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function loadRelayKey(dataDir) {
|
|
59
|
-
const path = join(dataDir, "relay-key");
|
|
60
|
-
try {
|
|
61
|
-
const secretKey = fromBase64Url((await readFile(path, "utf8")).trim());
|
|
62
|
-
if (secretKey.length !== 32)
|
|
63
|
-
throw new Error(`bad relay key in ${path}`);
|
|
64
|
-
return { secretKey, publicKey: ed25519.getPublicKey(secretKey) };
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (error.code !== "ENOENT")
|
|
67
|
-
throw error;
|
|
68
|
-
const { secretKey, publicKey } = ed25519.keygen();
|
|
69
|
-
try {
|
|
70
|
-
await writeFile(path, `${toBase64Url(secretKey)}
|
|
71
|
-
`, { flag: "wx", mode: 384 });
|
|
72
|
-
} catch (race) {
|
|
73
|
-
if (race.code !== "EEXIST")
|
|
74
|
-
throw race;
|
|
75
|
-
return loadRelayKey(dataDir);
|
|
76
|
-
}
|
|
77
|
-
return { secretKey, publicKey };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function pairingOffers(ttlMs = 5 * 60 * 1000) {
|
|
81
|
-
const live = new Map;
|
|
82
|
-
const prune = () => {
|
|
83
|
-
const now = Date.now();
|
|
84
|
-
for (const [secret, expiresAt] of live)
|
|
85
|
-
if (expiresAt <= now)
|
|
86
|
-
live.delete(secret);
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
create() {
|
|
90
|
-
prune();
|
|
91
|
-
const secret = toBase64Url(randomBytes(16));
|
|
92
|
-
const expiresAt = Date.now() + ttlMs;
|
|
93
|
-
live.set(secret, expiresAt);
|
|
94
|
-
return { secret, expiresAt };
|
|
95
|
-
},
|
|
96
|
-
consume(secret) {
|
|
97
|
-
prune();
|
|
98
|
-
const candidate = Buffer.from(secret);
|
|
99
|
-
for (const offer of live.keys()) {
|
|
100
|
-
const offerBytes = Buffer.from(offer);
|
|
101
|
-
if (offerBytes.length !== candidate.length)
|
|
102
|
-
continue;
|
|
103
|
-
if (timingSafeEqual(offerBytes, candidate)) {
|
|
104
|
-
live.delete(offer);
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export { DevicesDoc, ensureDevices, isRegistered, registerDevice, revokeDevice, loadHostKey, loadRelayKey, pairingOffers };
|