codex-relay 1.0.4 → 1.0.6
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/LICENSE +190 -0
- package/README.md +3 -2
- package/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +41 -3
- package/dist/cli.js +97 -4
- package/dist/paths.js +425 -3
- package/dist/src.js +226 -321
- package/package.json +5 -3
- package/src/api-schema.ts +61 -1
package/dist/paths.js
CHANGED
|
@@ -1,5 +1,427 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { connect } from "@tursodatabase/database";
|
|
5
|
+
import { fromByteArray, toByteArray } from "base64-js";
|
|
6
|
+
import { homedir, networkInterfaces, platform } from "node:os";
|
|
7
|
+
//#region src/pairing-store.ts
|
|
8
|
+
async function createTursoPairingSessionStore(path) {
|
|
9
|
+
if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
|
|
10
|
+
const db = await connect(path);
|
|
11
|
+
await db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS pairing_sessions (
|
|
13
|
+
token_hash TEXT PRIMARY KEY,
|
|
14
|
+
client_session_id TEXT,
|
|
15
|
+
client_name TEXT,
|
|
16
|
+
expires_at INTEGER NOT NULL,
|
|
17
|
+
key_epoch INTEGER,
|
|
18
|
+
mobile_to_server_key TEXT,
|
|
19
|
+
server_to_mobile_key TEXT,
|
|
20
|
+
last_mobile_counter INTEGER,
|
|
21
|
+
next_server_counter INTEGER,
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
updated_at INTEGER NOT NULL
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS pending_pairings (
|
|
27
|
+
approval_code TEXT PRIMARY KEY,
|
|
28
|
+
client_session_id TEXT,
|
|
29
|
+
client_name TEXT,
|
|
30
|
+
client_ephemeral_public_key TEXT NOT NULL,
|
|
31
|
+
client_nonce TEXT NOT NULL,
|
|
32
|
+
server_url TEXT NOT NULL,
|
|
33
|
+
approved INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
expires_at INTEGER NOT NULL,
|
|
35
|
+
created_at INTEGER NOT NULL,
|
|
36
|
+
updated_at INTEGER NOT NULL
|
|
37
|
+
);
|
|
38
|
+
`);
|
|
39
|
+
await ensurePairingSessionColumns();
|
|
40
|
+
async function countActive(now) {
|
|
41
|
+
const row = await db.prepare("SELECT COUNT(DISTINCT COALESCE(client_session_id, token_hash)) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
|
|
42
|
+
return Number(row?.count ?? 0);
|
|
43
|
+
}
|
|
44
|
+
async function deleteSession(tokenHash) {
|
|
45
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(tokenHash);
|
|
46
|
+
}
|
|
47
|
+
async function deletePendingPairing(approvalCode) {
|
|
48
|
+
await db.prepare("DELETE FROM pending_pairings WHERE approval_code = ?").run(approvalCode);
|
|
49
|
+
}
|
|
50
|
+
async function getPendingPairing(approvalCode, now) {
|
|
51
|
+
const row = await db.prepare(`SELECT approval_code AS approvalCode,
|
|
52
|
+
client_session_id AS clientSessionId,
|
|
53
|
+
client_name AS clientName,
|
|
54
|
+
client_ephemeral_public_key AS clientEphemeralPublicKey,
|
|
55
|
+
client_nonce AS clientNonce,
|
|
56
|
+
server_url AS serverUrl,
|
|
57
|
+
approved,
|
|
58
|
+
expires_at AS expiresAt
|
|
59
|
+
FROM pending_pairings
|
|
60
|
+
WHERE approval_code = ?`).get(approvalCode);
|
|
61
|
+
if (!row) return;
|
|
62
|
+
const expiresAt = Number(row.expiresAt);
|
|
63
|
+
if (now > expiresAt) {
|
|
64
|
+
await deletePendingPairing(approvalCode);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
approvalCode: String(row.approvalCode),
|
|
69
|
+
approved: Number(row.approved) === 1,
|
|
70
|
+
clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
|
|
71
|
+
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
72
|
+
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
73
|
+
clientNonce: String(row.clientNonce),
|
|
74
|
+
expiresAt,
|
|
75
|
+
serverUrl: String(row.serverUrl)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
async approvePendingPairing(approvalCode, now) {
|
|
80
|
+
const pending = await getPendingPairing(approvalCode, now);
|
|
81
|
+
if (!pending) return;
|
|
82
|
+
await db.prepare("UPDATE pending_pairings SET approved = 1, updated_at = ? WHERE approval_code = ?").run(now, approvalCode);
|
|
83
|
+
return {
|
|
84
|
+
...pending,
|
|
85
|
+
approved: true
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
async clearAll() {
|
|
89
|
+
return await db.transaction(async () => {
|
|
90
|
+
const sessionRow = await db.prepare("SELECT COUNT(*) AS count FROM pairing_sessions").get();
|
|
91
|
+
const pendingRow = await db.prepare("SELECT COUNT(*) AS count FROM pending_pairings").get();
|
|
92
|
+
await db.prepare("DELETE FROM pairing_sessions").run();
|
|
93
|
+
await db.prepare("DELETE FROM pending_pairings").run();
|
|
94
|
+
return {
|
|
95
|
+
pendingPairingsCleared: Number(pendingRow?.count ?? 0),
|
|
96
|
+
sessionsCleared: Number(sessionRow?.count ?? 0)
|
|
97
|
+
};
|
|
98
|
+
})();
|
|
99
|
+
},
|
|
100
|
+
countActive,
|
|
101
|
+
async createPendingPairing(pairing) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
await db.prepare(`INSERT INTO pending_pairings (
|
|
104
|
+
approval_code,
|
|
105
|
+
client_session_id,
|
|
106
|
+
client_name,
|
|
107
|
+
client_ephemeral_public_key,
|
|
108
|
+
client_nonce,
|
|
109
|
+
server_url,
|
|
110
|
+
approved,
|
|
111
|
+
expires_at,
|
|
112
|
+
created_at,
|
|
113
|
+
updated_at
|
|
114
|
+
)
|
|
115
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientSessionId ?? null, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
|
|
116
|
+
},
|
|
117
|
+
async createSession(tokenHash, session) {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const secure = encodeSecureSession(session.secureSession);
|
|
120
|
+
if (session.clientSessionId) {
|
|
121
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
122
|
+
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
123
|
+
}
|
|
124
|
+
await db.prepare(`INSERT INTO pairing_sessions (
|
|
125
|
+
token_hash,
|
|
126
|
+
client_session_id,
|
|
127
|
+
client_name,
|
|
128
|
+
expires_at,
|
|
129
|
+
key_epoch,
|
|
130
|
+
mobile_to_server_key,
|
|
131
|
+
server_to_mobile_key,
|
|
132
|
+
last_mobile_counter,
|
|
133
|
+
next_server_counter,
|
|
134
|
+
created_at,
|
|
135
|
+
updated_at
|
|
136
|
+
)
|
|
137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(tokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
|
|
138
|
+
return countActive(now);
|
|
139
|
+
},
|
|
140
|
+
deleteSession,
|
|
141
|
+
deletePendingPairing,
|
|
142
|
+
getPendingPairing,
|
|
143
|
+
async getValidSession(tokenHash, now) {
|
|
144
|
+
const row = await db.prepare(`SELECT client_name AS clientName,
|
|
145
|
+
client_session_id AS clientSessionId,
|
|
146
|
+
expires_at AS expiresAt,
|
|
147
|
+
key_epoch AS keyEpoch,
|
|
148
|
+
mobile_to_server_key AS mobileToServerKey,
|
|
149
|
+
server_to_mobile_key AS serverToMobileKey,
|
|
150
|
+
last_mobile_counter AS lastMobileCounter,
|
|
151
|
+
next_server_counter AS nextServerCounter
|
|
152
|
+
FROM pairing_sessions
|
|
153
|
+
WHERE token_hash = ?`).get(tokenHash);
|
|
154
|
+
if (!row) return;
|
|
155
|
+
const expiresAt = Number(row.expiresAt);
|
|
156
|
+
if (now > expiresAt) {
|
|
157
|
+
await deleteSession(tokenHash);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
162
|
+
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
163
|
+
expiresAt,
|
|
164
|
+
secureSession: decodeSecureSession(row)
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
async pruneExpired(now) {
|
|
168
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE expires_at <= ?").run(now);
|
|
169
|
+
await db.prepare("DELETE FROM pending_pairings WHERE expires_at <= ?").run(now);
|
|
170
|
+
},
|
|
171
|
+
async rotateSession(oldTokenHash, newTokenHash, session) {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const secure = encodeSecureSession(session.secureSession);
|
|
174
|
+
await db.transaction(async () => {
|
|
175
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
|
|
176
|
+
if (session.clientSessionId) {
|
|
177
|
+
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
178
|
+
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
179
|
+
}
|
|
180
|
+
await db.prepare(`INSERT INTO pairing_sessions (
|
|
181
|
+
token_hash,
|
|
182
|
+
client_session_id,
|
|
183
|
+
client_name,
|
|
184
|
+
expires_at,
|
|
185
|
+
key_epoch,
|
|
186
|
+
mobile_to_server_key,
|
|
187
|
+
server_to_mobile_key,
|
|
188
|
+
last_mobile_counter,
|
|
189
|
+
next_server_counter,
|
|
190
|
+
created_at,
|
|
191
|
+
updated_at
|
|
192
|
+
)
|
|
193
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newTokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
|
|
194
|
+
})();
|
|
195
|
+
return countActive(now);
|
|
196
|
+
},
|
|
197
|
+
async updateSecureSession(tokenHash, secureSession) {
|
|
198
|
+
const secure = encodeSecureSession(secureSession);
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
await db.prepare(`UPDATE pairing_sessions
|
|
201
|
+
SET key_epoch = ?,
|
|
202
|
+
mobile_to_server_key = ?,
|
|
203
|
+
server_to_mobile_key = ?,
|
|
204
|
+
last_mobile_counter = ?,
|
|
205
|
+
next_server_counter = ?,
|
|
206
|
+
updated_at = ?
|
|
207
|
+
WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
async function ensurePairingSessionColumns() {
|
|
211
|
+
const rows = await db.prepare("PRAGMA table_info(pairing_sessions)").all();
|
|
212
|
+
const columns = new Set(resultRows(rows).map((row) => String(row.name)));
|
|
213
|
+
for (const [column, sql] of [
|
|
214
|
+
["client_session_id", "ALTER TABLE pairing_sessions ADD COLUMN client_session_id TEXT"],
|
|
215
|
+
["key_epoch", "ALTER TABLE pairing_sessions ADD COLUMN key_epoch INTEGER"],
|
|
216
|
+
["mobile_to_server_key", "ALTER TABLE pairing_sessions ADD COLUMN mobile_to_server_key TEXT"],
|
|
217
|
+
["server_to_mobile_key", "ALTER TABLE pairing_sessions ADD COLUMN server_to_mobile_key TEXT"],
|
|
218
|
+
["last_mobile_counter", "ALTER TABLE pairing_sessions ADD COLUMN last_mobile_counter INTEGER"],
|
|
219
|
+
["next_server_counter", "ALTER TABLE pairing_sessions ADD COLUMN next_server_counter INTEGER"]
|
|
220
|
+
]) if (!columns.has(column)) await db.exec(sql);
|
|
221
|
+
const pendingRows = await db.prepare("PRAGMA table_info(pending_pairings)").all();
|
|
222
|
+
if (!new Set(resultRows(pendingRows).map((row) => String(row.name))).has("client_session_id")) await db.exec("ALTER TABLE pending_pairings ADD COLUMN client_session_id TEXT");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function encodeSecureSession(session) {
|
|
226
|
+
if (!session) return;
|
|
227
|
+
return {
|
|
228
|
+
keyEpoch: session.keyEpoch,
|
|
229
|
+
lastMobileCounter: session.lastMobileCounter,
|
|
230
|
+
mobileToServerKey: fromByteArray(session.mobileToServerKey),
|
|
231
|
+
nextServerCounter: session.nextServerCounter,
|
|
232
|
+
serverToMobileKey: fromByteArray(session.serverToMobileKey)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function decodeSecureSession(row) {
|
|
236
|
+
if (typeof row.mobileToServerKey !== "string" || typeof row.serverToMobileKey !== "string" || row.keyEpoch === null || row.lastMobileCounter === null || row.nextServerCounter === null) return;
|
|
237
|
+
return {
|
|
238
|
+
keyEpoch: Number(row.keyEpoch),
|
|
239
|
+
lastMobileCounter: Number(row.lastMobileCounter),
|
|
240
|
+
mobileToServerKey: toByteArray(row.mobileToServerKey),
|
|
241
|
+
nextServerCounter: Number(row.nextServerCounter),
|
|
242
|
+
serverToMobileKey: toByteArray(row.serverToMobileKey)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function resultRows(result) {
|
|
246
|
+
if (Array.isArray(result)) return result;
|
|
247
|
+
if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/pairing-url-candidates.ts
|
|
252
|
+
function getConnectUrlGuidance(url) {
|
|
253
|
+
const host = parseUrlHost(url);
|
|
254
|
+
if (!host) return;
|
|
255
|
+
if (isLocalhost(host) || isUnspecifiedHost(host)) return "This address is only reachable from this computer. Use a same-Wi-Fi address, Tailscale, or CODEX_RELAY_PUBLIC_URL for mobile pairing.";
|
|
256
|
+
if (isTailscaleHost(host)) return "Using Tailscale. Keep Tailscale connected on both this computer and the phone.";
|
|
257
|
+
if (isPrivateIPv4Host(host) || isLocalIPv6Host(host)) return "Using a local Wi-Fi/LAN address. Keep the phone and computer on the same network; if pairing is flaky, try Tailscale.";
|
|
258
|
+
return "Using a configured or public address. Make sure the phone can reach it before pairing.";
|
|
259
|
+
}
|
|
260
|
+
function createPairingQrPayload(details) {
|
|
261
|
+
const primaryServerUrl = details.serverUrls[0];
|
|
262
|
+
if (!primaryServerUrl) throw new Error("Pairing QR requires at least one server URL.");
|
|
263
|
+
const url = new URL("codex-relay://pair");
|
|
264
|
+
url.searchParams.set("serverUrl", primaryServerUrl);
|
|
265
|
+
url.searchParams.set("serverPublicKey", details.serverPublicKey);
|
|
266
|
+
const hosts = compactCandidateHosts(primaryServerUrl, details.serverUrls);
|
|
267
|
+
if (hosts.length > 0) url.searchParams.set("h", hosts.join(","));
|
|
268
|
+
return url.toString();
|
|
269
|
+
}
|
|
270
|
+
function getConnectUrlCandidates(details) {
|
|
271
|
+
return dedupeCandidates([
|
|
272
|
+
...configuredConnectUrlCandidates(),
|
|
273
|
+
...tailscaleConnectUrlCandidates(details.port),
|
|
274
|
+
...localNetworkConnectUrlCandidates(details.port),
|
|
275
|
+
{
|
|
276
|
+
label: "Server",
|
|
277
|
+
url: details.listenUrl
|
|
278
|
+
}
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
function normalizeUrl(value) {
|
|
282
|
+
if (!value) return;
|
|
283
|
+
const trimmed = value.trim().replace(/\/$/, "");
|
|
284
|
+
if (!trimmed) return;
|
|
285
|
+
try {
|
|
286
|
+
const url = new URL(trimmed);
|
|
287
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString().replace(/\/$/, "") : void 0;
|
|
288
|
+
} catch {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function configuredConnectUrlCandidates() {
|
|
293
|
+
const configuredUrl = normalizeUrl(process.env.CODEX_RELAY_PUBLIC_URL);
|
|
294
|
+
return configuredUrl ? [{
|
|
295
|
+
label: "Configured",
|
|
296
|
+
url: configuredUrl
|
|
297
|
+
}] : [];
|
|
298
|
+
}
|
|
299
|
+
function tailscaleConnectUrlCandidates(port) {
|
|
300
|
+
const status = getTailscaleStatus();
|
|
301
|
+
const candidates = [];
|
|
302
|
+
for (const ip of status?.Self?.TailscaleIPs ?? []) if (ip.startsWith("100.") && ip.includes(".")) candidates.push({
|
|
303
|
+
label: "Tailscale",
|
|
304
|
+
url: `http://${ip}:${port}`
|
|
305
|
+
});
|
|
306
|
+
const dnsName = status?.Self?.DNSName?.replace(/\.$/, "");
|
|
307
|
+
if (dnsName) {
|
|
308
|
+
const servedUrl = getTailscaleServeHttpsUrl(dnsName, port);
|
|
309
|
+
candidates.push({
|
|
310
|
+
label: servedUrl ? "Tailscale Serve" : "Tailscale DNS",
|
|
311
|
+
url: servedUrl ?? `http://${dnsName}:${port}`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
for (const ip of status?.Self?.TailscaleIPs ?? []) if (ip.includes(".")) candidates.push({
|
|
315
|
+
label: "Tailscale",
|
|
316
|
+
url: `http://${ip}:${port}`
|
|
317
|
+
});
|
|
318
|
+
return candidates;
|
|
319
|
+
}
|
|
320
|
+
function localNetworkConnectUrlCandidates(port) {
|
|
321
|
+
const candidates = [];
|
|
322
|
+
for (const [name, addresses] of Object.entries(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) candidates.push({
|
|
323
|
+
label: name,
|
|
324
|
+
url: `http://${address.address}:${port}`
|
|
325
|
+
});
|
|
326
|
+
return candidates;
|
|
327
|
+
}
|
|
328
|
+
function dedupeCandidates(candidates) {
|
|
329
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const candidate of candidates) {
|
|
331
|
+
const url = normalizeUrl(candidate.url);
|
|
332
|
+
if (url && !deduped.has(url)) deduped.set(url, {
|
|
333
|
+
...candidate,
|
|
334
|
+
url
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return [...deduped.values()];
|
|
338
|
+
}
|
|
339
|
+
function compactCandidateHosts(primaryServerUrl, serverUrls) {
|
|
340
|
+
const primary = parseUrl(primaryServerUrl);
|
|
341
|
+
if (!primary) return [];
|
|
342
|
+
const hosts = [];
|
|
343
|
+
for (const serverUrl of serverUrls.slice(1)) {
|
|
344
|
+
const candidate = parseUrl(serverUrl);
|
|
345
|
+
if (candidate && candidate.protocol === primary.protocol && candidate.port === primary.port && !hosts.includes(candidate.hostname)) hosts.push(candidate.hostname);
|
|
346
|
+
}
|
|
347
|
+
return hosts;
|
|
348
|
+
}
|
|
349
|
+
function parseUrl(url) {
|
|
350
|
+
try {
|
|
351
|
+
return new URL(url);
|
|
352
|
+
} catch {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function parseUrlHost(url) {
|
|
357
|
+
try {
|
|
358
|
+
return new URL(url).hostname.toLowerCase();
|
|
359
|
+
} catch {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function isLocalhost(host) {
|
|
364
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
365
|
+
}
|
|
366
|
+
function isUnspecifiedHost(host) {
|
|
367
|
+
return host === "0.0.0.0" || host === "::";
|
|
368
|
+
}
|
|
369
|
+
function isTailscaleHost(host) {
|
|
370
|
+
return host.endsWith(".ts.net") || host.endsWith(".beta.tailscale.net") || isTailscaleIPv4Host(host);
|
|
371
|
+
}
|
|
372
|
+
function isPrivateIPv4Host(host) {
|
|
373
|
+
const octets = host.split(".").map(Number);
|
|
374
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet))) return false;
|
|
375
|
+
return octets[0] === 10 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31 || octets[0] === 192 && octets[1] === 168 || octets[0] === 169 && octets[1] === 254;
|
|
376
|
+
}
|
|
377
|
+
function isTailscaleIPv4Host(host) {
|
|
378
|
+
const octets = host.split(".").map(Number);
|
|
379
|
+
return octets.length === 4 && octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127;
|
|
380
|
+
}
|
|
381
|
+
function isLocalIPv6Host(host) {
|
|
382
|
+
const normalized = host.replace(/^\[/, "").replace(/\]$/, "");
|
|
383
|
+
return normalized.startsWith("fe80:") || normalized.startsWith("fc") || normalized.startsWith("fd");
|
|
384
|
+
}
|
|
385
|
+
function getTailscaleStatus() {
|
|
386
|
+
try {
|
|
387
|
+
const output = execFileSync("tailscale", ["status", "--json"], {
|
|
388
|
+
encoding: "utf8",
|
|
389
|
+
stdio: [
|
|
390
|
+
"ignore",
|
|
391
|
+
"pipe",
|
|
392
|
+
"ignore"
|
|
393
|
+
],
|
|
394
|
+
timeout: 1500
|
|
395
|
+
});
|
|
396
|
+
return JSON.parse(output);
|
|
397
|
+
} catch {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function getTailscaleServeHttpsUrl(dnsName, port) {
|
|
402
|
+
try {
|
|
403
|
+
const output = execFileSync("tailscale", [
|
|
404
|
+
"serve",
|
|
405
|
+
"status",
|
|
406
|
+
"--json"
|
|
407
|
+
], {
|
|
408
|
+
encoding: "utf8",
|
|
409
|
+
stdio: [
|
|
410
|
+
"ignore",
|
|
411
|
+
"pipe",
|
|
412
|
+
"ignore"
|
|
413
|
+
],
|
|
414
|
+
timeout: 1500
|
|
415
|
+
});
|
|
416
|
+
const serveStatus = JSON.parse(output);
|
|
417
|
+
const portKey = String(port);
|
|
418
|
+
const hostPort = `${dnsName}:${portKey}`;
|
|
419
|
+
return serveStatus.TCP?.[portKey]?.HTTPS && serveStatus.Web?.[hostPort] ? `https://${hostPort}` : void 0;
|
|
420
|
+
} catch {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
3
425
|
//#region src/paths.ts
|
|
4
426
|
const appDataDirectoryName = "codex-relay";
|
|
5
427
|
function codexRelayHome() {
|
|
@@ -22,4 +444,4 @@ function defaultCodexRelayHome() {
|
|
|
22
444
|
}
|
|
23
445
|
}
|
|
24
446
|
//#endregion
|
|
25
|
-
export { codexRelayHome as n, legacyCodexRelayDataPath as r, codexRelayDataPath as t };
|
|
447
|
+
export { getConnectUrlCandidates as a, createPairingQrPayload as i, codexRelayHome as n, getConnectUrlGuidance as o, legacyCodexRelayDataPath as r, createTursoPairingSessionStore as s, codexRelayDataPath as t };
|