@lifeaitools/clauth 1.18.1 → 1.19.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/cli/commands/serve.js +93 -3
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -3337,6 +3337,80 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3337
3337
|
});
|
|
3338
3338
|
}
|
|
3339
3339
|
|
|
3340
|
+
// Self-heal: mint a connector run-token from the Cloudflare API using the
|
|
3341
|
+
// vault's `cloudflare` token, so `cloudflared tunnel run --token` can start
|
|
3342
|
+
// the tunnel with ZERO local files (~/.cloudflared/config.yml + creds JSON).
|
|
3343
|
+
// This survives a home-dir wipe — clauth has the keys, so it rebuilds the
|
|
3344
|
+
// connector itself. Returns null on any failure so startTunnel() can fall
|
|
3345
|
+
// back to the legacy file-based `tunnel run`.
|
|
3346
|
+
async function getTunnelRunToken() {
|
|
3347
|
+
try {
|
|
3348
|
+
if (!password) return null;
|
|
3349
|
+
const sbUrl = (api.getBaseUrl() || "").replace("/functions/v1/auth-vault", "");
|
|
3350
|
+
const sbKey = api.getAnonKey();
|
|
3351
|
+
|
|
3352
|
+
// Resolve tunnelId: clauth_config.tunnel_id, else config.yml `tunnel:` line
|
|
3353
|
+
let tid = null;
|
|
3354
|
+
if (sbUrl && sbKey) {
|
|
3355
|
+
try {
|
|
3356
|
+
const r = await fetch(`${sbUrl}/rest/v1/clauth_config?key=eq.tunnel_id&select=value`,
|
|
3357
|
+
{ headers: { apikey: sbKey, Authorization: `Bearer ${sbKey}` }, signal: AbortSignal.timeout(5000) });
|
|
3358
|
+
if (r.ok) {
|
|
3359
|
+
const rows = await r.json();
|
|
3360
|
+
if (rows.length && rows[0].value && rows[0].value !== "null") {
|
|
3361
|
+
tid = typeof rows[0].value === "string" ? JSON.parse(rows[0].value) : rows[0].value;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
} catch {}
|
|
3365
|
+
}
|
|
3366
|
+
if (!tid) {
|
|
3367
|
+
try {
|
|
3368
|
+
const yml = fs.readFileSync(path.join(os.homedir(), ".cloudflared", "config.yml"), "utf8");
|
|
3369
|
+
const m = yml.match(/^\s*tunnel:\s*(\S+)/m);
|
|
3370
|
+
if (m) tid = m[1];
|
|
3371
|
+
} catch {}
|
|
3372
|
+
}
|
|
3373
|
+
if (!tid) return null;
|
|
3374
|
+
|
|
3375
|
+
// CF API token from the vault
|
|
3376
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
3377
|
+
const cr = await api.retrieve(password, machineHash, token, timestamp, "cloudflare");
|
|
3378
|
+
const cfToken = cr?.value;
|
|
3379
|
+
if (!cfToken) return null;
|
|
3380
|
+
|
|
3381
|
+
// accountId: clauth_config.cf_account_id, else first account on the token
|
|
3382
|
+
let accountId = null;
|
|
3383
|
+
if (sbUrl && sbKey) {
|
|
3384
|
+
try {
|
|
3385
|
+
const r = await fetch(`${sbUrl}/rest/v1/clauth_config?key=eq.cf_account_id&select=value`,
|
|
3386
|
+
{ headers: { apikey: sbKey, Authorization: `Bearer ${sbKey}` }, signal: AbortSignal.timeout(5000) });
|
|
3387
|
+
if (r.ok) {
|
|
3388
|
+
const rows = await r.json();
|
|
3389
|
+
if (rows.length && rows[0].value && rows[0].value !== "null") {
|
|
3390
|
+
accountId = typeof rows[0].value === "string" ? JSON.parse(rows[0].value) : rows[0].value;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
} catch {}
|
|
3394
|
+
}
|
|
3395
|
+
if (!accountId) {
|
|
3396
|
+
const ar = await fetch("https://api.cloudflare.com/client/v4/accounts",
|
|
3397
|
+
{ headers: { Authorization: `Bearer ${cfToken}` }, signal: AbortSignal.timeout(8000) });
|
|
3398
|
+
const ad = await ar.json();
|
|
3399
|
+
accountId = ad?.result?.[0]?.id || null;
|
|
3400
|
+
}
|
|
3401
|
+
if (!accountId) return null;
|
|
3402
|
+
|
|
3403
|
+
// Mint the connector run-token
|
|
3404
|
+
const tr = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/cfd_tunnel/${tid}/token`,
|
|
3405
|
+
{ headers: { Authorization: `Bearer ${cfToken}` }, signal: AbortSignal.timeout(8000) });
|
|
3406
|
+
const td = await tr.json();
|
|
3407
|
+
if (td?.success && td.result) return td.result;
|
|
3408
|
+
return null;
|
|
3409
|
+
} catch {
|
|
3410
|
+
return null;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3340
3414
|
async function startTunnel() {
|
|
3341
3415
|
if (tunnelProc) {
|
|
3342
3416
|
// Already running — ensure status reflects reality (caller may have reset it to "starting")
|
|
@@ -3387,9 +3461,15 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3387
3461
|
// Named tunnel (fixed subdomain) or quick tunnel (random URL)
|
|
3388
3462
|
let args;
|
|
3389
3463
|
if (tunnelHostname) {
|
|
3390
|
-
//
|
|
3391
|
-
//
|
|
3392
|
-
|
|
3464
|
+
// Self-heal first: mint a connector token from the CF API (clauth has
|
|
3465
|
+
// the keys) and run `--token` — no local config.yml/creds needed.
|
|
3466
|
+
// Fall back to the legacy file-based `tunnel run` only if minting fails.
|
|
3467
|
+
const runToken = await getTunnelRunToken();
|
|
3468
|
+
if (runToken) {
|
|
3469
|
+
args = ["tunnel", "run", "--token", runToken];
|
|
3470
|
+
} else {
|
|
3471
|
+
args = ["tunnel", "run"];
|
|
3472
|
+
}
|
|
3393
3473
|
tunnelUrl = `https://${tunnelHostname}`;
|
|
3394
3474
|
} else {
|
|
3395
3475
|
// Quick tunnel — random URL each session
|
|
@@ -6220,6 +6300,16 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
6220
6300
|
body: JSON.stringify({ key: "tunnel_hostname", value: JSON.stringify(hostname) }),
|
|
6221
6301
|
});
|
|
6222
6302
|
|
|
6303
|
+
// Persist tunnel_id too — required for token-based self-heal after a
|
|
6304
|
+
// daemon restart or a ~/.cloudflared wipe (getTunnelRunToken reads it).
|
|
6305
|
+
if (tunnelId) {
|
|
6306
|
+
await fetch(`${sbUrl}/rest/v1/clauth_config`, {
|
|
6307
|
+
method: "POST",
|
|
6308
|
+
headers: { apikey: sbKey, Authorization: `Bearer ${sbKey}`, "Content-Type": "application/json", Prefer: "resolution=merge-duplicates" },
|
|
6309
|
+
body: JSON.stringify({ key: "tunnel_id", value: JSON.stringify(tunnelId) }),
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6223
6313
|
tunnelHostname = hostname;
|
|
6224
6314
|
tunnelUrl = `https://${hostname}`;
|
|
6225
6315
|
|