pinomad 0.7.0 → 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/README.md +6 -59
- package/dist/{chunk-6ysezfy5.js → chunk-1r8a2434.js} +11 -5
- package/dist/{chunk-ywmm63ks.js → chunk-3p1eysm9.js} +4 -2
- package/dist/{chunk-12kaxggg.js → chunk-73vxwjs7.js} +2 -2
- package/dist/chunk-a4b61tx5.js +2984 -0
- package/dist/{chunk-fbkd73zr.js → chunk-cytnycs9.js} +9 -9
- package/dist/{chunk-07769pqx.js → chunk-ddh6wfv2.js} +8 -8
- package/dist/{chunk-sz3rzk55.js → chunk-fes7wjw1.js} +5 -5
- package/dist/{chunk-e4ahjbmz.js → chunk-jgd38rna.js} +210 -7
- package/dist/{chunk-1nyzr0mg.js → chunk-jjtr7fr5.js} +2323 -830
- package/dist/{chunk-7qb9tq54.js → chunk-khcpv54k.js} +64 -310
- package/dist/{chunk-m4c5gvce.js → chunk-mv94ksvb.js} +22 -5
- package/dist/{chunk-8yme2wep.js → chunk-p15hqqjz.js} +1 -1
- package/dist/{chunk-rtwxjb90.js → chunk-p56xf1r8.js} +1 -1
- package/dist/{chunk-1t58fh0k.js → chunk-ptjyk94f.js} +5 -5
- package/dist/{chunk-0xh6cy9v.js → chunk-r2ssa2ex.js} +1 -1
- package/dist/chunk-sygeb9qb.js +430 -0
- package/dist/{chunk-b1h374jr.js → chunk-v414b5pp.js} +8 -2
- package/dist/pinomad.js +10 -10
- package/package.json +2 -1
- package/web/assets/index-5DfEA9Zb.js +398 -0
- package/web/assets/{index-C0rwAo0H.css → index-D6qIPhPy.css} +1 -1
- package/web/index.html +2 -2
- package/dist/chunk-sj7rnvh4.js +0 -719
- package/dist/chunk-znhn8af7.js +0 -33
- package/web/assets/index-DGee2kNU.js +0 -394
|
@@ -1,287 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
HOST_NAME_MAX_UNITS
|
|
3
|
-
|
|
2
|
+
HOST_NAME_MAX_UNITS,
|
|
3
|
+
fromBase64Url,
|
|
4
|
+
parseEndpointSet,
|
|
5
|
+
toBase64Url
|
|
6
|
+
} from "./chunk-sygeb9qb.js";
|
|
4
7
|
|
|
5
8
|
// packages/protocol/src/relay.ts
|
|
6
9
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
7
|
-
|
|
8
|
-
// packages/protocol/src/noise.ts
|
|
9
|
-
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
10
|
-
import { x25519 } from "@noble/curves/ed25519.js";
|
|
11
|
-
import { blake2s } from "@noble/hashes/blake2.js";
|
|
12
|
-
import { hmac } from "@noble/hashes/hmac.js";
|
|
13
|
-
var PROTOCOL_NAME = new TextEncoder().encode("Noise_IK_25519_ChaChaPoly_BLAKE2s");
|
|
14
|
-
var KEY_LENGTH = 32;
|
|
15
|
-
var TAG_LENGTH = 16;
|
|
16
|
-
var MAX_MESSAGE = 65535;
|
|
17
|
-
var MAX_NONCE = 2n ** 64n - 1n;
|
|
18
|
-
var hash = (data) => blake2s(data, { dkLen: KEY_LENGTH });
|
|
19
|
-
var concat = (...parts) => {
|
|
20
|
-
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
21
|
-
let offset = 0;
|
|
22
|
-
for (const part of parts) {
|
|
23
|
-
out.set(part, offset);
|
|
24
|
-
offset += part.length;
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
};
|
|
28
|
-
function hkdf(chainingKey, ikm, outputs) {
|
|
29
|
-
const tempKey = hmac(blake2s, chainingKey, ikm);
|
|
30
|
-
const out1 = hmac(blake2s, tempKey, new Uint8Array([1]));
|
|
31
|
-
const out2 = hmac(blake2s, tempKey, concat(out1, new Uint8Array([2])));
|
|
32
|
-
return [out1, out2];
|
|
33
|
-
}
|
|
34
|
-
function generateKeyPair() {
|
|
35
|
-
return keyPairFromPrivate(x25519.utils.randomSecretKey());
|
|
36
|
-
}
|
|
37
|
-
function keyPairFromPrivate(privateKey) {
|
|
38
|
-
if (privateKey.length !== KEY_LENGTH)
|
|
39
|
-
throw new Error(`X25519 private key must be ${KEY_LENGTH} bytes`);
|
|
40
|
-
return { publicKey: x25519.getPublicKey(privateKey), privateKey };
|
|
41
|
-
}
|
|
42
|
-
var dh = (pair, remotePublic) => x25519.getSharedSecret(pair.privateKey, remotePublic);
|
|
43
|
-
|
|
44
|
-
class NoiseCipherState {
|
|
45
|
-
#key;
|
|
46
|
-
#nonce = 0n;
|
|
47
|
-
setKey(key) {
|
|
48
|
-
this.#key = key;
|
|
49
|
-
this.#nonce = 0n;
|
|
50
|
-
}
|
|
51
|
-
encrypt(plaintext, ad = new Uint8Array(0)) {
|
|
52
|
-
const key = this.#key;
|
|
53
|
-
if (key === undefined)
|
|
54
|
-
return plaintext;
|
|
55
|
-
return chacha20poly1305(key, this.#takeNonce(), ad).encrypt(plaintext);
|
|
56
|
-
}
|
|
57
|
-
decrypt(ciphertext, ad = new Uint8Array(0)) {
|
|
58
|
-
const key = this.#key;
|
|
59
|
-
if (key === undefined)
|
|
60
|
-
return ciphertext;
|
|
61
|
-
return chacha20poly1305(key, this.#takeNonce(), ad).decrypt(ciphertext);
|
|
62
|
-
}
|
|
63
|
-
#takeNonce() {
|
|
64
|
-
if (this.#nonce === MAX_NONCE)
|
|
65
|
-
throw new Error("nonce exhausted");
|
|
66
|
-
const nonce = new Uint8Array(12);
|
|
67
|
-
new DataView(nonce.buffer).setBigUint64(4, this.#nonce, true);
|
|
68
|
-
this.#nonce += 1n;
|
|
69
|
-
return nonce;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
class SymmetricState {
|
|
74
|
-
#ck;
|
|
75
|
-
#h;
|
|
76
|
-
cipher = new NoiseCipherState;
|
|
77
|
-
constructor() {
|
|
78
|
-
const name = PROTOCOL_NAME;
|
|
79
|
-
this.#h = name.length <= KEY_LENGTH ? concat(name, new Uint8Array(KEY_LENGTH - name.length)) : hash(name);
|
|
80
|
-
this.#ck = this.#h.slice();
|
|
81
|
-
}
|
|
82
|
-
mixKey(ikm) {
|
|
83
|
-
const [ck, tempKey] = hkdf(this.#ck, ikm, 2);
|
|
84
|
-
this.#ck = ck;
|
|
85
|
-
this.cipher.setKey(tempKey.subarray(0, KEY_LENGTH));
|
|
86
|
-
}
|
|
87
|
-
mixHash(data) {
|
|
88
|
-
this.#h = hash(concat(this.#h, data));
|
|
89
|
-
}
|
|
90
|
-
encryptAndHash(plaintext) {
|
|
91
|
-
const ciphertext = this.cipher.encrypt(plaintext, this.#h);
|
|
92
|
-
this.mixHash(ciphertext);
|
|
93
|
-
return ciphertext;
|
|
94
|
-
}
|
|
95
|
-
decryptAndHash(ciphertext) {
|
|
96
|
-
const plaintext = this.cipher.decrypt(ciphertext, this.#h);
|
|
97
|
-
this.mixHash(ciphertext);
|
|
98
|
-
return plaintext;
|
|
99
|
-
}
|
|
100
|
-
get handshakeHash() {
|
|
101
|
-
return this.#h;
|
|
102
|
-
}
|
|
103
|
-
split() {
|
|
104
|
-
const [key1, key2] = hkdf(this.#ck, new Uint8Array(0), 2);
|
|
105
|
-
const first = new NoiseCipherState;
|
|
106
|
-
first.setKey(key1.subarray(0, KEY_LENGTH));
|
|
107
|
-
const second = new NoiseCipherState;
|
|
108
|
-
second.setKey(key2.subarray(0, KEY_LENGTH));
|
|
109
|
-
return [first, second];
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function take(message, size) {
|
|
113
|
-
if (message.offset + size > message.buffer.length)
|
|
114
|
-
throw new Error("malformed Noise message");
|
|
115
|
-
const slice = message.buffer.subarray(message.offset, message.offset + size);
|
|
116
|
-
message.offset += size;
|
|
117
|
-
return slice;
|
|
118
|
-
}
|
|
119
|
-
function readMessage1(state, message, own) {
|
|
120
|
-
const remoteEphemeral = take(message, KEY_LENGTH).slice();
|
|
121
|
-
state.mixHash(remoteEphemeral);
|
|
122
|
-
state.mixKey(dh(own.static, remoteEphemeral));
|
|
123
|
-
const remoteStatic = state.decryptAndHash(take(message, KEY_LENGTH + TAG_LENGTH)).slice();
|
|
124
|
-
if (remoteStatic.length !== KEY_LENGTH)
|
|
125
|
-
throw new Error("bad remote static key");
|
|
126
|
-
state.mixKey(dh(own.static, remoteStatic));
|
|
127
|
-
return { remoteEphemeral, remoteStatic };
|
|
128
|
-
}
|
|
129
|
-
function writeMessage2(state, buffer, own, remoteEphemeral, remoteStatic) {
|
|
130
|
-
buffer.push(own.ephemeral.publicKey);
|
|
131
|
-
state.mixHash(own.ephemeral.publicKey);
|
|
132
|
-
state.mixKey(dh(own.ephemeral, remoteEphemeral));
|
|
133
|
-
state.mixKey(dh(own.ephemeral, remoteStatic));
|
|
134
|
-
}
|
|
135
|
-
var checkLength = (message, what) => {
|
|
136
|
-
if (message.length > MAX_MESSAGE)
|
|
137
|
-
throw new Error(`${what} exceeds ${MAX_MESSAGE} bytes`);
|
|
138
|
-
};
|
|
139
|
-
function initialize(prologue, responderStatic) {
|
|
140
|
-
const state = new SymmetricState;
|
|
141
|
-
state.mixHash(prologue);
|
|
142
|
-
state.mixHash(responderStatic);
|
|
143
|
-
return state;
|
|
144
|
-
}
|
|
145
|
-
function respondIK(options) {
|
|
146
|
-
const ephemeral = options.ephemeral ?? generateKeyPair();
|
|
147
|
-
const state = initialize(options.prologue, options.static.publicKey);
|
|
148
|
-
let remoteEphemeral;
|
|
149
|
-
let remoteStatic;
|
|
150
|
-
return {
|
|
151
|
-
readMessage1(message) {
|
|
152
|
-
checkLength(message, "handshake message");
|
|
153
|
-
const reader = { buffer: message, offset: 0 };
|
|
154
|
-
const remote = readMessage1(state, reader, { static: options.static });
|
|
155
|
-
remoteEphemeral = remote.remoteEphemeral;
|
|
156
|
-
remoteStatic = remote.remoteStatic;
|
|
157
|
-
const payload = state.decryptAndHash(take(reader, message.length - reader.offset));
|
|
158
|
-
return { payload, remoteStatic };
|
|
159
|
-
},
|
|
160
|
-
writeMessage2(payload) {
|
|
161
|
-
if (remoteEphemeral === undefined || remoteStatic === undefined)
|
|
162
|
-
throw new Error("message 2 before message 1");
|
|
163
|
-
const buffer = [];
|
|
164
|
-
writeMessage2(state, buffer, { ephemeral }, remoteEphemeral, remoteStatic);
|
|
165
|
-
buffer.push(state.encryptAndHash(payload));
|
|
166
|
-
const message = concat(...buffer);
|
|
167
|
-
checkLength(message, "handshake message");
|
|
168
|
-
const [receive, send] = state.split();
|
|
169
|
-
return { message, result: { send, receive, handshakeHash: state.handshakeHash } };
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// packages/protocol/src/secure-channel.ts
|
|
175
|
-
var SECURE_PROLOGUE = new TextEncoder().encode("pinomad/1");
|
|
176
|
-
var utf8 = new TextEncoder;
|
|
177
|
-
var utf8Decode = new TextDecoder("utf-8", { fatal: true });
|
|
178
|
-
var encodeHostHello = (hello) => utf8.encode(JSON.stringify(hello));
|
|
179
|
-
function decodeJson(bytes) {
|
|
180
|
-
try {
|
|
181
|
-
return JSON.parse(utf8Decode.decode(bytes));
|
|
182
|
-
} catch {
|
|
183
|
-
throw new Error("handshake payload is not JSON");
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
var isString = (value) => typeof value === "string";
|
|
187
|
-
function decodeClientHello(bytes) {
|
|
188
|
-
const value = decodeJson(bytes);
|
|
189
|
-
if (value?.v !== 1)
|
|
190
|
-
throw new Error("unsupported client hello");
|
|
191
|
-
const pair = value.pair;
|
|
192
|
-
if (pair === undefined)
|
|
193
|
-
return { v: 1 };
|
|
194
|
-
const secret = pair.secret;
|
|
195
|
-
const name = pair.name;
|
|
196
|
-
if (!isString(secret) || !isString(name))
|
|
197
|
-
throw new Error("malformed pair");
|
|
198
|
-
return { v: 1, pair: { secret, name } };
|
|
199
|
-
}
|
|
200
|
-
var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
201
|
-
function toBase64Url(bytes) {
|
|
202
|
-
let out = "";
|
|
203
|
-
for (let index = 0;index < bytes.length; index += 3) {
|
|
204
|
-
const a = bytes[index];
|
|
205
|
-
const b = bytes[index + 1];
|
|
206
|
-
const c = bytes[index + 2];
|
|
207
|
-
out += B64_ALPHABET[a >> 2] + B64_ALPHABET[(a & 3) << 4 | (b ?? 0) >> 4];
|
|
208
|
-
if (b !== undefined)
|
|
209
|
-
out += B64_ALPHABET[(b & 15) << 2 | (c ?? 0) >> 6];
|
|
210
|
-
if (c !== undefined)
|
|
211
|
-
out += B64_ALPHABET[c & 63];
|
|
212
|
-
}
|
|
213
|
-
return out;
|
|
214
|
-
}
|
|
215
|
-
function fromBase64Url(text) {
|
|
216
|
-
if (text.length % 4 === 1)
|
|
217
|
-
throw new Error("invalid base64url");
|
|
218
|
-
const lookup = new Map(Array.from(B64_ALPHABET, (char, index) => [char, index]));
|
|
219
|
-
const bytes = [];
|
|
220
|
-
for (let index = 0;index < text.length; index += 4) {
|
|
221
|
-
const quad = text.slice(index, index + 4);
|
|
222
|
-
const values = [...quad].map((char) => {
|
|
223
|
-
const value = lookup.get(char);
|
|
224
|
-
if (value === undefined)
|
|
225
|
-
throw new Error("invalid base64url");
|
|
226
|
-
return value;
|
|
227
|
-
});
|
|
228
|
-
const packed = values.reduce((bits, value, index2) => bits | value << 18 - index2 * 6, 0);
|
|
229
|
-
bytes.push(packed >> 16 & 255);
|
|
230
|
-
if (values.length > 2)
|
|
231
|
-
bytes.push(packed >> 8 & 255);
|
|
232
|
-
if (values.length > 3)
|
|
233
|
-
bytes.push(packed & 255);
|
|
234
|
-
}
|
|
235
|
-
return new Uint8Array(bytes);
|
|
236
|
-
}
|
|
237
|
-
var MAX_BODY = 65535 - 16 - 1;
|
|
238
|
-
|
|
239
|
-
class SecureSession {
|
|
240
|
-
#send;
|
|
241
|
-
#receive;
|
|
242
|
-
#pending = [];
|
|
243
|
-
constructor(result) {
|
|
244
|
-
this.#send = result.send;
|
|
245
|
-
this.#receive = result.receive;
|
|
246
|
-
}
|
|
247
|
-
seal(text) {
|
|
248
|
-
const body = utf8.encode(text);
|
|
249
|
-
const chunks = [];
|
|
250
|
-
for (let offset = 0;offset <= body.length; offset += MAX_BODY) {
|
|
251
|
-
const last = offset + MAX_BODY >= body.length;
|
|
252
|
-
const record = new Uint8Array(1 + Math.min(MAX_BODY, body.length - offset));
|
|
253
|
-
record[0] = last ? 0 : 1;
|
|
254
|
-
record.set(body.subarray(offset, offset + MAX_BODY), 1);
|
|
255
|
-
chunks.push(this.#send.encrypt(record));
|
|
256
|
-
if (last)
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
return chunks;
|
|
260
|
-
}
|
|
261
|
-
open(chunk) {
|
|
262
|
-
const record = this.#receive.decrypt(chunk);
|
|
263
|
-
const flag = record[0];
|
|
264
|
-
if (flag !== 0 && flag !== 1)
|
|
265
|
-
throw new Error("bad record flag");
|
|
266
|
-
this.#pending.push(record.subarray(1));
|
|
267
|
-
if (flag === 1)
|
|
268
|
-
return;
|
|
269
|
-
const whole = concatParts(this.#pending);
|
|
270
|
-
this.#pending = [];
|
|
271
|
-
return utf8Decode.decode(whole);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
function concatParts(parts) {
|
|
275
|
-
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
276
|
-
let offset = 0;
|
|
277
|
-
for (const part of parts) {
|
|
278
|
-
out.set(part, offset);
|
|
279
|
-
offset += part.length;
|
|
280
|
-
}
|
|
281
|
-
return out;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// packages/protocol/src/relay.ts
|
|
285
10
|
var RELAY_CLOSE = {
|
|
286
11
|
badSignature: 4601,
|
|
287
12
|
hostNotAllowed: 4603,
|
|
@@ -293,10 +18,10 @@ var RELAY_CLOSE = {
|
|
|
293
18
|
function encodeRelayFrame(frame) {
|
|
294
19
|
return JSON.stringify(frame);
|
|
295
20
|
}
|
|
296
|
-
var
|
|
21
|
+
var isString = (value) => typeof value === "string";
|
|
297
22
|
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
298
|
-
var isBoundedString = (value, max = 64) =>
|
|
299
|
-
function
|
|
23
|
+
var isBoundedString = (value, max = 64) => isString(value) && value.length > 0 && value.length <= max;
|
|
24
|
+
function decodeJson(text) {
|
|
300
25
|
try {
|
|
301
26
|
return JSON.parse(text);
|
|
302
27
|
} catch {
|
|
@@ -304,16 +29,16 @@ function decodeJson2(text) {
|
|
|
304
29
|
}
|
|
305
30
|
}
|
|
306
31
|
function decodeRelayToHost(text) {
|
|
307
|
-
const value =
|
|
32
|
+
const value = decodeJson(text);
|
|
308
33
|
switch (value?.t) {
|
|
309
34
|
case "challenge":
|
|
310
|
-
if (!
|
|
35
|
+
if (!isString(value.nonce))
|
|
311
36
|
throw new Error("malformed challenge");
|
|
312
37
|
return { t: "challenge", nonce: value.nonce };
|
|
313
38
|
case "registered":
|
|
314
39
|
return decodeRegistered(value);
|
|
315
40
|
case "incoming":
|
|
316
|
-
if (!
|
|
41
|
+
if (!isString(value.connId))
|
|
317
42
|
throw new Error("malformed incoming");
|
|
318
43
|
return { t: "incoming", connId: value.connId };
|
|
319
44
|
case "pairing-request":
|
|
@@ -323,27 +48,27 @@ function decodeRelayToHost(text) {
|
|
|
323
48
|
case "push-result":
|
|
324
49
|
return decodePushResult(value);
|
|
325
50
|
case "joined":
|
|
326
|
-
if (!
|
|
51
|
+
if (!isString(value.username))
|
|
327
52
|
throw new Error("malformed joined");
|
|
328
53
|
return { t: "joined", username: value.username };
|
|
329
54
|
case "left":
|
|
330
55
|
return { t: "left" };
|
|
331
56
|
case "refused":
|
|
332
|
-
if (!
|
|
57
|
+
if (!isString(value.reason))
|
|
333
58
|
throw new Error("malformed refused");
|
|
334
59
|
return { t: "refused", reason: value.reason };
|
|
335
60
|
default:
|
|
336
61
|
throw new Error("unknown relay frame");
|
|
337
62
|
}
|
|
338
63
|
}
|
|
339
|
-
var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) =>
|
|
64
|
+
var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString(entry) && entry.length <= 64);
|
|
340
65
|
function decodeRegistered(value) {
|
|
341
66
|
if (value.features !== undefined && !isFeatureList(value.features))
|
|
342
67
|
throw new Error("malformed registered");
|
|
343
68
|
const declared = value.features === undefined ? {} : { features: value.features };
|
|
344
69
|
if (value.push === undefined)
|
|
345
70
|
return { t: "registered", ...declared };
|
|
346
|
-
if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(
|
|
71
|
+
if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString)) {
|
|
347
72
|
throw new Error("malformed registered");
|
|
348
73
|
}
|
|
349
74
|
const push = value.push.filter((entry) => entry === "production" || entry === "development");
|
|
@@ -379,8 +104,17 @@ function decodePushResult(value) {
|
|
|
379
104
|
}
|
|
380
105
|
return { t: "push-result", requestId: value.requestId, result: value.result, reason: value.reason };
|
|
381
106
|
}
|
|
107
|
+
function decodePairingEndpointSet(value) {
|
|
108
|
+
const parsed = parseEndpointSet(value);
|
|
109
|
+
if (parsed.kind === "absent" || parsed.kind === "unsupported")
|
|
110
|
+
return;
|
|
111
|
+
if (parsed.kind === "malformed" || parsed.endpointSet.endpoints.length === 0) {
|
|
112
|
+
throw new Error("malformed pairing");
|
|
113
|
+
}
|
|
114
|
+
return parsed.endpointSet;
|
|
115
|
+
}
|
|
382
116
|
function decodeHostToRelay(text) {
|
|
383
|
-
const value =
|
|
117
|
+
const value = decodeJson(text);
|
|
384
118
|
switch (value?.t) {
|
|
385
119
|
case "register":
|
|
386
120
|
return decodeRegister(value);
|
|
@@ -390,7 +124,13 @@ function decodeHostToRelay(text) {
|
|
|
390
124
|
if (!isBoundedString(value.requestId) || !isBoundedString(value.secret) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) {
|
|
391
125
|
throw new Error("malformed pairing");
|
|
392
126
|
}
|
|
393
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
t: "pairing",
|
|
129
|
+
requestId: value.requestId,
|
|
130
|
+
secret: value.secret,
|
|
131
|
+
expiresAt: value.expiresAt,
|
|
132
|
+
...value.endpointSet === undefined ? {} : { endpointSet: decodePairingEndpointSet(value.endpointSet) }
|
|
133
|
+
};
|
|
394
134
|
case "pairing-refused":
|
|
395
135
|
if (!isBoundedString(value.requestId) || !isBoundedString(value.reason)) {
|
|
396
136
|
throw new Error("malformed pairing-refused");
|
|
@@ -424,18 +164,18 @@ function decodePush(value) {
|
|
|
424
164
|
};
|
|
425
165
|
}
|
|
426
166
|
function decodeRegister(value) {
|
|
427
|
-
if (value.t !== "register" || !
|
|
167
|
+
if (value.t !== "register" || !isString(value.hostId) || !isString(value.signature)) {
|
|
428
168
|
throw new Error("malformed register");
|
|
429
169
|
}
|
|
430
170
|
if (value.op !== undefined && value.op !== "join" && value.op !== "leave")
|
|
431
171
|
throw new Error("malformed register");
|
|
432
|
-
if (value.signatureV2 !== undefined && !
|
|
172
|
+
if (value.signatureV2 !== undefined && !isString(value.signatureV2))
|
|
433
173
|
throw new Error("malformed register");
|
|
434
|
-
if (value.noisePublicKey !== undefined && !
|
|
174
|
+
if (value.noisePublicKey !== undefined && !isString(value.noisePublicKey))
|
|
435
175
|
throw new Error("malformed register");
|
|
436
|
-
if (value.joinToken !== undefined && !
|
|
176
|
+
if (value.joinToken !== undefined && !isString(value.joinToken))
|
|
437
177
|
throw new Error("malformed register");
|
|
438
|
-
if (value.name !== undefined && !
|
|
178
|
+
if (value.name !== undefined && !isString(value.name))
|
|
439
179
|
throw new Error("malformed register");
|
|
440
180
|
if (value.features !== undefined && !isFeatureList(value.features))
|
|
441
181
|
throw new Error("malformed register");
|
|
@@ -456,9 +196,9 @@ function relayHostId(publicKey) {
|
|
|
456
196
|
throw new Error("Ed25519 public key must be 32 bytes");
|
|
457
197
|
return toBase64Url(publicKey);
|
|
458
198
|
}
|
|
459
|
-
var
|
|
199
|
+
var utf8 = new TextEncoder;
|
|
460
200
|
function relayChallengeMessage(args) {
|
|
461
|
-
return
|
|
201
|
+
return utf8.encode(`pinomad-relay/1
|
|
462
202
|
${args.relayOrigin}
|
|
463
203
|
${args.hostId}
|
|
464
204
|
${args.nonce}`);
|
|
@@ -474,7 +214,7 @@ function verifyRelayChallenge(signature, args) {
|
|
|
474
214
|
}
|
|
475
215
|
}
|
|
476
216
|
function relayChallengeMessageV2(args) {
|
|
477
|
-
return
|
|
217
|
+
return utf8.encode(`pinomad-relay/2
|
|
478
218
|
${args.relayOrigin}
|
|
479
219
|
${args.hostId}
|
|
480
220
|
${args.nonce}
|
|
@@ -493,7 +233,7 @@ function verifyRelayChallengeV2(signature, args) {
|
|
|
493
233
|
|
|
494
234
|
// apps/host/src/relay-link.ts
|
|
495
235
|
import { randomBytes } from "node:crypto";
|
|
496
|
-
import { WebSocket
|
|
236
|
+
import { WebSocket } from "ws";
|
|
497
237
|
function relayWsBase(origin) {
|
|
498
238
|
const normalized = origin.replace(/\/+$/, "");
|
|
499
239
|
if (normalized.startsWith("https://"))
|
|
@@ -510,6 +250,8 @@ var REJECTION_REASONS = {
|
|
|
510
250
|
var PUSH_TIMEOUT_MS = 1e4;
|
|
511
251
|
function startRelayLink(options) {
|
|
512
252
|
const origin = options.origin.replace(/\/+$/, "");
|
|
253
|
+
if (origin.includes("#"))
|
|
254
|
+
throw new Error(`relay origin must not include a fragment: ${origin}`);
|
|
513
255
|
const wsBase = relayWsBase(origin);
|
|
514
256
|
const hostId = relayHostId(options.signingKey.publicKey);
|
|
515
257
|
const minDelay = options.reconnectDelayMs?.min ?? 1000;
|
|
@@ -536,7 +278,7 @@ function startRelayLink(options) {
|
|
|
536
278
|
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
537
279
|
const pingTimer = setInterval(() => {
|
|
538
280
|
for (const socket of watched) {
|
|
539
|
-
if (socket.readyState !==
|
|
281
|
+
if (socket.readyState !== WebSocket.OPEN)
|
|
540
282
|
continue;
|
|
541
283
|
if (ponged.get(socket) === true) {
|
|
542
284
|
ponged.set(socket, false);
|
|
@@ -556,7 +298,7 @@ function startRelayLink(options) {
|
|
|
556
298
|
function connectControl() {
|
|
557
299
|
if (stopped)
|
|
558
300
|
return;
|
|
559
|
-
const socket = new
|
|
301
|
+
const socket = new WebSocket(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
|
|
560
302
|
control = socket;
|
|
561
303
|
pushEnvironments = undefined;
|
|
562
304
|
nameDeclared = false;
|
|
@@ -630,11 +372,23 @@ function startRelayLink(options) {
|
|
|
630
372
|
break;
|
|
631
373
|
}
|
|
632
374
|
accountOffer().then((result) => {
|
|
633
|
-
if (socket !== control || socket.readyState !==
|
|
375
|
+
if (socket !== control || socket.readyState !== WebSocket.OPEN)
|
|
376
|
+
return;
|
|
377
|
+
const checked = result.ok && result.endpointSet !== undefined ? parseEndpointSet(result.endpointSet) : undefined;
|
|
378
|
+
if (result.ok && (checked?.kind === "malformed" || checked?.kind === "valid" && checked.endpointSet.endpoints.length === 0)) {
|
|
379
|
+
refused("invalid_endpoint");
|
|
634
380
|
return;
|
|
635
|
-
|
|
381
|
+
}
|
|
382
|
+
const endpointSet = checked?.kind === "valid" ? checked.endpointSet : undefined;
|
|
383
|
+
socket.send(encodeRelayFrame(result.ok ? {
|
|
384
|
+
t: "pairing",
|
|
385
|
+
requestId: frame.requestId,
|
|
386
|
+
secret: result.secret,
|
|
387
|
+
expiresAt: result.expiresAt,
|
|
388
|
+
...endpointSet === undefined ? {} : { endpointSet }
|
|
389
|
+
} : { t: "pairing-refused", requestId: frame.requestId, reason: result.reason }));
|
|
636
390
|
}, () => {
|
|
637
|
-
if (socket === control && socket.readyState ===
|
|
391
|
+
if (socket === control && socket.readyState === WebSocket.OPEN)
|
|
638
392
|
refused("error");
|
|
639
393
|
});
|
|
640
394
|
break;
|
|
@@ -643,7 +397,7 @@ function startRelayLink(options) {
|
|
|
643
397
|
if (pendingAccepts >= maxPendingAccepts)
|
|
644
398
|
break;
|
|
645
399
|
pendingAccepts += 1;
|
|
646
|
-
const data2 = new
|
|
400
|
+
const data2 = new WebSocket(`${wsBase}/accept/${frame.connId}`, { handshakeTimeout: pingIntervalMs });
|
|
647
401
|
watch(data2);
|
|
648
402
|
const release = () => {
|
|
649
403
|
pendingAccepts -= 1;
|
|
@@ -686,7 +440,7 @@ function startRelayLink(options) {
|
|
|
686
440
|
if (name === undefined || name === reportedName)
|
|
687
441
|
return;
|
|
688
442
|
const socket = control;
|
|
689
|
-
if (socket === undefined || socket.readyState !==
|
|
443
|
+
if (socket === undefined || socket.readyState !== WebSocket.OPEN || !nameDeclared)
|
|
690
444
|
return;
|
|
691
445
|
reportedName = name;
|
|
692
446
|
socket.send(encodeRelayFrame({ t: "name", name }));
|
|
@@ -698,7 +452,7 @@ function startRelayLink(options) {
|
|
|
698
452
|
state: () => state,
|
|
699
453
|
sendPush(request) {
|
|
700
454
|
const socket = control;
|
|
701
|
-
if (stopped || socket === undefined || socket.readyState !==
|
|
455
|
+
if (stopped || socket === undefined || socket.readyState !== WebSocket.OPEN || state.status !== "registered") {
|
|
702
456
|
return Promise.resolve({ result: "retry", reason: "control disconnected" });
|
|
703
457
|
}
|
|
704
458
|
const envs = pushEnvironments;
|
|
@@ -737,4 +491,4 @@ function startRelayLink(options) {
|
|
|
737
491
|
};
|
|
738
492
|
}
|
|
739
493
|
|
|
740
|
-
export {
|
|
494
|
+
export { RELAY_CLOSE, encodeRelayFrame, decodeRelayToHost, decodeHostToRelay, relayHostId, signRelayChallenge, verifyRelayChallenge, signRelayChallengeV2, verifyRelayChallengeV2, relayWsBase, startRelayLink };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connectRemoteDurable
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import"./chunk-
|
|
5
|
-
import
|
|
3
|
+
} from "./chunk-a4b61tx5.js";
|
|
4
|
+
import"./chunk-jgd38rna.js";
|
|
5
|
+
import {
|
|
6
|
+
isLoopbackUrl
|
|
7
|
+
} from "./chunk-sygeb9qb.js";
|
|
6
8
|
import {
|
|
7
9
|
hostAddress
|
|
8
10
|
} from "./chunk-ngf1zatm.js";
|
|
@@ -10,16 +12,30 @@ import"./chunk-df32avw6.js";
|
|
|
10
12
|
|
|
11
13
|
// apps/host/src/cli/pair.ts
|
|
12
14
|
import { renderUnicodeCompact } from "uqr";
|
|
15
|
+
function pairingQr(link) {
|
|
16
|
+
try {
|
|
17
|
+
return renderUnicodeCompact(link);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error instanceof Error && error.message === "Data too long")
|
|
20
|
+
return;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
async function printPairing({ url, token }) {
|
|
14
25
|
const remote = await connectRemoteDurable({ url, token, reconnectDelayMs: { min: 200, max: 2000 } });
|
|
15
26
|
try {
|
|
16
27
|
const { url: pairing, expiresAt } = await remote.controller.createPairing();
|
|
17
|
-
if (
|
|
28
|
+
if (isLoopbackUrl(pairing)) {
|
|
18
29
|
console.log(pairing);
|
|
19
30
|
console.log("This link only works on this machine — give it to the desktop app.");
|
|
20
31
|
console.log("To pair a phone, restart the host with --remote-port or --relay, then run pair again.");
|
|
21
32
|
} else {
|
|
22
|
-
|
|
33
|
+
const qr = pairingQr(pairing);
|
|
34
|
+
if (qr === undefined) {
|
|
35
|
+
console.log("This link is too long for a QR code. Copy it below and paste it into the app instead.");
|
|
36
|
+
} else {
|
|
37
|
+
console.log(qr);
|
|
38
|
+
}
|
|
23
39
|
console.log(pairing);
|
|
24
40
|
}
|
|
25
41
|
console.log(`Expires at ${new Date(expiresAt).toLocaleTimeString()}`);
|
|
@@ -40,5 +56,6 @@ if (false)
|
|
|
40
56
|
;
|
|
41
57
|
export {
|
|
42
58
|
printPairing,
|
|
59
|
+
pairingQr,
|
|
43
60
|
main
|
|
44
61
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/host/src/distribution.ts
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
var packagedVersion = "0.
|
|
3
|
+
var packagedVersion = "0.8.0";
|
|
4
4
|
var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
|
|
5
5
|
var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
|
|
6
6
|
var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
main,
|
|
4
4
|
restartService
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-73vxwjs7.js";
|
|
6
6
|
import"./chunk-kzbzacz9.js";
|
|
7
|
-
import"./chunk-
|
|
8
|
-
import"./chunk-
|
|
9
|
-
import"./chunk-
|
|
10
|
-
import"./chunk-
|
|
7
|
+
import"./chunk-p56xf1r8.js";
|
|
8
|
+
import"./chunk-a4b61tx5.js";
|
|
9
|
+
import"./chunk-jgd38rna.js";
|
|
10
|
+
import"./chunk-sygeb9qb.js";
|
|
11
11
|
import"./chunk-ngf1zatm.js";
|
|
12
12
|
import"./chunk-df32avw6.js";
|
|
13
13
|
export {
|