xaq-cli 0.1.0-nightly.71 → 0.1.0-nightly.73

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.
Files changed (88) hide show
  1. package/README.md +7 -1
  2. package/dist/{admin-CWofnT_Q.js → admin-B2t1ATpC.js} +1 -1
  3. package/dist/{admin-Btrm8mCd.js → admin-DI2rumeX.js} +8 -7
  4. package/dist/{admin-Btrm8mCd.js.map → admin-DI2rumeX.js.map} +1 -1
  5. package/dist/{admin-5gThRwgj.js → admin-DeCN2VDX.js} +5 -5
  6. package/dist/{admin-5gThRwgj.js.map → admin-DeCN2VDX.js.map} +1 -1
  7. package/dist/{api-BFpElwqK.js → api-CmTgScTD.js} +2 -2
  8. package/dist/{api-BFpElwqK.js.map → api-CmTgScTD.js.map} +1 -1
  9. package/dist/{auth-CXmBdMqw.js → auth-B8_i8XFz.js} +1 -1
  10. package/dist/{auth-D-GmwNv-.js → auth-BLYwD_zT.js} +3 -3
  11. package/dist/{auth-D-GmwNv-.js.map → auth-BLYwD_zT.js.map} +1 -1
  12. package/dist/brand-Bg9VPAym.js +55 -0
  13. package/dist/brand-Bg9VPAym.js.map +1 -0
  14. package/dist/cli.js +17 -17
  15. package/dist/{commands-CdA6-WE7.js → commands-BXSQe3Za.js} +10 -10
  16. package/dist/{commands-CdA6-WE7.js.map → commands-BXSQe3Za.js.map} +1 -1
  17. package/dist/{commands-BNVLjYXA.js → commands-tyhvFqiJ.js} +20 -20
  18. package/dist/{commands-BNVLjYXA.js.map → commands-tyhvFqiJ.js.map} +1 -1
  19. package/dist/{config-CNVi0v5t.js → config-C4vBEO-V.js} +1 -1
  20. package/dist/{config-C9XmYZWO.js → config-D3jl_sOt.js} +30 -11
  21. package/dist/config-D3jl_sOt.js.map +1 -0
  22. package/dist/{connect-TtJ9n0Rw.js → connect-D7oQXLIF.js} +9 -9
  23. package/dist/{connect-TtJ9n0Rw.js.map → connect-D7oQXLIF.js.map} +1 -1
  24. package/dist/{crypto-fv2fj3S8.js → crypto-s8x6up-l.js} +2 -2
  25. package/dist/{crypto-fv2fj3S8.js.map → crypto-s8x6up-l.js.map} +1 -1
  26. package/dist/fsutil-DGrtqggb.js +117 -0
  27. package/dist/fsutil-DGrtqggb.js.map +1 -0
  28. package/dist/hermes-B89Nuqw-.js +3 -0
  29. package/dist/{hermes-DHnDTYJp.js → hermes-BXvD29dj.js} +6 -5
  30. package/dist/{hermes-DHnDTYJp.js.map → hermes-BXvD29dj.js.map} +1 -1
  31. package/dist/{import-DsN5NZT3.js → import-D0af7Fv-.js} +5 -5
  32. package/dist/{import-DsN5NZT3.js.map → import-D0af7Fv-.js.map} +1 -1
  33. package/dist/{instance-Dj9TpJB2.js → instance-CPpDQZ1c.js} +4 -4
  34. package/dist/{instance-Dj9TpJB2.js.map → instance-CPpDQZ1c.js.map} +1 -1
  35. package/dist/instance-yXK1-2SE.js +2 -0
  36. package/dist/{listen-B9iO7HgQ.js → listen-Ce24Ito9.js} +23 -10
  37. package/dist/listen-Ce24Ito9.js.map +1 -0
  38. package/dist/listen-DhKMan39.js +2 -0
  39. package/dist/{mcpheader-B8c5YO1B.js → mcpheader-Dl8sPYPf.js} +1 -1
  40. package/dist/{mcpheader-C9H4vudu.js → mcpheader-cMO9RS_j.js} +15 -6
  41. package/dist/mcpheader-cMO9RS_j.js.map +1 -0
  42. package/dist/mcpsetup-D7QlLvCo.js +2 -0
  43. package/dist/{mcpsetup-DXXo2qjf.js → mcpsetup-DjMhHnJo.js} +6 -5
  44. package/dist/{mcpsetup-DXXo2qjf.js.map → mcpsetup-DjMhHnJo.js.map} +1 -1
  45. package/dist/paths-Bk9CoSYL.js +2 -0
  46. package/dist/{paths-yzOKD6ow.js → paths-Bo6qtWNh.js} +15 -11
  47. package/dist/paths-Bo6qtWNh.js.map +1 -0
  48. package/dist/profile-BpGWft1B.js +2 -0
  49. package/dist/{profile-NmMKAczz.js → profile-l29_RlPn.js} +6 -95
  50. package/dist/profile-l29_RlPn.js.map +1 -0
  51. package/dist/recipes-CDQv42qy.js +3 -0
  52. package/dist/{recipes-DG_-doGq.js → recipes-D_quZbpA.js} +4 -4
  53. package/dist/{recipes-DG_-doGq.js.map → recipes-D_quZbpA.js.map} +1 -1
  54. package/dist/{server-CDqDp1q3.js → server-BlOM8g-z.js} +8 -8
  55. package/dist/{server-CDqDp1q3.js.map → server-BlOM8g-z.js.map} +1 -1
  56. package/dist/service-Cw_iN4OR.js +2 -0
  57. package/dist/{service-C7jfo-Ex.js → service-DjWgCF6M.js} +11 -10
  58. package/dist/service-DjWgCF6M.js.map +1 -0
  59. package/dist/{service-BVn3soZZ.js → service-DsZwNQ0Y.js} +50 -20
  60. package/dist/service-DsZwNQ0Y.js.map +1 -0
  61. package/dist/{service-BaWGPmSd.js → service-DyTQZeH9.js} +1 -1
  62. package/dist/{setup-token-CwuSzfLK.js → setup-token-DBqR_h01.js} +15 -3
  63. package/dist/setup-token-DBqR_h01.js.map +1 -0
  64. package/dist/skill/SKILL.md +1 -0
  65. package/dist/skill-DAsfVipI.js +2 -0
  66. package/dist/{skill-1UQj3qgW.js → skill-Dgc-4Dop.js} +22 -2
  67. package/dist/skill-Dgc-4Dop.js.map +1 -0
  68. package/package.json +1 -1
  69. package/dist/brand--ZB8jTn1.js +0 -32
  70. package/dist/brand--ZB8jTn1.js.map +0 -1
  71. package/dist/config-C9XmYZWO.js.map +0 -1
  72. package/dist/hermes-CKyzt1mH.js +0 -3
  73. package/dist/instance-CwWbFbJ8.js +0 -2
  74. package/dist/listen-B9iO7HgQ.js.map +0 -1
  75. package/dist/listen-CXVKG9XD.js +0 -2
  76. package/dist/mcpheader-C9H4vudu.js.map +0 -1
  77. package/dist/mcpsetup-BnHudwpq.js +0 -2
  78. package/dist/paths-d-C2kdjo.js +0 -2
  79. package/dist/paths-yzOKD6ow.js.map +0 -1
  80. package/dist/profile-CcMZ4hn7.js +0 -2
  81. package/dist/profile-NmMKAczz.js.map +0 -1
  82. package/dist/recipes-C7TIDlRj.js +0 -3
  83. package/dist/service-BVn3soZZ.js.map +0 -1
  84. package/dist/service-C7jfo-Ex.js.map +0 -1
  85. package/dist/service-DxDyKDQ-.js +0 -2
  86. package/dist/setup-token-CwuSzfLK.js.map +0 -1
  87. package/dist/skill-1UQj3qgW.js.map +0 -1
  88. package/dist/skill-pelq18JQ.js +0 -2
package/README.md CHANGED
@@ -51,13 +51,19 @@ and is delivered once the listener is back: nothing is lost or delivered twice.
51
51
  `master.key`, `agentinbox.db` and settings. Nothing is moved; `~/.xaq` is only for new installs.
52
52
  - every agent key (`aib_…`), Gmail and Calendar connection, grant, webhook and Pub/Sub setup;
53
53
  - the listener's profiles, keys, logs and event cursor (`~/.config/agentinbox`, `~/.local/state/agentinbox`,
54
- `~/.local/share/agentinbox` stay in use while the `…/xaq` ones don't exist);
54
+ `~/.local/share/agentinbox` stay in use until the matching `…/xaq` directory holds xaq's own files);
55
55
  - the names your agents already use: a profile set up with agentinbox keeps its MCP server name `agentinbox`
56
56
  (`agentinbox-<profile>`), so Hermes routes confined to `["mcp-agentinbox"]`, cron jobs limited to
57
57
  `["agentinbox"]`, the Hermes route `/webhooks/agentinbox…` and Claude Code's `mcp__agentinbox` keep working;
58
58
  - `AGENTINBOX_*` environment variables: still read when the `XAQ_*` one isn't set, and `--exec` commands still get
59
59
  `AGENTINBOX_EVENT_FILE` and the other ids next to the new `XAQ_*` ones.
60
60
 
61
+ Another, unrelated program is also called xaq and may have its own `~/.config/xaq` (or `~/.xaq`). xaq counts such
62
+ a directory as its own only when it holds xaq's files (`profiles/`, `listen/`, `app/`; in `~/.xaq` its database or
63
+ `master.key`), checked without reading anything. Otherwise an existing agentinbox directory is used; and where there
64
+ is none, xaq only adds its own files next to the other program's and never changes, renames or deletes anything it
65
+ didn't create.
66
+
61
67
  **Changes:**
62
68
  - the command is `xaq` (`npx xaq-cli …`); `agentinbox` (from the `agentinbox-cli` package, now a shim) and
63
69
  `npx agentinbox-cli …` still work: they run xaq after saying so;
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export { n as CLEANUP_SIGNALS, o as daemonOrigin, s as openAdminChannel } from "./admin-5gThRwgj.js";
2
+ export { n as CLEANUP_SIGNALS, o as daemonOrigin, s as openAdminChannel } from "./admin-DeCN2VDX.js";
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as NPX } from "./version-BxH7OuWD.js";
3
- import { i as saveProfile, l as ensurePrivateDir, n as loadProfile } from "./profile-NmMKAczz.js";
4
- import { c as profilePath, i as deadPath, o as logPath, p as validProfileName } from "./paths-yzOKD6ow.js";
5
- import { b as TransportError, f as isHermesRoute, x as checkTransport } from "./recipes-DG_-doGq.js";
3
+ import { i as ensurePrivateDir } from "./fsutil-DGrtqggb.js";
4
+ import { c as profilePath, i as deadPath, o as logPath, p as validProfileName } from "./paths-Bo6qtWNh.js";
5
+ import { i as saveProfile, n as loadProfile } from "./profile-l29_RlPn.js";
6
+ import { b as TransportError, f as isHermesRoute, x as checkTransport } from "./recipes-D_quZbpA.js";
6
7
  import { i as safeText, r as formatDuration } from "./ui-B8Sd4Y7Y.js";
7
- import { D as ApiError, N as getSelf, b as forwardEvent, g as resolveListen, h as forwardSecret, m as describeFilter, o as routeWarning, u as DEFAULT_LISTEN_TYPES } from "./hermes-DHnDTYJp.js";
8
- import { c as readDead, d as runCommand, f as Dispatcher, i as outputLine, l as readStatus, m as hasAccess, p as describeGrants, s as pidAlive, u as writeDead } from "./listen-B9iO7HgQ.js";
9
- import { r as installedServiceLabel } from "./service-C7jfo-Ex.js";
8
+ import { D as ApiError, N as getSelf, b as forwardEvent, g as resolveListen, h as forwardSecret, m as describeFilter, o as routeWarning, u as DEFAULT_LISTEN_TYPES } from "./hermes-BXvD29dj.js";
9
+ import { c as readDead, d as runCommand, f as Dispatcher, i as outputLine, l as readStatus, m as hasAccess, p as describeGrants, s as pidAlive, u as writeDead } from "./listen-Ce24Ito9.js";
10
+ import { r as installedServiceLabel } from "./service-DjWgCF6M.js";
10
11
  import pc from "picocolors";
11
12
  //#region server/listen/admin.ts
12
13
  const out = (s = "") => process.stdout.write(`${s}\n`);
