claude-relay 2.2.2 → 2.2.3
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/bin/cli.js +54 -26
- package/lib/pages.js +24 -6
- package/lib/server.js +5 -3
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -312,6 +312,33 @@ function getLocalIP() {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// --- Certs ---
|
|
315
|
+
function isRoutableIP(addr) {
|
|
316
|
+
if (addr.startsWith("10.")) return true;
|
|
317
|
+
if (addr.startsWith("192.168.")) return true;
|
|
318
|
+
if (addr.startsWith("100.")) {
|
|
319
|
+
var second = parseInt(addr.split(".")[1], 10);
|
|
320
|
+
return second >= 64 && second <= 127; // CGNAT (Tailscale)
|
|
321
|
+
}
|
|
322
|
+
if (addr.startsWith("172.")) {
|
|
323
|
+
var second = parseInt(addr.split(".")[1], 10);
|
|
324
|
+
return second >= 16 && second <= 31;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getAllIPs() {
|
|
330
|
+
var ips = [];
|
|
331
|
+
var ifaces = os.networkInterfaces();
|
|
332
|
+
for (var addrs of Object.values(ifaces)) {
|
|
333
|
+
for (var j = 0; j < addrs.length; j++) {
|
|
334
|
+
if (addrs[j].family === "IPv4" && !addrs[j].internal && isRoutableIP(addrs[j].address)) {
|
|
335
|
+
ips.push(addrs[j].address);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return ips;
|
|
340
|
+
}
|
|
341
|
+
|
|
315
342
|
function ensureCerts(ip) {
|
|
316
343
|
var homeDir = os.homedir();
|
|
317
344
|
var certDir = path.join(homeDir, ".claude-relay", "certs");
|
|
@@ -336,21 +363,29 @@ function ensureCerts(ip) {
|
|
|
336
363
|
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
337
364
|
} catch (e) {}
|
|
338
365
|
|
|
366
|
+
// Collect all IPv4 addresses (Tailscale + LAN)
|
|
367
|
+
var allIPs = getAllIPs();
|
|
368
|
+
|
|
339
369
|
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
340
370
|
var needRegen = false;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (certText.indexOf(
|
|
345
|
-
|
|
346
|
-
|
|
371
|
+
try {
|
|
372
|
+
var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
|
|
373
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
374
|
+
if (certText.indexOf(allIPs[i]) === -1) {
|
|
375
|
+
needRegen = true;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (e) { needRegen = true; }
|
|
347
380
|
if (!needRegen) return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
348
381
|
}
|
|
349
382
|
|
|
350
383
|
fs.mkdirSync(certDir, { recursive: true });
|
|
351
384
|
|
|
352
385
|
var domains = ["localhost", "127.0.0.1", "::1"];
|
|
353
|
-
|
|
386
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
387
|
+
if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
|
|
388
|
+
}
|
|
354
389
|
|
|
355
390
|
try {
|
|
356
391
|
execSync(
|
|
@@ -1435,22 +1470,6 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1435
1470
|
var wantRemote = false;
|
|
1436
1471
|
var wantPush = false;
|
|
1437
1472
|
|
|
1438
|
-
// If everything is already set up, skip straight to QR
|
|
1439
|
-
var tsReady = getTailscaleIP() !== null;
|
|
1440
|
-
var mcReady = hasMkcert();
|
|
1441
|
-
if (tsReady && mcReady && config.tls) {
|
|
1442
|
-
console.clear();
|
|
1443
|
-
printLogo();
|
|
1444
|
-
log("");
|
|
1445
|
-
log(sym.pointer + " " + a.bold + "Setup Notifications" + a.reset);
|
|
1446
|
-
log(sym.bar);
|
|
1447
|
-
log(sym.done + " " + a.green + "Tailscale" + a.reset + a.dim + " · " + getTailscaleIP() + a.reset);
|
|
1448
|
-
log(sym.done + " " + a.green + "HTTPS" + a.reset + a.dim + " · mkcert installed" + a.reset);
|
|
1449
|
-
log(sym.bar);
|
|
1450
|
-
showSetupQR();
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
1473
|
console.clear();
|
|
1455
1474
|
printLogo();
|
|
1456
1475
|
log("");
|
|
@@ -1564,10 +1583,19 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1564
1583
|
|
|
1565
1584
|
function showSetupQR() {
|
|
1566
1585
|
var tsIP = getTailscaleIP();
|
|
1586
|
+
var lanIP = null;
|
|
1587
|
+
if (!wantRemote) {
|
|
1588
|
+
var allIPs = getAllIPs();
|
|
1589
|
+
for (var j = 0; j < allIPs.length; j++) {
|
|
1590
|
+
if (!allIPs[j].startsWith("100.")) { lanIP = allIPs[j]; break; }
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
|
|
1594
|
+
var setupQuery = wantRemote ? "" : "?mode=lan";
|
|
1567
1595
|
// Always use HTTP onboarding URL for QR/setup when TLS is active
|
|
1568
1596
|
var setupUrl = config.tls
|
|
1569
|
-
? "http://" +
|
|
1570
|
-
: "http://" +
|
|
1597
|
+
? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
|
|
1598
|
+
: "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
|
|
1571
1599
|
log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
|
|
1572
1600
|
log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
|
|
1573
1601
|
log(sym.bar + " " + a.bold + setupUrl + a.reset);
|
|
@@ -1576,7 +1604,7 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1576
1604
|
var lines = code.split("\n").map(function (l) { return " " + sym.bar + " " + l; }).join("\n");
|
|
1577
1605
|
console.log(lines);
|
|
1578
1606
|
log(sym.bar);
|
|
1579
|
-
if (
|
|
1607
|
+
if (wantRemote) {
|
|
1580
1608
|
log(sym.bar + " " + a.dim + "Can't connect? Make sure Tailscale is installed on your phone too." + a.reset);
|
|
1581
1609
|
} else {
|
|
1582
1610
|
log(sym.bar + " " + a.dim + "Can't connect? Your phone must be on the same Wi-Fi network." + a.reset);
|
package/lib/pages.js
CHANGED
|
@@ -40,7 +40,7 @@ function pinPageHtml() {
|
|
|
40
40
|
'</script></div></body></html>';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function setupPageHtml(httpsUrl, httpUrl, hasCert) {
|
|
43
|
+
function setupPageHtml(httpsUrl, httpUrl, hasCert, lanMode) {
|
|
44
44
|
return `<!DOCTYPE html><html lang="en"><head>
|
|
45
45
|
<meta charset="UTF-8">
|
|
46
46
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
@@ -195,6 +195,7 @@ h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
|
|
|
195
195
|
<div class="step-desc">Install Claude Relay as an app for quick access and a full-screen experience.</div>
|
|
196
196
|
|
|
197
197
|
<div class="platform-ios">
|
|
198
|
+
<div class="check-status warn">On iOS, push notifications only work from the installed app. This step is required.</div>
|
|
198
199
|
<div id="ios-not-safari" class="check-status warn" style="display:none">You must use <b>Safari</b> to install. Open this page in Safari first.</div>
|
|
199
200
|
<div id="ios-safari-steps">
|
|
200
201
|
<div class="instruction"><div class="inst-num">1</div>
|
|
@@ -237,6 +238,7 @@ h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
|
|
|
237
238
|
</div>
|
|
238
239
|
|
|
239
240
|
<div id="pwa-status" class="check-status pending">After installing, open Claude Relay from your home screen to continue setup.</div>
|
|
241
|
+
<button class="skip-link" id="pwa-skip" onclick="nextStep()" style="display:none">Skip for now</button>
|
|
240
242
|
</div>
|
|
241
243
|
|
|
242
244
|
<!-- Step 3: Push Notifications -->
|
|
@@ -270,6 +272,7 @@ h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
|
|
|
270
272
|
var httpsUrl = ${JSON.stringify(httpsUrl)};
|
|
271
273
|
var httpUrl = ${JSON.stringify(httpUrl)};
|
|
272
274
|
var hasCert = ${hasCert ? 'true' : 'false'};
|
|
275
|
+
var lanMode = ${lanMode ? 'true' : 'false'};
|
|
273
276
|
var isHttps = location.protocol === "https:";
|
|
274
277
|
var ua = navigator.userAgent;
|
|
275
278
|
var isIOS = /iPhone|iPad|iPod/.test(ua);
|
|
@@ -326,10 +329,17 @@ if (isStandalone && localStorage.getItem("setup-pending")) {
|
|
|
326
329
|
|
|
327
330
|
function buildSteps(hasPushSub) {
|
|
328
331
|
steps = [];
|
|
329
|
-
if (!isTailscale && !isLocal) steps.push("tailscale");
|
|
332
|
+
if (!isTailscale && !isLocal && !lanMode) steps.push("tailscale");
|
|
330
333
|
if (hasCert && !isHttps) steps.push("cert");
|
|
331
|
-
if (
|
|
332
|
-
|
|
334
|
+
if (isAndroid) {
|
|
335
|
+
// Android: push first (works in browser), then PWA as optional
|
|
336
|
+
if ((isHttps || isLocal) && !hasPushSub) steps.push("push");
|
|
337
|
+
if (!isStandalone) steps.push("pwa");
|
|
338
|
+
} else {
|
|
339
|
+
// iOS: PWA required for push, so install first
|
|
340
|
+
if (!isStandalone) steps.push("pwa");
|
|
341
|
+
if ((isHttps || isLocal) && !hasPushSub) steps.push("push");
|
|
342
|
+
}
|
|
333
343
|
steps.push("done");
|
|
334
344
|
|
|
335
345
|
// Trigger HTTPS check now that steps are built
|
|
@@ -350,6 +360,14 @@ function buildSteps(hasPushSub) {
|
|
|
350
360
|
localStorage.setItem("setup-pending", String(stepsBeforePwa + 1));
|
|
351
361
|
}
|
|
352
362
|
|
|
363
|
+
// Android: PWA is optional, show skip button and update text
|
|
364
|
+
if (isAndroid && steps.indexOf("pwa") !== -1) {
|
|
365
|
+
var pwaSkip = document.getElementById("pwa-skip");
|
|
366
|
+
var pwaStatus = document.getElementById("pwa-status");
|
|
367
|
+
if (pwaSkip) pwaSkip.style.display = "block";
|
|
368
|
+
if (pwaStatus) pwaStatus.textContent = "Optional: install for quick access and full-screen experience.";
|
|
369
|
+
}
|
|
370
|
+
|
|
353
371
|
// Push: show warning if not on HTTPS
|
|
354
372
|
if (!isHttps && !isLocal) {
|
|
355
373
|
pushBtn.style.display = "none";
|
|
@@ -393,7 +411,7 @@ function showStep(idx) {
|
|
|
393
411
|
function nextStep() {
|
|
394
412
|
// After cert step on HTTP, redirect to HTTPS for remaining steps
|
|
395
413
|
if (!isHttps && steps[currentStep] === "cert") {
|
|
396
|
-
location.replace(httpsUrl + "/setup");
|
|
414
|
+
location.replace(httpsUrl + "/setup" + (lanMode ? "?mode=lan" : ""));
|
|
397
415
|
return;
|
|
398
416
|
}
|
|
399
417
|
if (currentStep < steps.length - 1) showStep(currentStep + 1);
|
|
@@ -616,7 +634,7 @@ if (!isHttps && !isLocal) {
|
|
|
616
634
|
var ac = new AbortController();
|
|
617
635
|
setTimeout(function() { ac.abort(); }, 3000);
|
|
618
636
|
fetch(info.httpsUrl + "/info", { signal: ac.signal, mode: "no-cors" })
|
|
619
|
-
.then(function() { location.replace(info.httpsUrl + "/setup"); })
|
|
637
|
+
.then(function() { location.replace(info.httpsUrl + "/setup" + (lanMode ? "?mode=lan" : "")); })
|
|
620
638
|
.catch(function() { init(); });
|
|
621
639
|
}).catch(function() { init(); });
|
|
622
640
|
} else {
|
package/lib/server.js
CHANGED
|
@@ -202,16 +202,17 @@ function createServer(opts) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
// Setup page
|
|
205
|
-
if (
|
|
205
|
+
if (fullUrl === "/setup" && req.method === "GET") {
|
|
206
206
|
var host = req.headers.host || "localhost";
|
|
207
207
|
var hostname = host.split(":")[0];
|
|
208
208
|
var protocol = tlsOptions ? "https" : "http";
|
|
209
209
|
var setupUrl = protocol + "://" + hostname + ":" + portNum;
|
|
210
|
+
var lanMode = /[?&]mode=lan/.test(req.url);
|
|
210
211
|
res.writeHead(200, {
|
|
211
212
|
"Content-Type": "text/html; charset=utf-8",
|
|
212
213
|
"Access-Control-Allow-Origin": "*",
|
|
213
214
|
});
|
|
214
|
-
res.end(setupPageHtml(setupUrl, setupUrl, !!caContent));
|
|
215
|
+
res.end(setupPageHtml(setupUrl, setupUrl, !!caContent, lanMode));
|
|
215
216
|
return;
|
|
216
217
|
}
|
|
217
218
|
|
|
@@ -364,8 +365,9 @@ function createServer(opts) {
|
|
|
364
365
|
var hostname = host.split(":")[0];
|
|
365
366
|
var httpsSetupUrl = "https://" + hostname + ":" + portNum;
|
|
366
367
|
var httpSetupUrl = "http://" + hostname + ":" + (portNum + 1);
|
|
368
|
+
var lanMode = /[?&]mode=lan/.test(req.url);
|
|
367
369
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
368
|
-
res.end(setupPageHtml(httpsSetupUrl, httpSetupUrl, !!caContent));
|
|
370
|
+
res.end(setupPageHtml(httpsSetupUrl, httpSetupUrl, !!caContent, lanMode));
|
|
369
371
|
return;
|
|
370
372
|
}
|
|
371
373
|
|