pinomad 0.7.0 → 0.9.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 +7 -58
- package/dist/{chunk-8yme2wep.js → chunk-014b7ne7.js} +1 -1
- package/dist/{chunk-07769pqx.js → chunk-0tesv2sr.js} +8 -8
- package/dist/{chunk-ywmm63ks.js → chunk-3p1eysm9.js} +4 -2
- package/dist/chunk-4sepcfpy.js +2994 -0
- package/dist/{chunk-0xh6cy9v.js → chunk-5gp9svzs.js} +5 -3
- package/dist/{chunk-12kaxggg.js → chunk-76pvsg2d.js} +2 -2
- package/dist/{chunk-1nyzr0mg.js → chunk-ctzgjctt.js} +2395 -839
- package/dist/{chunk-e4ahjbmz.js → chunk-jgd38rna.js} +210 -7
- package/dist/{chunk-7qb9tq54.js → chunk-khcpv54k.js} +64 -310
- package/dist/{chunk-1t58fh0k.js → chunk-m824ds0q.js} +5 -5
- package/dist/{chunk-sz3rzk55.js → chunk-qgexy01f.js} +5 -5
- package/dist/chunk-sygeb9qb.js +430 -0
- package/dist/{chunk-b1h374jr.js → chunk-v414b5pp.js} +8 -2
- package/dist/{chunk-m4c5gvce.js → chunk-vax4zspw.js} +22 -5
- package/dist/{chunk-6ysezfy5.js → chunk-vxmj0200.js} +305 -23
- package/dist/{chunk-rtwxjb90.js → chunk-vyvyaavr.js} +1 -1
- package/dist/{chunk-r6w5aqkw.js → chunk-wb10efyq.js} +17 -0
- package/dist/{chunk-fbkd73zr.js → chunk-yhf7bj5q.js} +9 -9
- package/dist/pinomad.js +10 -10
- package/package.json +12 -8
- package/web/assets/index-1g60bhv2.js +395 -0
- package/web/assets/{index-C0rwAo0H.css → index-DoyHMPwf.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
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// packages/protocol/src/endpoints.ts
|
|
2
|
+
var ENDPOINT_SET_SCHEMA_VERSION = 1;
|
|
3
|
+
var MAX_HOST_ENDPOINTS = 4;
|
|
4
|
+
var MAX_HOST_ENDPOINT_URL_LENGTH = 1024;
|
|
5
|
+
function isLoopbackHostname(hostname) {
|
|
6
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
7
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
8
|
+
}
|
|
9
|
+
function isLoopbackUrl(url) {
|
|
10
|
+
try {
|
|
11
|
+
return isLoopbackHostname(new URL(url).hostname);
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
var isSocketUrl = (value) => {
|
|
18
|
+
if (value.length === 0)
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
const protocol = new URL(value).protocol;
|
|
22
|
+
return protocol === "ws:" || protocol === "wss:";
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function parseEndpointSet(value) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return { kind: "absent" };
|
|
30
|
+
if (!isRecord(value))
|
|
31
|
+
return { kind: "malformed" };
|
|
32
|
+
const version = value.schemaVersion;
|
|
33
|
+
if (typeof version !== "number")
|
|
34
|
+
return { kind: "malformed" };
|
|
35
|
+
if (version !== ENDPOINT_SET_SCHEMA_VERSION)
|
|
36
|
+
return { kind: "unsupported" };
|
|
37
|
+
const raw = value.endpoints;
|
|
38
|
+
if (!Array.isArray(raw))
|
|
39
|
+
return { kind: "malformed" };
|
|
40
|
+
if (raw.length > MAX_HOST_ENDPOINTS)
|
|
41
|
+
return { kind: "malformed" };
|
|
42
|
+
const endpoints = [];
|
|
43
|
+
const roles = new Map;
|
|
44
|
+
for (const entry of raw) {
|
|
45
|
+
if (!isRecord(entry))
|
|
46
|
+
return { kind: "malformed" };
|
|
47
|
+
const transport = entry.transport;
|
|
48
|
+
if (typeof transport !== "string")
|
|
49
|
+
return { kind: "malformed" };
|
|
50
|
+
if (transport !== "webSocket")
|
|
51
|
+
continue;
|
|
52
|
+
const role = entry.role;
|
|
53
|
+
if (role !== "direct" && role !== "relay")
|
|
54
|
+
return { kind: "malformed" };
|
|
55
|
+
const url = entry.url;
|
|
56
|
+
if (typeof url !== "string" || url.length > MAX_HOST_ENDPOINT_URL_LENGTH)
|
|
57
|
+
return { kind: "malformed" };
|
|
58
|
+
if (!isSocketUrl(url))
|
|
59
|
+
return { kind: "malformed" };
|
|
60
|
+
const prior = roles.get(url);
|
|
61
|
+
if (prior !== undefined) {
|
|
62
|
+
if (prior !== role)
|
|
63
|
+
return { kind: "malformed" };
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
roles.set(url, role);
|
|
67
|
+
endpoints.push({ transport: "webSocket", role, url });
|
|
68
|
+
}
|
|
69
|
+
if (endpoints.length === 0 && raw.length > 0)
|
|
70
|
+
return { kind: "unsupported" };
|
|
71
|
+
return { kind: "valid", endpointSet: { schemaVersion: ENDPOINT_SET_SCHEMA_VERSION, endpoints } };
|
|
72
|
+
}
|
|
73
|
+
function encodeEndpointSetParam(endpointSet) {
|
|
74
|
+
return encodeURIComponent(JSON.stringify(endpointSet));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// packages/protocol/src/settings.ts
|
|
78
|
+
var HOST_SETTING_KEY_SET = {
|
|
79
|
+
defaultModel: true,
|
|
80
|
+
defaultThinkingLevel: true,
|
|
81
|
+
steeringMode: true,
|
|
82
|
+
followUpMode: true,
|
|
83
|
+
acceptAccountDevices: true,
|
|
84
|
+
rtcStun: true,
|
|
85
|
+
hostName: true
|
|
86
|
+
};
|
|
87
|
+
var HOST_SETTING_KEYS = Object.keys(HOST_SETTING_KEY_SET);
|
|
88
|
+
var THINKING_LEVEL_KEYS = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
|
|
89
|
+
var THINKING_LEVELS = Object.keys(THINKING_LEVEL_KEYS);
|
|
90
|
+
var isThinkingLevel = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(THINKING_LEVEL_KEYS, value);
|
|
91
|
+
var isQueueMode = (value) => value === "all" || value === "one-at-a-time";
|
|
92
|
+
var HOST_NAME_FALLBACK = "pinomad";
|
|
93
|
+
var HOST_NAME_MAX_UNITS = 64;
|
|
94
|
+
var isHostNameControl = (code) => code < 32 || code >= 127 && code <= 159;
|
|
95
|
+
function cleanHostName(name) {
|
|
96
|
+
let cleaned = name;
|
|
97
|
+
for (;; ) {
|
|
98
|
+
const local = cleaned.endsWith(".local") ? cleaned.slice(0, -".local".length) : cleaned;
|
|
99
|
+
const next = [...local.trim()].filter((c) => !isHostNameControl(c.codePointAt(0))).join("").slice(0, HOST_NAME_MAX_UNITS);
|
|
100
|
+
if (next === cleaned)
|
|
101
|
+
return next;
|
|
102
|
+
cleaned = next;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function normalizeHostName(name) {
|
|
106
|
+
const cleaned = cleanHostName(name);
|
|
107
|
+
return cleaned === "" ? HOST_NAME_FALLBACK : cleaned;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// packages/protocol/src/noise.ts
|
|
111
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
112
|
+
import { x25519 } from "@noble/curves/ed25519.js";
|
|
113
|
+
import { blake2s } from "@noble/hashes/blake2.js";
|
|
114
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
115
|
+
var PROTOCOL_NAME = new TextEncoder().encode("Noise_IK_25519_ChaChaPoly_BLAKE2s");
|
|
116
|
+
var KEY_LENGTH = 32;
|
|
117
|
+
var TAG_LENGTH = 16;
|
|
118
|
+
var MAX_MESSAGE = 65535;
|
|
119
|
+
var MAX_NONCE = 2n ** 64n - 1n;
|
|
120
|
+
var hash = (data) => blake2s(data, { dkLen: KEY_LENGTH });
|
|
121
|
+
var concat = (...parts) => {
|
|
122
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
123
|
+
let offset = 0;
|
|
124
|
+
for (const part of parts) {
|
|
125
|
+
out.set(part, offset);
|
|
126
|
+
offset += part.length;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
};
|
|
130
|
+
function hkdf(chainingKey, ikm, outputs) {
|
|
131
|
+
const tempKey = hmac(blake2s, chainingKey, ikm);
|
|
132
|
+
const out1 = hmac(blake2s, tempKey, new Uint8Array([1]));
|
|
133
|
+
const out2 = hmac(blake2s, tempKey, concat(out1, new Uint8Array([2])));
|
|
134
|
+
return [out1, out2];
|
|
135
|
+
}
|
|
136
|
+
function generateKeyPair() {
|
|
137
|
+
return keyPairFromPrivate(x25519.utils.randomSecretKey());
|
|
138
|
+
}
|
|
139
|
+
function keyPairFromPrivate(privateKey) {
|
|
140
|
+
if (privateKey.length !== KEY_LENGTH)
|
|
141
|
+
throw new Error(`X25519 private key must be ${KEY_LENGTH} bytes`);
|
|
142
|
+
return { publicKey: x25519.getPublicKey(privateKey), privateKey };
|
|
143
|
+
}
|
|
144
|
+
var dh = (pair, remotePublic) => x25519.getSharedSecret(pair.privateKey, remotePublic);
|
|
145
|
+
|
|
146
|
+
class NoiseCipherState {
|
|
147
|
+
#key;
|
|
148
|
+
#nonce = 0n;
|
|
149
|
+
setKey(key) {
|
|
150
|
+
this.#key = key;
|
|
151
|
+
this.#nonce = 0n;
|
|
152
|
+
}
|
|
153
|
+
encrypt(plaintext, ad = new Uint8Array(0)) {
|
|
154
|
+
const key = this.#key;
|
|
155
|
+
if (key === undefined)
|
|
156
|
+
return plaintext;
|
|
157
|
+
return chacha20poly1305(key, this.#takeNonce(), ad).encrypt(plaintext);
|
|
158
|
+
}
|
|
159
|
+
decrypt(ciphertext, ad = new Uint8Array(0)) {
|
|
160
|
+
const key = this.#key;
|
|
161
|
+
if (key === undefined)
|
|
162
|
+
return ciphertext;
|
|
163
|
+
return chacha20poly1305(key, this.#takeNonce(), ad).decrypt(ciphertext);
|
|
164
|
+
}
|
|
165
|
+
#takeNonce() {
|
|
166
|
+
if (this.#nonce === MAX_NONCE)
|
|
167
|
+
throw new Error("nonce exhausted");
|
|
168
|
+
const nonce = new Uint8Array(12);
|
|
169
|
+
new DataView(nonce.buffer).setBigUint64(4, this.#nonce, true);
|
|
170
|
+
this.#nonce += 1n;
|
|
171
|
+
return nonce;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class SymmetricState {
|
|
176
|
+
#ck;
|
|
177
|
+
#h;
|
|
178
|
+
cipher = new NoiseCipherState;
|
|
179
|
+
constructor() {
|
|
180
|
+
const name = PROTOCOL_NAME;
|
|
181
|
+
this.#h = name.length <= KEY_LENGTH ? concat(name, new Uint8Array(KEY_LENGTH - name.length)) : hash(name);
|
|
182
|
+
this.#ck = this.#h.slice();
|
|
183
|
+
}
|
|
184
|
+
mixKey(ikm) {
|
|
185
|
+
const [ck, tempKey] = hkdf(this.#ck, ikm, 2);
|
|
186
|
+
this.#ck = ck;
|
|
187
|
+
this.cipher.setKey(tempKey.subarray(0, KEY_LENGTH));
|
|
188
|
+
}
|
|
189
|
+
mixHash(data) {
|
|
190
|
+
this.#h = hash(concat(this.#h, data));
|
|
191
|
+
}
|
|
192
|
+
encryptAndHash(plaintext) {
|
|
193
|
+
const ciphertext = this.cipher.encrypt(plaintext, this.#h);
|
|
194
|
+
this.mixHash(ciphertext);
|
|
195
|
+
return ciphertext;
|
|
196
|
+
}
|
|
197
|
+
decryptAndHash(ciphertext) {
|
|
198
|
+
const plaintext = this.cipher.decrypt(ciphertext, this.#h);
|
|
199
|
+
this.mixHash(ciphertext);
|
|
200
|
+
return plaintext;
|
|
201
|
+
}
|
|
202
|
+
get handshakeHash() {
|
|
203
|
+
return this.#h;
|
|
204
|
+
}
|
|
205
|
+
split() {
|
|
206
|
+
const [key1, key2] = hkdf(this.#ck, new Uint8Array(0), 2);
|
|
207
|
+
const first = new NoiseCipherState;
|
|
208
|
+
first.setKey(key1.subarray(0, KEY_LENGTH));
|
|
209
|
+
const second = new NoiseCipherState;
|
|
210
|
+
second.setKey(key2.subarray(0, KEY_LENGTH));
|
|
211
|
+
return [first, second];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function take(message, size) {
|
|
215
|
+
if (message.offset + size > message.buffer.length)
|
|
216
|
+
throw new Error("malformed Noise message");
|
|
217
|
+
const slice = message.buffer.subarray(message.offset, message.offset + size);
|
|
218
|
+
message.offset += size;
|
|
219
|
+
return slice;
|
|
220
|
+
}
|
|
221
|
+
function writeMessage1(state, buffer, own, remoteStatic) {
|
|
222
|
+
buffer.push(own.ephemeral.publicKey);
|
|
223
|
+
state.mixHash(own.ephemeral.publicKey);
|
|
224
|
+
state.mixKey(dh(own.ephemeral, remoteStatic));
|
|
225
|
+
buffer.push(state.encryptAndHash(own.static.publicKey));
|
|
226
|
+
state.mixKey(dh(own.static, remoteStatic));
|
|
227
|
+
}
|
|
228
|
+
function readMessage1(state, message, own) {
|
|
229
|
+
const remoteEphemeral = take(message, KEY_LENGTH).slice();
|
|
230
|
+
state.mixHash(remoteEphemeral);
|
|
231
|
+
state.mixKey(dh(own.static, remoteEphemeral));
|
|
232
|
+
const remoteStatic = state.decryptAndHash(take(message, KEY_LENGTH + TAG_LENGTH)).slice();
|
|
233
|
+
if (remoteStatic.length !== KEY_LENGTH)
|
|
234
|
+
throw new Error("bad remote static key");
|
|
235
|
+
state.mixKey(dh(own.static, remoteStatic));
|
|
236
|
+
return { remoteEphemeral, remoteStatic };
|
|
237
|
+
}
|
|
238
|
+
function writeMessage2(state, buffer, own, remoteEphemeral, remoteStatic) {
|
|
239
|
+
buffer.push(own.ephemeral.publicKey);
|
|
240
|
+
state.mixHash(own.ephemeral.publicKey);
|
|
241
|
+
state.mixKey(dh(own.ephemeral, remoteEphemeral));
|
|
242
|
+
state.mixKey(dh(own.ephemeral, remoteStatic));
|
|
243
|
+
}
|
|
244
|
+
function readMessage2Tokens(state, message, own) {
|
|
245
|
+
const remoteEphemeral = take(message, KEY_LENGTH).slice();
|
|
246
|
+
state.mixHash(remoteEphemeral);
|
|
247
|
+
state.mixKey(dh(own.ephemeral, remoteEphemeral));
|
|
248
|
+
state.mixKey(dh(own.static, remoteEphemeral));
|
|
249
|
+
return remoteEphemeral;
|
|
250
|
+
}
|
|
251
|
+
var checkLength = (message, what) => {
|
|
252
|
+
if (message.length > MAX_MESSAGE)
|
|
253
|
+
throw new Error(`${what} exceeds ${MAX_MESSAGE} bytes`);
|
|
254
|
+
};
|
|
255
|
+
function initialize(prologue, responderStatic) {
|
|
256
|
+
const state = new SymmetricState;
|
|
257
|
+
state.mixHash(prologue);
|
|
258
|
+
state.mixHash(responderStatic);
|
|
259
|
+
return state;
|
|
260
|
+
}
|
|
261
|
+
function initiateIK(options) {
|
|
262
|
+
const ephemeral = options.ephemeral ?? generateKeyPair();
|
|
263
|
+
const state = initialize(options.prologue, options.remoteStatic);
|
|
264
|
+
return {
|
|
265
|
+
writeMessage1(payload) {
|
|
266
|
+
const buffer = [];
|
|
267
|
+
writeMessage1(state, buffer, { static: options.static, ephemeral }, options.remoteStatic);
|
|
268
|
+
buffer.push(state.encryptAndHash(payload));
|
|
269
|
+
const message = concat(...buffer);
|
|
270
|
+
checkLength(message, "handshake message");
|
|
271
|
+
return message;
|
|
272
|
+
},
|
|
273
|
+
readMessage2(message) {
|
|
274
|
+
checkLength(message, "handshake message");
|
|
275
|
+
const reader = { buffer: message, offset: 0 };
|
|
276
|
+
readMessage2Tokens(state, reader, { ephemeral, static: options.static });
|
|
277
|
+
const payload = state.decryptAndHash(take(reader, message.length - reader.offset));
|
|
278
|
+
const [send, receive] = state.split();
|
|
279
|
+
return { payload, result: { send, receive, handshakeHash: state.handshakeHash } };
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function respondIK(options) {
|
|
284
|
+
const ephemeral = options.ephemeral ?? generateKeyPair();
|
|
285
|
+
const state = initialize(options.prologue, options.static.publicKey);
|
|
286
|
+
let remoteEphemeral;
|
|
287
|
+
let remoteStatic;
|
|
288
|
+
return {
|
|
289
|
+
readMessage1(message) {
|
|
290
|
+
checkLength(message, "handshake message");
|
|
291
|
+
const reader = { buffer: message, offset: 0 };
|
|
292
|
+
const remote = readMessage1(state, reader, { static: options.static });
|
|
293
|
+
remoteEphemeral = remote.remoteEphemeral;
|
|
294
|
+
remoteStatic = remote.remoteStatic;
|
|
295
|
+
const payload = state.decryptAndHash(take(reader, message.length - reader.offset));
|
|
296
|
+
return { payload, remoteStatic };
|
|
297
|
+
},
|
|
298
|
+
writeMessage2(payload) {
|
|
299
|
+
if (remoteEphemeral === undefined || remoteStatic === undefined)
|
|
300
|
+
throw new Error("message 2 before message 1");
|
|
301
|
+
const buffer = [];
|
|
302
|
+
writeMessage2(state, buffer, { ephemeral }, remoteEphemeral, remoteStatic);
|
|
303
|
+
buffer.push(state.encryptAndHash(payload));
|
|
304
|
+
const message = concat(...buffer);
|
|
305
|
+
checkLength(message, "handshake message");
|
|
306
|
+
const [receive, send] = state.split();
|
|
307
|
+
return { message, result: { send, receive, handshakeHash: state.handshakeHash } };
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// packages/protocol/src/secure-channel.ts
|
|
313
|
+
var SECURE_PROLOGUE = new TextEncoder().encode("pinomad/1");
|
|
314
|
+
var SIGNAL_PROLOGUE = new TextEncoder().encode("pinomad-signal/1");
|
|
315
|
+
var utf8 = new TextEncoder;
|
|
316
|
+
var utf8Decode = new TextDecoder("utf-8", { fatal: true });
|
|
317
|
+
var encodeClientHello = (hello) => utf8.encode(JSON.stringify(hello));
|
|
318
|
+
var encodeHostHello = (hello) => utf8.encode(JSON.stringify(hello));
|
|
319
|
+
function decodeJson(bytes) {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(utf8Decode.decode(bytes));
|
|
322
|
+
} catch {
|
|
323
|
+
throw new Error("handshake payload is not JSON");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
var isString = (value) => typeof value === "string";
|
|
327
|
+
function decodeClientHello(bytes) {
|
|
328
|
+
const value = decodeJson(bytes);
|
|
329
|
+
if (value?.v !== 1)
|
|
330
|
+
throw new Error("unsupported client hello");
|
|
331
|
+
const pair = value.pair;
|
|
332
|
+
if (pair === undefined)
|
|
333
|
+
return { v: 1 };
|
|
334
|
+
const secret = pair.secret;
|
|
335
|
+
const name = pair.name;
|
|
336
|
+
if (!isString(secret) || !isString(name))
|
|
337
|
+
throw new Error("malformed pair");
|
|
338
|
+
return { v: 1, pair: { secret, name } };
|
|
339
|
+
}
|
|
340
|
+
function decodeHostHello(bytes) {
|
|
341
|
+
const value = decodeJson(bytes);
|
|
342
|
+
if (value?.v !== 1)
|
|
343
|
+
throw new Error("unsupported host hello");
|
|
344
|
+
return { v: 1 };
|
|
345
|
+
}
|
|
346
|
+
var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
347
|
+
function toBase64Url(bytes) {
|
|
348
|
+
let out = "";
|
|
349
|
+
for (let index = 0;index < bytes.length; index += 3) {
|
|
350
|
+
const a = bytes[index];
|
|
351
|
+
const b = bytes[index + 1];
|
|
352
|
+
const c = bytes[index + 2];
|
|
353
|
+
out += B64_ALPHABET[a >> 2] + B64_ALPHABET[(a & 3) << 4 | (b ?? 0) >> 4];
|
|
354
|
+
if (b !== undefined)
|
|
355
|
+
out += B64_ALPHABET[(b & 15) << 2 | (c ?? 0) >> 6];
|
|
356
|
+
if (c !== undefined)
|
|
357
|
+
out += B64_ALPHABET[c & 63];
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
function fromBase64Url(text) {
|
|
362
|
+
if (text.length % 4 === 1)
|
|
363
|
+
throw new Error("invalid base64url");
|
|
364
|
+
const lookup = new Map(Array.from(B64_ALPHABET, (char, index) => [char, index]));
|
|
365
|
+
const bytes = [];
|
|
366
|
+
for (let index = 0;index < text.length; index += 4) {
|
|
367
|
+
const quad = text.slice(index, index + 4);
|
|
368
|
+
const values = [...quad].map((char) => {
|
|
369
|
+
const value = lookup.get(char);
|
|
370
|
+
if (value === undefined)
|
|
371
|
+
throw new Error("invalid base64url");
|
|
372
|
+
return value;
|
|
373
|
+
});
|
|
374
|
+
const packed = values.reduce((bits, value, index2) => bits | value << 18 - index2 * 6, 0);
|
|
375
|
+
bytes.push(packed >> 16 & 255);
|
|
376
|
+
if (values.length > 2)
|
|
377
|
+
bytes.push(packed >> 8 & 255);
|
|
378
|
+
if (values.length > 3)
|
|
379
|
+
bytes.push(packed & 255);
|
|
380
|
+
}
|
|
381
|
+
return new Uint8Array(bytes);
|
|
382
|
+
}
|
|
383
|
+
var MAX_BODY = 65535 - 16 - 1;
|
|
384
|
+
|
|
385
|
+
class SecureSession {
|
|
386
|
+
#send;
|
|
387
|
+
#receive;
|
|
388
|
+
#pending = [];
|
|
389
|
+
constructor(result) {
|
|
390
|
+
this.#send = result.send;
|
|
391
|
+
this.#receive = result.receive;
|
|
392
|
+
}
|
|
393
|
+
seal(text) {
|
|
394
|
+
const body = utf8.encode(text);
|
|
395
|
+
const chunks = [];
|
|
396
|
+
for (let offset = 0;offset <= body.length; offset += MAX_BODY) {
|
|
397
|
+
const last = offset + MAX_BODY >= body.length;
|
|
398
|
+
const record = new Uint8Array(1 + Math.min(MAX_BODY, body.length - offset));
|
|
399
|
+
record[0] = last ? 0 : 1;
|
|
400
|
+
record.set(body.subarray(offset, offset + MAX_BODY), 1);
|
|
401
|
+
chunks.push(this.#send.encrypt(record));
|
|
402
|
+
if (last)
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
return chunks;
|
|
406
|
+
}
|
|
407
|
+
open(chunk) {
|
|
408
|
+
const record = this.#receive.decrypt(chunk);
|
|
409
|
+
const flag = record[0];
|
|
410
|
+
if (flag !== 0 && flag !== 1)
|
|
411
|
+
throw new Error("bad record flag");
|
|
412
|
+
this.#pending.push(record.subarray(1));
|
|
413
|
+
if (flag === 1)
|
|
414
|
+
return;
|
|
415
|
+
const whole = concatParts(this.#pending);
|
|
416
|
+
this.#pending = [];
|
|
417
|
+
return utf8Decode.decode(whole);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function concatParts(parts) {
|
|
421
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
422
|
+
let offset = 0;
|
|
423
|
+
for (const part of parts) {
|
|
424
|
+
out.set(part, offset);
|
|
425
|
+
offset += part.length;
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export { ENDPOINT_SET_SCHEMA_VERSION, MAX_HOST_ENDPOINT_URL_LENGTH, isLoopbackUrl, parseEndpointSet, encodeEndpointSetParam, HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_NAME_MAX_UNITS, cleanHostName, normalizeHostName, generateKeyPair, keyPairFromPrivate, initiateIK, respondIK, SECURE_PROLOGUE, SIGNAL_PROLOGUE, encodeClientHello, encodeHostHello, decodeClientHello, decodeHostHello, toBase64Url, fromBase64Url, SecureSession };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RTC_DEFAULT_STUN
|
|
3
|
+
} from "./chunk-jgd38rna.js";
|
|
1
4
|
import {
|
|
2
5
|
HOST_SETTING_KEYS,
|
|
3
6
|
cleanHostName,
|
|
4
7
|
isQueueMode,
|
|
5
8
|
isThinkingLevel,
|
|
6
9
|
normalizeHostName
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-sygeb9qb.js";
|
|
8
11
|
|
|
9
12
|
// apps/host/src/host-value.ts
|
|
10
13
|
function constantValue(value) {
|
|
@@ -63,12 +66,14 @@ var fileValueError = (key, value, models) => {
|
|
|
63
66
|
return isQueueMode(value) ? undefined : "malformed";
|
|
64
67
|
case "acceptAccountDevices":
|
|
65
68
|
return typeof value === "boolean" ? undefined : "malformed";
|
|
69
|
+
case "rtcStun":
|
|
70
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry !== "") ? undefined : "malformed";
|
|
66
71
|
case "hostName":
|
|
67
72
|
return typeof value === "string" ? undefined : "malformed";
|
|
68
73
|
}
|
|
69
74
|
};
|
|
70
75
|
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
71
|
-
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && a.hostName === b.hostName && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
76
|
+
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && (a.rtcStun === undefined || b.rtcStun === undefined ? a.rtcStun === b.rtcStun : sameList(a.rtcStun, b.rtcStun, (x, y) => x === y)) && a.hostName === b.hostName && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
72
77
|
var parse = (path, text) => {
|
|
73
78
|
let parsed;
|
|
74
79
|
try {
|
|
@@ -113,6 +118,7 @@ async function openHostSettings(options) {
|
|
|
113
118
|
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
114
119
|
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
115
120
|
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
121
|
+
rtcStun: fileValues.rtcStun ?? RTC_DEFAULT_STUN,
|
|
116
122
|
hostName: normalizeHostName(fileValues.hostName ?? hostname()),
|
|
117
123
|
custom
|
|
118
124
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connectRemoteDurable
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import"./chunk-
|
|
5
|
-
import
|
|
3
|
+
} from "./chunk-4sepcfpy.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
|
};
|