@@ -215,4 +216,4 @@ async function replayDead(flags, opts = {}) {
215
216
  //#endregion
216
217
  export { clearDead, printDead, printSecret, printStatus, replayDead };
217
218
 
218
- //# sourceMappingURL=admin-Btrm8mCd.js.map
219
+ //# sourceMappingURL=admin-DI2rumeX.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin-Btrm8mCd.js","names":[],"sources":["../server/listen/admin.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport { ApiError, getSelf } from \"./api.ts\";\nimport { describeGrants, hasAccess } from \"./grants.ts\";\nimport { Dispatcher, type Job, type Target } from \"./dispatcher.ts\";\nimport { runCommand } from \"./exec.ts\";\nimport { ensurePrivateDir } from \"./fsutil.ts\";\nimport { forwardEvent } from \"./forward.ts\";\nimport { routeWarning } from \"./hermes.ts\";\nimport { describeFilter, type ListenFlags, MAX_BATCH, resolveListen } from \"./options.ts\";\nimport { outputLine } from \"./listen.ts\";\nimport { isHermesRoute } from \"./recipes.ts\";\nimport { DEFAULT_PROFILE, deadPath, logPath, profilePath, validProfileName } from \"./paths.ts\";\nimport { loadProfile, saveProfile } from \"./profile.ts\";\nimport { DEFAULT_LISTEN_TYPES, forwardSecret } from \"./options.ts\";\nimport { installedServiceLabel } from \"./service.ts\";\nimport { type DeadEntry, pidAlive, readDead, readStatus, writeDead } from \"./state.ts\";\nimport { checkTransport, TransportError } from \"./transport.ts\";\nimport { formatDuration, safeText } from \"./ui.ts\";\nimport { NPX } from \"./version.ts\";\n\nconst out = (s = \"\") => process.stdout.write(`${s}\\n`);\nconst flagFor = (profile: string) => (profile === DEFAULT_PROFILE ? \"\" : ` --profile ${profile}`);\nconst ago = (iso: string | null | undefined) => (iso ? `${formatDuration(Date.now() - Date.parse(iso))} ago` : \"never\");\n\n// ───────────── listen secret ─────────────\n\n/** Prints the forward-mode signing secret (generating and saving it on first use). */\nexport function printSecret(profileName: string, opts: { rotate?: boolean }): void {\n const name = validProfileName(profileName);\n const profile = loadProfile(name);\n let { secret, created } = forwardSecret(profile);\n if (opts.rotate) {\n secret = forwardSecret({ ...profile, forward: undefined, forwardSecret: undefined }).secret;\n created = true;\n }\n if (created) {\n if (profile.forward) profile.forward = { ...profile.forward, secret };\n else profile.forwardSecret = secret;\n saveProfile(name, profile);\n process.stderr.write(pc.gray(`${opts.rotate ? \"Rotated\" : \"Generated\"} the forward signing secret for profile \"${name}\"${opts.rotate ? \"; restart the listener\" : \"\"}.\\n`));\n }\n // The secret alone on stdout, so `export XAQ_WEBHOOK_SECRET=$(xaq listen secret)` works.\n out(secret);\n}\n\n// ───────────── listen status ─────────────\n\nexport async function printStatus(profileName: string): Promise<void> {\n const name = validProfileName(profileName);\n const profile = loadProfile(name);\n const st = readStatus(name);\n const alive = st ? pidAlive(st.pid) && !st.stoppedAt : false;\n const dead = readDead(name);\n\n out(`${pc.bold(\"Profile\")} ${name} ${pc.gray(profilePath(name))}`);\n out(`${pc.bold(\"Agent\")} ${safeText(profile.agentName)} ${pc.gray(`(${profile.agentId})`)}`);\n out(`${pc.bold(\"Daemon\")} ${profile.url}`);\n out(`${pc.bold(\"Consumer\")} ${st?.consumer ?? profile.consumer}`);\n // What the last run used (flags included), else what the profile has saved.\n const exec = st && st.exec !== undefined ? st.exec : profile.exec;\n const forward = st && st.forward !== undefined ? st.forward : profile.forward?.url;\n const targets = [exec ? `exec ${safeText(exec, 120)}` : null, forward ? `forward ${forward}` : null].filter(Boolean);\n out(`${pc.bold(\"Runs\")} ${targets.length ? targets.join(\" · \") : pc.gray(\"nothing saved (pass --exec/--forward with --save)\")}`);\n out(\n `${pc.bold(\"Events\")} ${\n profile.types?.length\n ? profile.types.join(\", \")\n : `${DEFAULT_LISTEN_TYPES.join(\", \")} ${pc.gray(\"(default types; add --types all --save for every event)\")}`\n }`,\n );\n // The running listener's filters (flags included), else the profile's.\n let saved: ReturnType<typeof resolveListen>[\"config\"] | null = null;\n try {\n saved = resolveListen(name, profile, {}).config;\n } catch {\n // An invalid saved setting: `listen` itself reports it.\n }\n out(`${pc.bold(\"Filters\")} ${(alive && st?.filters) || (saved ? describeFilter(saved) : \"?\")}`);\n const batchSeconds = alive && st && st.batchSeconds !== undefined ? st.batchSeconds : (saved?.batchSeconds ?? 0);\n if (batchSeconds) out(`${pc.bold(\"Batch\")} ${formatDuration(batchSeconds * 1000)} window, up to ${MAX_BATCH} events per --exec run`);\n const route = forward && isHermesRoute(forward) ? routeWarning(name, forward) : null;\n if (route?.level === \"warn\") out(`${pc.bold(\"Route\")} ${pc.yellow(route.message)}`);\n out(`${pc.bold(\"Output\")} ${outputLine(name, { exec: exec ?? null, forward: forward ? { url: forward } : null }, logPath(name))}`);\n\n let svc = \"not installed\";\n if (process.platform === \"linux\" || process.platform === \"darwin\") {\n const installed = installedServiceLabel(name);\n if (installed) svc = `installed (${installed}; details: ${NPX} listen service status${flagFor(name)})`;\n }\n out(`${pc.bold(\"Service\")} ${svc}`);\n\n try {\n // Never send the key over a connection `listen` itself would refuse.\n await checkTransport(profile.url, { insecure: profile.insecure });\n const self = await getSelf(profile.url, profile.apiKey, 5000);\n out(`${pc.bold(\"Key\")} ${pc.green(\"valid\")}`);\n // The access the console actually granted, in plain words (what the agent should report to its human).\n const lines = describeGrants(self.grants, self.connections);\n out(`${pc.bold(\"Access\")} ${hasAccess(self) ? lines[0] : pc.yellow(lines[0]!)}`);\n for (const l of lines.slice(1)) out(` ${l}`);\n } catch (err) {\n const revoked = err instanceof ApiError && (err.status === 401 || err.status === 403);\n const why = err instanceof ApiError && err.status === 0 ? (err.cause as string) : err instanceof Error ? err.message.split(\"\\n\")[0] : String(err);\n if (err instanceof TransportError) out(`${pc.bold(\"Key\")} ${pc.red(`not checked: ${why}`)}`);\n else out(`${pc.bold(\"Key\")} ${revoked ? pc.red(`rejected: ${(err as Error).message}`) : pc.yellow(`not checked (${why})`)}`);\n }\n\n if (!st) {\n out(`${pc.bold(\"Listener\")} ${pc.gray(\"has not run yet\")}`);\n } else {\n const state = !alive\n ? pc.gray(`not running${st.stoppedAt ? ` (stopped ${ago(st.stoppedAt)}${st.exitReason ? `: ${safeText(st.exitReason, 160)}` : \"\"})` : \" (exited unexpectedly)\"}`)\n : st.connected\n ? pc.green(`running (pid ${st.pid}), connected ${ago(st.connectedSince)}`)\n : pc.yellow(`running (pid ${st.pid}), disconnected${st.lastDisconnect ? ` ${ago(st.lastDisconnect.at)}: ${safeText(st.lastDisconnect.reason, 160)}` : \"\"}`);\n out(`${pc.bold(\"Listener\")} ${state}`);\n out(\n `${pc.bold(\"Cursor\")} acked #${st.ackedSeq} · received #${st.receivedSeq}${alive && st.running ? ` · ${st.running} in progress` : \"\"}${st.skipped ? ` · ${st.skipped} skipped by filters` : \"\"} · last event ${ago(st.lastEventAt)}`,\n );\n if (st.recent.length) {\n out(pc.bold(\"Recent\"));\n for (const r of st.recent.slice(0, 8)) {\n const icon = r.outcome === \"succeeded\" ? pc.green(\"✓\") : r.outcome === \"dead\" ? pc.red(\"✖\") : r.outcome === \"retrying\" ? pc.yellow(\"↻\") : pc.gray(\"·\");\n out(` ${icon} #${r.seq} ${r.type} ${pc.gray(`${ago(r.at)}${r.attempts > 1 ? `, ${r.attempts} attempts` : \"\"}${r.error && r.outcome !== \"succeeded\" ? `, ${safeText(r.error, 100)}` : \"\"}`)}`);\n }\n }\n }\n out(`${pc.bold(\"Dead\")} ${dead.length ? pc.red(`${dead.length} event${dead.length > 1 ? \"s\" : \"\"}`) + pc.gray(` (inspect: ${NPX} listen dead${flagFor(name)})`) : \"0\"}`);\n out(`${pc.bold(\"Log\")} ${logPath(name)}`);\n}\n\n// ───────────── listen dead ─────────────\n\nexport function printDead(profileName: string, opts: { json?: boolean }): void {\n const name = validProfileName(profileName);\n const entries = readDead(name);\n if (opts.json) {\n for (const e of entries) out(JSON.stringify(e));\n return;\n }\n if (!entries.length) {\n out(`No dead events for profile \"${name}\".`);\n return;\n }\n for (const e of entries) {\n const m = e.payload.data.message;\n out(`${pc.red(\"✖\")} #${e.seq} ${e.type} ${pc.gray(e.id)} ${pc.gray(`died ${ago(e.deadAt)} after ${e.attempts} attempts`)}`);\n if (m) out(` from ${safeText(m.from?.email ?? \"?\", 80)} \"${safeText(m.subject, 80)}\"`);\n out(pc.gray(` ${safeText(e.error, 300)}`));\n }\n out();\n out(pc.gray(`${entries.length} dead event${entries.length > 1 ? \"s\" : \"\"} in ${deadPath(name)}.`));\n out(pc.gray(`Replay with: ${NPX} listen dead --replay${flagFor(name)} (one: --id <evt_…>; drop all: --clear)`));\n out(pc.gray(`Replay uses the profile's saved --exec/--forward; if you ran them as flags only, pass them again, e.g. --replay --exec '<command>'.`));\n}\n\nexport function clearDead(profileName: string, id?: string): void {\n const name = validProfileName(profileName);\n const entries = readDead(name);\n const keep = id ? entries.filter((e) => e.id !== id) : [];\n writeDead(name, keep);\n out(`Removed ${entries.length - keep.length} dead event${entries.length - keep.length === 1 ? \"\" : \"s\"}.`);\n}\n\n/**\n * Runs dead events again through the profile's targets (or the ones passed as flags). Successes are\n * removed from dead.jsonl; failures stay. Returns how many still failed.\n */\nexport async function replayDead(flags: ListenFlags & { id?: string }, opts: { retryDelayMs?: (n: number) => number } = {}): Promise<number> {\n const name = validProfileName(flags.profile ?? DEFAULT_PROFILE);\n const profile = loadProfile(name);\n const { config } = resolveListen(name, profile, { ...flags, save: false });\n const all = readDead(name);\n const chosen = all.filter((e) => !flags.id || e.id === flags.id);\n if (!chosen.length) {\n out(flags.id ? `No dead event with id ${flags.id}.` : `No dead events for profile \"${name}\".`);\n return 0;\n }\n const available: Target[] = [];\n if (config.exec) available.push(\"exec\");\n if (config.forward) available.push(\"forward\");\n if (!available.length) throw new Error(`Nothing to replay to: pass --recipe <tool> (claude-code, codex, cursor, hermes), --exec '<command>' and/or --forward <url> (or save them in the profile with \\`${NPX} listen --save …\\`).`);\n if (config.exec && process.platform === \"win32\") throw new Error(\"--exec isn't supported on Windows; replay with --forward instead.\");\n\n const timeoutMs = config.timeoutSeconds * 1000;\n if (config.cwdIsDefault) ensurePrivateDir(config.cwd);\n const failed = new Map<string, DeadEntry>();\n const bySeq = new Map(chosen.map((e) => [e.seq, e]));\n const dispatcher = new Dispatcher({\n targets: available,\n concurrency: config.concurrency,\n maxAttempts: config.maxAttempts,\n retryDelayMs: opts.retryDelayMs,\n run: async (job: Job, signal) => {\n const errors: string[] = [];\n if (job.remaining.has(\"exec\") && config.exec) {\n const r = await runCommand(config.exec, job.payload, { timeoutMs, signal, cwd: config.cwd });\n if (r.ok) job.remaining.delete(\"exec\");\n else errors.push(`command: ${r.error}`);\n }\n if (job.remaining.has(\"forward\") && config.forward) {\n const r = await forwardEvent(config.forward.url, config.forward.secret, job.payload, { timeoutMs: Math.min(timeoutMs, 30_000), signal, wrap: isHermesRoute(config.forward.url) });\n if (r.ok) job.remaining.delete(\"forward\");\n else errors.push(`forward: ${r.error}`);\n }\n return errors;\n },\n onAdvance: () => {},\n onDead: (job) => {\n const e = bySeq.get(job.seq)!;\n failed.set(e.id, { ...e, deadAt: new Date().toISOString(), attempts: e.attempts + job.attempts, error: job.lastError ?? \"failed\" });\n out(`${pc.red(\"✖\")} #${job.seq} still failing: ${safeText(job.lastError ?? \"failed\", 200)}`);\n },\n onFinished: (job) => {\n if (job.outcome === \"succeeded\") out(`${pc.green(\"✓\")} #${job.seq} ${job.payload.type} replayed`);\n },\n });\n out(`Replaying ${chosen.length} dead event${chosen.length > 1 ? \"s\" : \"\"} (up to ${config.maxAttempts} attempts each)…`);\n // Only the targets that never succeeded (a forward that went through isn't POSTed again).\n for (const e of [...chosen].sort((a, b) => a.seq - b.seq)) {\n const targets = e.targets?.length ? e.targets.filter((t) => available.includes(t)) : available;\n if (!targets.length) {\n out(`${pc.yellow(\"!\")} #${e.seq} needs ${e.targets.join(\" and \")}, which isn't configured now; pass --${e.targets[0]} to replay it.`);\n failed.set(e.id, e);\n continue;\n }\n dispatcher.push(e.payload, { targets });\n }\n await dispatcher.idle();\n\n const replayed = new Set(chosen.map((e) => e.id));\n // Re-read so entries added by a running listener meanwhile are kept.\n const current = readDead(name);\n writeDead(name, [...current.filter((e) => !replayed.has(e.id)), ...failed.values()]);\n out(`${chosen.length - failed.size} replayed, ${failed.size} still dead.`);\n return failed.size;\n}\n"],"mappings":";;;;;;;;;;;AAoBA,MAAM,OAAO,IAAI,OAAO,QAAQ,OAAO,MAAM,GAAG,EAAE,GAAG;AACrD,MAAM,WAAW,YAAqB,YAAA,YAA8B,KAAK,cAAc;AACvF,MAAM,OAAO,QAAoC,MAAM,GAAG,eAAe,KAAK,IAAI,IAAI,KAAK,MAAM,GAAG,CAAC,EAAE,QAAQ;;AAK/G,SAAgB,YAAY,aAAqB,MAAkC;CACjF,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,YAAY,IAAI;CAChC,IAAI,EAAE,QAAQ,YAAY,cAAc,OAAO;CAC/C,IAAI,KAAK,QAAQ;EACf,SAAS,cAAc;GAAE,GAAG;GAAS,SAAS,KAAA;GAAW,eAAe,KAAA;EAAU,CAAC,CAAC,CAAC;EACrF,UAAU;CACZ;CACA,IAAI,SAAS;EACX,IAAI,QAAQ,SAAS,QAAQ,UAAU;GAAE,GAAG,QAAQ;GAAS;EAAO;OAC/D,QAAQ,gBAAgB;EAC7B,YAAY,MAAM,OAAO;EACzB,QAAQ,OAAO,MAAM,GAAG,KAAK,GAAG,KAAK,SAAS,YAAY,YAAY,2CAA2C,KAAK,GAAG,KAAK,SAAS,2BAA2B,GAAG,IAAI,CAAC;CAC5K;CAEA,IAAI,MAAM;AACZ;AAIA,eAAsB,YAAY,aAAoC;CACpE,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,KAAK,WAAW,IAAI;CAC1B,MAAM,QAAQ,KAAK,SAAS,GAAG,GAAG,KAAK,CAAC,GAAG,YAAY;CACvD,MAAM,OAAO,SAAS,IAAI;CAE1B,IAAI,GAAG,GAAG,KAAK,SAAS,EAAE,KAAK,KAAK,IAAI,GAAG,KAAK,YAAY,IAAI,CAAC,GAAG;CACpE,IAAI,GAAG,GAAG,KAAK,OAAO,EAAE,OAAO,SAAS,QAAQ,SAAS,EAAE,GAAG,GAAG,KAAK,IAAI,QAAQ,QAAQ,EAAE,GAAG;CAC/F,IAAI,GAAG,GAAG,KAAK,QAAQ,EAAE,MAAM,QAAQ,KAAK;CAC5C,IAAI,GAAG,GAAG,KAAK,UAAU,EAAE,IAAI,IAAI,YAAY,QAAQ,UAAU;CAEjE,MAAM,OAAO,MAAM,GAAG,SAAS,KAAA,IAAY,GAAG,OAAO,QAAQ;CAC7D,MAAM,UAAU,MAAM,GAAG,YAAY,KAAA,IAAY,GAAG,UAAU,QAAQ,SAAS;CAC/E,MAAM,UAAU,CAAC,OAAO,QAAQ,SAAS,MAAM,GAAG,MAAM,MAAM,UAAU,WAAW,YAAY,IAAI,CAAC,CAAC,OAAO,OAAO;CACnH,IAAI,GAAG,GAAG,KAAK,MAAM,EAAE,QAAQ,QAAQ,SAAS,QAAQ,KAAK,KAAK,IAAI,GAAG,KAAK,mDAAmD,GAAG;CACpI,IACE,GAAG,GAAG,KAAK,QAAQ,EAAE,MACnB,QAAQ,OAAO,SACX,QAAQ,MAAM,KAAK,IAAI,IACvB,GAAG,qBAAqB,KAAK,IAAI,EAAE,GAAG,GAAG,KAAK,yDAAyD,KAE/G;CAEA,IAAI,QAA2D;CAC/D,IAAI;EACF,QAAQ,cAAc,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC;CAC3C,QAAQ,CAER;CACA,IAAI,GAAG,GAAG,KAAK,SAAS,EAAE,KAAM,SAAS,IAAI,YAAa,QAAQ,eAAe,KAAK,IAAI,MAAM;CAChG,MAAM,eAAe,SAAS,MAAM,GAAG,iBAAiB,KAAA,IAAY,GAAG,eAAgB,OAAO,gBAAgB;CAC9G,IAAI,cAAc,IAAI,GAAG,GAAG,KAAK,OAAO,EAAE,OAAO,eAAe,eAAe,GAAI,EAAE,wCAAkD;CACvI,MAAM,QAAQ,WAAW,cAAc,OAAO,IAAI,aAAa,MAAM,OAAO,IAAI;CAChF,IAAI,OAAO,UAAU,QAAQ,IAAI,GAAG,GAAG,KAAK,OAAO,EAAE,OAAO,GAAG,OAAO,MAAM,OAAO,GAAG;CACtF,IAAI,GAAG,GAAG,KAAK,QAAQ,EAAE,MAAM,WAAW,MAAM;EAAE,MAAM,QAAQ;EAAM,SAAS,UAAU,EAAE,KAAK,QAAQ,IAAI;CAAK,GAAG,QAAQ,IAAI,CAAC,GAAG;CAEpI,IAAI,MAAM;CACV,IAAI,QAAQ,aAAa,WAAW,QAAQ,aAAa,UAAU;EACjE,MAAM,YAAY,sBAAsB,IAAI;EAC5C,IAAI,WAAW,MAAM,cAAc,UAAU,aAAa,IAAI,wBAAwB,QAAQ,IAAI,EAAE;CACtG;CACA,IAAI,GAAG,GAAG,KAAK,SAAS,EAAE,KAAK,KAAK;CAEpC,IAAI;EAEF,MAAM,eAAe,QAAQ,KAAK,EAAE,UAAU,QAAQ,SAAS,CAAC;EAChE,MAAM,OAAO,MAAM,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,GAAI;EAC5D,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,GAAG,MAAM,OAAO,GAAG;EAElD,MAAM,QAAQ,eAAe,KAAK,QAAQ,KAAK,WAAW;EAC1D,IAAI,GAAG,GAAG,KAAK,QAAQ,EAAE,MAAM,UAAU,IAAI,IAAI,MAAM,KAAK,GAAG,OAAO,MAAM,EAAG,GAAG;EAClF,KAAK,MAAM,KAAK,MAAM,MAAM,CAAC,GAAG,IAAI,aAAa,GAAG;CACtD,SAAS,KAAK;EACZ,MAAM,UAAU,eAAe,aAAa,IAAI,WAAW,OAAO,IAAI,WAAW;EACjF,MAAM,MAAM,eAAe,YAAY,IAAI,WAAW,IAAK,IAAI,QAAmB,eAAe,QAAQ,IAAI,QAAQ,MAAM,IAAI,CAAC,CAAC,KAAK,OAAO,GAAG;EAChJ,IAAI,eAAe,gBAAgB,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,GAAG,IAAI,gBAAgB,KAAK,GAAG;OAC5F,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,UAAU,GAAG,IAAI,aAAc,IAAc,SAAS,IAAI,GAAG,OAAO,gBAAgB,IAAI,EAAE,GAAG;CACnI;CAEA,IAAI,CAAC,IACH,IAAI,GAAG,GAAG,KAAK,UAAU,EAAE,IAAI,GAAG,KAAK,iBAAiB,GAAG;MACtD;EACL,MAAM,QAAQ,CAAC,QACX,GAAG,KAAK,cAAc,GAAG,YAAY,aAAa,IAAI,GAAG,SAAS,IAAI,GAAG,aAAa,KAAK,SAAS,GAAG,YAAY,GAAG,MAAM,GAAG,KAAK,0BAA0B,IAC9J,GAAG,YACD,GAAG,MAAM,gBAAgB,GAAG,IAAI,eAAe,IAAI,GAAG,cAAc,GAAG,IACvE,GAAG,OAAO,gBAAgB,GAAG,IAAI,iBAAiB,GAAG,iBAAiB,IAAI,IAAI,GAAG,eAAe,EAAE,EAAE,IAAI,SAAS,GAAG,eAAe,QAAQ,GAAG,MAAM,IAAI;EAC9J,IAAI,GAAG,GAAG,KAAK,UAAU,EAAE,IAAI,OAAO;EACtC,IACE,GAAG,GAAG,KAAK,QAAQ,EAAE,aAAa,GAAG,SAAS,eAAe,GAAG,cAAc,SAAS,GAAG,UAAU,MAAM,GAAG,QAAQ,gBAAgB,KAAK,GAAG,UAAU,MAAM,GAAG,QAAQ,uBAAuB,GAAG,gBAAgB,IAAI,GAAG,WAAW,GACtO;EACA,IAAI,GAAG,OAAO,QAAQ;GACpB,IAAI,GAAG,KAAK,QAAQ,CAAC;GACrB,KAAK,MAAM,KAAK,GAAG,OAAO,MAAM,GAAG,CAAC,GAAG;IACrC,MAAM,OAAO,EAAE,YAAY,cAAc,GAAG,MAAM,GAAG,IAAI,EAAE,YAAY,SAAS,GAAG,IAAI,GAAG,IAAI,EAAE,YAAY,aAAa,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG;IACrJ,IAAI,KAAK,KAAK,IAAI,EAAE,IAAI,GAAG,EAAE,KAAK,GAAG,GAAG,KAAK,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,IAAI,KAAK,EAAE,SAAS,aAAa,KAAK,EAAE,SAAS,EAAE,YAAY,cAAc,KAAK,SAAS,EAAE,OAAO,GAAG,MAAM,IAAI,GAAG;GAC/L;EACF;CACF;CACA,IAAI,GAAG,GAAG,KAAK,MAAM,EAAE,QAAQ,KAAK,SAAS,GAAG,IAAI,GAAG,KAAK,OAAO,QAAQ,KAAK,SAAS,IAAI,MAAM,IAAI,IAAI,GAAG,KAAK,cAAc,IAAI,cAAc,QAAQ,IAAI,EAAE,EAAE,IAAI,KAAK;CAC5K,IAAI,GAAG,GAAG,KAAK,KAAK,EAAE,SAAS,QAAQ,IAAI,GAAG;AAChD;AAIA,SAAgB,UAAU,aAAqB,MAAgC;CAC7E,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,SAAS,IAAI;CAC7B,IAAI,KAAK,MAAM;EACb,KAAK,MAAM,KAAK,SAAS,IAAI,KAAK,UAAU,CAAC,CAAC;EAC9C;CACF;CACA,IAAI,CAAC,QAAQ,QAAQ;EACnB,IAAI,+BAA+B,KAAK,GAAG;EAC3C;CACF;CACA,KAAK,MAAM,KAAK,SAAS;EACvB,MAAM,IAAI,EAAE,QAAQ,KAAK;EACzB,IAAI,GAAG,GAAG,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,KAAK,GAAG,GAAG,KAAK,EAAE,EAAE,EAAE,IAAI,GAAG,KAAK,QAAQ,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,UAAU,GAAG;EAC3H,IAAI,GAAG,IAAI,YAAY,SAAS,EAAE,MAAM,SAAS,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;EACzF,IAAI,GAAG,KAAK,OAAO,SAAS,EAAE,OAAO,GAAG,GAAG,CAAC;CAC9C;CACA,IAAI;CACJ,IAAI,GAAG,KAAK,GAAG,QAAQ,OAAO,aAAa,QAAQ,SAAS,IAAI,MAAM,GAAG,MAAM,SAAS,IAAI,EAAE,EAAE,CAAC;CACjG,IAAI,GAAG,KAAK,gBAAgB,IAAI,uBAAuB,QAAQ,IAAI,EAAE,0CAA0C,CAAC;CAChH,IAAI,GAAG,KAAK,qIAAqI,CAAC;AACpJ;AAEA,SAAgB,UAAU,aAAqB,IAAmB;CAChE,MAAM,OAAO,iBAAiB,WAAW;CACzC,MAAM,UAAU,SAAS,IAAI;CAC7B,MAAM,OAAO,KAAK,QAAQ,QAAQ,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;CACxD,UAAU,MAAM,IAAI;CACpB,IAAI,WAAW,QAAQ,SAAS,KAAK,OAAO,aAAa,QAAQ,SAAS,KAAK,WAAW,IAAI,KAAK,IAAI,EAAE;AAC3G;;;;;AAMA,eAAsB,WAAW,OAAsC,OAAiD,CAAC,GAAoB;CAC3I,MAAM,OAAO,iBAAiB,MAAM,WAAA,SAA0B;CAC9D,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,EAAE,WAAW,cAAc,MAAM,SAAS;EAAE,GAAG;EAAO,MAAM;CAAM,CAAC;CAEzE,MAAM,SADM,SAAS,IACJ,CAAC,CAAC,QAAQ,MAAM,CAAC,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE;CAC/D,IAAI,CAAC,OAAO,QAAQ;EAClB,IAAI,MAAM,KAAK,yBAAyB,MAAM,GAAG,KAAK,+BAA+B,KAAK,GAAG;EAC7F,OAAO;CACT;CACA,MAAM,YAAsB,CAAC;CAC7B,IAAI,OAAO,MAAM,UAAU,KAAK,MAAM;CACtC,IAAI,OAAO,SAAS,UAAU,KAAK,SAAS;CAC5C,IAAI,CAAC,UAAU,QAAQ,MAAM,IAAI,MAAM,kKAAkK,IAAI,qBAAqB;CAClO,IAAI,OAAO,QAAQ,QAAQ,aAAa,SAAS,MAAM,IAAI,MAAM,mEAAmE;CAEpI,MAAM,YAAY,OAAO,iBAAiB;CAC1C,IAAI,OAAO,cAAc,iBAAiB,OAAO,GAAG;CACpD,MAAM,yBAAS,IAAI,IAAuB;CAC1C,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;CACnD,MAAM,aAAa,IAAI,WAAW;EAChC,SAAS;EACT,aAAa,OAAO;EACpB,aAAa,OAAO;EACpB,cAAc,KAAK;EACnB,KAAK,OAAO,KAAU,WAAW;GAC/B,MAAM,SAAmB,CAAC;GAC1B,IAAI,IAAI,UAAU,IAAI,MAAM,KAAK,OAAO,MAAM;IAC5C,MAAM,IAAI,MAAM,WAAW,OAAO,MAAM,IAAI,SAAS;KAAE;KAAW;KAAQ,KAAK,OAAO;IAAI,CAAC;IAC3F,IAAI,EAAE,IAAI,IAAI,UAAU,OAAO,MAAM;SAChC,OAAO,KAAK,YAAY,EAAE,OAAO;GACxC;GACA,IAAI,IAAI,UAAU,IAAI,SAAS,KAAK,OAAO,SAAS;IAClD,MAAM,IAAI,MAAM,aAAa,OAAO,QAAQ,KAAK,OAAO,QAAQ,QAAQ,IAAI,SAAS;KAAE,WAAW,KAAK,IAAI,WAAW,GAAM;KAAG;KAAQ,MAAM,cAAc,OAAO,QAAQ,GAAG;IAAE,CAAC;IAChL,IAAI,EAAE,IAAI,IAAI,UAAU,OAAO,SAAS;SACnC,OAAO,KAAK,YAAY,EAAE,OAAO;GACxC;GACA,OAAO;EACT;EACA,iBAAiB,CAAC;EAClB,SAAS,QAAQ;GACf,MAAM,IAAI,MAAM,IAAI,IAAI,GAAG;GAC3B,OAAO,IAAI,EAAE,IAAI;IAAE,GAAG;IAAG,yBAAQ,IAAI,KAAK,EAAA,CAAE,YAAY;IAAG,UAAU,EAAE,WAAW,IAAI;IAAU,OAAO,IAAI,aAAa;GAAS,CAAC;GAClI,IAAI,GAAG,GAAG,IAAI,GAAG,EAAE,IAAI,IAAI,IAAI,kBAAkB,SAAS,IAAI,aAAa,UAAU,GAAG,GAAG;EAC7F;EACA,aAAa,QAAQ;GACnB,IAAI,IAAI,YAAY,aAAa,IAAI,GAAG,GAAG,MAAM,GAAG,EAAE,IAAI,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK,UAAU;EAClG;CACF,CAAC;CACD,IAAI,aAAa,OAAO,OAAO,aAAa,OAAO,SAAS,IAAI,MAAM,GAAG,UAAU,OAAO,YAAY,iBAAiB;CAEvH,KAAK,MAAM,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG;EACzD,MAAM,UAAU,EAAE,SAAS,SAAS,EAAE,QAAQ,QAAQ,MAAM,UAAU,SAAS,CAAC,CAAC,IAAI;EACrF,IAAI,CAAC,QAAQ,QAAQ;GACnB,IAAI,GAAG,GAAG,OAAO,GAAG,EAAE,IAAI,EAAE,IAAI,SAAS,EAAE,QAAQ,KAAK,OAAO,EAAE,uCAAuC,EAAE,QAAQ,GAAG,eAAe;GACpI,OAAO,IAAI,EAAE,IAAI,CAAC;GAClB;EACF;EACA,WAAW,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;CACxC;CACA,MAAM,WAAW,KAAK;CAEtB,MAAM,WAAW,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,EAAE,CAAC;CAEhD,MAAM,UAAU,SAAS,IAAI;CAC7B,UAAU,MAAM,CAAC,GAAG,QAAQ,QAAQ,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,OAAO,CAAC,CAAC;CACnF,IAAI,GAAG,OAAO,SAAS,OAAO,KAAK,aAAa,OAAO,KAAK,aAAa;CACzE,OAAO,OAAO;AAChB"}
1
+ {"version":3,"file":"admin-DI2rumeX.js","names":[],"sources":["../server/listen/admin.ts"],"sourcesContent":["import pc from \"picocolors\";\nimport { ApiError, getSelf } from \"./api.ts\";\nimport { describeGrants, hasAccess } from \"./grants.ts\";\nimport { Dispatcher, type Job, type Target } from \"./dispatcher.ts\";\nimport { runCommand } from \"./exec.ts\";\nimport { ensurePrivateDir } from \"./fsutil.ts\";\nimport { forwardEvent } from \"./forward.ts\";\nimport { routeWarning } from \"./hermes.ts\";\nimport { describeFilter, type ListenFlags, MAX_BATCH, resolveListen } from \"./options.ts\";\nimport { outputLine } from \"./listen.ts\";\nimport { isHermesRoute } from \"./recipes.ts\";\nimport { DEFAULT_PROFILE, deadPath, logPath, profilePath, validProfileName } from \"./paths.ts\";\nimport { loadProfile, saveProfile } from \"./profile.ts\";\nimport { DEFAULT_LISTEN_TYPES, forwardSecret } from \"./options.ts\";\nimport { installedServiceLabel } from \"./service.ts\";\nimport { type DeadEntry, pidAlive, readDead, readStatus, writeDead } from \"./state.ts\";\nimport { checkTransport, TransportError } from \"./transport.ts\";\nimport { formatDuration, safeText } from \"./ui.ts\";\nimport { NPX } from \"./version.ts\";\n\nconst out = (s = \"\") => process.stdout.write(`${s}\\n`);\nconst flagFor = (profile: string) => (profile === DEFAULT_PROFILE ? \"\" : ` --profile ${profile}`);\nconst ago = (iso: string | null | undefined) => (iso ? `${formatDuration(Date.now() - Date.parse(iso))} ago` : \"never\");\n\n// ───────────── listen secret ─────────────\n\n/** Prints the forward-mode signing secret (generating and saving it on first use). */\nexport function printSecret(profileName: string, opts: { rotate?: boolean }): void {\n const name = validProfileName(profileName);\n const profile = loadProfile(name);\n let { secret, created } = forwardSecret(profile);\n if (opts.rotate) {\n secret = forwardSecret({ ...profile, forward: undefined, forwardSecret: undefined }).secret;\n created = true;\n }\n if (created) {\n if (profile.forward) profile.forward = { ...profile.forward, secret };\n else profile.forwardSecret = secret;\n saveProfile(name, profile);\n process.stderr.write(pc.gray(`${opts.rotate ? \"Rotated\" : \"Generated\"} the forward signing secret for profile \"${name}\"${opts.rotate ? \"; restart the listener\" : \"\"}.\\n`));\n }\n // The secret alone on stdout, so `export XAQ_WEBHOOK_SECRET=$(xaq listen secret)` works.\n out(secret);\n}\n\n// ───────────── listen status ─────────────\n\nexport async function printStatus(profileName: string): Promise<void> {\n const name = validProfileName(profileName);\n const profile = loadProfile(name);\n const st = readStatus(name);\n const alive = st ? pidAlive(st.pid) && !st.stoppedAt : false;\n const dead = readDead(name);\n\n out(`${pc.bold(\"Profile\")} ${name} ${pc.gray(profilePath(name))}`);\n out(`${pc.bold(\"Agent\")} ${safeText(profile.agentName)} ${pc.gray(`(${profile.agentId})`)}`);\n out(`${pc.bold(\"Daemon\")} ${profile.url}`);\n out(`${pc.bold(\"Consumer\")} ${st?.consumer ?? profile.consumer}`);\n // What the last run used (flags included), else what the profile has saved.\n const exec = st && st.exec !== undefined ? st.exec : profile.exec;\n const forward = st && st.forward !== undefined ? st.forward : profile.forward?.url;\n const targets = [exec ? `exec ${safeText(exec, 120)}` : null, forward ? `forward ${forward}` : null].filter(Boolean);\n out(`${pc.bold(\"Runs\")} ${targets.length ? targets.join(\" · \") : pc.gray(\"nothing saved (pass --exec/--forward with --save)\")}`);\n out(\n `${pc.bold(\"Events\")} ${\n profile.types?.length\n ? profile.types.join(\", \")\n : `${DEFAULT_LISTEN_TYPES.join(\", \")} ${pc.gray(\"(default types; add --types all --save for every event)\")}`\n }`,\n );\n // The running listener's filters (flags included), else the profile's.\n let saved: ReturnType<typeof resolveListen>[\"config\"] | null = null;\n try {\n saved = resolveListen(name, profile, {}).config;\n } catch {\n // An invalid saved setting: `listen` itself reports it.\n }\n out(`${pc.bold(\"Filters\")} ${(alive && st?.filters) || (saved ? describeFilter(saved) : \"?\")}`);\n const batchSeconds = alive && st && st.batchSeconds !== undefined ? st.batchSeconds : (saved?.batchSeconds ?? 0);\n if (batchSeconds) out(`${pc.bold(\"Batch\")} ${formatDuration(batchSeconds * 1000)} window, up to ${MAX_BATCH} events per --exec run`);\n const route = forward && isHermesRoute(forward) ? routeWarning(name, forward) : null;\n if (route?.level === \"warn\") out(`${pc.bold(\"Route\")} ${pc.yellow(route.message)}`);\n out(`${pc.bold(\"Output\")} ${outputLine(name, { exec: exec ?? null, forward: forward ? { url: forward } : null }, logPath(name))}`);\n\n let svc = \"not installed\";\n if (process.platform === \"linux\" || process.platform === \"darwin\") {\n const installed = installedServiceLabel(name);\n if (installed) svc = `installed (${installed}; details: ${NPX} listen service status${flagFor(name)})`;\n }\n out(`${pc.bold(\"Service\")} ${svc}`);\n\n try {\n // Never send the key over a connection `listen` itself would refuse.\n await checkTransport(profile.url, { insecure: profile.insecure });\n const self = await getSelf(profile.url, profile.apiKey, 5000);\n out(`${pc.bold(\"Key\")} ${pc.green(\"valid\")}`);\n // The access the console actually granted, in plain words (what the agent should report to its human).\n const lines = describeGrants(self.grants, self.connections);\n out(`${pc.bold(\"Access\")} ${hasAccess(self) ? lines[0] : pc.yellow(lines[0]!)}`);\n for (const l of lines.slice(1)) out(` ${l}`);\n } catch (err) {\n const revoked = err instanceof ApiError && (err.status === 401 || err.status === 403);\n const why = err instanceof ApiError && err.status === 0 ? (err.cause as string) : err instanceof Error ? err.message.split(\"\\n\")[0] : String(err);\n if (err instanceof TransportError) out(`${pc.bold(\"Key\")} ${pc.red(`not checked: ${why}`)}`);\n else out(`${pc.bold(\"Key\")} ${revoked ? pc.red(`rejected: ${(err as Error).message}`) : pc.yellow(`not checked (${why})`)}`);\n }\n\n if (!st) {\n out(`${pc.bold(\"Listener\")} ${pc.gray(\"has not run yet\")}`);\n } else {\n const state = !alive\n ? pc.gray(`not running${st.stoppedAt ? ` (stopped ${ago(st.stoppedAt)}${st.exitReason ? `: ${safeText(st.exitReason, 160)}` : \"\"})` : \" (exited unexpectedly)\"}`)\n : st.connected\n ? pc.green(`running (pid ${st.pid}), connected ${ago(st.connectedSince)}`)\n : pc.yellow(`running (pid ${st.pid}), disconnected${st.lastDisconnect ? ` ${ago(st.lastDisconnect.at)}: ${safeText(st.lastDisconnect.reason, 160)}` : \"\"}`);\n out(`${pc.bold(\"Listener\")} ${state}`);\n out(\n `${pc.bold(\"Cursor\")} acked #${st.ackedSeq} · received #${st.receivedSeq}${alive && st.running ? ` · ${st.running} in progress` : \"\"}${st.skipped ? ` · ${st.skipped} skipped by filters` : \"\"} · last event ${ago(st.lastEventAt)}`,\n );\n if (st.recent.length) {\n out(pc.bold(\"Recent\"));\n for (const r of st.recent.slice(0, 8)) {\n const icon = r.outcome === \"succeeded\" ? pc.green(\"✓\") : r.outcome === \"dead\" ? pc.red(\"✖\") : r.outcome === \"retrying\" ? pc.yellow(\"↻\") : pc.gray(\"·\");\n out(` ${icon} #${r.seq} ${r.type} ${pc.gray(`${ago(r.at)}${r.attempts > 1 ? `, ${r.attempts} attempts` : \"\"}${r.error && r.outcome !== \"succeeded\" ? `, ${safeText(r.error, 100)}` : \"\"}`)}`);\n }\n }\n }\n out(`${pc.bold(\"Dead\")} ${dead.length ? pc.red(`${dead.length} event${dead.length > 1 ? \"s\" : \"\"}`) + pc.gray(` (inspect: ${NPX} listen dead${flagFor(name)})`) : \"0\"}`);\n out(`${pc.bold(\"Log\")} ${logPath(name)}`);\n}\n\n// ───────────── listen dead ─────────────\n\nexport function printDead(profileName: string, opts: { json?: boolean }): void {\n const name = validProfileName(profileName);\n const entries = readDead(name);\n if (opts.json) {\n for (const e of entries) out(JSON.stringify(e));\n return;\n }\n if (!entries.length) {\n out(`No dead events for profile \"${name}\".`);\n return;\n }\n for (const e of entries) {\n const m = e.payload.data.message;\n out(`${pc.red(\"✖\")} #${e.seq} ${e.type} ${pc.gray(e.id)} ${pc.gray(`died ${ago(e.deadAt)} after ${e.attempts} attempts`)}`);\n if (m) out(` from ${safeText(m.from?.email ?? \"?\", 80)} \"${safeText(m.subject, 80)}\"`);\n out(pc.gray(` ${safeText(e.error, 300)}`));\n }\n out();\n out(pc.gray(`${entries.length} dead event${entries.length > 1 ? \"s\" : \"\"} in ${deadPath(name)}.`));\n out(pc.gray(`Replay with: ${NPX} listen dead --replay${flagFor(name)} (one: --id <evt_…>; drop all: --clear)`));\n out(pc.gray(`Replay uses the profile's saved --exec/--forward; if you ran them as flags only, pass them again, e.g. --replay --exec '<command>'.`));\n}\n\nexport function clearDead(profileName: string, id?: string): void {\n const name = validProfileName(profileName);\n const entries = readDead(name);\n const keep = id ? entries.filter((e) => e.id !== id) : [];\n writeDead(name, keep);\n out(`Removed ${entries.length - keep.length} dead event${entries.length - keep.length === 1 ? \"\" : \"s\"}.`);\n}\n\n/**\n * Runs dead events again through the profile's targets (or the ones passed as flags). Successes are\n * removed from dead.jsonl; failures stay. Returns how many still failed.\n */\nexport async function replayDead(flags: ListenFlags & { id?: string }, opts: { retryDelayMs?: (n: number) => number } = {}): Promise<number> {\n const name = validProfileName(flags.profile ?? DEFAULT_PROFILE);\n const profile = loadProfile(name);\n const { config } = resolveListen(name, profile, { ...flags, save: false });\n const all = readDead(name);\n const chosen = all.filter((e) => !flags.id || e.id === flags.id);\n if (!chosen.length) {\n out(flags.id ? `No dead event with id ${flags.id}.` : `No dead events for profile \"${name}\".`);\n return 0;\n }\n const available: Target[] = [];\n if (config.exec) available.push(\"exec\");\n if (config.forward) available.push(\"forward\");\n if (!available.length) throw new Error(`Nothing to replay to: pass --recipe <tool> (claude-code, codex, cursor, hermes), --exec '<command>' and/or --forward <url> (or save them in the profile with \\`${NPX} listen --save …\\`).`);\n if (config.exec && process.platform === \"win32\") throw new Error(\"--exec isn't supported on Windows; replay with --forward instead.\");\n\n const timeoutMs = config.timeoutSeconds * 1000;\n if (config.cwdIsDefault) ensurePrivateDir(config.cwd);\n const failed = new Map<string, DeadEntry>();\n const bySeq = new Map(chosen.map((e) => [e.seq, e]));\n const dispatcher = new Dispatcher({\n targets: available,\n concurrency: config.concurrency,\n maxAttempts: config.maxAttempts,\n retryDelayMs: opts.retryDelayMs,\n run: async (job: Job, signal) => {\n const errors: string[] = [];\n if (job.remaining.has(\"exec\") && config.exec) {\n const r = await runCommand(config.exec, job.payload, { timeoutMs, signal, cwd: config.cwd });\n if (r.ok) job.remaining.delete(\"exec\");\n else errors.push(`command: ${r.error}`);\n }\n if (job.remaining.has(\"forward\") && config.forward) {\n const r = await forwardEvent(config.forward.url, config.forward.secret, job.payload, { timeoutMs: Math.min(timeoutMs, 30_000), signal, wrap: isHermesRoute(config.forward.url) });\n if (r.ok) job.remaining.delete(\"forward\");\n else errors.push(`forward: ${r.error}`);\n }\n return errors;\n },\n onAdvance: () => {},\n onDead: (job) => {\n const e = bySeq.get(job.seq)!;\n failed.set(e.id, { ...e, deadAt: new Date().toISOString(), attempts: e.attempts + job.attempts, error: job.lastError ?? \"failed\" });\n out(`${pc.red(\"✖\")} #${job.seq} still failing: ${safeText(job.lastError ?? \"failed\", 200)}`);\n },\n onFinished: (job) => {\n if (job.outcome === \"succeeded\") out(`${pc.green(\"✓\")} #${job.seq} ${job.payload.type} replayed`);\n },\n });\n out(`Replaying ${chosen.length} dead event${chosen.length > 1 ? \"s\" : \"\"} (up to ${config.maxAttempts} attempts each)…`);\n // Only the targets that never succeeded (a forward that went through isn't POSTed again).\n for (const e of [...chosen].sort((a, b) => a.seq - b.seq)) {\n const targets = e.targets?.length ? e.targets.filter((t) => available.includes(t)) : available;\n if (!targets.length) {\n out(`${pc.yellow(\"!\")} #${e.seq} needs ${e.targets.join(\" and \")}, which isn't configured now; pass --${e.targets[0]} to replay it.`);\n failed.set(e.id, e);\n continue;\n }\n dispatcher.push(e.payload, { targets });\n }\n await dispatcher.idle();\n\n const replayed = new Set(chosen.map((e) => e.id));\n // Re-read so entries added by a running listener meanwhile are kept.\n const current = readDead(name);\n writeDead(name, [...current.filter((e) => !replayed.has(e.id)), ...failed.values()]);\n out(`${chosen.length - failed.size} replayed, ${failed.size} still dead.`);\n return failed.size;\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { i as sha256 } from "./crypto-fv2fj3S8.js";
3
- import { r as SESSION_COOKIE, t as CSRF_HEADER, y as SESSION_MAX_AGE_MS } from "./auth-D-GmwNv-.js";
4
- import { n as dataFiles } from "./config-C9XmYZWO.js";
5
- import { c as runningInstance } from "./instance-Dj9TpJB2.js";
2
+ import { i as sha256 } from "./crypto-s8x6up-l.js";
3
+ import { r as SESSION_COOKIE, t as CSRF_HEADER, y as SESSION_MAX_AGE_MS } from "./auth-BLYwD_zT.js";
4
+ import { n as dataFiles } from "./config-D3jl_sOt.js";
5
+ import { c as runningInstance } from "./instance-CPpDQZ1c.js";
6
6
  import { existsSync } from "node:fs";
7
7
  import { DatabaseSync } from "node:sqlite";
8
8
  import { randomBytes } from "node:crypto";
@@ -159,4 +159,4 @@ function openAdminChannel(opts) {
159
159
  //#endregion
160
160
  export { createCliSession as a, CLI_SESSION_TTL_MS as i, CLEANUP_SIGNALS as n, daemonOrigin as o, CLI_SESSION_LABEL as r, openAdminChannel as s, AdminApiError as t };
161
161
 
162
- //# sourceMappingURL=admin-5gThRwgj.js.map
162
+ //# sourceMappingURL=admin-DeCN2VDX.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin-5gThRwgj.js","names":[],"sources":["../server/connections-cli/admin.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { SESSION_COOKIE, CSRF_HEADER } from \"../auth/index.ts\";\nimport { SESSION_MAX_AGE_MS } from \"../auth/sessions.ts\";\nimport { dataFiles } from \"../config.ts\";\nimport { sha256 } from \"../crypto.ts\";\nimport { type InstanceInfo, runningInstance } from \"../instance.ts\";\n\n/**\n * The CLI's admin channel to the daemon on this machine (`connections …`, `import`), without the console password.\n *\n * Whoever can write the data directory already holds master.key and the database (`reset-password` relies on the\n * same thing), so the CLI proves it is the admin by writing there: it inserts a one-time admin session (only\n * sha256(token) is stored, like console sessions) that expires after 15 minutes, uses it as the session cookie\n * against the daemon's own /api on loopback, and deletes it when the command ends. Nothing new listens, no\n * token file sits on disk, and the daemon's admin auth (requireAdmin + the CSRF header) is unchanged: every API\n * call is checked, audited and rate-limited exactly like the console's.\n *\n * Console sessions slide forward on use up to SESSION_MAX_AGE_MS after created_at. The CLI's row is written with\n * created_at = expiry − SESSION_MAX_AGE_MS, so that hard limit is the 15-minute expiry and the row can never be\n * extended (see docs/FOLLOWUPS.md: a session kind column would say this directly).\n */\n\nexport const CLI_SESSION_TTL_MS = 15 * 60_000;\nexport const CLI_SESSION_LABEL = \"xaq CLI (local admin, one-time)\";\n\nexport class AdminApiError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n readonly hint?: string,\n /** The parsed response body (some routes answer an error with their own shape, e.g. /finish). */\n readonly body?: unknown,\n ) {\n super(message);\n this.name = \"AdminApiError\";\n }\n}\n\nexport interface AdminChannel {\n /** The daemon's loopback origin, e.g. http://127.0.0.1:8425. */\n readonly baseUrl: string;\n request<T>(method: string, path: string, body?: unknown): Promise<T>;\n /**\n * A fingerprint of the connection's stored credential (issued_at plus its randomised ciphertext), read from the\n * database: it changes on every saved sign-in, so a Reconnect's completion is seen even on an active connection.\n * null when nothing is stored (or it can't be read). Never the credential itself.\n */\n credentialStamp(connectionId: string): string | null;\n /** Deletes the one-time session. Idempotent. */\n close(): void;\n}\n\n/** Signals that end a CLI command: the one-time session is deleted on each (and on a normal exit). */\nexport const CLEANUP_SIGNALS = [\"SIGINT\", \"SIGTERM\", \"SIGHUP\"] as const;\n\nexport interface OpenAdminOptions {\n dataDir: string;\n /** Tests: the running instance, instead of reading the lock file. */\n instance?: InstanceInfo | null;\n /** Tests: the daemon origin, instead of deriving it from the instance. */\n baseUrl?: string;\n fetch?: typeof fetch;\n clock?: () => number;\n}\n\n/** Where the CLI reaches the daemon: its bind address, or loopback when it listens on every interface. */\nexport function daemonOrigin(instance: Pick<InstanceInfo, \"host\" | \"port\">): string {\n const host = instance.host;\n const any = !host || host === \"0.0.0.0\" || host === \"::\" || host === \"[::]\";\n const h = any ? \"127.0.0.1\" : host.replace(/^\\[|\\]$/g, \"\");\n return `http://${h.includes(\":\") ? `[${h}]` : h}:${instance.port}`;\n}\n\n/** Inserts the one-time admin session; returns the token. */\nexport function createCliSession(dbPath: string, clock: () => number = Date.now): { token: string; remove: () => void } {\n const token = randomBytes(32).toString(\"base64url\");\n const hash = sha256(token);\n const expires = clock() + CLI_SESSION_TTL_MS;\n const db = new DatabaseSync(dbPath);\n try {\n db.exec(\"PRAGMA busy_timeout = 5000\");\n db.prepare(\"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\").run(\n hash,\n new Date(expires - SESSION_MAX_AGE_MS).toISOString(),\n new Date(expires).toISOString(),\n \"local-cli\",\n CLI_SESSION_LABEL,\n );\n } finally {\n db.close();\n }\n let removed = false;\n return {\n token,\n remove() {\n if (removed) return;\n removed = true;\n try {\n const d = new DatabaseSync(dbPath);\n try {\n d.exec(\"PRAGMA busy_timeout = 5000\");\n d.prepare(\"DELETE FROM admin_sessions WHERE token_hash = ?\").run(hash);\n } finally {\n d.close();\n }\n } catch {\n // It expires on its own within 15 minutes.\n }\n },\n };\n}\n\nexport function openAdminChannel(opts: OpenAdminOptions): AdminChannel {\n // xaq.db, or agentinbox.db in a data directory from before the rename (rename-xaq: keep).\n const dbPath = dataFiles(opts.dataDir).db;\n if (!existsSync(dbPath)) {\n throw new Error(\n `No xaq data in ${opts.dataDir}. Run this on the machine xaq runs on, as the user it runs as ` +\n \"(with sudo for a system service), or pass its data directory with --data.\",\n );\n }\n const instance = opts.instance === undefined ? runningInstance(opts.dataDir) : opts.instance;\n if (!instance && !opts.baseUrl) throw new Error(`xaq isn't running with the data directory ${opts.dataDir}. Start it first (or pass --data).`);\n const baseUrl = opts.baseUrl ?? daemonOrigin(instance!);\n let session: ReturnType<typeof createCliSession>;\n try {\n session = createCliSession(dbPath, opts.clock);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n throw new Error(\n `Can't open ${dbPath}${code ? ` (${code})` : \"\"}: ${(err as Error).message}. Run this as the user xaq runs as (with sudo for a system service).`,\n );\n }\n const onExit = () => session.remove();\n process.once(\"exit\", onExit);\n const doFetch = opts.fetch ?? fetch;\n const send = (method: string, path: string, body: unknown) => {\n const headers: Record<string, string> = { cookie: `${SESSION_COOKIE}=${session.token}`, [CSRF_HEADER]: \"1\", accept: \"application/json\" };\n if (body !== undefined) headers[\"content-type\"] = \"application/json\";\n return doFetch(`${baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(120_000) });\n };\n return {\n baseUrl,\n async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n let res: Response;\n try {\n res = await send(method, path, body);\n // The session's 15 minutes ran out (a slow sign-in): mint a fresh one and retry once.\n if (res.status === 401) {\n await res.body?.cancel().catch(() => {});\n session.remove();\n session = createCliSession(dbPath, opts.clock);\n res = await send(method, path, body);\n }\n } catch (err) {\n throw new Error(`Couldn't reach xaq at ${baseUrl}: ${(err as Error).message}`);\n }\n if (res.status === 204) return undefined as T;\n const text = await res.text();\n let data: unknown = null;\n try {\n data = text ? JSON.parse(text) : null;\n } catch {\n // Not JSON: reported below.\n }\n if (!res.ok) {\n const e = (data as { error?: { code?: string; message?: string; hint?: string } } | null)?.error;\n throw new AdminApiError(res.status, e?.code ?? `http_${res.status}`, e?.message ?? `HTTP ${res.status} from ${path}`, e?.hint, data);\n }\n return data as T;\n },\n credentialStamp(connectionId: string): string | null {\n try {\n const db = new DatabaseSync(dbPath, { readOnly: true });\n try {\n db.exec(\"PRAGMA busy_timeout = 5000\");\n const r = db.prepare(\"SELECT issued_at, access_token_enc, header_value_enc FROM connection_credentials WHERE connection_id = ?\").get(connectionId) as\n | { issued_at: string | null; access_token_enc: string | null; header_value_enc: string | null }\n | undefined;\n return r ? sha256(`${r.issued_at ?? \"\"}|${r.access_token_enc ?? \"\"}|${r.header_value_enc ?? \"\"}`) : null;\n } finally {\n db.close();\n }\n } catch {\n return null;\n }\n },\n close() {\n process.removeListener(\"exit\", onExit);\n session.remove();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"admin-DeCN2VDX.js","names":[],"sources":["../server/connections-cli/admin.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { SESSION_COOKIE, CSRF_HEADER } from \"../auth/index.ts\";\nimport { SESSION_MAX_AGE_MS } from \"../auth/sessions.ts\";\nimport { dataFiles } from \"../config.ts\";\nimport { sha256 } from \"../crypto.ts\";\nimport { type InstanceInfo, runningInstance } from \"../instance.ts\";\n\n/**\n * The CLI's admin channel to the daemon on this machine (`connections …`, `import`), without the console password.\n *\n * Whoever can write the data directory already holds master.key and the database (`reset-password` relies on the\n * same thing), so the CLI proves it is the admin by writing there: it inserts a one-time admin session (only\n * sha256(token) is stored, like console sessions) that expires after 15 minutes, uses it as the session cookie\n * against the daemon's own /api on loopback, and deletes it when the command ends. Nothing new listens, no\n * token file sits on disk, and the daemon's admin auth (requireAdmin + the CSRF header) is unchanged: every API\n * call is checked, audited and rate-limited exactly like the console's.\n *\n * Console sessions slide forward on use up to SESSION_MAX_AGE_MS after created_at. The CLI's row is written with\n * created_at = expiry − SESSION_MAX_AGE_MS, so that hard limit is the 15-minute expiry and the row can never be\n * extended (see docs/FOLLOWUPS.md: a session kind column would say this directly).\n */\n\nexport const CLI_SESSION_TTL_MS = 15 * 60_000;\nexport const CLI_SESSION_LABEL = \"xaq CLI (local admin, one-time)\";\n\nexport class AdminApiError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n readonly hint?: string,\n /** The parsed response body (some routes answer an error with their own shape, e.g. /finish). */\n readonly body?: unknown,\n ) {\n super(message);\n this.name = \"AdminApiError\";\n }\n}\n\nexport interface AdminChannel {\n /** The daemon's loopback origin, e.g. http://127.0.0.1:8425. */\n readonly baseUrl: string;\n request<T>(method: string, path: string, body?: unknown): Promise<T>;\n /**\n * A fingerprint of the connection's stored credential (issued_at plus its randomised ciphertext), read from the\n * database: it changes on every saved sign-in, so a Reconnect's completion is seen even on an active connection.\n * null when nothing is stored (or it can't be read). Never the credential itself.\n */\n credentialStamp(connectionId: string): string | null;\n /** Deletes the one-time session. Idempotent. */\n close(): void;\n}\n\n/** Signals that end a CLI command: the one-time session is deleted on each (and on a normal exit). */\nexport const CLEANUP_SIGNALS = [\"SIGINT\", \"SIGTERM\", \"SIGHUP\"] as const;\n\nexport interface OpenAdminOptions {\n dataDir: string;\n /** Tests: the running instance, instead of reading the lock file. */\n instance?: InstanceInfo | null;\n /** Tests: the daemon origin, instead of deriving it from the instance. */\n baseUrl?: string;\n fetch?: typeof fetch;\n clock?: () => number;\n}\n\n/** Where the CLI reaches the daemon: its bind address, or loopback when it listens on every interface. */\nexport function daemonOrigin(instance: Pick<InstanceInfo, \"host\" | \"port\">): string {\n const host = instance.host;\n const any = !host || host === \"0.0.0.0\" || host === \"::\" || host === \"[::]\";\n const h = any ? \"127.0.0.1\" : host.replace(/^\\[|\\]$/g, \"\");\n return `http://${h.includes(\":\") ? `[${h}]` : h}:${instance.port}`;\n}\n\n/** Inserts the one-time admin session; returns the token. */\nexport function createCliSession(dbPath: string, clock: () => number = Date.now): { token: string; remove: () => void } {\n const token = randomBytes(32).toString(\"base64url\");\n const hash = sha256(token);\n const expires = clock() + CLI_SESSION_TTL_MS;\n const db = new DatabaseSync(dbPath);\n try {\n db.exec(\"PRAGMA busy_timeout = 5000\");\n db.prepare(\"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\").run(\n hash,\n new Date(expires - SESSION_MAX_AGE_MS).toISOString(),\n new Date(expires).toISOString(),\n \"local-cli\",\n CLI_SESSION_LABEL,\n );\n } finally {\n db.close();\n }\n let removed = false;\n return {\n token,\n remove() {\n if (removed) return;\n removed = true;\n try {\n const d = new DatabaseSync(dbPath);\n try {\n d.exec(\"PRAGMA busy_timeout = 5000\");\n d.prepare(\"DELETE FROM admin_sessions WHERE token_hash = ?\").run(hash);\n } finally {\n d.close();\n }\n } catch {\n // It expires on its own within 15 minutes.\n }\n },\n };\n}\n\nexport function openAdminChannel(opts: OpenAdminOptions): AdminChannel {\n // xaq.db, or agentinbox.db in a data directory from before the rename (rename-xaq: keep).\n const dbPath = dataFiles(opts.dataDir).db;\n if (!existsSync(dbPath)) {\n throw new Error(\n `No xaq data in ${opts.dataDir}. Run this on the machine xaq runs on, as the user it runs as ` +\n \"(with sudo for a system service), or pass its data directory with --data.\",\n );\n }\n const instance = opts.instance === undefined ? runningInstance(opts.dataDir) : opts.instance;\n if (!instance && !opts.baseUrl) throw new Error(`xaq isn't running with the data directory ${opts.dataDir}. Start it first (or pass --data).`);\n const baseUrl = opts.baseUrl ?? daemonOrigin(instance!);\n let session: ReturnType<typeof createCliSession>;\n try {\n session = createCliSession(dbPath, opts.clock);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n throw new Error(\n `Can't open ${dbPath}${code ? ` (${code})` : \"\"}: ${(err as Error).message}. Run this as the user xaq runs as (with sudo for a system service).`,\n );\n }\n const onExit = () => session.remove();\n process.once(\"exit\", onExit);\n const doFetch = opts.fetch ?? fetch;\n const send = (method: string, path: string, body: unknown) => {\n const headers: Record<string, string> = { cookie: `${SESSION_COOKIE}=${session.token}`, [CSRF_HEADER]: \"1\", accept: \"application/json\" };\n if (body !== undefined) headers[\"content-type\"] = \"application/json\";\n return doFetch(`${baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(120_000) });\n };\n return {\n baseUrl,\n async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n let res: Response;\n try {\n res = await send(method, path, body);\n // The session's 15 minutes ran out (a slow sign-in): mint a fresh one and retry once.\n if (res.status === 401) {\n await res.body?.cancel().catch(() => {});\n session.remove();\n session = createCliSession(dbPath, opts.clock);\n res = await send(method, path, body);\n }\n } catch (err) {\n throw new Error(`Couldn't reach xaq at ${baseUrl}: ${(err as Error).message}`);\n }\n if (res.status === 204) return undefined as T;\n const text = await res.text();\n let data: unknown = null;\n try {\n data = text ? JSON.parse(text) : null;\n } catch {\n // Not JSON: reported below.\n }\n if (!res.ok) {\n const e = (data as { error?: { code?: string; message?: string; hint?: string } } | null)?.error;\n throw new AdminApiError(res.status, e?.code ?? `http_${res.status}`, e?.message ?? `HTTP ${res.status} from ${path}`, e?.hint, data);\n }\n return data as T;\n },\n credentialStamp(connectionId: string): string | null {\n try {\n const db = new DatabaseSync(dbPath, { readOnly: true });\n try {\n db.exec(\"PRAGMA busy_timeout = 5000\");\n const r = db.prepare(\"SELECT issued_at, access_token_enc, header_value_enc FROM connection_credentials WHERE connection_id = ?\").get(connectionId) as\n | { issued_at: string | null; access_token_enc: string | null; header_value_enc: string | null }\n | undefined;\n return r ? sha256(`${r.issued_at ?? \"\"}|${r.access_token_enc ?? \"\"}|${r.header_value_enc ?? \"\"}`) : null;\n } finally {\n db.close();\n }\n } catch {\n return null;\n }\n },\n close() {\n process.removeListener(\"exit\", onExit);\n session.remove();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as AdminApiError } from "./admin-5gThRwgj.js";
2
+ import { t as AdminApiError } from "./admin-DeCN2VDX.js";
3
3
  //#region server/connections-cli/api.ts
4
4
  /** The routes aren't there: an older daemon, or brokerUpstreams is off. */
5
5
  var BrokerOffError = class extends Error {
@@ -86,4 +86,4 @@ var ConnectionsApi = class {
86
86
  //#endregion
87
87
  export { ConnectionsApi };
88
88
 
89
- //# sourceMappingURL=api-BFpElwqK.js.map
89
+ //# sourceMappingURL=api-CmTgScTD.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-BFpElwqK.js","names":[],"sources":["../server/connections-cli/api.ts"],"sourcesContent":["import type {\n AccessLevel,\n Agent,\n ConnectionGrant,\n ConnectionProbe,\n ConnectionView,\n CreateConnectionInput,\n PresetView,\n StartConnectInput,\n StartConnectResult,\n} from \"../../shared/types.ts\";\nimport { AdminApiError, type AdminChannel } from \"./admin.ts\";\n\n/**\n * The admin connections API (server/api/connections.ts, DTOs in shared/types.ts) as the CLI uses it. While the\n * `brokerUpstreams` setting is off every /api/connections route answers 404; the CLI says so in plain words.\n */\n\nexport type { ConnectionProbe, ConnectionView, PresetView, StartConnectResult };\n\n/** The routes aren't there: an older daemon, or brokerUpstreams is off. */\nexport class BrokerOffError extends Error {\n constructor() {\n super(\n \"This xaq doesn't have connections turned on yet (the brokerUpstreams setting), or is older than this CLI. \" +\n \"Update it, turn connections on in the console's Settings, then run this again.\",\n );\n this.name = \"BrokerOffError\";\n }\n}\n\nconst enc = encodeURIComponent;\n\nexport class ConnectionsApi {\n constructor(readonly channel: AdminChannel) {}\n\n private async call<T>(method: string, path: string, body?: unknown): Promise<T> {\n try {\n return await this.channel.request<T>(method, path, body);\n } catch (err) {\n if (err instanceof AdminApiError && err.status === 404 && err.code === \"not_found\" && path.startsWith(\"/api/connections\")) throw new BrokerOffError();\n throw err;\n }\n }\n\n list(): Promise<ConnectionView[]> {\n return this.call(\"GET\", \"/api/connections\");\n }\n presets(): Promise<PresetView[]> {\n return this.call(\"GET\", \"/api/connections/presets\");\n }\n /** With `connectionId`, the probe uses that connection's private-target allow (when it covers the URL). */\n probe(url: string, connectionId?: string): Promise<ConnectionProbe> {\n return this.call(\"POST\", \"/api/connections/probe\", connectionId ? { url, connectionId } : { url });\n }\n create(input: CreateConnectionInput): Promise<ConnectionView> {\n return this.call(\"POST\", \"/api/connections\", input);\n }\n get(id: string): Promise<ConnectionView> {\n return this.call(\"GET\", `/api/connections/${enc(id)}`);\n }\n /** Stores a static header (and connects), or starts OAuth and returns the authorize URL. */\n connect(id: string, input: StartConnectInput = {}): Promise<StartConnectResult> {\n return this.call(\"POST\", `/api/connections/${enc(id)}/connect`, input);\n }\n /** The admin-only private-target allow, pinned server-side to the URL's exact scheme://host:port. */\n allowPrivateTarget(id: string, allow: boolean): Promise<ConnectionView> {\n return this.call(\"PUT\", `/api/connections/${enc(id)}/private-target`, { allow });\n }\n /** The kill switch: disabled at once, pending calls cancelled, credentials deleted. */\n kill(id: string): Promise<void> {\n return this.call(\"POST\", `/api/connections/${enc(id)}/kill`, {});\n }\n remove(id: string): Promise<void> {\n return this.call(\"DELETE\", `/api/connections/${enc(id)}`);\n }\n async bySlug(slug: string): Promise<ConnectionView> {\n const all = await this.list();\n const found = all.find((c) => c.slug === slug);\n if (!found) throw new Error(`No connection \"${slug}\". ${all.length ? `There are: ${all.map((c) => c.slug).join(\", \")}.` : \"There are none yet.\"}`);\n return found;\n }\n grants(id: string): Promise<ConnectionGrant[]> {\n return this.call(\"GET\", `/api/connections/${enc(id)}/grants`);\n }\n /** The full list: agents not in it lose access. */\n setGrants(id: string, grants: { agentId: string; level: AccessLevel }[]): Promise<ConnectionGrant[]> {\n return this.call(\"PUT\", `/api/connections/${enc(id)}/grants`, { grants });\n }\n agents(): Promise<Agent[]> {\n return this.channel.request(\"GET\", \"/api/agents\");\n }\n\n /** Paste mode: the address the browser landed on after approving (the daemon checks state and issuer). */\n async finish(url: string): Promise<StartConnectResult> {\n try {\n return await this.call<StartConnectResult>(\"POST\", \"/api/connections/finish\", { url });\n } catch (err) {\n // A refused sign-in is a 400 carrying a StartConnectResult, not the usual error envelope.\n const body = err instanceof AdminApiError ? (err.body as StartConnectResult | null) : null;\n if (body?.status === \"error\") return body;\n throw err;\n }\n }\n}\n"],"mappings":";;;;AAqBA,IAAa,iBAAb,cAAoC,MAAM;CACxC,cAAc;EACZ,MACE,0LAEF;EACA,KAAK,OAAO;CACd;AACF;AAEA,MAAM,MAAM;AAEZ,IAAa,iBAAb,MAA4B;CACL;CAArB,YAAY,SAAgC;EAAvB,KAAA,UAAA;CAAwB;CAE7C,MAAc,KAAQ,QAAgB,MAAc,MAA4B;EAC9E,IAAI;GACF,OAAO,MAAM,KAAK,QAAQ,QAAW,QAAQ,MAAM,IAAI;EACzD,SAAS,KAAK;GACZ,IAAI,eAAe,iBAAiB,IAAI,WAAW,OAAO,IAAI,SAAS,eAAe,KAAK,WAAW,kBAAkB,GAAG,MAAM,IAAI,eAAe;GACpJ,MAAM;EACR;CACF;CAEA,OAAkC;EAChC,OAAO,KAAK,KAAK,OAAO,kBAAkB;CAC5C;CACA,UAAiC;EAC/B,OAAO,KAAK,KAAK,OAAO,0BAA0B;CACpD;;CAEA,MAAM,KAAa,cAAiD;EAClE,OAAO,KAAK,KAAK,QAAQ,0BAA0B,eAAe;GAAE;GAAK;EAAa,IAAI,EAAE,IAAI,CAAC;CACnG;CACA,OAAO,OAAuD;EAC5D,OAAO,KAAK,KAAK,QAAQ,oBAAoB,KAAK;CACpD;CACA,IAAI,IAAqC;EACvC,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,GAAG;CACvD;;CAEA,QAAQ,IAAY,QAA2B,CAAC,GAAgC;EAC9E,OAAO,KAAK,KAAK,QAAQ,oBAAoB,IAAI,EAAE,EAAE,WAAW,KAAK;CACvE;;CAEA,mBAAmB,IAAY,OAAyC;EACtE,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,EAAE,kBAAkB,EAAE,MAAM,CAAC;CACjF;;CAEA,KAAK,IAA2B;EAC9B,OAAO,KAAK,KAAK,QAAQ,oBAAoB,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;CACjE;CACA,OAAO,IAA2B;EAChC,OAAO,KAAK,KAAK,UAAU,oBAAoB,IAAI,EAAE,GAAG;CAC1D;CACA,MAAM,OAAO,MAAuC;EAClD,MAAM,MAAM,MAAM,KAAK,KAAK;EAC5B,MAAM,QAAQ,IAAI,MAAM,MAAM,EAAE,SAAS,IAAI;EAC7C,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,kBAAkB,KAAK,KAAK,IAAI,SAAS,cAAc,IAAI,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,KAAK,uBAAuB;EACjJ,OAAO;CACT;CACA,OAAO,IAAwC;EAC7C,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,EAAE,QAAQ;CAC9D;;CAEA,UAAU,IAAY,QAA+E;EACnG,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC;CAC1E;CACA,SAA2B;EACzB,OAAO,KAAK,QAAQ,QAAQ,OAAO,aAAa;CAClD;;CAGA,MAAM,OAAO,KAA0C;EACrD,IAAI;GACF,OAAO,MAAM,KAAK,KAAyB,QAAQ,2BAA2B,EAAE,IAAI,CAAC;EACvF,SAAS,KAAK;GAEZ,MAAM,OAAO,eAAe,gBAAiB,IAAI,OAAqC;GACtF,IAAI,MAAM,WAAW,SAAS,OAAO;GACrC,MAAM;EACR;CACF;AACF"}
1
+ {"version":3,"file":"api-CmTgScTD.js","names":[],"sources":["../server/connections-cli/api.ts"],"sourcesContent":["import type {\n AccessLevel,\n Agent,\n ConnectionGrant,\n ConnectionProbe,\n ConnectionView,\n CreateConnectionInput,\n PresetView,\n StartConnectInput,\n StartConnectResult,\n} from \"../../shared/types.ts\";\nimport { AdminApiError, type AdminChannel } from \"./admin.ts\";\n\n/**\n * The admin connections API (server/api/connections.ts, DTOs in shared/types.ts) as the CLI uses it. While the\n * `brokerUpstreams` setting is off every /api/connections route answers 404; the CLI says so in plain words.\n */\n\nexport type { ConnectionProbe, ConnectionView, PresetView, StartConnectResult };\n\n/** The routes aren't there: an older daemon, or brokerUpstreams is off. */\nexport class BrokerOffError extends Error {\n constructor() {\n super(\n \"This xaq doesn't have connections turned on yet (the brokerUpstreams setting), or is older than this CLI. \" +\n \"Update it, turn connections on in the console's Settings, then run this again.\",\n );\n this.name = \"BrokerOffError\";\n }\n}\n\nconst enc = encodeURIComponent;\n\nexport class ConnectionsApi {\n constructor(readonly channel: AdminChannel) {}\n\n private async call<T>(method: string, path: string, body?: unknown): Promise<T> {\n try {\n return await this.channel.request<T>(method, path, body);\n } catch (err) {\n if (err instanceof AdminApiError && err.status === 404 && err.code === \"not_found\" && path.startsWith(\"/api/connections\")) throw new BrokerOffError();\n throw err;\n }\n }\n\n list(): Promise<ConnectionView[]> {\n return this.call(\"GET\", \"/api/connections\");\n }\n presets(): Promise<PresetView[]> {\n return this.call(\"GET\", \"/api/connections/presets\");\n }\n /** With `connectionId`, the probe uses that connection's private-target allow (when it covers the URL). */\n probe(url: string, connectionId?: string): Promise<ConnectionProbe> {\n return this.call(\"POST\", \"/api/connections/probe\", connectionId ? { url, connectionId } : { url });\n }\n create(input: CreateConnectionInput): Promise<ConnectionView> {\n return this.call(\"POST\", \"/api/connections\", input);\n }\n get(id: string): Promise<ConnectionView> {\n return this.call(\"GET\", `/api/connections/${enc(id)}`);\n }\n /** Stores a static header (and connects), or starts OAuth and returns the authorize URL. */\n connect(id: string, input: StartConnectInput = {}): Promise<StartConnectResult> {\n return this.call(\"POST\", `/api/connections/${enc(id)}/connect`, input);\n }\n /** The admin-only private-target allow, pinned server-side to the URL's exact scheme://host:port. */\n allowPrivateTarget(id: string, allow: boolean): Promise<ConnectionView> {\n return this.call(\"PUT\", `/api/connections/${enc(id)}/private-target`, { allow });\n }\n /** The kill switch: disabled at once, pending calls cancelled, credentials deleted. */\n kill(id: string): Promise<void> {\n return this.call(\"POST\", `/api/connections/${enc(id)}/kill`, {});\n }\n remove(id: string): Promise<void> {\n return this.call(\"DELETE\", `/api/connections/${enc(id)}`);\n }\n async bySlug(slug: string): Promise<ConnectionView> {\n const all = await this.list();\n const found = all.find((c) => c.slug === slug);\n if (!found) throw new Error(`No connection \"${slug}\". ${all.length ? `There are: ${all.map((c) => c.slug).join(\", \")}.` : \"There are none yet.\"}`);\n return found;\n }\n grants(id: string): Promise<ConnectionGrant[]> {\n return this.call(\"GET\", `/api/connections/${enc(id)}/grants`);\n }\n /** The full list: agents not in it lose access. */\n setGrants(id: string, grants: { agentId: string; level: AccessLevel }[]): Promise<ConnectionGrant[]> {\n return this.call(\"PUT\", `/api/connections/${enc(id)}/grants`, { grants });\n }\n agents(): Promise<Agent[]> {\n return this.channel.request(\"GET\", \"/api/agents\");\n }\n\n /** Paste mode: the address the browser landed on after approving (the daemon checks state and issuer). */\n async finish(url: string): Promise<StartConnectResult> {\n try {\n return await this.call<StartConnectResult>(\"POST\", \"/api/connections/finish\", { url });\n } catch (err) {\n // A refused sign-in is a 400 carrying a StartConnectResult, not the usual error envelope.\n const body = err instanceof AdminApiError ? (err.body as StartConnectResult | null) : null;\n if (body?.status === \"error\") return body;\n throw err;\n }\n }\n}\n"],"mappings":";;;;AAqBA,IAAa,iBAAb,cAAoC,MAAM;CACxC,cAAc;EACZ,MACE,0LAEF;EACA,KAAK,OAAO;CACd;AACF;AAEA,MAAM,MAAM;AAEZ,IAAa,iBAAb,MAA4B;CACL;CAArB,YAAY,SAAgC;EAAvB,KAAA,UAAA;CAAwB;CAE7C,MAAc,KAAQ,QAAgB,MAAc,MAA4B;EAC9E,IAAI;GACF,OAAO,MAAM,KAAK,QAAQ,QAAW,QAAQ,MAAM,IAAI;EACzD,SAAS,KAAK;GACZ,IAAI,eAAe,iBAAiB,IAAI,WAAW,OAAO,IAAI,SAAS,eAAe,KAAK,WAAW,kBAAkB,GAAG,MAAM,IAAI,eAAe;GACpJ,MAAM;EACR;CACF;CAEA,OAAkC;EAChC,OAAO,KAAK,KAAK,OAAO,kBAAkB;CAC5C;CACA,UAAiC;EAC/B,OAAO,KAAK,KAAK,OAAO,0BAA0B;CACpD;;CAEA,MAAM,KAAa,cAAiD;EAClE,OAAO,KAAK,KAAK,QAAQ,0BAA0B,eAAe;GAAE;GAAK;EAAa,IAAI,EAAE,IAAI,CAAC;CACnG;CACA,OAAO,OAAuD;EAC5D,OAAO,KAAK,KAAK,QAAQ,oBAAoB,KAAK;CACpD;CACA,IAAI,IAAqC;EACvC,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,GAAG;CACvD;;CAEA,QAAQ,IAAY,QAA2B,CAAC,GAAgC;EAC9E,OAAO,KAAK,KAAK,QAAQ,oBAAoB,IAAI,EAAE,EAAE,WAAW,KAAK;CACvE;;CAEA,mBAAmB,IAAY,OAAyC;EACtE,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,EAAE,kBAAkB,EAAE,MAAM,CAAC;CACjF;;CAEA,KAAK,IAA2B;EAC9B,OAAO,KAAK,KAAK,QAAQ,oBAAoB,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;CACjE;CACA,OAAO,IAA2B;EAChC,OAAO,KAAK,KAAK,UAAU,oBAAoB,IAAI,EAAE,GAAG;CAC1D;CACA,MAAM,OAAO,MAAuC;EAClD,MAAM,MAAM,MAAM,KAAK,KAAK;EAC5B,MAAM,QAAQ,IAAI,MAAM,MAAM,EAAE,SAAS,IAAI;EAC7C,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,kBAAkB,KAAK,KAAK,IAAI,SAAS,cAAc,IAAI,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,KAAK,uBAAuB;EACjJ,OAAO;CACT;CACA,OAAO,IAAwC;EAC7C,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,EAAE,QAAQ;CAC9D;;CAEA,UAAU,IAAY,QAA+E;EACnG,OAAO,KAAK,KAAK,OAAO,oBAAoB,IAAI,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC;CAC1E;CACA,SAA2B;EACzB,OAAO,KAAK,QAAQ,QAAQ,OAAO,aAAa;CAClD;;CAGA,MAAM,OAAO,KAA0C;EACrD,IAAI;GACF,OAAO,MAAM,KAAK,KAAyB,QAAQ,2BAA2B,EAAE,IAAI,CAAC;EACvF,SAAS,KAAK;GAEZ,MAAM,OAAO,eAAe,gBAAiB,IAAI,OAAqC;GACtF,IAAI,MAAM,WAAW,SAAS,OAAO;GACrC,MAAM;EACR;CACF;AACF"}
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export { l as currentSetupToken, f as isClaimed, g as resetAdminPassword } from "./auth-D-GmwNv-.js";
2
+ export { l as currentSetupToken, f as isClaimed, g as resetAdminPassword } from "./auth-BLYwD_zT.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as HttpError } from "./errors-FbMS3QBo.js";
3
- import { i as sha256 } from "./crypto-fv2fj3S8.js";
4
- import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-CwuSzfLK.js";
3
+ import { i as sha256 } from "./crypto-s8x6up-l.js";
4
+ import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-DBqR_h01.js";
5
5
  import { isIP, isIPv4, isIPv6 } from "node:net";
6
6
  import { getConnInfo } from "@hono/node-server/conninfo";
7
7
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
@@ -547,4 +547,4 @@ function rejection(c, check) {
547
547
  //#endregion
548
548
  export { clientKey as S, sessionToken as _, adminAuth as a, BAN_MS as b, csrfGuard as c, hasSession as d, isClaimed as f, resetAdminPassword as g, requireAdmin as h, Semaphore as i, currentSetupToken as l, rejection as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, lockedOut as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, startSession as v, LoginLimiter as x, SESSION_MAX_AGE_MS as y };
549
549
 
550
- //# sourceMappingURL=auth-D-GmwNv-.js.map
550
+ //# sourceMappingURL=auth-BLYwD_zT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-D-GmwNv-.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/proxy.ts","../server/auth/sessions.ts","../server/auth/index.ts"],"sourcesContent":["import { isIPv6 } from \"node:net\";\nimport { sha256 } from \"../crypto.ts\";\n\n/** Distinct wrong secrets per client within the window before a ban. */\nexport const MAX_FAILURES = 5;\n/** Raw failed attempts per client within the window (repeats of the same wrong secret included). */\nexport const MAX_ATTEMPTS = 30;\n/** Distinct wrong secrets across all clients within the window before every attempt is slowed down. */\nexport const GLOBAL_MAX_FAILURES = 100;\n/** Delay per attempt while the global budget is exceeded. */\nexport const GLOBAL_SLOWDOWN_MS = 2000;\nexport const WINDOW_MS = 15 * 60_000;\nexport const BAN_MS = 15 * 60_000;\n/** Tracked clients; the least recently seen is dropped beyond this. */\nexport const MAX_ENTRIES = 10_000;\n\ninterface Entry {\n tried: Set<string>;\n attempts: number;\n since: number;\n bannedUntil: number;\n}\n\n/**\n * Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30\n * attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is\n * rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale\n * password can't spend every attempt, and can't make the server hash it again either. Past a global\n * budget of distinct failures, every attempt is slowed down (never refused, so many clients can't\n * lock the owner out).\n */\nexport class LoginLimiter {\n private readonly entries = new Map<string, Entry>();\n private global = { failures: 0, since: 0 };\n\n constructor(private readonly clock: () => number = Date.now) {}\n\n /** Minutes until this client may try again, or 0. */\n bannedMinutes(ip: string): number {\n const t = this.clock();\n const entry = this.entries.get(clientKey(ip));\n const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;\n return until > t ? Math.ceil((until - t) / 60_000) : 0;\n }\n\n /** The global failure budget is exhausted: slow every attempt down. */\n overGlobalBudget(): boolean {\n return this.global.failures >= GLOBAL_MAX_FAILURES && this.clock() - this.global.since < WINDOW_MS;\n }\n\n /** The client already failed with exactly this secret in the current window. */\n knownBad(ip: string, secret: string): boolean {\n const entry = this.current(clientKey(ip), this.clock());\n return !!entry?.tried.has(sha256(secret));\n }\n\n /** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */\n fail(ip: string, secret: string, opts: { global?: boolean } = {}): boolean {\n return this.record(ip, secret, opts).banned;\n }\n\n /**\n * Counts an attempt with this secret now, before it is verified, so concurrent requests can't\n * all pass the ban check first. `countedGlobally` says whether it used global budget (refund it\n * with `refundGlobal` when the secret turns out to be right).\n */\n record(ip: string, secret: string, opts: { global?: boolean } = {}): { banned: boolean; countedGlobally: boolean } {\n const t = this.clock();\n const key = clientKey(ip);\n const entry = this.current(key, t) ?? { tried: new Set<string>(), attempts: 0, since: t, bannedUntil: 0 };\n const hash = sha256(secret);\n let countedGlobally = false;\n if (!entry.tried.has(hash)) {\n entry.tried.add(hash);\n if (opts.global !== false) {\n if (t - this.global.since >= WINDOW_MS) this.global = { failures: 0, since: t };\n this.global.failures++;\n countedGlobally = true;\n }\n }\n entry.attempts++;\n const banned = entry.tried.size >= MAX_FAILURES || entry.attempts >= MAX_ATTEMPTS;\n if (banned) entry.bannedUntil = t + BAN_MS;\n this.touch(key, entry);\n return { banned, countedGlobally };\n }\n\n refundGlobal(): void {\n if (this.global.failures > 0) this.global.failures--;\n }\n\n clear(ip: string): void {\n this.entries.delete(clientKey(ip));\n }\n\n /** After the password changes, previously wrong secrets may be right. */\n reset(): void {\n this.entries.clear();\n this.global = { failures: 0, since: 0 };\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The client's entry if its window (or ban) is still running. */\n private current(key: string, t: number): Entry | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.bannedUntil > t || (!entry.bannedUntil && t - entry.since < WINDOW_MS)) return entry;\n this.entries.delete(key);\n return undefined;\n }\n\n /** Map order doubles as LRU order: re-insert on use, evict from the front. */\n private touch(key: string, entry: Entry): void {\n this.entries.delete(key);\n this.entries.set(key, entry);\n while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value!);\n }\n}\n\n/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */\nexport function clientKey(ip: string): string {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase();\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(addr);\n if (mapped) return mapped[1]!;\n if (!isIPv6(addr)) return addr;\n const [head = \"\", tail] = addr.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill(\"0\"), ...right];\n return `${groups\n .slice(0, 4)\n .map((g) => g.replace(/^0+(?=.)/, \"\"))\n .join(\":\")}::/64`;\n}\n","import { isIPv4, isIPv6 } from \"node:net\";\n\n/** An address as a family and a big integer, with IPv4-mapped IPv6 (::ffff:a.b.c.d) treated as IPv4. */\ninterface Addr {\n v6: boolean;\n n: bigint;\n}\n\nfunction parseV4(ip: string): bigint {\n return ip.split(\".\").reduce((acc, part) => (acc << 8n) | BigInt(Number(part)), 0n);\n}\n\nexport function parseAddress(raw: string): Addr | null {\n const ip = raw.trim().replace(/^\\[|\\]$/g, \"\").replace(/%.*$/, \"\").toLowerCase();\n if (isIPv4(ip)) return { v6: false, n: parseV4(ip) };\n if (!isIPv6(ip)) return null;\n const mapped = /^(?:0{0,4}:){0,5}:?ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(ip);\n if (mapped && isIPv4(mapped[1]!)) return { v6: false, n: parseV4(mapped[1]!) };\n // Expand an embedded IPv4 tail into two hextets, then \"::\".\n let text = ip;\n const v4Tail = /(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(text);\n if (v4Tail) {\n const n = parseV4(v4Tail[1]!);\n text = text.slice(0, -v4Tail[1]!.length) + `${(n >> 16n).toString(16)}:${(n & 0xffffn).toString(16)}`;\n }\n const [head = \"\", tail] = text.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array<string>(8 - left.length - right.length).fill(\"0\"), ...right];\n if (groups.length !== 8) return null;\n return { v6: true, n: groups.reduce((acc, g) => (acc << 16n) | BigInt(Number.parseInt(g || \"0\", 16)), 0n) };\n}\n\ninterface Range {\n v6: boolean;\n base: bigint;\n bits: number;\n}\n\n/**\n * Parses \"10.0.0.5\", \"10.0.0.0/8\", \"fd00::/8\", \"::ffff:10.0.0.0/104\"; null when invalid. A /0\n * (trust everyone) is refused: it would let any client pick its own address.\n */\nexport function parseRange(entry: string): Range | null {\n const parts = entry.trim().split(\"/\");\n if (parts.length > 2) return null;\n const [ip = \"\", prefix] = parts;\n const addr = parseAddress(ip);\n if (!addr) return null;\n let bits = addr.v6 ? 128 : 32;\n if (prefix !== undefined) {\n if (!/^\\d{1,3}$/.test(prefix)) return null;\n bits = Number(prefix);\n // A mapped IPv4 range written in IPv6 form: drop the 96-bit ::ffff: prefix.\n if (!addr.v6 && ip.includes(\":\")) bits -= 96;\n if (bits <= 0 || bits > (addr.v6 ? 128 : 32)) return null;\n }\n const width = BigInt(addr.v6 ? 128 : 32);\n const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << (width - BigInt(bits));\n return { v6: addr.v6, base: addr.n & mask, bits };\n}\n\nfunction inRange(addr: Addr, r: Range): boolean {\n if (addr.v6 !== r.v6) return false;\n const width = BigInt(r.v6 ? 128 : 32);\n const mask = r.bits === 0 ? 0n : ((1n << BigInt(r.bits)) - 1n) << (width - BigInt(r.bits));\n return (addr.n & mask) === r.base;\n}\n\nconst LOOPBACK = [parseRange(\"127.0.0.0/8\")!, parseRange(\"::1/128\")!];\n\nlet cache: { raw: string; ranges: Range[] } | undefined;\n\n/** TRUST_PROXY_FROM as ranges. Invalid entries are reported once and ignored. */\nfunction trustedRanges(raw: string): Range[] {\n if (cache?.raw === raw) return cache.ranges;\n const ranges: Range[] = [];\n const invalid: string[] = [];\n for (const entry of raw.split(/[,\\s]+/).filter(Boolean)) {\n const r = parseRange(entry);\n if (r) ranges.push(r);\n else invalid.push(entry);\n }\n if (invalid.length) process.stderr.write(`xaq: ignoring invalid TRUST_PROXY_FROM entries: ${invalid.join(\", \")}\\n`);\n cache = { raw, ranges };\n return ranges;\n}\n\n/**\n * Whether forwarding headers from this socket peer may be believed: loopback always (a proxy on\n * the same host), anything else only when listed in TRUST_PROXY_FROM (IPs or CIDRs, comma-separated).\n * The daemon usually binds 0.0.0.0, so trusting whole private ranges would let any LAN or tailnet\n * peer forge its address.\n */\nexport function isTrustedProxy(peer: string, trustFrom = process.env.TRUST_PROXY_FROM ?? \"\"): boolean {\n const addr = parseAddress(peer);\n if (!addr) return false;\n return [...LOOPBACK, ...trustedRanges(trustFrom)].some((r) => inRange(addr, r));\n}\n","import { randomBytes } from \"node:crypto\";\nimport { sha256 } from \"../crypto.ts\";\nimport type { Db } from \"../db/index.ts\";\n\nexport const SESSION_TTL_MS = 30 * 24 * 3600_000;\n/** Sliding refresh never extends a session past this age: sign in again after 90 days. */\nexport const SESSION_MAX_AGE_MS = 90 * 24 * 3600_000;\n/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */\nconst REFRESH_AFTER_MS = 24 * 3600_000;\n\nexport interface SessionCheck {\n valid: boolean;\n /** The expiry was extended: re-send the cookie so the browser keeps it as long. */\n refreshed: boolean;\n}\n\n/** Admin console sessions. Only sha256(token) is stored. */\nexport class SessionStore {\n constructor(\n private readonly db: Db,\n private readonly clock: () => number = Date.now,\n ) {}\n\n create(meta: { ip?: string | null; userAgent?: string | null } = {}): string {\n const token = randomBytes(32).toString(\"base64url\");\n const t = this.clock();\n this.db.run(\n \"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\",\n sha256(token),\n new Date(t).toISOString(),\n new Date(t + SESSION_TTL_MS).toISOString(),\n meta.ip ?? null,\n meta.userAgent?.slice(0, 300) ?? null,\n );\n return token;\n }\n\n check(token: string | undefined | null): SessionCheck {\n if (!token) return { valid: false, refreshed: false };\n const hash = sha256(token);\n const row = this.db.get<{ created_at: string; expires_at: string }>(\n \"SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?\",\n hash,\n );\n if (!row) return { valid: false, refreshed: false };\n const t = this.clock();\n const expires = Date.parse(row.expires_at);\n const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;\n if (!(expires > t) || !(hardLimit > t)) {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", hash);\n return { valid: false, refreshed: false };\n }\n const next = Math.min(t + SESSION_TTL_MS, hardLimit);\n if (next - expires < REFRESH_AFTER_MS) return { valid: true, refreshed: false };\n this.db.run(\"UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?\", new Date(next).toISOString(), hash);\n return { valid: true, refreshed: true };\n }\n\n delete(token: string): void {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", sha256(token));\n }\n\n /** Revokes every session, optionally keeping one (the caller's own). */\n deleteAll(exceptToken?: string | null): number {\n if (exceptToken) return this.db.run(\"DELETE FROM admin_sessions WHERE token_hash != ?\", sha256(exceptToken)).changes;\n return this.db.run(\"DELETE FROM admin_sessions\").changes;\n }\n\n pruneExpired(): number {\n return this.db.run(\"DELETE FROM admin_sessions WHERE expires_at <= ?\", new Date(this.clock()).toISOString()).changes;\n }\n}\n","import { isIP } from \"node:net\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { AppEnv } from \"../api/util.ts\";\nimport type { CoreContext } from \"../context.ts\";\nimport { HttpError } from \"../util/errors.ts\";\nimport { clientKey, GLOBAL_SLOWDOWN_MS, LoginLimiter } from \"./lockout.ts\";\nimport { isTrustedProxy } from \"./proxy.ts\";\nimport { SESSION_TTL_MS, SessionStore } from \"./sessions.ts\";\nimport { ensureSetupToken, regenerateSetupToken } from \"./setup-token.ts\";\n\nexport const SESSION_COOKIE = \"aib_session\";\n// Header names keep the pre-rename spelling: a console tab or script from before the rename still sends it.\nexport const CSRF_HEADER = \"x-agentinbox-csrf\";\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const MAX_PASSWORD_LENGTH = 1024;\n\n/** Secret checks (scrypt) allowed at once: libuv's default thread pool size. */\nexport const MAX_CONCURRENT_VERIFICATIONS = 4;\n/** Requests from one client that may wait behind its in-flight check; more get 429. */\nexport const MAX_QUEUED_PER_CLIENT = 4;\n\n/** Hands out at most `max` slots; waiters are served in order. */\nexport class Semaphore {\n private active = 0;\n private readonly waiters: (() => void)[] = [];\n\n constructor(private readonly max: number) {}\n\n get inUse(): number {\n return this.active;\n }\n\n async acquire(): Promise<() => void> {\n if (this.active < this.max) this.active++;\n else await new Promise<void>((resolve) => this.waiters.push(resolve));\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = this.waiters.shift();\n if (next) next();\n else this.active--;\n };\n }\n}\n\n/** Per-context admin auth state: the session store, the failed-login limiter and verification queues. */\nexport interface AdminAuth {\n sessions: SessionStore;\n limiter: LoginLimiter;\n /** One secret check per client at a time: the tail of each client's queue. */\n lanes: Map<string, { tail: Promise<void>; queued: number }>;\n verifySlots: Semaphore;\n}\n\n/** What admin auth needs from the context (the CLI builds this without loading the master key). */\nexport type AuthCore = Pick<CoreContext, \"db\" | \"settings\" | \"audit\" | \"config\">;\n\nconst instances = new WeakMap<AuthCore, AdminAuth>();\n\nexport function adminAuth(ctx: AuthCore): AdminAuth {\n let auth = instances.get(ctx);\n if (!auth) {\n auth = { sessions: new SessionStore(ctx.db), limiter: new LoginLimiter(), lanes: new Map(), verifySlots: new Semaphore(MAX_CONCURRENT_VERIFICATIONS) };\n instances.set(ctx, auth);\n }\n return auth;\n}\n\n/** Claimed = an admin password exists. Before that only the setup-token claim works. */\nexport const isClaimed = (ctx: AuthCore): boolean => !!ctx.settings.getState(\"admin.passwordHash\");\n\n/** The current setup token while unclaimed (created on demand), otherwise null. */\nexport function currentSetupToken(ctx: AuthCore): string | null {\n return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);\n}\n\n/** `xaq reset-password`: back to the unclaimed state with a fresh setup link. */\nexport function resetAdminPassword(ctx: AuthCore): string {\n const auth = adminAuth(ctx);\n ctx.db.tx(() => {\n ctx.settings.deleteState(\"admin.passwordHash\");\n auth.sessions.deleteAll();\n });\n auth.limiter.reset();\n ctx.audit.record({ actorType: \"system\", action: \"admin.password_reset\", outcome: \"ok\" });\n return regenerateSetupToken(ctx.config.dataDir);\n}\n\nfunction socketAddress(c: Context): string | null {\n try {\n return getConnInfo(c).remote.address ?? null;\n } catch {\n // No Node socket (e.g. app.request() without bindings).\n return null;\n }\n}\n\n/**\n * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the\n * connection comes from a trusted proxy (loopback, or TRUST_PROXY_FROM); it is read from the\n * right, since proxies append the address they saw and entries further left are client-controlled.\n * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of xaq.\n */\n/** Some proxies (Azure, IIS) append \":port\" to X-Forwarded-For entries. */\nfunction stripPort(entry: string): string {\n const v6 = /^\\[([^\\]]+)\\](?::\\d+)?$/.exec(entry);\n if (v6) return v6[1]!;\n const v4 = /^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/.exec(entry);\n return v4 ? v4[1]! : entry;\n}\n\nlet warnedUntrustedProxy = false;\n\n/** Upgrades from the old private-range rule silently lost proxy trust; say so once. */\nfunction warnUntrustedProxy(peer: string): void {\n if (warnedUntrustedProxy) return;\n warnedUntrustedProxy = true;\n process.stderr.write(\n `xaq: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\\n`,\n );\n}\n\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && !isTrustedProxy(peer) && c.req.header(\"x-forwarded-for\")) warnUntrustedProxy(peer);\n if (trustProxy && peer && isTrustedProxy(peer)) {\n const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? \"1\", 10) || 1);\n const chain = (c.req.header(\"x-forwarded-for\") ?? \"\")\n .split(\",\")\n .map((s) => stripPort(s.trim()))\n .filter(Boolean);\n const forwarded = chain[chain.length - hops];\n if (forwarded && isIP(forwarded)) return forwarded;\n }\n return peer ?? \"unknown\";\n}\n\nfunction isSecure(c: Context, trustProxy: boolean): boolean {\n if (new URL(c.req.url).protocol === \"https:\") return true;\n const peer = socketAddress(c);\n return trustProxy && !!peer && isTrustedProxy(peer) && c.req.header(\"x-forwarded-proto\")?.split(\",\")[0]?.trim().toLowerCase() === \"https\";\n}\n\nexport function sessionToken(c: Context): string | undefined {\n return getCookie(c, SESSION_COOKIE) || undefined;\n}\n\nfunction writeSessionCookie(c: Context, token: string, trustProxy: boolean): void {\n setCookie(c, SESSION_COOKIE, token, {\n httpOnly: true,\n sameSite: \"Lax\",\n path: \"/\",\n secure: isSecure(c, trustProxy),\n maxAge: Math.floor(SESSION_TTL_MS / 1000),\n });\n}\n\n/** Creates a session for the requester and sets the cookie. */\nexport function startSession(c: Context, ctx: AuthCore): void {\n const token = adminAuth(ctx).sessions.create({ ip: clientIp(c, ctx.config.trustProxy), userAgent: c.req.header(\"user-agent\") });\n writeSessionCookie(c, token, ctx.config.trustProxy);\n}\n\nexport function endSession(c: Context, ctx: AuthCore): void {\n const token = sessionToken(c);\n if (token) adminAuth(ctx).sessions.delete(token);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n}\n\n/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */\nexport function hasSession(c: Context, ctx: AuthCore): boolean {\n const token = sessionToken(c);\n const check = adminAuth(ctx).sessions.check(token);\n if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);\n return check.valid;\n}\n\n/** Requires an admin session on every route it guards. */\nexport function requireAdmin(ctx: AuthCore): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (!hasSession(c, ctx)) {\n if (!isClaimed(ctx)) {\n throw new HttpError(401, \"setup_required\", \"This instance has not been claimed yet\", \"Open the setup link printed by xaq on startup.\");\n }\n throw new HttpError(401, \"unauthorized\", \"Sign in to continue\");\n }\n c.set(\"admin\", true);\n await next();\n };\n}\n\nconst MUTATING = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server\n * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.\n */\nexport function csrfGuard(): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (MUTATING.has(c.req.method) && c.req.header(CSRF_HEADER) !== \"1\") {\n throw new HttpError(403, \"csrf\", `Missing ${CSRF_HEADER} header`, `Send \"${CSRF_HEADER}: 1\" with every mutating /api request.`);\n }\n await next();\n };\n}\n\n/** 429 response for a locked-out address. */\nexport function lockedOut(c: Context, minutes: number): Response {\n c.header(\"Retry-After\", String(minutes * 60));\n const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? \"\" : \"s\"}.`;\n return c.json({ error: { code: \"locked_out\", message } }, 429);\n}\n\nexport type CheckResult =\n | { ok: true }\n | { ok: false; lockedMinutes: number; nowBanned: boolean; /** Too many concurrent attempts from this client. */ busy?: boolean };\n\nexport interface CheckOptions {\n /**\n * Count toward (and be slowed down by) the instance-wide failure budget. Off for the setup claim:\n * it's only open while unclaimed and needs the token.\n */\n global?: boolean;\n /** Test hook: how long to wait while over the global budget. */\n slowdownMs?: number;\n}\n\n/**\n * Verifies a secret under the failed-login limiter.\n * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most\n * MAX_CONCURRENT_VERIFICATIONS run at once overall.\n * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so\n * concurrent requests can't all slip past the ban.\n * - Locked-out clients and secrets this client already failed with never reach `verify`.\n * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.\n */\nexport async function checkSecret(\n ctx: AuthCore,\n ip: string,\n secret: string,\n verify: () => boolean | Promise<boolean>,\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const auth = adminAuth(ctx);\n const key = clientKey(ip);\n const lane = auth.lanes.get(key) ?? { tail: Promise.resolve(), queued: 0 };\n if (lane.queued > MAX_QUEUED_PER_CLIENT) return { ok: false, lockedMinutes: 0, nowBanned: false, busy: true };\n lane.queued++;\n auth.lanes.set(key, lane);\n const previous = lane.tail;\n let done!: () => void;\n lane.tail = new Promise<void>((resolve) => (done = resolve));\n try {\n await previous;\n return await checkOnce(auth, ip, secret, verify, opts);\n } finally {\n done();\n if (--lane.queued === 0) auth.lanes.delete(key);\n }\n}\n\nasync function checkOnce(auth: AdminAuth, ip: string, secret: string, verify: () => boolean | Promise<boolean>, opts: CheckOptions): Promise<CheckResult> {\n const { limiter } = auth;\n const global = opts.global !== false;\n if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) {\n await new Promise((r) => setTimeout(r, opts.slowdownMs ?? GLOBAL_SLOWDOWN_MS));\n }\n // From here to the reservation everything is synchronous.\n const locked = limiter.bannedMinutes(ip);\n if (locked) return { ok: false, lockedMinutes: locked, nowBanned: false };\n if (limiter.knownBad(ip, secret)) return { ok: false, lockedMinutes: 0, nowBanned: limiter.fail(ip, secret, { global }) };\n const reservation = limiter.record(ip, secret, { global });\n const release = await auth.verifySlots.acquire();\n let ok = false;\n try {\n ok = await verify();\n } finally {\n release();\n }\n if (!ok) return { ok: false, lockedMinutes: 0, nowBanned: reservation.banned };\n limiter.clear(ip);\n if (reservation.countedGlobally) limiter.refundGlobal();\n return { ok: true };\n}\n\n/** The 429 for a rejected check (locked out or too many at once), or null to continue. */\nexport function rejection(c: Context, check: CheckResult): Response | null {\n if (check.ok) return null;\n if (check.busy) {\n c.header(\"Retry-After\", \"2\");\n return c.json({ error: { code: \"too_many_requests\", message: \"Another sign-in attempt from your address is still being checked. Try again in a moment.\" } }, 429);\n }\n return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-BLYwD_zT.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/proxy.ts","../server/auth/sessions.ts","../server/auth/index.ts"],"sourcesContent":["import { isIPv6 } from \"node:net\";\nimport { sha256 } from \"../crypto.ts\";\n\n/** Distinct wrong secrets per client within the window before a ban. */\nexport const MAX_FAILURES = 5;\n/** Raw failed attempts per client within the window (repeats of the same wrong secret included). */\nexport const MAX_ATTEMPTS = 30;\n/** Distinct wrong secrets across all clients within the window before every attempt is slowed down. */\nexport const GLOBAL_MAX_FAILURES = 100;\n/** Delay per attempt while the global budget is exceeded. */\nexport const GLOBAL_SLOWDOWN_MS = 2000;\nexport const WINDOW_MS = 15 * 60_000;\nexport const BAN_MS = 15 * 60_000;\n/** Tracked clients; the least recently seen is dropped beyond this. */\nexport const MAX_ENTRIES = 10_000;\n\ninterface Entry {\n tried: Set<string>;\n attempts: number;\n since: number;\n bannedUntil: number;\n}\n\n/**\n * Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30\n * attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is\n * rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale\n * password can't spend every attempt, and can't make the server hash it again either. Past a global\n * budget of distinct failures, every attempt is slowed down (never refused, so many clients can't\n * lock the owner out).\n */\nexport class LoginLimiter {\n private readonly entries = new Map<string, Entry>();\n private global = { failures: 0, since: 0 };\n\n constructor(private readonly clock: () => number = Date.now) {}\n\n /** Minutes until this client may try again, or 0. */\n bannedMinutes(ip: string): number {\n const t = this.clock();\n const entry = this.entries.get(clientKey(ip));\n const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;\n return until > t ? Math.ceil((until - t) / 60_000) : 0;\n }\n\n /** The global failure budget is exhausted: slow every attempt down. */\n overGlobalBudget(): boolean {\n return this.global.failures >= GLOBAL_MAX_FAILURES && this.clock() - this.global.since < WINDOW_MS;\n }\n\n /** The client already failed with exactly this secret in the current window. */\n knownBad(ip: string, secret: string): boolean {\n const entry = this.current(clientKey(ip), this.clock());\n return !!entry?.tried.has(sha256(secret));\n }\n\n /** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */\n fail(ip: string, secret: string, opts: { global?: boolean } = {}): boolean {\n return this.record(ip, secret, opts).banned;\n }\n\n /**\n * Counts an attempt with this secret now, before it is verified, so concurrent requests can't\n * all pass the ban check first. `countedGlobally` says whether it used global budget (refund it\n * with `refundGlobal` when the secret turns out to be right).\n */\n record(ip: string, secret: string, opts: { global?: boolean } = {}): { banned: boolean; countedGlobally: boolean } {\n const t = this.clock();\n const key = clientKey(ip);\n const entry = this.current(key, t) ?? { tried: new Set<string>(), attempts: 0, since: t, bannedUntil: 0 };\n const hash = sha256(secret);\n let countedGlobally = false;\n if (!entry.tried.has(hash)) {\n entry.tried.add(hash);\n if (opts.global !== false) {\n if (t - this.global.since >= WINDOW_MS) this.global = { failures: 0, since: t };\n this.global.failures++;\n countedGlobally = true;\n }\n }\n entry.attempts++;\n const banned = entry.tried.size >= MAX_FAILURES || entry.attempts >= MAX_ATTEMPTS;\n if (banned) entry.bannedUntil = t + BAN_MS;\n this.touch(key, entry);\n return { banned, countedGlobally };\n }\n\n refundGlobal(): void {\n if (this.global.failures > 0) this.global.failures--;\n }\n\n clear(ip: string): void {\n this.entries.delete(clientKey(ip));\n }\n\n /** After the password changes, previously wrong secrets may be right. */\n reset(): void {\n this.entries.clear();\n this.global = { failures: 0, since: 0 };\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The client's entry if its window (or ban) is still running. */\n private current(key: string, t: number): Entry | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.bannedUntil > t || (!entry.bannedUntil && t - entry.since < WINDOW_MS)) return entry;\n this.entries.delete(key);\n return undefined;\n }\n\n /** Map order doubles as LRU order: re-insert on use, evict from the front. */\n private touch(key: string, entry: Entry): void {\n this.entries.delete(key);\n this.entries.set(key, entry);\n while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value!);\n }\n}\n\n/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */\nexport function clientKey(ip: string): string {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase();\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(addr);\n if (mapped) return mapped[1]!;\n if (!isIPv6(addr)) return addr;\n const [head = \"\", tail] = addr.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill(\"0\"), ...right];\n return `${groups\n .slice(0, 4)\n .map((g) => g.replace(/^0+(?=.)/, \"\"))\n .join(\":\")}::/64`;\n}\n","import { isIPv4, isIPv6 } from \"node:net\";\n\n/** An address as a family and a big integer, with IPv4-mapped IPv6 (::ffff:a.b.c.d) treated as IPv4. */\ninterface Addr {\n v6: boolean;\n n: bigint;\n}\n\nfunction parseV4(ip: string): bigint {\n return ip.split(\".\").reduce((acc, part) => (acc << 8n) | BigInt(Number(part)), 0n);\n}\n\nexport function parseAddress(raw: string): Addr | null {\n const ip = raw.trim().replace(/^\\[|\\]$/g, \"\").replace(/%.*$/, \"\").toLowerCase();\n if (isIPv4(ip)) return { v6: false, n: parseV4(ip) };\n if (!isIPv6(ip)) return null;\n const mapped = /^(?:0{0,4}:){0,5}:?ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(ip);\n if (mapped && isIPv4(mapped[1]!)) return { v6: false, n: parseV4(mapped[1]!) };\n // Expand an embedded IPv4 tail into two hextets, then \"::\".\n let text = ip;\n const v4Tail = /(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(text);\n if (v4Tail) {\n const n = parseV4(v4Tail[1]!);\n text = text.slice(0, -v4Tail[1]!.length) + `${(n >> 16n).toString(16)}:${(n & 0xffffn).toString(16)}`;\n }\n const [head = \"\", tail] = text.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array<string>(8 - left.length - right.length).fill(\"0\"), ...right];\n if (groups.length !== 8) return null;\n return { v6: true, n: groups.reduce((acc, g) => (acc << 16n) | BigInt(Number.parseInt(g || \"0\", 16)), 0n) };\n}\n\ninterface Range {\n v6: boolean;\n base: bigint;\n bits: number;\n}\n\n/**\n * Parses \"10.0.0.5\", \"10.0.0.0/8\", \"fd00::/8\", \"::ffff:10.0.0.0/104\"; null when invalid. A /0\n * (trust everyone) is refused: it would let any client pick its own address.\n */\nexport function parseRange(entry: string): Range | null {\n const parts = entry.trim().split(\"/\");\n if (parts.length > 2) return null;\n const [ip = \"\", prefix] = parts;\n const addr = parseAddress(ip);\n if (!addr) return null;\n let bits = addr.v6 ? 128 : 32;\n if (prefix !== undefined) {\n if (!/^\\d{1,3}$/.test(prefix)) return null;\n bits = Number(prefix);\n // A mapped IPv4 range written in IPv6 form: drop the 96-bit ::ffff: prefix.\n if (!addr.v6 && ip.includes(\":\")) bits -= 96;\n if (bits <= 0 || bits > (addr.v6 ? 128 : 32)) return null;\n }\n const width = BigInt(addr.v6 ? 128 : 32);\n const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << (width - BigInt(bits));\n return { v6: addr.v6, base: addr.n & mask, bits };\n}\n\nfunction inRange(addr: Addr, r: Range): boolean {\n if (addr.v6 !== r.v6) return false;\n const width = BigInt(r.v6 ? 128 : 32);\n const mask = r.bits === 0 ? 0n : ((1n << BigInt(r.bits)) - 1n) << (width - BigInt(r.bits));\n return (addr.n & mask) === r.base;\n}\n\nconst LOOPBACK = [parseRange(\"127.0.0.0/8\")!, parseRange(\"::1/128\")!];\n\nlet cache: { raw: string; ranges: Range[] } | undefined;\n\n/** TRUST_PROXY_FROM as ranges. Invalid entries are reported once and ignored. */\nfunction trustedRanges(raw: string): Range[] {\n if (cache?.raw === raw) return cache.ranges;\n const ranges: Range[] = [];\n const invalid: string[] = [];\n for (const entry of raw.split(/[,\\s]+/).filter(Boolean)) {\n const r = parseRange(entry);\n if (r) ranges.push(r);\n else invalid.push(entry);\n }\n if (invalid.length) process.stderr.write(`xaq: ignoring invalid TRUST_PROXY_FROM entries: ${invalid.join(\", \")}\\n`);\n cache = { raw, ranges };\n return ranges;\n}\n\n/**\n * Whether forwarding headers from this socket peer may be believed: loopback always (a proxy on\n * the same host), anything else only when listed in TRUST_PROXY_FROM (IPs or CIDRs, comma-separated).\n * The daemon usually binds 0.0.0.0, so trusting whole private ranges would let any LAN or tailnet\n * peer forge its address.\n */\nexport function isTrustedProxy(peer: string, trustFrom = process.env.TRUST_PROXY_FROM ?? \"\"): boolean {\n const addr = parseAddress(peer);\n if (!addr) return false;\n return [...LOOPBACK, ...trustedRanges(trustFrom)].some((r) => inRange(addr, r));\n}\n","import { randomBytes } from \"node:crypto\";\nimport { sha256 } from \"../crypto.ts\";\nimport type { Db } from \"../db/index.ts\";\n\nexport const SESSION_TTL_MS = 30 * 24 * 3600_000;\n/** Sliding refresh never extends a session past this age: sign in again after 90 days. */\nexport const SESSION_MAX_AGE_MS = 90 * 24 * 3600_000;\n/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */\nconst REFRESH_AFTER_MS = 24 * 3600_000;\n\nexport interface SessionCheck {\n valid: boolean;\n /** The expiry was extended: re-send the cookie so the browser keeps it as long. */\n refreshed: boolean;\n}\n\n/** Admin console sessions. Only sha256(token) is stored. */\nexport class SessionStore {\n constructor(\n private readonly db: Db,\n private readonly clock: () => number = Date.now,\n ) {}\n\n create(meta: { ip?: string | null; userAgent?: string | null } = {}): string {\n const token = randomBytes(32).toString(\"base64url\");\n const t = this.clock();\n this.db.run(\n \"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\",\n sha256(token),\n new Date(t).toISOString(),\n new Date(t + SESSION_TTL_MS).toISOString(),\n meta.ip ?? null,\n meta.userAgent?.slice(0, 300) ?? null,\n );\n return token;\n }\n\n check(token: string | undefined | null): SessionCheck {\n if (!token) return { valid: false, refreshed: false };\n const hash = sha256(token);\n const row = this.db.get<{ created_at: string; expires_at: string }>(\n \"SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?\",\n hash,\n );\n if (!row) return { valid: false, refreshed: false };\n const t = this.clock();\n const expires = Date.parse(row.expires_at);\n const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;\n if (!(expires > t) || !(hardLimit > t)) {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", hash);\n return { valid: false, refreshed: false };\n }\n const next = Math.min(t + SESSION_TTL_MS, hardLimit);\n if (next - expires < REFRESH_AFTER_MS) return { valid: true, refreshed: false };\n this.db.run(\"UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?\", new Date(next).toISOString(), hash);\n return { valid: true, refreshed: true };\n }\n\n delete(token: string): void {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", sha256(token));\n }\n\n /** Revokes every session, optionally keeping one (the caller's own). */\n deleteAll(exceptToken?: string | null): number {\n if (exceptToken) return this.db.run(\"DELETE FROM admin_sessions WHERE token_hash != ?\", sha256(exceptToken)).changes;\n return this.db.run(\"DELETE FROM admin_sessions\").changes;\n }\n\n pruneExpired(): number {\n return this.db.run(\"DELETE FROM admin_sessions WHERE expires_at <= ?\", new Date(this.clock()).toISOString()).changes;\n }\n}\n","import { isIP } from \"node:net\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { AppEnv } from \"../api/util.ts\";\nimport type { CoreContext } from \"../context.ts\";\nimport { HttpError } from \"../util/errors.ts\";\nimport { clientKey, GLOBAL_SLOWDOWN_MS, LoginLimiter } from \"./lockout.ts\";\nimport { isTrustedProxy } from \"./proxy.ts\";\nimport { SESSION_TTL_MS, SessionStore } from \"./sessions.ts\";\nimport { ensureSetupToken, regenerateSetupToken } from \"./setup-token.ts\";\n\nexport const SESSION_COOKIE = \"aib_session\";\n// Header names keep the pre-rename spelling: a console tab or script from before the rename still sends it.\nexport const CSRF_HEADER = \"x-agentinbox-csrf\";\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const MAX_PASSWORD_LENGTH = 1024;\n\n/** Secret checks (scrypt) allowed at once: libuv's default thread pool size. */\nexport const MAX_CONCURRENT_VERIFICATIONS = 4;\n/** Requests from one client that may wait behind its in-flight check; more get 429. */\nexport const MAX_QUEUED_PER_CLIENT = 4;\n\n/** Hands out at most `max` slots; waiters are served in order. */\nexport class Semaphore {\n private active = 0;\n private readonly waiters: (() => void)[] = [];\n\n constructor(private readonly max: number) {}\n\n get inUse(): number {\n return this.active;\n }\n\n async acquire(): Promise<() => void> {\n if (this.active < this.max) this.active++;\n else await new Promise<void>((resolve) => this.waiters.push(resolve));\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = this.waiters.shift();\n if (next) next();\n else this.active--;\n };\n }\n}\n\n/** Per-context admin auth state: the session store, the failed-login limiter and verification queues. */\nexport interface AdminAuth {\n sessions: SessionStore;\n limiter: LoginLimiter;\n /** One secret check per client at a time: the tail of each client's queue. */\n lanes: Map<string, { tail: Promise<void>; queued: number }>;\n verifySlots: Semaphore;\n}\n\n/** What admin auth needs from the context (the CLI builds this without loading the master key). */\nexport type AuthCore = Pick<CoreContext, \"db\" | \"settings\" | \"audit\" | \"config\">;\n\nconst instances = new WeakMap<AuthCore, AdminAuth>();\n\nexport function adminAuth(ctx: AuthCore): AdminAuth {\n let auth = instances.get(ctx);\n if (!auth) {\n auth = { sessions: new SessionStore(ctx.db), limiter: new LoginLimiter(), lanes: new Map(), verifySlots: new Semaphore(MAX_CONCURRENT_VERIFICATIONS) };\n instances.set(ctx, auth);\n }\n return auth;\n}\n\n/** Claimed = an admin password exists. Before that only the setup-token claim works. */\nexport const isClaimed = (ctx: AuthCore): boolean => !!ctx.settings.getState(\"admin.passwordHash\");\n\n/** The current setup token while unclaimed (created on demand), otherwise null. */\nexport function currentSetupToken(ctx: AuthCore): string | null {\n return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);\n}\n\n/** `xaq reset-password`: back to the unclaimed state with a fresh setup link. */\nexport function resetAdminPassword(ctx: AuthCore): string {\n const auth = adminAuth(ctx);\n ctx.db.tx(() => {\n ctx.settings.deleteState(\"admin.passwordHash\");\n auth.sessions.deleteAll();\n });\n auth.limiter.reset();\n ctx.audit.record({ actorType: \"system\", action: \"admin.password_reset\", outcome: \"ok\" });\n return regenerateSetupToken(ctx.config.dataDir);\n}\n\nfunction socketAddress(c: Context): string | null {\n try {\n return getConnInfo(c).remote.address ?? null;\n } catch {\n // No Node socket (e.g. app.request() without bindings).\n return null;\n }\n}\n\n/**\n * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the\n * connection comes from a trusted proxy (loopback, or TRUST_PROXY_FROM); it is read from the\n * right, since proxies append the address they saw and entries further left are client-controlled.\n * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of xaq.\n */\n/** Some proxies (Azure, IIS) append \":port\" to X-Forwarded-For entries. */\nfunction stripPort(entry: string): string {\n const v6 = /^\\[([^\\]]+)\\](?::\\d+)?$/.exec(entry);\n if (v6) return v6[1]!;\n const v4 = /^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/.exec(entry);\n return v4 ? v4[1]! : entry;\n}\n\nlet warnedUntrustedProxy = false;\n\n/** Upgrades from the old private-range rule silently lost proxy trust; say so once. */\nfunction warnUntrustedProxy(peer: string): void {\n if (warnedUntrustedProxy) return;\n warnedUntrustedProxy = true;\n process.stderr.write(\n `xaq: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\\n`,\n );\n}\n\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && !isTrustedProxy(peer) && c.req.header(\"x-forwarded-for\")) warnUntrustedProxy(peer);\n if (trustProxy && peer && isTrustedProxy(peer)) {\n const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? \"1\", 10) || 1);\n const chain = (c.req.header(\"x-forwarded-for\") ?? \"\")\n .split(\",\")\n .map((s) => stripPort(s.trim()))\n .filter(Boolean);\n const forwarded = chain[chain.length - hops];\n if (forwarded && isIP(forwarded)) return forwarded;\n }\n return peer ?? \"unknown\";\n}\n\nfunction isSecure(c: Context, trustProxy: boolean): boolean {\n if (new URL(c.req.url).protocol === \"https:\") return true;\n const peer = socketAddress(c);\n return trustProxy && !!peer && isTrustedProxy(peer) && c.req.header(\"x-forwarded-proto\")?.split(\",\")[0]?.trim().toLowerCase() === \"https\";\n}\n\nexport function sessionToken(c: Context): string | undefined {\n return getCookie(c, SESSION_COOKIE) || undefined;\n}\n\nfunction writeSessionCookie(c: Context, token: string, trustProxy: boolean): void {\n setCookie(c, SESSION_COOKIE, token, {\n httpOnly: true,\n sameSite: \"Lax\",\n path: \"/\",\n secure: isSecure(c, trustProxy),\n maxAge: Math.floor(SESSION_TTL_MS / 1000),\n });\n}\n\n/** Creates a session for the requester and sets the cookie. */\nexport function startSession(c: Context, ctx: AuthCore): void {\n const token = adminAuth(ctx).sessions.create({ ip: clientIp(c, ctx.config.trustProxy), userAgent: c.req.header(\"user-agent\") });\n writeSessionCookie(c, token, ctx.config.trustProxy);\n}\n\nexport function endSession(c: Context, ctx: AuthCore): void {\n const token = sessionToken(c);\n if (token) adminAuth(ctx).sessions.delete(token);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n}\n\n/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */\nexport function hasSession(c: Context, ctx: AuthCore): boolean {\n const token = sessionToken(c);\n const check = adminAuth(ctx).sessions.check(token);\n if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);\n return check.valid;\n}\n\n/** Requires an admin session on every route it guards. */\nexport function requireAdmin(ctx: AuthCore): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (!hasSession(c, ctx)) {\n if (!isClaimed(ctx)) {\n throw new HttpError(401, \"setup_required\", \"This instance has not been claimed yet\", \"Open the setup link printed by xaq on startup.\");\n }\n throw new HttpError(401, \"unauthorized\", \"Sign in to continue\");\n }\n c.set(\"admin\", true);\n await next();\n };\n}\n\nconst MUTATING = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server\n * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.\n */\nexport function csrfGuard(): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (MUTATING.has(c.req.method) && c.req.header(CSRF_HEADER) !== \"1\") {\n throw new HttpError(403, \"csrf\", `Missing ${CSRF_HEADER} header`, `Send \"${CSRF_HEADER}: 1\" with every mutating /api request.`);\n }\n await next();\n };\n}\n\n/** 429 response for a locked-out address. */\nexport function lockedOut(c: Context, minutes: number): Response {\n c.header(\"Retry-After\", String(minutes * 60));\n const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? \"\" : \"s\"}.`;\n return c.json({ error: { code: \"locked_out\", message } }, 429);\n}\n\nexport type CheckResult =\n | { ok: true }\n | { ok: false; lockedMinutes: number; nowBanned: boolean; /** Too many concurrent attempts from this client. */ busy?: boolean };\n\nexport interface CheckOptions {\n /**\n * Count toward (and be slowed down by) the instance-wide failure budget. Off for the setup claim:\n * it's only open while unclaimed and needs the token.\n */\n global?: boolean;\n /** Test hook: how long to wait while over the global budget. */\n slowdownMs?: number;\n}\n\n/**\n * Verifies a secret under the failed-login limiter.\n * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most\n * MAX_CONCURRENT_VERIFICATIONS run at once overall.\n * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so\n * concurrent requests can't all slip past the ban.\n * - Locked-out clients and secrets this client already failed with never reach `verify`.\n * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.\n */\nexport async function checkSecret(\n ctx: AuthCore,\n ip: string,\n secret: string,\n verify: () => boolean | Promise<boolean>,\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const auth = adminAuth(ctx);\n const key = clientKey(ip);\n const lane = auth.lanes.get(key) ?? { tail: Promise.resolve(), queued: 0 };\n if (lane.queued > MAX_QUEUED_PER_CLIENT) return { ok: false, lockedMinutes: 0, nowBanned: false, busy: true };\n lane.queued++;\n auth.lanes.set(key, lane);\n const previous = lane.tail;\n let done!: () => void;\n lane.tail = new Promise<void>((resolve) => (done = resolve));\n try {\n await previous;\n return await checkOnce(auth, ip, secret, verify, opts);\n } finally {\n done();\n if (--lane.queued === 0) auth.lanes.delete(key);\n }\n}\n\nasync function checkOnce(auth: AdminAuth, ip: string, secret: string, verify: () => boolean | Promise<boolean>, opts: CheckOptions): Promise<CheckResult> {\n const { limiter } = auth;\n const global = opts.global !== false;\n if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) {\n await new Promise((r) => setTimeout(r, opts.slowdownMs ?? GLOBAL_SLOWDOWN_MS));\n }\n // From here to the reservation everything is synchronous.\n const locked = limiter.bannedMinutes(ip);\n if (locked) return { ok: false, lockedMinutes: locked, nowBanned: false };\n if (limiter.knownBad(ip, secret)) return { ok: false, lockedMinutes: 0, nowBanned: limiter.fail(ip, secret, { global }) };\n const reservation = limiter.record(ip, secret, { global });\n const release = await auth.verifySlots.acquire();\n let ok = false;\n try {\n ok = await verify();\n } finally {\n release();\n }\n if (!ok) return { ok: false, lockedMinutes: 0, nowBanned: reservation.banned };\n limiter.clear(ip);\n if (reservation.countedGlobally) limiter.refundGlobal();\n return { ok: true };\n}\n\n/** The 429 for a rejected check (locked out or too many at once), or null to continue. */\nexport function rejection(c: Context, check: CheckResult): Response | null {\n if (check.ok) return null;\n if (check.busy) {\n c.header(\"Retry-After\", \"2\");\n return c.json({ error: { code: \"too_many_requests\", message: \"Another sign-in attempt from your address is still being checked. Try again in a moment.\" } }, 429);\n }\n return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ const LEGACY_PRODUCT = "agentinbox";
5
+ /**
6
+ * `XAQ_<name>`, else the pre-rename `AGENTINBOX_<name>`. A set `XAQ_` variable wins even when empty
7
+ * (an empty value is how some settings are switched off).
8
+ */
9
+ function envVar(name, env = process.env) {
10
+ const value = env[`XAQ_${name}`];
11
+ return value !== void 0 ? value : env[`AGENTINBOX_${name}`];
12
+ }
13
+ /** Whether this process runs under `service install` (XAQ_SERVICE=1; AGENTINBOX_SERVICE=1 in units from before the rename). */
14
+ const underService = (env = process.env) => envVar("SERVICE", env) === "1";
15
+ /**
16
+ * Our directory under `base` (an XDG base directory, or the home directory with `dot`). Another, unrelated
17
+ * program is also called xaq and may own a `xaq` directory there (`~/.config/xaq` with its credentials and
18
+ * history), so a `xaq` directory counts as ours only when it holds one of `markers`, paths only we create
19
+ * there. That is checked with a stat: nothing in it is read.
20
+ * 1. `xaq` when it holds a marker: ours (also what nightly.71/72 set up);
21
+ * 2. `agentinbox` (from before the rename) when it exists;
22
+ * 3. else `xaq`: ours when it doesn't exist yet; when it belongs to the other program ("shared"), we only add
23
+ * our own paths beside its files, and never change, rename or delete anything we didn't create.
24
+ * Nothing is ever moved.
25
+ */
26
+ function pickDir(base, markers, opts = {}) {
27
+ const name = (n) => join(base, opts.dot ? `.${n}` : n);
28
+ const next = name("xaq");
29
+ if (markers.some((m) => isPresent(join(next, m)))) return {
30
+ dir: next,
31
+ kind: "ours"
32
+ };
33
+ const legacy = name(LEGACY_PRODUCT);
34
+ if (existsSync(legacy)) return {
35
+ dir: legacy,
36
+ kind: "legacy"
37
+ };
38
+ return {
39
+ dir: next,
40
+ kind: isPresent(next) ? "shared" : "ours"
41
+ };
42
+ }
43
+ /** A stat, nothing more (no symlink is followed into reading anything). */
44
+ function isPresent(path) {
45
+ try {
46
+ statSync(path);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+ //#endregion
53
+ export { underService as i, envVar as n, pickDir as r, LEGACY_PRODUCT as t };
54
+
55
+ //# sourceMappingURL=brand-Bg9VPAym.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-Bg9VPAym.js","names":[],"sources":["../server/brand.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * The product name, and the one it had before (agentinbox → xaq). Installs from before the rename keep\n * working: their environment variables, data directory, services and agent-side names are still found.\n */\nexport const PRODUCT = \"xaq\";\nexport const LEGACY_PRODUCT = \"agentinbox\";\n\n/**\n * `XAQ_<name>`, else the pre-rename `AGENTINBOX_<name>`. A set `XAQ_` variable wins even when empty\n * (an empty value is how some settings are switched off).\n */\nexport function envVar(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {\n const value = env[`XAQ_${name}`];\n return value !== undefined ? value : env[`AGENTINBOX_${name}`];\n}\n\n/** Whether this process runs under `service install` (XAQ_SERVICE=1; AGENTINBOX_SERVICE=1 in units from before the rename). */\nexport const underService = (env: NodeJS.ProcessEnv = process.env) => envVar(\"SERVICE\", env) === \"1\";\n\n/**\n * What pickDir chose: `xaq` holding our files (or not there yet), the pre-rename `agentinbox`, or a `xaq`\n * that another program owns, which we share by only adding our own paths to it.\n */\nexport type DirKind = \"ours\" | \"legacy\" | \"shared\";\n\n/**\n * Our directory under `base` (an XDG base directory, or the home directory with `dot`). Another, unrelated\n * program is also called xaq and may own a `xaq` directory there (`~/.config/xaq` with its credentials and\n * history), so a `xaq` directory counts as ours only when it holds one of `markers`, paths only we create\n * there. That is checked with a stat: nothing in it is read.\n * 1. `xaq` when it holds a marker: ours (also what nightly.71/72 set up);\n * 2. `agentinbox` (from before the rename) when it exists;\n * 3. else `xaq`: ours when it doesn't exist yet; when it belongs to the other program (\"shared\"), we only add\n * our own paths beside its files, and never change, rename or delete anything we didn't create.\n * Nothing is ever moved.\n */\nexport function pickDir(base: string, markers: string[], opts: { dot?: boolean } = {}): { dir: string; kind: DirKind } {\n const name = (n: string) => join(base, opts.dot ? `.${n}` : n);\n const next = name(PRODUCT);\n if (markers.some((m) => isPresent(join(next, m)))) return { dir: next, kind: \"ours\" };\n const legacy = name(LEGACY_PRODUCT);\n if (existsSync(legacy)) return { dir: legacy, kind: \"legacy\" };\n return { dir: next, kind: isPresent(next) ? \"shared\" : \"ours\" };\n}\n\n/** A stat, nothing more (no symlink is followed into reading anything). */\nfunction isPresent(path: string): boolean {\n try {\n statSync(path);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;AAQA,MAAa,iBAAiB;;;;;AAM9B,SAAgB,OAAO,MAAc,MAAyB,QAAQ,KAAyB;CAC7F,MAAM,QAAQ,IAAI,OAAO;CACzB,OAAO,UAAU,KAAA,IAAY,QAAQ,IAAI,cAAc;AACzD;;AAGA,MAAa,gBAAgB,MAAyB,QAAQ,QAAQ,OAAO,WAAW,GAAG,MAAM;;;;;;;;;;;;AAmBjG,SAAgB,QAAQ,MAAc,SAAmB,OAA0B,CAAC,GAAmC;CACrH,MAAM,QAAQ,MAAc,KAAK,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC;CAC7D,MAAM,OAAO,KAAA,KAAY;CACzB,IAAI,QAAQ,MAAM,MAAM,UAAU,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO;EAAE,KAAK;EAAM,MAAM;CAAO;CACpF,MAAM,SAAS,KAAK,cAAc;CAClC,IAAI,WAAW,MAAM,GAAG,OAAO;EAAE,KAAK;EAAQ,MAAM;CAAS;CAC7D,OAAO;EAAE,KAAK;EAAM,MAAM,UAAU,IAAI,IAAI,WAAW;CAAO;AAChE;;AAGA,SAAS,UAAU,MAAuB;CACxC,IAAI;EACF,SAAS,IAAI;EACb,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF"}