@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.
@@ -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
- // Named tunnel: cloudflared tunnel run (uses ~/.cloudflared/config.yml)
3391
- // Config maps hostname local service, so no --url needed
3392
- args = ["tunnel", "run"];
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {