claude-bridge-cli 1.3.1 → 1.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/bin/cli.js +136 -3
- package/lib/relay-client.js +19 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -2,9 +2,112 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { parseArgs } = require("node:util");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const os = require("node:os");
|
|
5
8
|
const { startBridge } = require("../lib/bridge");
|
|
6
9
|
const { startRelay } = require("../lib/relay-client");
|
|
7
10
|
|
|
11
|
+
// ── Pair flow (browser-OTP based auth) ────────────────────────────────────
|
|
12
|
+
// When --token isn't supplied, the CLI:
|
|
13
|
+
// 1. POSTs /pair-cli/start to the relay's HTTP base (public endpoint)
|
|
14
|
+
// 2. Prints the short code + pair URL — user opens it, OTPs into CF Access,
|
|
15
|
+
// confirms in the browser
|
|
16
|
+
// 3. Long-polls /pair-cli/claim until the bearer arrives
|
|
17
|
+
// 4. Saves the bearer to ~/.claude-bridge/auth.json so subsequent starts
|
|
18
|
+
// pick it up automatically
|
|
19
|
+
// Replaces the old workflow of running `claude-relay-ctl provision <m>` on
|
|
20
|
+
// the host and copy-pasting a long bearer onto the CLI command line.
|
|
21
|
+
|
|
22
|
+
function authFilePath() {
|
|
23
|
+
return path.join(os.homedir(), ".claude-bridge", "auth.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readAuthFile() {
|
|
27
|
+
try { return JSON.parse(fs.readFileSync(authFilePath(), "utf8")); }
|
|
28
|
+
catch { return { bearers: {} }; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeAuthFile(data) {
|
|
32
|
+
const fp = authFilePath();
|
|
33
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
34
|
+
fs.writeFileSync(fp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function relayHttpBase(wsUrl) {
|
|
38
|
+
// wss://host/agent/ws → https://host
|
|
39
|
+
const u = new URL(wsUrl);
|
|
40
|
+
const proto = u.protocol === "wss:" ? "https:" : "http:";
|
|
41
|
+
return `${proto}//${u.host}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function pairInteractive(relayWsUrl, machine, ephemeral) {
|
|
45
|
+
const base = relayHttpBase(relayWsUrl);
|
|
46
|
+
process.stdout.write(`[bridge] Starting pair flow for "${machine}" against ${base}\n`);
|
|
47
|
+
if (ephemeral) process.stdout.write(`[bridge] Ephemeral mode — bearer will NOT be saved to disk.\n`);
|
|
48
|
+
|
|
49
|
+
const startResp = await fetch(`${base}/pair-cli/start`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ machine }),
|
|
53
|
+
});
|
|
54
|
+
if (!startResp.ok) {
|
|
55
|
+
const text = await startResp.text();
|
|
56
|
+
throw new Error(`pair start failed (${startResp.status}): ${text}`);
|
|
57
|
+
}
|
|
58
|
+
const { code, poll_token, pair_url, expires_in } = await startResp.json();
|
|
59
|
+
|
|
60
|
+
console.log("\n ┌─────────────────────────────────────────────────┐");
|
|
61
|
+
console.log(" │ Open this URL in a browser to pair the device │");
|
|
62
|
+
console.log(" ├─────────────────────────────────────────────────┤");
|
|
63
|
+
console.log(` │ ${pair_url.padEnd(47)} │`);
|
|
64
|
+
console.log(" │ │");
|
|
65
|
+
console.log(` │ Confirm this code matches: ${code.padEnd(17)}│`);
|
|
66
|
+
console.log(" └─────────────────────────────────────────────────┘");
|
|
67
|
+
console.log(` Code expires in ${expires_in}s. Waiting for confirmation…\n`);
|
|
68
|
+
|
|
69
|
+
const deadline = Date.now() + expires_in * 1000;
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
let resp;
|
|
72
|
+
try {
|
|
73
|
+
resp = await fetch(`${base}/pair-cli/claim`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
body: JSON.stringify({ poll_token }),
|
|
77
|
+
});
|
|
78
|
+
} catch (e) {
|
|
79
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (resp.status === 202) continue; // still waiting, re-poll
|
|
83
|
+
if (resp.status === 404 || resp.status === 410) {
|
|
84
|
+
throw new Error("pair expired or invalidated — re-run to start over");
|
|
85
|
+
}
|
|
86
|
+
if (!resp.ok) {
|
|
87
|
+
const text = await resp.text();
|
|
88
|
+
throw new Error(`pair claim failed (${resp.status}): ${text}`);
|
|
89
|
+
}
|
|
90
|
+
const { bearer, machine: confirmed } = await resp.json();
|
|
91
|
+
if (ephemeral) {
|
|
92
|
+
console.log(`[bridge] ✓ Paired as "${confirmed}" (in-memory only, nothing written to disk).`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(`[bridge] ✓ Paired as "${confirmed}". Bearer saved to ${authFilePath()}`);
|
|
95
|
+
const auth = readAuthFile();
|
|
96
|
+
auth.bearers = auth.bearers || {};
|
|
97
|
+
auth.bearers[base] = auth.bearers[base] || {};
|
|
98
|
+
auth.bearers[base][confirmed] = bearer;
|
|
99
|
+
writeAuthFile(auth);
|
|
100
|
+
}
|
|
101
|
+
return bearer;
|
|
102
|
+
}
|
|
103
|
+
throw new Error("pair window expired — re-run to start over");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function lookupSavedBearer(relayWsUrl, machine) {
|
|
107
|
+
const auth = readAuthFile();
|
|
108
|
+
return auth.bearers?.[relayHttpBase(relayWsUrl)]?.[machine] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
8
111
|
const HELP = `
|
|
9
112
|
claude-code-bridge — Bridge server for Claude Code CLI
|
|
10
113
|
|
|
@@ -25,7 +128,12 @@ Options:
|
|
|
25
128
|
Relay options (connect to a remote relay server):
|
|
26
129
|
--relay-url <url> WebSocket URL of the relay server
|
|
27
130
|
--machine <name> Machine name for the relay
|
|
28
|
-
--token <bearer> Machine bearer
|
|
131
|
+
--token <bearer> Machine bearer (optional — if omitted, the CLI starts
|
|
132
|
+
a browser-OTP pair flow and caches the bearer at
|
|
133
|
+
~/.claude-bridge/auth.json for next time)
|
|
134
|
+
--ephemeral Pair on every start, never read/write auth.json.
|
|
135
|
+
Forces a fresh browser-OTP each launch — use on
|
|
136
|
+
machines you don't want to leave any saved bearer on.
|
|
29
137
|
--cf-id <id> Cloudflare Access Client ID (optional)
|
|
30
138
|
--cf-secret <secret> Cloudflare Access Client Secret (optional)
|
|
31
139
|
|
|
@@ -33,6 +141,7 @@ Environment variables:
|
|
|
33
141
|
All options can be set via env vars with BRIDGE_ prefix:
|
|
34
142
|
BRIDGE_PORT, BRIDGE_HOST, BRIDGE_CWD, BRIDGE_TIMEOUT,
|
|
35
143
|
BRIDGE_RELAY_URL, BRIDGE_MACHINE_NAME, BRIDGE_MACHINE_TOKEN,
|
|
144
|
+
BRIDGE_EPHEMERAL=1 (force fresh pair every start, no saved bearer),
|
|
36
145
|
BRIDGE_CF_ID, BRIDGE_CF_SECRET
|
|
37
146
|
`;
|
|
38
147
|
|
|
@@ -60,6 +169,7 @@ function main() {
|
|
|
60
169
|
"relay-url": { type: "string", default: env("RELAY_URL", "") },
|
|
61
170
|
machine: { type: "string", default: env("MACHINE_NAME", "") },
|
|
62
171
|
token: { type: "string", default: env("MACHINE_TOKEN", "") },
|
|
172
|
+
ephemeral: { type: "boolean", default: env("EPHEMERAL", "") === "1" },
|
|
63
173
|
"cf-id": { type: "string", default: env("CF_ID", "") },
|
|
64
174
|
"cf-secret": { type: "string", default: env("CF_SECRET", "") },
|
|
65
175
|
},
|
|
@@ -76,6 +186,7 @@ function main() {
|
|
|
76
186
|
url: values["relay-url"],
|
|
77
187
|
machine: values.machine,
|
|
78
188
|
token: values.token,
|
|
189
|
+
ephemeral: !!values.ephemeral,
|
|
79
190
|
cfId: values["cf-id"],
|
|
80
191
|
cfSecret: values["cf-secret"],
|
|
81
192
|
} : null,
|
|
@@ -133,10 +244,32 @@ async function run(config) {
|
|
|
133
244
|
|
|
134
245
|
// Start relay client if configured
|
|
135
246
|
if (config.relay) {
|
|
136
|
-
if (!config.relay.machine
|
|
137
|
-
console.error("[bridge] ERROR: --machine
|
|
247
|
+
if (!config.relay.machine) {
|
|
248
|
+
console.error("[bridge] ERROR: --machine required when using --relay-url");
|
|
138
249
|
process.exit(1);
|
|
139
250
|
}
|
|
251
|
+
// Resolve a bearer:
|
|
252
|
+
// --token → use it directly (never look at auth.json)
|
|
253
|
+
// --ephemeral → always pair fresh, never read or write auth.json
|
|
254
|
+
// default → saved auth.json → fall back to pair flow on miss
|
|
255
|
+
if (!config.relay.token) {
|
|
256
|
+
const saved = config.relay.ephemeral
|
|
257
|
+
? null
|
|
258
|
+
: lookupSavedBearer(config.relay.url, config.relay.machine);
|
|
259
|
+
if (saved) {
|
|
260
|
+
config.relay.token = saved;
|
|
261
|
+
console.log(`[bridge] Using saved bearer from ${authFilePath()}`);
|
|
262
|
+
} else {
|
|
263
|
+
try {
|
|
264
|
+
config.relay.token = await pairInteractive(
|
|
265
|
+
config.relay.url, config.relay.machine, config.relay.ephemeral
|
|
266
|
+
);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error(`[bridge] ERROR: ${e.message}`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
140
273
|
console.log(`[bridge] Connecting to relay as "${config.relay.machine}"...`);
|
|
141
274
|
startRelay(config);
|
|
142
275
|
} else {
|
package/lib/relay-client.js
CHANGED
|
@@ -46,6 +46,25 @@ function startRelay(config) {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
ws.on("close", (code, reason) => {
|
|
49
|
+
// 4003 = bearer rejected by the relay (verify_machine_token failed).
|
|
50
|
+
// The bearer has been rotated or admin-removed; the cached one in
|
|
51
|
+
// auth.json is useless. Stop the reconnect loop and tell the user
|
|
52
|
+
// exactly how to recover.
|
|
53
|
+
if (code === 4003) {
|
|
54
|
+
const reasonStr = reason && reason.toString ? reason.toString() : "unauthorized";
|
|
55
|
+
console.error("");
|
|
56
|
+
console.error(`[relay] Authorization rejected (${code} ${reasonStr}).`);
|
|
57
|
+
console.error("[relay] The saved bearer no longer matches what the relay expects — it was probably rotated or revoked.");
|
|
58
|
+
console.error("[relay] Recover by deleting the cached auth and re-pairing:");
|
|
59
|
+
if (process.platform === "win32") {
|
|
60
|
+
console.error(" del %USERPROFILE%\\.claude-bridge\\auth.json");
|
|
61
|
+
} else {
|
|
62
|
+
console.error(" rm ~/.claude-bridge/auth.json");
|
|
63
|
+
}
|
|
64
|
+
console.error(` claude-bridge-cli start --relay-url ${config.relay.url} --machine ${config.relay.machine}`);
|
|
65
|
+
console.error("");
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
49
68
|
console.log(`[relay] Disconnected (${code}). Reconnecting in ${backoff / 1000}s...`);
|
|
50
69
|
setTimeout(connect, backoff);
|
|
51
70
|
backoff = Math.min(60000, backoff * 2);
|
package/package.json
CHANGED