agentinbox-cli 0.1.0-nightly.4 → 0.1.0-nightly.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/cli.js +101 -24
- package/dist/cli.js.map +1 -1
- package/dist/{instance-BnbMB2EK.js → instance-CpTBwgrK.js} +7 -3
- package/dist/{instance-BnbMB2EK.js.map → instance-CpTBwgrK.js.map} +1 -1
- package/dist/instance-Dh5kDs31.js +2 -0
- package/dist/{server-F3DTB7YC.js → server-B0zZclGt.js} +72 -33
- package/dist/server-B0zZclGt.js.map +1 -0
- package/dist/service-CFnr0e9w.js +473 -0
- package/dist/service-CFnr0e9w.js.map +1 -0
- package/dist/{settings-DkA2sgzt.js → settings-BIuG-X7b.js} +12 -3
- package/dist/settings-BIuG-X7b.js.map +1 -0
- package/dist/settings-BK7h99j7.js +2 -0
- package/dist/skill-BJFn2bHK.js +2 -0
- package/dist/urls-Dp8pBGMU.js +2 -0
- package/dist/{urls-DmnonkJo.js → urls-U_LfYV61.js} +7 -2
- package/dist/urls-U_LfYV61.js.map +1 -0
- package/package.json +1 -1
- package/dist/instance-BEEO0AHU.js +0 -2
- package/dist/server-F3DTB7YC.js.map +0 -1
- package/dist/service-lHEFbRJQ.js +0 -333
- package/dist/service-lHEFbRJQ.js.map +0 -1
- package/dist/settings-DkA2sgzt.js.map +0 -1
- package/dist/settings-DoPS659W.js +0 -2
- package/dist/skill-BWWtinsE.js +0 -2
- package/dist/urls-CNYhbeUQ.js +0 -2
- package/dist/urls-DmnonkJo.js.map +0 -1
package/README.md
CHANGED
|
@@ -224,6 +224,27 @@ Only one agentinbox runs per data directory; a second one refuses to start.
|
|
|
224
224
|
|
|
225
225
|
When a newer version is published, the console shows **Update available** with the exact command to run.
|
|
226
226
|
|
|
227
|
+
The unit runs the Node.js binary you installed it with. If you remove or replace that Node, run `service install` again.
|
|
228
|
+
If the service doesn't come up, `service install` rolls back and prints where to look (`systemctl --user status
|
|
229
|
+
agentinbox`, `journalctl --user -u agentinbox`). Over `su` or `sudo -u` there is often no systemd user session: log
|
|
230
|
+
in as that user directly, or enable lingering and set `XDG_RUNTIME_DIR=/run/user/$(id -u)` first.
|
|
231
|
+
|
|
232
|
+
A service that hasn't been claimed yet doesn't print its setup token to the journal. Get the link with
|
|
233
|
+
`npx agentinbox-cli reset-password --data <dir>`, or read the token from `<data>/setup-token`.
|
|
234
|
+
|
|
235
|
+
### With your own supervisor
|
|
236
|
+
|
|
237
|
+
On other systems, or to manage it yourself (Docker, runit, supervisord, pm2), install the command and run it in the
|
|
238
|
+
foreground:
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
npm i -g agentinbox-cli
|
|
242
|
+
agentinbox start --no-open --data /var/lib/agentinbox
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Use the installed `agentinbox` binary rather than `npx`: npx doesn't pass stop signals on, so agentinbox wouldn't shut
|
|
246
|
+
down cleanly.
|
|
247
|
+
|
|
227
248
|
### Behind a reverse proxy
|
|
228
249
|
|
|
229
250
|
agentinbox serves plain HTTP. To expose it beyond your network, put it behind a TLS-terminating proxy (Caddy, nginx,
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
import
|
|
4
|
+
import { join, resolve, sep } from "node:path";
|
|
5
|
+
//#region server/node-check.ts
|
|
6
|
+
/** The oldest Node.js agentinbox runs on (node:sqlite unflagged, import.meta.dirname). Keep in sync with package.json engines. */
|
|
7
|
+
const MIN_NODE = "22.13.0";
|
|
8
|
+
/** An explanation when `version` (e.g. process.versions.node) is too old, else null. */
|
|
9
|
+
function nodeVersionError(version) {
|
|
10
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
11
|
+
const have = parse(version);
|
|
12
|
+
const need = parse(MIN_NODE);
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
if ((have[i] ?? 0) > need[i]) return null;
|
|
15
|
+
if ((have[i] ?? 0) < need[i]) return `agentinbox needs Node.js 22.13 or newer (you have ${version.replace(/^v?/, "v")}). Install a current Node.js from https://nodejs.org and try again.`;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const problem = nodeVersionError(process.versions.node);
|
|
20
|
+
if (problem) {
|
|
21
|
+
console.error(problem);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
7
25
|
//#region server/cli.ts
|
|
26
|
+
const { Command, InvalidArgumentError } = await import("commander");
|
|
27
|
+
const { default: pc } = await import("picocolors");
|
|
28
|
+
process.title = "agentinbox";
|
|
8
29
|
const emitWarning = process.emitWarning.bind(process);
|
|
9
30
|
process.emitWarning = ((warning, ...rest) => {
|
|
10
31
|
const message = typeof warning === "string" ? warning : warning.message;
|
|
@@ -40,8 +61,9 @@ const configInput = (o) => ({
|
|
|
40
61
|
dataDir: o.data
|
|
41
62
|
});
|
|
42
63
|
const program = new Command("agentinbox").description("Self-hosted Gmail broker for AI agents: MCP tools, webhooks, guardrails and a web console.").version(version, "-v, --version", "print the version").helpOption("-h, --help", "show help").addHelpText("after", "\nWithout installing: npx agentinbox-cli [command]. To install the command: npm i -g agentinbox-cli").showSuggestionAfterError();
|
|
43
|
-
withServerOptions(program.command("start", { isDefault: true }).description("run agentinbox in the foreground (default)")).option("--no-open", "don't open a browser on first run").option("--dev", "development: API only, the console runs on Vite (:5173)").action(async (o) => {
|
|
44
|
-
|
|
64
|
+
withServerOptions(program.command("start", { isDefault: true }).description("run agentinbox in the foreground (default)")).option("--no-open", "don't open a browser on first run").option("--dev", "development: API only, the console runs on Vite (:5173)").allowExcessArguments().action(async (o, cmd) => {
|
|
65
|
+
if (cmd.args.length) throw new Error(unknownCommand(cmd.args[0]));
|
|
66
|
+
const { runForeground } = await import("./server-B0zZclGt.js");
|
|
45
67
|
await runForeground({
|
|
46
68
|
...configInput(o),
|
|
47
69
|
dev: !!o.dev,
|
|
@@ -52,43 +74,43 @@ withServerOptions(program.command("start", { isDefault: true }).description("run
|
|
|
52
74
|
const service = program.command("service").description("run agentinbox as a background service (systemd / launchd)");
|
|
53
75
|
withServerOptions(service.command("install").description("install and start the service; re-run to update")).action(async (o) => {
|
|
54
76
|
const { loadConfig } = await import("./config-CIvgMqLx.js");
|
|
55
|
-
const { installService } = await import("./service-
|
|
77
|
+
const { installService } = await import("./service-CFnr0e9w.js");
|
|
56
78
|
await installService(loadConfig(configInput(o), version));
|
|
57
79
|
});
|
|
58
|
-
service.command("uninstall").description("stop and remove the service (keeps your data)").option("-d, --data <dir>", "data directory (default ~/.agentinbox, env AGENTINBOX_DATA)").action(async (o) => {
|
|
59
|
-
const {
|
|
60
|
-
|
|
61
|
-
uninstallService(loadConfig(configInput(o), version));
|
|
80
|
+
service.command("uninstall").description("stop and remove the service (keeps your data)").option("-d, --data <dir>", "data directory, if the installed service doesn't say (default ~/.agentinbox, env AGENTINBOX_DATA)").action(async (o) => {
|
|
81
|
+
const { uninstallService } = await import("./service-CFnr0e9w.js");
|
|
82
|
+
uninstallService({ dataDir: resolve(o.data ?? process.env.AGENTINBOX_DATA ?? join(homedir(), ".agentinbox")) });
|
|
62
83
|
});
|
|
63
84
|
service.command("status").description("show service status").action(async () => {
|
|
64
|
-
const { serviceStatus } = await import("./service-
|
|
85
|
+
const { serviceStatus } = await import("./service-CFnr0e9w.js");
|
|
65
86
|
serviceStatus();
|
|
66
87
|
});
|
|
67
88
|
const skill = program.command("skill").description("the agentinbox agent skill (SKILL.md)");
|
|
68
89
|
skill.command("print").description("print SKILL.md to stdout").option("--url <url>", "agentinbox server URL to fill in (env AGENTINBOX_URL)").action(async (o) => {
|
|
69
|
-
const { renderSkill } = await import("./skill-
|
|
70
|
-
process.stdout.write(renderSkill(serverUrl(o.url)));
|
|
90
|
+
const { renderSkill } = await import("./skill-BJFn2bHK.js");
|
|
91
|
+
process.stdout.write(renderSkill(await serverUrl(o.url)));
|
|
71
92
|
});
|
|
72
|
-
skill.command("install").description("install SKILL.md for an agent (no running server needed)").option("-t, --target <target>", "claude, codex, agents, or a skills directory", "claude").option("--url <url>", "agentinbox server URL agents connect to (env AGENTINBOX_URL)").action(async (o) => {
|
|
73
|
-
const { renderSkill, skillPath, writeSkill } = await import("./skill-
|
|
74
|
-
|
|
93
|
+
skill.command("install").description("install SKILL.md for an agent (no running server needed)").option("-t, --target <target>", "claude, codex, agents, or a skills directory path", "claude").option("--url <url>", "agentinbox server URL agents connect to (env AGENTINBOX_URL)").action(async (o) => {
|
|
94
|
+
const { renderSkill, SKILL_TARGETS, skillPath, writeSkill } = await import("./skill-BJFn2bHK.js");
|
|
95
|
+
checkSkillTarget(o.target, Object.keys(SKILL_TARGETS));
|
|
96
|
+
const url = await serverUrl(o.url);
|
|
75
97
|
const path = skillPath(o.target);
|
|
76
98
|
writeSkill(path, renderSkill(url));
|
|
77
99
|
console.log(`Installed the agentinbox skill to ${pc.cyan(path)} (server ${url}).`);
|
|
78
100
|
if (!o.url && !process.env.AGENTINBOX_URL) console.log(pc.dim("Agents on other machines need the server's address: re-run with --url https://your-server."));
|
|
79
101
|
});
|
|
80
|
-
|
|
102
|
+
program.command("reset-password").description("remove the admin password and print a new setup link").option("-d, --data <dir>", "data directory (default ~/.agentinbox, env AGENTINBOX_DATA)").option("-p, --port <port>", "port of the running instance, used for the printed link", parsePort).option("--host <host>", "host of the running instance, used for the printed link").action(async (o) => {
|
|
81
103
|
const dataDir = resolve(o.data ?? process.env.AGENTINBOX_DATA ?? join(homedir(), ".agentinbox"));
|
|
82
104
|
const dbPath = join(dataDir, "agentinbox.db");
|
|
83
105
|
if (!existsSync(dbPath)) throw new Error(`No agentinbox data in ${dataDir}. If agentinbox runs as a system service, run this with sudo; otherwise pass the data directory it uses with --data.`);
|
|
84
106
|
const { loadConfig } = await import("./config-CIvgMqLx.js");
|
|
85
107
|
const { Db } = await import("./db-CXhlUuVP.js");
|
|
86
108
|
const { createBus } = await import("./bus-OKIGut72.js");
|
|
87
|
-
const { SettingsStore } = await import("./settings-
|
|
109
|
+
const { SettingsStore } = await import("./settings-BK7h99j7.js");
|
|
88
110
|
const { AuditLog } = await import("./audit-Bf3TvAAN.js");
|
|
89
111
|
const { currentSetupToken, isClaimed, resetAdminPassword } = await import("./auth-X5SGuNNB.js");
|
|
90
|
-
const { runningInstance } = await import("./instance-
|
|
91
|
-
const { consoleUrls } = await import("./urls-
|
|
112
|
+
const { runningInstance } = await import("./instance-Dh5kDs31.js");
|
|
113
|
+
const { consoleUrls } = await import("./urls-Dp8pBGMU.js");
|
|
92
114
|
const running = runningInstance(dataDir);
|
|
93
115
|
const config = loadConfig({
|
|
94
116
|
dataDir,
|
|
@@ -116,9 +138,22 @@ withServerOptions(program.command("reset-password").description("remove the admi
|
|
|
116
138
|
db.close();
|
|
117
139
|
}
|
|
118
140
|
});
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
/**
|
|
142
|
+
* The URL written into SKILL.md: --url, AGENTINBOX_URL, else the agentinbox running on this
|
|
143
|
+
* machine (its lock file), else http://localhost:8425.
|
|
144
|
+
*/
|
|
145
|
+
async function serverUrl(flag) {
|
|
146
|
+
let raw = flag ?? process.env.AGENTINBOX_URL;
|
|
147
|
+
if (!raw) {
|
|
148
|
+
const { runningInstance } = await import("./instance-Dh5kDs31.js");
|
|
149
|
+
const running = runningInstance(resolve(process.env.AGENTINBOX_DATA || join(homedir(), ".agentinbox")));
|
|
150
|
+
const host = running && ![
|
|
151
|
+
"0.0.0.0",
|
|
152
|
+
"::",
|
|
153
|
+
""
|
|
154
|
+
].includes(running.host) ? running.host : "localhost";
|
|
155
|
+
raw = `http://${host.includes(":") ? `[${host}]` : host}:${running?.port || process.env.PORT || 8425}`;
|
|
156
|
+
}
|
|
122
157
|
let url;
|
|
123
158
|
try {
|
|
124
159
|
url = new URL(raw);
|
|
@@ -128,6 +163,48 @@ function serverUrl(flag) {
|
|
|
128
163
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("--url must start with http:// or https://");
|
|
129
164
|
return (url.origin + url.pathname).replace(/\/+$/, "");
|
|
130
165
|
}
|
|
166
|
+
/** A known agent name, or something that is clearly a path; typos like "claud" are refused. */
|
|
167
|
+
function checkSkillTarget(target, known) {
|
|
168
|
+
if (known.includes(target) || target.includes("/") || target.includes(sep) || target.startsWith(".") || isDir(target)) return;
|
|
169
|
+
const near = closest(target, known);
|
|
170
|
+
throw new Error(`Unknown skill target "${target}".${near ? ` Did you mean ${near}?` : ""} Use ${known.join(", ")}, or a directory path such as ./skills.`);
|
|
171
|
+
}
|
|
172
|
+
function isDir(path) {
|
|
173
|
+
try {
|
|
174
|
+
return statSync(path).isDirectory();
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function unknownCommand(name) {
|
|
180
|
+
const near = closest(name, [...program.commands.map((c) => c.name()), "help"]);
|
|
181
|
+
return `unknown command '${name}'${near ? `\n(Did you mean ${near}?)` : ""}\nRun agentinbox --help for the list of commands.`;
|
|
182
|
+
}
|
|
183
|
+
/** The candidate within edit distance 2 (or a prefix match), if any. */
|
|
184
|
+
function closest(word, candidates) {
|
|
185
|
+
const distance = (a, b) => {
|
|
186
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
187
|
+
for (let i = 1; i <= a.length; i++) {
|
|
188
|
+
let prev = row[0];
|
|
189
|
+
row[0] = i;
|
|
190
|
+
for (let j = 1; j <= b.length; j++) {
|
|
191
|
+
const cur = row[j];
|
|
192
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
193
|
+
prev = cur;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return row[b.length];
|
|
197
|
+
};
|
|
198
|
+
let best = null;
|
|
199
|
+
for (const c of candidates) {
|
|
200
|
+
const d = c.startsWith(word) && word.length >= 3 ? 0 : distance(word.toLowerCase(), c);
|
|
201
|
+
if (d <= 2 && (!best || d < best.d)) best = {
|
|
202
|
+
name: c,
|
|
203
|
+
d
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return best?.name ?? null;
|
|
207
|
+
}
|
|
131
208
|
try {
|
|
132
209
|
await program.parseAsync(process.argv);
|
|
133
210
|
} catch (err) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../server/cli.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { Command, InvalidArgumentError } from \"commander\";\nimport pc from \"picocolors\";\n\n// node:sqlite is stable enough for us but still warns on Node 22. This runs before any server\n// module loads: everything that touches node:sqlite is imported dynamically below.\nconst emitWarning = process.emitWarning.bind(process);\nprocess.emitWarning = ((warning: string | Error, ...rest: unknown[]) => {\n const message = typeof warning === \"string\" ? warning : warning.message;\n const type = typeof rest[0] === \"string\" ? rest[0] : (rest[0] as { type?: string } | undefined)?.type;\n if ((type === \"ExperimentalWarning\" || (warning as Error).name === \"ExperimentalWarning\") && /SQLite/i.test(message)) return;\n return (emitWarning as (...args: unknown[]) => void)(warning, ...rest);\n}) as typeof process.emitWarning;\n\n/** package.json sits one level above both dist/cli.js and server/cli.ts. */\nfunction packageVersion(): string {\n try {\n return (JSON.parse(readFileSync(join(import.meta.dirname, \"..\", \"package.json\"), \"utf8\")) as { version: string }).version;\n } catch {\n return \"0.0.0\";\n }\n}\n\nconst version = packageVersion();\n\ninterface ServerFlags {\n port?: number;\n host?: string;\n data?: string;\n}\n\nconst validPort = (value: string) => /^\\d+$/.test(value) && Number(value) <= 65535;\n\nfunction parsePort(value: string): number {\n if (!validPort(value)) throw new InvalidArgumentError(\"Port must be a number from 0 to 65535.\");\n return Number(value);\n}\n\n/** --port, else a validated PORT env (a typo there would otherwise become NaN deep inside). */\nfunction portOption(flag: number | undefined): number | undefined {\n if (flag !== undefined) return flag;\n const env = process.env.PORT?.trim();\n if (!env) return undefined;\n if (!validPort(env)) throw new Error(`PORT=\"${process.env.PORT}\" is not a valid port. Use a number from 0 to 65535.`);\n return Number(env);\n}\n\nconst withServerOptions = (cmd: Command) =>\n cmd\n .option(\"-p, --port <port>\", \"port to listen on (default 8425, env PORT)\", parsePort)\n .option(\"--host <host>\", \"bind address (default 0.0.0.0, env HOST)\")\n .option(\"-d, --data <dir>\", \"data directory (default ~/.agentinbox, env AGENTINBOX_DATA)\");\n\nconst configInput = (o: ServerFlags) => ({ port: portOption(o.port), host: o.host, dataDir: o.data });\n\nconst program = new Command(\"agentinbox\")\n .description(\"Self-hosted Gmail broker for AI agents: MCP tools, webhooks, guardrails and a web console.\")\n .version(version, \"-v, --version\", \"print the version\")\n .helpOption(\"-h, --help\", \"show help\")\n .addHelpText(\"after\", \"\\nWithout installing: npx agentinbox-cli [command]. To install the command: npm i -g agentinbox-cli\")\n .showSuggestionAfterError();\n\nwithServerOptions(program.command(\"start\", { isDefault: true }).description(\"run agentinbox in the foreground (default)\"))\n .option(\"--no-open\", \"don't open a browser on first run\")\n .option(\"--dev\", \"development: API only, the console runs on Vite (:5173)\")\n .action(async (o: ServerFlags & { open: boolean; dev?: boolean }) => {\n const { runForeground } = await import(\"./server.ts\");\n await runForeground({ ...configInput(o), dev: !!o.dev, open: o.open, version });\n });\n\nconst service = program.command(\"service\").description(\"run agentinbox as a background service (systemd / launchd)\");\n\nwithServerOptions(service.command(\"install\").description(\"install and start the service; re-run to update\")).action(async (o: ServerFlags) => {\n const { loadConfig } = await import(\"./config.ts\");\n const { installService } = await import(\"./service.ts\");\n await installService(loadConfig(configInput(o), version));\n});\n\nservice\n .command(\"uninstall\")\n .description(\"stop and remove the service (keeps your data)\")\n .option(\"-d, --data <dir>\", \"data directory (default ~/.agentinbox, env AGENTINBOX_DATA)\")\n .action(async (o: ServerFlags) => {\n const { loadConfig } = await import(\"./config.ts\");\n const { uninstallService } = await import(\"./service.ts\");\n uninstallService(loadConfig(configInput(o), version));\n });\n\nservice\n .command(\"status\")\n .description(\"show service status\")\n .action(async () => {\n const { serviceStatus } = await import(\"./service.ts\");\n serviceStatus();\n });\n\nconst skill = program.command(\"skill\").description(\"the agentinbox agent skill (SKILL.md)\");\n\nskill\n .command(\"print\")\n .description(\"print SKILL.md to stdout\")\n .option(\"--url <url>\", \"agentinbox server URL to fill in (env AGENTINBOX_URL)\")\n .action(async (o: { url?: string }) => {\n const { renderSkill } = await import(\"./skill/index.ts\");\n process.stdout.write(renderSkill(serverUrl(o.url)));\n });\n\nskill\n .command(\"install\")\n .description(\"install SKILL.md for an agent (no running server needed)\")\n .option(\"-t, --target <target>\", \"claude, codex, agents, or a skills directory\", \"claude\")\n .option(\"--url <url>\", \"agentinbox server URL agents connect to (env AGENTINBOX_URL)\")\n .action(async (o: { target: string; url?: string }) => {\n const { renderSkill, skillPath, writeSkill } = await import(\"./skill/index.ts\");\n const url = serverUrl(o.url);\n const path = skillPath(o.target);\n writeSkill(path, renderSkill(url));\n console.log(`Installed the agentinbox skill to ${pc.cyan(path)} (server ${url}).`);\n if (!o.url && !process.env.AGENTINBOX_URL) {\n console.log(pc.dim(\"Agents on other machines need the server's address: re-run with --url https://your-server.\"));\n }\n });\n\nwithServerOptions(program.command(\"reset-password\").description(\"remove the admin password and print a new setup link\")).action(\n async (o: ServerFlags) => {\n // Never create a data directory or master key here: a wrong --data (or a service running as\n // another user) must not silently produce a fresh instance.\n const dataDir = resolve(o.data ?? process.env.AGENTINBOX_DATA ?? join(homedir(), \".agentinbox\"));\n const dbPath = join(dataDir, \"agentinbox.db\");\n if (!existsSync(dbPath)) {\n throw new Error(\n `No agentinbox data in ${dataDir}. If agentinbox runs as a system service, run this with sudo; ` +\n \"otherwise pass the data directory it uses with --data.\",\n );\n }\n const { loadConfig } = await import(\"./config.ts\");\n const { Db } = await import(\"./db/index.ts\");\n const { createBus } = await import(\"./bus.ts\");\n const { SettingsStore } = await import(\"./settings.ts\");\n const { AuditLog } = await import(\"./audit.ts\");\n const { currentSetupToken, isClaimed, resetAdminPassword } = await import(\"./auth/index.ts\");\n const { runningInstance } = await import(\"./instance.ts\");\n const { consoleUrls } = await import(\"./urls.ts\");\n\n const running = runningInstance(dataDir);\n const config = loadConfig(\n { dataDir, port: portOption(o.port) ?? running?.port, host: o.host ?? (process.env.HOST || running?.host) },\n version,\n );\n const db = new Db(dbPath);\n try {\n const bus = createBus();\n const core = { config, db, settings: new SettingsStore(db, bus, config), audit: new AuditLog(db, bus) };\n const claimed = isClaimed(core);\n const token = claimed ? resetAdminPassword(core) : currentSetupToken(core)!;\n console.log(\n claimed\n ? \"\\n The admin password was removed and all console sessions were signed out.\\n Open this link to set a new password:\\n\"\n : \"\\n This instance hasn't been claimed yet, so there is no password to reset.\\n Open this link to claim it:\\n\",\n );\n const urls = consoleUrls(config.host, config.port);\n const publicUrl = core.settings.publicUrl();\n if (!urls.includes(publicUrl)) urls.push(publicUrl);\n for (const url of urls) console.log(` ${pc.cyan(`${url}/setup?token=${token}`)}`);\n console.log(pc.dim(`\\n ${running ? \"The running agentinbox picks this up immediately; no restart needed.\" : \"Start agentinbox to use it.\"}\\n`));\n } finally {\n db.close();\n }\n },\n);\n\n/** The URL written into SKILL.md: --url, AGENTINBOX_URL, or this machine's default. */\nfunction serverUrl(flag: string | undefined): string {\n const raw = flag ?? process.env.AGENTINBOX_URL ?? `http://localhost:${process.env.PORT || 8425}`;\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`Invalid --url \"${raw}\". Use a full URL such as https://mail.example.com.`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") throw new Error(\"--url must start with http:// or https://\");\n return (url.origin + url.pathname).replace(/\\/+$/, \"\");\n}\n\ntry {\n await program.parseAsync(process.argv);\n} catch (err) {\n console.error(pc.red(err instanceof Error ? err.message : String(err)));\n if (process.env.AGENTINBOX_DEBUG && err instanceof Error) console.error(err.stack);\n process.exit(1);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../server/node-check.ts","../server/cli.ts"],"sourcesContent":["/** The oldest Node.js agentinbox runs on (node:sqlite unflagged, import.meta.dirname). Keep in sync with package.json engines. */\nexport const MIN_NODE = \"22.13.0\";\n\n/** An explanation when `version` (e.g. process.versions.node) is too old, else null. */\nexport function nodeVersionError(version: string): string | null {\n const parse = (v: string) => v.replace(/^v/, \"\").split(\".\").map((n) => Number.parseInt(n, 10) || 0);\n const have = parse(version);\n const need = parse(MIN_NODE);\n for (let i = 0; i < 3; i++) {\n if ((have[i] ?? 0) > need[i]!) return null;\n if ((have[i] ?? 0) < need[i]!) return `agentinbox needs Node.js 22.13 or newer (you have ${version.replace(/^v?/, \"v\")}). Install a current Node.js from https://nodejs.org and try again.`;\n }\n return null;\n}\n\n// Runs on import, before anything that needs a newer Node is loaded.\nconst problem = nodeVersionError(process.versions.node);\nif (problem) {\n console.error(problem);\n process.exit(1);\n}\n","// Must stay the first import: exits with a clear message on a too-old Node.\nimport \"./node-check.ts\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve, sep } from \"node:path\";\nimport type { Command as CommandType } from \"commander\";\n\n// Loaded after the Node check (static imports are hoisted above it in the bundle).\nconst { Command, InvalidArgumentError } = await import(\"commander\");\nconst { default: pc } = await import(\"picocolors\");\n\nprocess.title = \"agentinbox\";\n\n// node:sqlite is stable enough for us but still warns on Node 22. This runs before any server\n// module loads: everything that touches node:sqlite is imported dynamically below.\nconst emitWarning = process.emitWarning.bind(process);\nprocess.emitWarning = ((warning: string | Error, ...rest: unknown[]) => {\n const message = typeof warning === \"string\" ? warning : warning.message;\n const type = typeof rest[0] === \"string\" ? rest[0] : (rest[0] as { type?: string } | undefined)?.type;\n if ((type === \"ExperimentalWarning\" || (warning as Error).name === \"ExperimentalWarning\") && /SQLite/i.test(message)) return;\n return (emitWarning as (...args: unknown[]) => void)(warning, ...rest);\n}) as typeof process.emitWarning;\n\n/** package.json sits one level above both dist/cli.js and server/cli.ts. */\nfunction packageVersion(): string {\n try {\n return (JSON.parse(readFileSync(join(import.meta.dirname, \"..\", \"package.json\"), \"utf8\")) as { version: string }).version;\n } catch {\n return \"0.0.0\";\n }\n}\n\nconst version = packageVersion();\n\ninterface ServerFlags {\n port?: number;\n host?: string;\n data?: string;\n}\n\nconst validPort = (value: string) => /^\\d+$/.test(value) && Number(value) <= 65535;\n\nfunction parsePort(value: string): number {\n if (!validPort(value)) throw new InvalidArgumentError(\"Port must be a number from 0 to 65535.\");\n return Number(value);\n}\n\n/** --port, else a validated PORT env (a typo there would otherwise become NaN deep inside). */\nfunction portOption(flag: number | undefined): number | undefined {\n if (flag !== undefined) return flag;\n const env = process.env.PORT?.trim();\n if (!env) return undefined;\n if (!validPort(env)) throw new Error(`PORT=\"${process.env.PORT}\" is not a valid port. Use a number from 0 to 65535.`);\n return Number(env);\n}\n\nconst withServerOptions = (cmd: CommandType) =>\n cmd\n .option(\"-p, --port <port>\", \"port to listen on (default 8425, env PORT)\", parsePort)\n .option(\"--host <host>\", \"bind address (default 0.0.0.0, env HOST)\")\n .option(\"-d, --data <dir>\", \"data directory (default ~/.agentinbox, env AGENTINBOX_DATA)\");\n\nconst configInput = (o: ServerFlags) => ({ port: portOption(o.port), host: o.host, dataDir: o.data });\n\nconst program = new Command(\"agentinbox\")\n .description(\"Self-hosted Gmail broker for AI agents: MCP tools, webhooks, guardrails and a web console.\")\n .version(version, \"-v, --version\", \"print the version\")\n .helpOption(\"-h, --help\", \"show help\")\n .addHelpText(\"after\", \"\\nWithout installing: npx agentinbox-cli [command]. To install the command: npm i -g agentinbox-cli\")\n .showSuggestionAfterError();\n\nwithServerOptions(program.command(\"start\", { isDefault: true }).description(\"run agentinbox in the foreground (default)\"))\n .option(\"--no-open\", \"don't open a browser on first run\")\n .option(\"--dev\", \"development: API only, the console runs on Vite (:5173)\")\n // `agentinbox typo` lands here as an extra argument: report it as the unknown command it is.\n .allowExcessArguments()\n .action(async (o: ServerFlags & { open: boolean; dev?: boolean }, cmd: CommandType) => {\n if (cmd.args.length) throw new Error(unknownCommand(cmd.args[0]!));\n const { runForeground } = await import(\"./server.ts\");\n await runForeground({ ...configInput(o), dev: !!o.dev, open: o.open, version });\n });\n\nconst service = program.command(\"service\").description(\"run agentinbox as a background service (systemd / launchd)\");\n\nwithServerOptions(service.command(\"install\").description(\"install and start the service; re-run to update\")).action(async (o: ServerFlags) => {\n const { loadConfig } = await import(\"./config.ts\");\n const { installService } = await import(\"./service.ts\");\n await installService(loadConfig(configInput(o), version));\n});\n\nservice\n .command(\"uninstall\")\n .description(\"stop and remove the service (keeps your data)\")\n .option(\"-d, --data <dir>\", \"data directory, if the installed service doesn't say (default ~/.agentinbox, env AGENTINBOX_DATA)\")\n .action(async (o: ServerFlags) => {\n const { uninstallService } = await import(\"./service.ts\");\n // No loadConfig: uninstalling must not create a data directory.\n uninstallService({ dataDir: resolve(o.data ?? process.env.AGENTINBOX_DATA ?? join(homedir(), \".agentinbox\")) });\n });\n\nservice\n .command(\"status\")\n .description(\"show service status\")\n .action(async () => {\n const { serviceStatus } = await import(\"./service.ts\");\n serviceStatus();\n });\n\nconst skill = program.command(\"skill\").description(\"the agentinbox agent skill (SKILL.md)\");\n\nskill\n .command(\"print\")\n .description(\"print SKILL.md to stdout\")\n .option(\"--url <url>\", \"agentinbox server URL to fill in (env AGENTINBOX_URL)\")\n .action(async (o: { url?: string }) => {\n const { renderSkill } = await import(\"./skill/index.ts\");\n process.stdout.write(renderSkill(await serverUrl(o.url)));\n });\n\nskill\n .command(\"install\")\n .description(\"install SKILL.md for an agent (no running server needed)\")\n .option(\"-t, --target <target>\", \"claude, codex, agents, or a skills directory path\", \"claude\")\n .option(\"--url <url>\", \"agentinbox server URL agents connect to (env AGENTINBOX_URL)\")\n .action(async (o: { target: string; url?: string }) => {\n const { renderSkill, SKILL_TARGETS, skillPath, writeSkill } = await import(\"./skill/index.ts\");\n checkSkillTarget(o.target, Object.keys(SKILL_TARGETS));\n const url = await serverUrl(o.url);\n const path = skillPath(o.target);\n writeSkill(path, renderSkill(url));\n console.log(`Installed the agentinbox skill to ${pc.cyan(path)} (server ${url}).`);\n if (!o.url && !process.env.AGENTINBOX_URL) {\n console.log(pc.dim(\"Agents on other machines need the server's address: re-run with --url https://your-server.\"));\n }\n });\n\nprogram\n .command(\"reset-password\")\n .description(\"remove the admin password and print a new setup link\")\n .option(\"-d, --data <dir>\", \"data directory (default ~/.agentinbox, env AGENTINBOX_DATA)\")\n .option(\"-p, --port <port>\", \"port of the running instance, used for the printed link\", parsePort)\n .option(\"--host <host>\", \"host of the running instance, used for the printed link\")\n .action(\n async (o: ServerFlags) => {\n // Never create a data directory or master key here: a wrong --data (or a service running as\n // another user) must not silently produce a fresh instance.\n const dataDir = resolve(o.data ?? process.env.AGENTINBOX_DATA ?? join(homedir(), \".agentinbox\"));\n const dbPath = join(dataDir, \"agentinbox.db\");\n if (!existsSync(dbPath)) {\n throw new Error(\n `No agentinbox data in ${dataDir}. If agentinbox runs as a system service, run this with sudo; ` +\n \"otherwise pass the data directory it uses with --data.\",\n );\n }\n const { loadConfig } = await import(\"./config.ts\");\n const { Db } = await import(\"./db/index.ts\");\n const { createBus } = await import(\"./bus.ts\");\n const { SettingsStore } = await import(\"./settings.ts\");\n const { AuditLog } = await import(\"./audit.ts\");\n const { currentSetupToken, isClaimed, resetAdminPassword } = await import(\"./auth/index.ts\");\n const { runningInstance } = await import(\"./instance.ts\");\n const { consoleUrls } = await import(\"./urls.ts\");\n\n const running = runningInstance(dataDir);\n const config = loadConfig(\n { dataDir, port: portOption(o.port) ?? running?.port, host: o.host ?? (process.env.HOST || running?.host) },\n version,\n );\n const db = new Db(dbPath);\n try {\n const bus = createBus();\n const core = { config, db, settings: new SettingsStore(db, bus, config), audit: new AuditLog(db, bus) };\n const claimed = isClaimed(core);\n const token = claimed ? resetAdminPassword(core) : currentSetupToken(core)!;\n console.log(\n claimed\n ? \"\\n The admin password was removed and all console sessions were signed out.\\n Open this link to set a new password:\\n\"\n : \"\\n This instance hasn't been claimed yet, so there is no password to reset.\\n Open this link to claim it:\\n\",\n );\n const urls = consoleUrls(config.host, config.port);\n const publicUrl = core.settings.publicUrl();\n if (!urls.includes(publicUrl)) urls.push(publicUrl);\n for (const url of urls) console.log(` ${pc.cyan(`${url}/setup?token=${token}`)}`);\n console.log(pc.dim(`\\n ${running ? \"The running agentinbox picks this up immediately; no restart needed.\" : \"Start agentinbox to use it.\"}\\n`));\n } finally {\n db.close();\n }\n },\n);\n\n/**\n * The URL written into SKILL.md: --url, AGENTINBOX_URL, else the agentinbox running on this\n * machine (its lock file), else http://localhost:8425.\n */\nasync function serverUrl(flag: string | undefined): Promise<string> {\n let raw = flag ?? process.env.AGENTINBOX_URL;\n if (!raw) {\n const { runningInstance } = await import(\"./instance.ts\");\n const running = runningInstance(resolve(process.env.AGENTINBOX_DATA || join(homedir(), \".agentinbox\")));\n const host = running && ![\"0.0.0.0\", \"::\", \"\"].includes(running.host) ? running.host : \"localhost\";\n raw = `http://${host.includes(\":\") ? `[${host}]` : host}:${running?.port || process.env.PORT || 8425}`;\n }\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`Invalid --url \"${raw}\". Use a full URL such as https://mail.example.com.`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") throw new Error(\"--url must start with http:// or https://\");\n return (url.origin + url.pathname).replace(/\\/+$/, \"\");\n}\n\n/** A known agent name, or something that is clearly a path; typos like \"claud\" are refused. */\nfunction checkSkillTarget(target: string, known: string[]): void {\n if (known.includes(target) || target.includes(\"/\") || target.includes(sep) || target.startsWith(\".\") || isDir(target)) return;\n const near = closest(target, known);\n throw new Error(\n `Unknown skill target \"${target}\".${near ? ` Did you mean ${near}?` : \"\"} Use ${known.join(\", \")}, or a directory path such as ./skills.`,\n );\n}\n\nfunction isDir(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction unknownCommand(name: string): string {\n const near = closest(name, [...program.commands.map((c) => c.name()), \"help\"]);\n return `unknown command '${name}'${near ? `\\n(Did you mean ${near}?)` : \"\"}\\nRun agentinbox --help for the list of commands.`;\n}\n\n/** The candidate within edit distance 2 (or a prefix match), if any. */\nfunction closest(word: string, candidates: string[]): string | null {\n const distance = (a: string, b: string) => {\n const row = Array.from({ length: b.length + 1 }, (_, j) => j);\n for (let i = 1; i <= a.length; i++) {\n let prev = row[0]!;\n row[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const cur = row[j]!;\n row[j] = Math.min(row[j]! + 1, row[j - 1]! + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));\n prev = cur;\n }\n }\n return row[b.length]!;\n };\n let best: { name: string; d: number } | null = null;\n for (const c of candidates) {\n const d = c.startsWith(word) && word.length >= 3 ? 0 : distance(word.toLowerCase(), c);\n if (d <= 2 && (!best || d < best.d)) best = { name: c, d };\n }\n return best?.name ?? null;\n}\n\ntry {\n await program.parseAsync(process.argv);\n} catch (err) {\n console.error(pc.red(err instanceof Error ? err.message : String(err)));\n if (process.env.AGENTINBOX_DEBUG && err instanceof Error) console.error(err.stack);\n process.exit(1);\n}\n"],"mappings":"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"}
|
|
@@ -111,7 +111,8 @@ function acquireInstanceLock(config) {
|
|
|
111
111
|
port: config.port,
|
|
112
112
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
113
113
|
bootId: bootId(),
|
|
114
|
-
processStart: processStartTime(process.pid)
|
|
114
|
+
processStart: processStartTime(process.pid),
|
|
115
|
+
service: process.env.AGENTINBOX_SERVICE === "1"
|
|
115
116
|
};
|
|
116
117
|
for (let attempt = 0;; attempt++) try {
|
|
117
118
|
writeFileSync(file, JSON.stringify(info), {
|
|
@@ -122,7 +123,10 @@ function acquireInstanceLock(config) {
|
|
|
122
123
|
} catch (err) {
|
|
123
124
|
if (err.code !== "EEXIST" || attempt > 0) throw err;
|
|
124
125
|
const other = runningInstance(dataDir);
|
|
125
|
-
if (other)
|
|
126
|
+
if (other) {
|
|
127
|
+
const where = `(pid ${other.pid}${other.port ? `, port ${other.port}` : ""})`;
|
|
128
|
+
throw new Error(other.service ? `agentinbox is already running as a background service with the data directory ${dataDir} ${where}. Use that one, stop it (\`${cliCommand("service uninstall")}\` keeps your data), or use another --data directory.` : `agentinbox is already running with the data directory ${dataDir} ${where}. Stop it first, or use another --data directory. If no such process exists, delete ${file}.`);
|
|
129
|
+
}
|
|
126
130
|
rmSync(file, { force: true });
|
|
127
131
|
}
|
|
128
132
|
heldByUs.add(dataDir);
|
|
@@ -180,4 +184,4 @@ function assertMasterKeyUsable(config, envKey = process.env.AGENTINBOX_MASTER_KE
|
|
|
180
184
|
//#endregion
|
|
181
185
|
export { lockPath as a, EVENT_TYPES as c, cliCommand as i, NPM_PACKAGE as l, assertMasterKeyUsable as n, processStartTime as o, bootId as r, runningInstance as s, acquireInstanceLock as t };
|
|
182
186
|
|
|
183
|
-
//# sourceMappingURL=instance-
|
|
187
|
+
//# sourceMappingURL=instance-CpTBwgrK.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance-BnbMB2EK.js","names":[],"sources":["../shared/types.ts","../server/instance.ts"],"sourcesContent":["// Shared contract between the server (server/) and the web console (src/).\n// Everything crossing the HTTP boundary of the console API (/api/*) is typed here.\n// Timestamps are ISO-8601 strings in UTC. IDs are prefixed opaque strings (e.g. \"agt_…\").\n\n/** The npm package (`npx agentinbox-cli`); the command it installs is `agentinbox`. */\nexport const NPM_PACKAGE = \"agentinbox-cli\";\n\n// ───────────────────────────── Domain enums ─────────────────────────────\n\n/** What an agent may do with a mailbox. `read` also grants search, labels, attachments and the event feed. */\nexport const CAPABILITIES = [\"read\", \"draft\", \"send\", \"organize\", \"trash\"] as const;\nexport type Capability = (typeof CAPABILITIES)[number];\n\n/**\n * How `send`/`reply`/`forward` behave for a grant.\n * - direct: sent immediately (still subject to allowlist + rate limits)\n * - draft_only: a Gmail draft is created instead; a human sends it from Gmail\n * - approval: a draft is created and an approval request is queued in the console;\n * approving sends the draft, rejecting deletes it\n */\nexport const SEND_MODES = [\"direct\", \"draft_only\", \"approval\"] as const;\nexport type SendMode = (typeof SEND_MODES)[number];\n\nexport const EVENT_TYPES = [\n \"message.received\", // new message landed in the mailbox (not sent by the account)\n \"message.sent\", // message sent from the account (by an agent or by a human)\n \"thread.reply_received\", // received message in a thread the account previously sent into\n \"label.added\",\n \"label.removed\",\n \"account.auth_failed\", // refresh token revoked/expired; account needs re-auth\n \"sync.gap\", // history was too old; a bounded resync ran and events may be missing\n] as const;\nexport type EventType = (typeof EVENT_TYPES)[number];\n\nexport type AccountStatus = \"active\" | \"needs_reauth\" | \"paused\" | \"error\";\nexport type IngestMode = \"pubsub\" | \"poll\";\nexport type DeliveryStatus = \"pending\" | \"succeeded\" | \"failed\" | \"dead\";\nexport type ApprovalStatus = \"pending\" | \"approved\" | \"rejected\" | \"sent\" | \"failed\" | \"expired\";\nexport type AuditOutcome = \"ok\" | \"denied\" | \"error\";\nexport type ActorType = \"agent\" | \"admin\" | \"system\";\n\n/** OAuth consent configuration the operator chose in Google Cloud. Drives setup guidance only. */\nexport type GoogleAppType = \"internal\" | \"external\";\n\n// ───────────────────────────── Setup / system ─────────────────────────────\n\nexport interface HealthResponse {\n status: \"ok\" | \"degraded\";\n version: string;\n setupComplete: boolean;\n}\n\n/** GET /api/setup/status (unauthenticated). Drives the console's first route decision: claim, login or app. */\nexport interface SetupStatus {\n claimed: boolean;\n /** The request carries a valid admin session. */\n authenticated: boolean;\n}\n\n/** Live state of the ingest pipeline (Pub/Sub pull loop or history polling). */\nexport interface IngestStatus {\n mode: IngestMode;\n running: boolean;\n lastPullAt: string | null;\n lastError: string | null;\n}\n\n/** GET /api/system: facts about this instance for the settings page and connect snippets. */\nexport interface SystemInfo {\n version: string;\n nodeVersion: string;\n dataDir: string;\n /** Running under `agentinbox service install` (systemd/launchd). */\n service: boolean;\n /** URLs this machine is reachable at (localhost first, then LAN addresses). */\n addresses: string[];\n publicUrl: string;\n ingest: IngestStatus;\n}\n\n/** GET /api/system/latest: update notice. */\nexport interface UpdateInfo {\n current: string;\n latest: string | null;\n updateAvailable: boolean;\n /** Shell command that runs (or reinstalls the service with) the latest version using this instance's options. */\n command: string;\n}\n\n/** Ordered setup checklist; the console's setup wizard and overview both render this. */\nexport interface SetupState {\n /** True once an admin password exists. Before that, only /api/setup/claim with the setup token works. */\n claimed: boolean;\n googleClientConfigured: boolean;\n /** Google project id (needed for Pub/Sub). */\n gcpProjectId: string | null;\n googleAppType: GoogleAppType | null;\n pubsub: PubsubState;\n accountsConnected: number;\n agentsCreated: number;\n /** All required steps done (claimed + client configured + ≥1 account). Pub/Sub is optional (poll fallback). */\n complete: boolean;\n}\n\nexport interface PubsubState {\n mode: IngestMode;\n topic: string | null; // projects/{p}/topics/{t}\n subscription: string | null; // projects/{p}/subscriptions/{s}\n /** Last provisioning/health error, human readable, with remediation hint. */\n lastError: string | null;\n /** Last time a Pub/Sub message was pulled (proves the pipeline works end-to-end). */\n lastMessageAt: string | null;\n}\n\nexport interface Settings {\n /** Externally reachable base URL of this instance, e.g. https://mail.example.com. Used in MCP snippets + OAuth redirect. */\n publicUrl: string;\n ingestMode: IngestMode;\n pollIntervalSeconds: number;\n /** Max characters of message body returned to agents per message (0 = unlimited). */\n maxBodyChars: number;\n /** Days to keep events, deliveries and audit entries. */\n retentionDays: number;\n /** Approval requests auto-expire after this many hours. */\n approvalTtlHours: number;\n /**\n * SSRF guard: webhook targets that resolve to private, loopback or link-local addresses are refused\n * unless their host matches one of these entries (hostnames, IPs or CIDRs, e.g. \"127.0.0.0/8\",\n * \"10.0.0.0/8\", \"n8n.internal\"). Defaults to loopback only.\n */\n webhookAllowlist: string[];\n}\n\nexport interface GoogleClientConfig {\n clientId: string;\n /** Write-only: never returned by the API. */\n clientSecret?: string;\n projectId: string;\n appType: GoogleAppType;\n}\n\n/** Returned by GET /api/google/client. */\nexport interface GoogleClientView {\n clientId: string | null;\n hasSecret: boolean;\n projectId: string | null;\n appType: GoogleAppType | null;\n /** The exact redirect URI the operator must register on the OAuth client. */\n redirectUri: string;\n /**\n * \"direct\": Google redirects straight back to this console (localhost or https publicUrl).\n * \"paste\": publicUrl is plain-http non-localhost; Google can't redirect here, so the console\n * asks the user to paste the URL they land on after consent.\n */\n callbackMode: \"direct\" | \"paste\";\n}\n\n// ───────────────────────────── Accounts ─────────────────────────────\n\nexport interface Account {\n id: string; // acc_…\n email: string;\n displayName: string | null;\n status: AccountStatus;\n lastError: string | null;\n historyId: string | null;\n watchExpiresAt: string | null;\n lastSyncAt: string | null;\n createdAt: string;\n}\n\nexport interface GmailLabel {\n id: string; // Gmail label id, e.g. \"INBOX\" or \"Label_123\"\n name: string;\n type: \"system\" | \"user\";\n}\n\n/** POST /api/accounts/connect → start OAuth. */\nexport interface PubsubTestResult {\n ok: boolean;\n message: string;\n}\n\nexport interface ConnectAccountResponse {\n authUrl: string;\n /** Echoed state; in paste mode the console POSTs the pasted URL to /api/accounts/connect/complete. */\n state: string;\n callbackMode: \"direct\" | \"paste\";\n}\n\n// ───────────────────────────── Agents & grants ─────────────────────────────\n\nexport interface Agent {\n id: string; // agt_…\n name: string;\n description: string | null;\n /** First chars of the key, for identification (e.g. \"aib_7Hk2\"). The full key is shown once at creation/rotation. */\n keyPrefix: string;\n disabled: boolean;\n lastUsedAt: string | null;\n createdAt: string;\n grants: Grant[];\n}\n\nexport interface Grant {\n id: string; // grt_…\n agentId: string;\n accountId: string;\n capabilities: Capability[];\n /** Gmail label ids the agent is confined to (thread-level: a thread is visible if any message carries one). null = whole mailbox. */\n labelScope: string[] | null;\n sendMode: SendMode;\n /** Recipient allowlist: exact addresses or \"*@domain.com\" globs. Empty = any recipient. */\n allowTo: string[];\n /** Only allow sending replies inside threads the agent can already see. */\n replyOnly: boolean;\n maxSendsPerHour: number | null;\n maxSendsPerDay: number | null;\n createdAt: string;\n}\n\nexport type GrantInput = Omit<Grant, \"id\" | \"agentId\" | \"createdAt\">;\n\nexport interface CreateAgentInput {\n name: string;\n description?: string | null;\n grants?: GrantInput[];\n}\n\n/** POST /api/agents/:id/test: runs MCP initialize + tools/list as the agent, in-process. */\nexport interface AgentTestResult {\n ok: boolean;\n tools: string[];\n message: string;\n}\n\n/** Returned once on create and on key rotation. */\nexport interface AgentWithKey {\n agent: Agent;\n apiKey: string; // aib_…\n}\n\n// ───────────────────────────── Events ─────────────────────────────\n\nexport interface Address {\n name: string | null;\n email: string;\n}\n\n/** Normalized message metadata carried in events. No bodies are stored server-side. */\nexport interface MessageSummary {\n id: string; // Gmail message id\n threadId: string;\n from: Address | null;\n to: Address[];\n cc: Address[];\n subject: string;\n snippet: string;\n date: string; // ISO\n labelIds: string[];\n hasAttachments: boolean;\n}\n\nexport interface MailEvent {\n id: string; // evt_…\n seq: number; // monotonic, used as cursor\n type: EventType;\n accountId: string;\n accountEmail: string;\n message: MessageSummary | null;\n /** For label.added / label.removed. */\n labelIds?: string[];\n /** Free-form detail for account/sync events. */\n detail?: Record<string, unknown>;\n createdAt: string;\n}\n\n// ───────────────────────────── Webhooks ─────────────────────────────\n\nexport interface WebhookFilter {\n accountIds?: string[];\n /** Match if the message has any of these label ids. */\n labelIds?: string[];\n /** Glob on sender address, e.g. \"*@stripe.com\". Any match passes. */\n from?: string[];\n /** Glob on any to/cc address. */\n to?: string[];\n /** Case-insensitive regular expression tested against the subject. */\n subject?: string;\n hasAttachments?: boolean;\n}\n\nexport interface Webhook {\n id: string; // whk_…\n agentId: string;\n name: string;\n url: string;\n eventTypes: EventType[];\n filter: WebhookFilter;\n /** Include the cleaned message body (markdown) in the payload. */\n includeBody: boolean;\n enabled: boolean;\n /** Shown once at creation and on rotation (\"whsec_…\", Standard Webhooks format). */\n secretPrefix: string;\n createdAt: string;\n stats: { succeeded24h: number; failed24h: number; lastDeliveryAt: string | null };\n}\n\nexport type WebhookInput = Omit<Webhook, \"id\" | \"secretPrefix\" | \"createdAt\" | \"stats\">;\n\nexport interface WebhookWithSecret {\n webhook: Webhook;\n secret: string;\n}\n\nexport interface Delivery {\n id: string; // dlv_…\n webhookId: string;\n eventId: string;\n eventType: EventType;\n status: DeliveryStatus;\n attempts: number;\n nextAttemptAt: string | null;\n lastStatusCode: number | null;\n lastError: string | null;\n /** First ~500 chars of the receiver's response body. */\n lastResponse: string | null;\n durationMs: number | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Webhook HTTP request body. Signed per https://www.standardwebhooks.com:\n * headers webhook-id, webhook-timestamp, webhook-signature (\"v1,<base64 hmac-sha256>\").\n */\nexport interface WebhookPayload {\n type: EventType;\n timestamp: string;\n data: MailEvent & {\n /** Present only when the webhook has includeBody and the event concerns a message. */\n content?: MessageContent;\n /** Set instead of `content` when includeBody was requested but the body could not be fetched. */\n contentError?: string;\n };\n}\n\n// ───────────────────────────── Message content (returned to agents) ─────────────────────────────\n\n/** Signals attached to content. Informational: the content itself is still delivered (cleaned, not censored). */\nexport interface ContentSignals {\n /** Invisible content (display:none, zero-size/transparent text, HTML comments, zero-width/tag unicode) was removed. */\n hiddenContentRemoved: boolean;\n /** Phrases that commonly appear in prompt-injection attempts (\"ignore previous instructions\", …). */\n suspiciousInstructions: string[];\n /** SPF/DKIM/DMARC results parsed from Authentication-Results. null when the header is absent. */\n auth: { spf: string | null; dkim: string | null; dmarc: string | null } | null;\n /** Sender is outside the account's own domain. */\n external: boolean;\n}\n\nexport interface Attachment {\n id: string; // Gmail attachment id\n filename: string;\n mimeType: string;\n size: number;\n}\n\nexport interface MessageContent {\n /** Body converted to markdown (from HTML) or the plain-text part. */\n body: string;\n truncated: boolean;\n attachments: Attachment[];\n signals: ContentSignals;\n}\n\n// ───────────────────────────── Approvals ─────────────────────────────\n\nexport interface Approval {\n id: string; // apr_…\n agentId: string;\n agentName: string;\n accountId: string;\n accountEmail: string;\n kind: \"send\" | \"reply\" | \"forward\";\n status: ApprovalStatus;\n draftId: string;\n to: Address[];\n cc: Address[];\n bcc: Address[];\n subject: string;\n /** Full body as the agent wrote it (markdown/plain). */\n body: string;\n threadId: string | null;\n attachments: { filename: string; size: number }[];\n /** Why the agent says it is sending (optional `reason` tool arg). */\n reason: string | null;\n decidedAt: string | null;\n decisionNote: string | null;\n sentMessageId: string | null;\n error: string | null;\n createdAt: string;\n expiresAt: string;\n}\n\n// ───────────────────────────── Audit ─────────────────────────────\n\nexport interface AuditEntry {\n id: number;\n ts: string;\n actorType: ActorType;\n actorId: string | null;\n actorName: string | null;\n action: string; // MCP tool name or console action, e.g. \"send_message\", \"agent.create\"\n accountId: string | null;\n target: string | null; // message/thread/draft id\n outcome: AuditOutcome;\n /** For denials: the guardrail that blocked it, e.g. \"allow_to\", \"label_scope\", \"rate_limit\". */\n reason: string | null;\n durationMs: number | null;\n detail: Record<string, unknown> | null;\n}\n\n// ───────────────────────────── Overview ─────────────────────────────\n\nexport interface OverviewStats {\n accounts: { total: number; active: number; needsAttention: number };\n agents: { total: number; activeLast24h: number };\n events24h: number;\n deliveries24h: { succeeded: number; failed: number };\n pendingApprovals: number;\n toolCalls24h: number;\n denied24h: number;\n /** Hourly buckets for the last 24h, oldest first. */\n activity: { hour: string; events: number; toolCalls: number }[];\n}\n\n// ───────────────────────────── Pagination & errors ─────────────────────────────\n\nexport interface Page<T> {\n items: T[];\n /** Opaque cursor for the next page; null when exhausted. */\n nextCursor: string | null;\n}\n\n/** All non-2xx console API responses have this body. */\nexport interface ApiError {\n error: { code: string; message: string; hint?: string };\n}\n\n// ───────────────────────────── Console API route map ─────────────────────────────\n// Session: cookie `aib_session` (HttpOnly, SameSite=Lax). Mutating requests must send header\n// `x-agentinbox-csrf: 1` (simple CSRF guard; the console fetch wrapper always sets it).\n//\n// Unauthenticated:\n// GET /api/health → HealthResponse\n// GET /api/setup/status → SetupStatus\n// POST /api/setup/claim {token,password} → 204 + session cookie (only while unclaimed)\n// POST /api/auth/login {password} → 204 + session cookie (5 failures/IP → 15 min lockout)\n// POST /api/auth/logout → 204\n// GET /oauth/callback?code&state → HTML (redirects to console /accounts?connected=…)\n//\n// Authenticated (admin session):\n// GET /api/setup/state → SetupState\n// GET /api/system → SystemInfo\n// GET /api/system/latest → UpdateInfo (npm registry check, cached 30 min)\n// GET /api/settings → Settings\n// PATCH /api/settings Partial<Settings> → Settings\n// POST /api/auth/password {current,next} → 204\n// GET /api/google/client → GoogleClientView\n// PUT /api/google/client GoogleClientConfig → GoogleClientView\n// POST /api/google/pubsub/provision → PubsubState (creates topic + IAM + subscription using a connected account's token)\n// POST /api/google/pubsub/test → PubsubTestResult\n// GET /api/accounts → Account[]\n// POST /api/accounts/connect → ConnectAccountResponse\n// POST /api/accounts/connect/complete {url} → Account (paste mode)\n// POST /api/accounts/:id/resync → Account\n// POST /api/accounts/:id/pause | /resume → Account\n// DELETE /api/accounts/:id → 204 (stops watch, revokes token, deletes grants)\n// GET /api/accounts/:id/labels → GmailLabel[]\n// GET /api/agents → Agent[]\n// POST /api/agents CreateAgentInput → AgentWithKey\n// GET /api/agents/:id → Agent\n// PATCH /api/agents/:id {name?,description?,disabled?} → Agent\n// POST /api/agents/:id/rotate-key → AgentWithKey\n// DELETE /api/agents/:id → 204\n// PUT /api/agents/:id/grants/:accountId GrantInput → Grant (upsert; one grant per agent×account)\n// DELETE /api/agents/:id/grants/:accountId → 204\n// POST /api/agents/:id/test → AgentTestResult (runs MCP initialize+tools/list as the agent)\n// GET /api/webhooks?agentId → Webhook[]\n// POST /api/webhooks WebhookInput → WebhookWithSecret\n// PATCH /api/webhooks/:id Partial<WebhookInput> → Webhook\n// POST /api/webhooks/:id/rotate-secret → WebhookWithSecret\n// POST /api/webhooks/:id/test → Delivery (sends a synthetic event synchronously)\n// DELETE /api/webhooks/:id → 204\n// GET /api/deliveries?webhookId&status&cursor → Page<Delivery>\n// POST /api/deliveries/:id/replay → Delivery\n// GET /api/events?accountId&type&cursor → Page<MailEvent>\n// GET /api/events/stream → text/event-stream of ConsoleStreamMessage (SSE `data:` = JSON)\n// GET /api/approvals?status → Approval[]\n// POST /api/approvals/:id/approve {note?} → Approval\n// POST /api/approvals/:id/reject {note?} → Approval\n// GET /api/audit?agentId&outcome&cursor → Page<AuditEntry>\n// GET /api/overview → OverviewStats\n//\n// Agent-facing (bearer = agent API key):\n// POST/GET/DELETE /mcp MCP streamable HTTP (stateless)\n// GET /skill.md the agent skill (public, no secrets)\n// GET /health HealthResponse (alias of /api/health for uptime monitors; 503 when degraded)\n\nexport type ConsoleStreamMessage =\n | { kind: \"event\"; data: MailEvent }\n | { kind: \"delivery\"; data: Delivery }\n | { kind: \"approval\"; data: Approval }\n | { kind: \"audit\"; data: AuditEntry }\n | { kind: \"account\"; data: Account };\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { NPM_PACKAGE } from \"../shared/types.ts\";\nimport type { Config } from \"./config.ts\";\nimport { Crypto } from \"./crypto.ts\";\n\n/**\n * How to run this CLI in printed hints: `npx agentinbox-cli` when run through npx (and in service\n * logs, read by someone who most likely installed that way), else the installed `agentinbox`.\n */\nexport function cliCommand(args: string): string {\n const viaNpx = process.env.npm_command === \"exec\" || /[\\\\/]_npx[\\\\/]/.test(process.argv[1] ?? \"\") || process.env.AGENTINBOX_SERVICE === \"1\";\n return `${viaNpx ? `npx ${NPM_PACKAGE}` : \"agentinbox\"} ${args}`;\n}\n\n// ───────────── Single instance per data directory ─────────────\n\nexport interface InstanceInfo {\n pid: number;\n host: string;\n port: number;\n startedAt: string;\n /** Kernel boot id (Linux), so a lock from before a reboot is never mistaken for a live one. */\n bootId?: string | null;\n /** The process's own start time as the OS reports it, so a reused pid doesn't look alive. */\n processStart?: string | null;\n}\n\nexport const lockPath = (dataDir: string) => join(dataDir, \"agentinbox.lock\");\n\n/** Data dirs locked by this process: a lock naming our own pid is live only if it's in here. */\nconst heldByUs = new Set<string>();\n\nexport function bootId(): string | null {\n try {\n return readFileSync(\"/proc/sys/kernel/random/boot_id\", \"utf8\").trim() || null;\n } catch {\n return null;\n }\n}\n\n/** Process start time: /proc (Linux, in clock ticks since boot) or `ps` elsewhere; null if unknown. */\nexport function processStartTime(pid: number): string | null {\n try {\n const stat = readFileSync(`/proc/${pid}/stat`, \"utf8\");\n // Fields after the parenthesized command name; starttime is field 22, i.e. index 19 here.\n return stat.slice(stat.lastIndexOf(\")\") + 2).split(\" \")[19] ?? null;\n } catch {\n // No /proc (macOS) or no such process.\n }\n if (process.platform === \"linux\" || process.platform === \"win32\") return null;\n try {\n return execFileSync(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(pid)], { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }).trim() || null;\n } catch {\n return null;\n }\n}\n\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * Whether the process a lock names is still the one that wrote it. Our own pid is live only if we\n * hold the lock (in containers every start is pid 1); otherwise the pid must exist, on the same\n * boot, with the same start time (when both are known).\n */\nfunction isLive(info: InstanceInfo, dataDir: string): boolean {\n if (info.pid === process.pid) return heldByUs.has(dataDir);\n if (!isAlive(info.pid)) return false;\n const boot = bootId();\n if (info.bootId && boot && info.bootId !== boot) return false;\n if (info.processStart) {\n const start = processStartTime(info.pid);\n if (start && start !== info.processStart) return false;\n }\n return true;\n}\n\nfunction readLock(dataDir: string): InstanceInfo | null {\n try {\n const info = JSON.parse(readFileSync(lockPath(dataDir), \"utf8\")) as InstanceInfo;\n return Number.isInteger(info.pid) && info.pid > 0 ? info : null;\n } catch {\n return null;\n }\n}\n\n/** The running instance for this data directory, if any. */\nexport function runningInstance(dataDir: string): InstanceInfo | null {\n const info = readLock(dataDir);\n return info && isLive(info, dataDir) ? info : null;\n}\n\nexport interface InstanceLock {\n /** Records the bound port (for `reset-password` and error messages). */\n update(port: number): void;\n release(): void;\n}\n\n/**\n * Takes `<data>/agentinbox.lock`. Two processes on one data directory would both run ingest and\n * deliver every webhook twice, even on different ports. A lock whose process is gone (dead,\n * rebooted, or its pid reused by something else) is replaced.\n */\nexport function acquireInstanceLock(config: Pick<Config, \"dataDir\" | \"host\" | \"port\">): InstanceLock {\n const dataDir = config.dataDir;\n const file = lockPath(dataDir);\n const info: InstanceInfo = {\n pid: process.pid,\n host: config.host,\n port: config.port,\n startedAt: new Date().toISOString(),\n bootId: bootId(),\n processStart: processStartTime(process.pid),\n };\n for (let attempt = 0; ; attempt++) {\n try {\n writeFileSync(file, JSON.stringify(info), { flag: \"wx\", mode: 0o600 });\n break;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt > 0) throw err;\n const other = runningInstance(dataDir);\n if (other) {\n throw new Error(\n `agentinbox is already running with the data directory ${dataDir} (pid ${other.pid}${other.port ? `, port ${other.port}` : \"\"}). ` +\n `Stop it first, or use another --data directory. If no such process exists, delete ${file}.`,\n );\n }\n rmSync(file, { force: true });\n }\n }\n heldByUs.add(dataDir);\n const ours = () => {\n const current = readLock(dataDir);\n return current?.pid === process.pid && current.startedAt === info.startedAt;\n };\n return {\n update(port) {\n info.port = port;\n if (ours()) writeFileSync(file, JSON.stringify(info), { mode: 0o600 });\n },\n release() {\n if (ours()) rmSync(file, { force: true });\n heldByUs.delete(dataDir);\n },\n };\n}\n\n// ───────────── Master key guard ─────────────\n\n/** One ciphertext from the database, or null when nothing encrypted is stored. */\nfunction sampleCiphertext(dbPath: string): string | null {\n if (!existsSync(dbPath)) return null;\n const db = new DatabaseSync(dbPath);\n try {\n const queries = [\n \"SELECT refresh_token_enc AS v FROM accounts WHERE refresh_token_enc LIKE 'v1:%' LIMIT 1\",\n \"SELECT secret_enc AS v FROM webhooks WHERE secret_enc LIKE 'v1:%' LIMIT 1\",\n \"SELECT json_extract(value, '$.clientSecretEnc') AS v FROM settings WHERE key = 'google.client'\",\n ];\n for (const sql of queries) {\n try {\n const v = (db.prepare(sql).get() as { v?: unknown } | undefined)?.v;\n if (typeof v === \"string\" && v.startsWith(\"v1:\")) return v;\n } catch {\n // Table not created yet.\n }\n }\n return null;\n } finally {\n db.close();\n }\n}\n\n/**\n * Refuses to start with a master key that can't read the stored secrets: a missing master.key would\n * otherwise be regenerated, silently making every refresh token and webhook secret unreadable.\n * `envKey` overrides AGENTINBOX_MASTER_KEY (used by `service install` to check the key it will install).\n */\nexport function assertMasterKeyUsable(config: Pick<Config, \"dbPath\" | \"masterKeyPath\">, envKey = process.env.AGENTINBOX_MASTER_KEY): void {\n const sample = sampleCiphertext(config.dbPath);\n if (!sample) return;\n let raw: string | null = envKey || null;\n const source = raw ? \"AGENTINBOX_MASTER_KEY\" : config.masterKeyPath;\n if (!raw && existsSync(config.masterKeyPath)) raw = readFileSync(config.masterKeyPath, \"utf8\").trim();\n if (!raw) {\n throw new Error(\n `${config.dbPath} holds encrypted secrets, but there is no master key: ${config.masterKeyPath} is missing and AGENTINBOX_MASTER_KEY is not set. ` +\n \"Restore master.key from your backup (or set AGENTINBOX_MASTER_KEY to the key used before). \" +\n \"agentinbox won't create a new key, because the stored Google tokens and secrets could never be read again.\",\n );\n }\n try {\n new Crypto(Buffer.from(raw, \"base64\")).decrypt(sample);\n } catch {\n throw new Error(\n `The master key from ${source} can't decrypt the secrets stored in ${config.dbPath}. ` +\n \"Use the key this data directory was created with (master.key from your backup, or the previous AGENTINBOX_MASTER_KEY).\",\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"instance-CpTBwgrK.js","names":[],"sources":["../shared/types.ts","../server/instance.ts"],"sourcesContent":["// Shared contract between the server (server/) and the web console (src/).\n// Everything crossing the HTTP boundary of the console API (/api/*) is typed here.\n// Timestamps are ISO-8601 strings in UTC. IDs are prefixed opaque strings (e.g. \"agt_…\").\n\n/** The npm package (`npx agentinbox-cli`); the command it installs is `agentinbox`. */\nexport const NPM_PACKAGE = \"agentinbox-cli\";\n\n// ───────────────────────────── Domain enums ─────────────────────────────\n\n/** What an agent may do with a mailbox. `read` also grants search, labels, attachments and the event feed. */\nexport const CAPABILITIES = [\"read\", \"draft\", \"send\", \"organize\", \"trash\"] as const;\nexport type Capability = (typeof CAPABILITIES)[number];\n\n/**\n * How `send`/`reply`/`forward` behave for a grant.\n * - direct: sent immediately (still subject to allowlist + rate limits)\n * - draft_only: a Gmail draft is created instead; a human sends it from Gmail\n * - approval: a draft is created and an approval request is queued in the console;\n * approving sends the draft, rejecting deletes it\n */\nexport const SEND_MODES = [\"direct\", \"draft_only\", \"approval\"] as const;\nexport type SendMode = (typeof SEND_MODES)[number];\n\nexport const EVENT_TYPES = [\n \"message.received\", // new message landed in the mailbox (not sent by the account)\n \"message.sent\", // message sent from the account (by an agent or by a human)\n \"thread.reply_received\", // received message in a thread the account previously sent into\n \"label.added\",\n \"label.removed\",\n \"account.auth_failed\", // refresh token revoked/expired; account needs re-auth\n \"sync.gap\", // history was too old; a bounded resync ran and events may be missing\n] as const;\nexport type EventType = (typeof EVENT_TYPES)[number];\n\nexport type AccountStatus = \"active\" | \"needs_reauth\" | \"paused\" | \"error\";\nexport type IngestMode = \"pubsub\" | \"poll\";\nexport type DeliveryStatus = \"pending\" | \"succeeded\" | \"failed\" | \"dead\";\nexport type ApprovalStatus = \"pending\" | \"approved\" | \"rejected\" | \"sent\" | \"failed\" | \"expired\";\nexport type AuditOutcome = \"ok\" | \"denied\" | \"error\";\nexport type ActorType = \"agent\" | \"admin\" | \"system\";\n\n/** OAuth consent configuration the operator chose in Google Cloud. Drives setup guidance only. */\nexport type GoogleAppType = \"internal\" | \"external\";\n\n// ───────────────────────────── Setup / system ─────────────────────────────\n\nexport interface HealthResponse {\n status: \"ok\" | \"degraded\";\n version: string;\n setupComplete: boolean;\n}\n\n/** GET /api/setup/status (unauthenticated). Drives the console's first route decision: claim, login or app. */\nexport interface SetupStatus {\n claimed: boolean;\n /** The request carries a valid admin session. */\n authenticated: boolean;\n}\n\n/** Live state of the ingest pipeline (Pub/Sub pull loop or history polling). */\nexport interface IngestStatus {\n mode: IngestMode;\n running: boolean;\n lastPullAt: string | null;\n lastError: string | null;\n}\n\n/** GET /api/system: facts about this instance for the settings page and connect snippets. */\nexport interface SystemInfo {\n version: string;\n nodeVersion: string;\n dataDir: string;\n /** Running under `agentinbox service install` (systemd/launchd). */\n service: boolean;\n /** URLs this machine is reachable at (localhost first, then LAN addresses). */\n addresses: string[];\n publicUrl: string;\n ingest: IngestStatus;\n}\n\n/** GET /api/system/latest: update notice. */\nexport interface UpdateInfo {\n current: string;\n latest: string | null;\n updateAvailable: boolean;\n /** Shell command that runs (or reinstalls the service with) the latest version using this instance's options. */\n command: string;\n}\n\n/** Ordered setup checklist; the console's setup wizard and overview both render this. */\nexport interface SetupState {\n /** True once an admin password exists. Before that, only /api/setup/claim with the setup token works. */\n claimed: boolean;\n googleClientConfigured: boolean;\n /** Google project id (needed for Pub/Sub). */\n gcpProjectId: string | null;\n googleAppType: GoogleAppType | null;\n pubsub: PubsubState;\n accountsConnected: number;\n agentsCreated: number;\n /** All required steps done (claimed + client configured + ≥1 account). Pub/Sub is optional (poll fallback). */\n complete: boolean;\n}\n\nexport interface PubsubState {\n mode: IngestMode;\n topic: string | null; // projects/{p}/topics/{t}\n subscription: string | null; // projects/{p}/subscriptions/{s}\n /** Last provisioning/health error, human readable, with remediation hint. */\n lastError: string | null;\n /** Last time a Pub/Sub message was pulled (proves the pipeline works end-to-end). */\n lastMessageAt: string | null;\n}\n\nexport interface Settings {\n /** Externally reachable base URL of this instance, e.g. https://mail.example.com. Used in MCP snippets + OAuth redirect. */\n publicUrl: string;\n ingestMode: IngestMode;\n pollIntervalSeconds: number;\n /** Max characters of message body returned to agents per message (0 = unlimited). */\n maxBodyChars: number;\n /** Days to keep events, deliveries and audit entries. */\n retentionDays: number;\n /** Approval requests auto-expire after this many hours. */\n approvalTtlHours: number;\n /**\n * SSRF guard: webhook targets that resolve to private, loopback or link-local addresses are refused\n * unless their host matches one of these entries (hostnames, IPs or CIDRs, e.g. \"127.0.0.0/8\",\n * \"10.0.0.0/8\", \"n8n.internal\"). Defaults to loopback only.\n */\n webhookAllowlist: string[];\n}\n\nexport interface GoogleClientConfig {\n clientId: string;\n /** Write-only: never returned by the API. */\n clientSecret?: string;\n projectId: string;\n appType: GoogleAppType;\n}\n\n/** Returned by GET /api/google/client. */\nexport interface GoogleClientView {\n clientId: string | null;\n hasSecret: boolean;\n projectId: string | null;\n appType: GoogleAppType | null;\n /** The exact redirect URI the operator must register on the OAuth client. */\n redirectUri: string;\n /**\n * \"direct\": Google redirects straight back to this console (localhost or https publicUrl).\n * \"paste\": publicUrl is plain-http non-localhost; Google can't redirect here, so the console\n * asks the user to paste the URL they land on after consent.\n */\n callbackMode: \"direct\" | \"paste\";\n}\n\n// ───────────────────────────── Accounts ─────────────────────────────\n\nexport interface Account {\n id: string; // acc_…\n email: string;\n displayName: string | null;\n status: AccountStatus;\n lastError: string | null;\n historyId: string | null;\n watchExpiresAt: string | null;\n lastSyncAt: string | null;\n createdAt: string;\n}\n\nexport interface GmailLabel {\n id: string; // Gmail label id, e.g. \"INBOX\" or \"Label_123\"\n name: string;\n type: \"system\" | \"user\";\n}\n\n/** POST /api/accounts/connect → start OAuth. */\nexport interface PubsubTestResult {\n ok: boolean;\n message: string;\n}\n\nexport interface ConnectAccountResponse {\n authUrl: string;\n /** Echoed state; in paste mode the console POSTs the pasted URL to /api/accounts/connect/complete. */\n state: string;\n callbackMode: \"direct\" | \"paste\";\n}\n\n// ───────────────────────────── Agents & grants ─────────────────────────────\n\nexport interface Agent {\n id: string; // agt_…\n name: string;\n description: string | null;\n /** First chars of the key, for identification (e.g. \"aib_7Hk2\"). The full key is shown once at creation/rotation. */\n keyPrefix: string;\n disabled: boolean;\n lastUsedAt: string | null;\n createdAt: string;\n grants: Grant[];\n}\n\nexport interface Grant {\n id: string; // grt_…\n agentId: string;\n accountId: string;\n capabilities: Capability[];\n /** Gmail label ids the agent is confined to (thread-level: a thread is visible if any message carries one). null = whole mailbox. */\n labelScope: string[] | null;\n sendMode: SendMode;\n /** Recipient allowlist: exact addresses or \"*@domain.com\" globs. Empty = any recipient. */\n allowTo: string[];\n /** Only allow sending replies inside threads the agent can already see. */\n replyOnly: boolean;\n maxSendsPerHour: number | null;\n maxSendsPerDay: number | null;\n createdAt: string;\n}\n\nexport type GrantInput = Omit<Grant, \"id\" | \"agentId\" | \"createdAt\">;\n\nexport interface CreateAgentInput {\n name: string;\n description?: string | null;\n grants?: GrantInput[];\n}\n\n/** POST /api/agents/:id/test: runs MCP initialize + tools/list as the agent, in-process. */\nexport interface AgentTestResult {\n ok: boolean;\n tools: string[];\n message: string;\n}\n\n/** Returned once on create and on key rotation. */\nexport interface AgentWithKey {\n agent: Agent;\n apiKey: string; // aib_…\n}\n\n// ───────────────────────────── Events ─────────────────────────────\n\nexport interface Address {\n name: string | null;\n email: string;\n}\n\n/** Normalized message metadata carried in events. No bodies are stored server-side. */\nexport interface MessageSummary {\n id: string; // Gmail message id\n threadId: string;\n from: Address | null;\n to: Address[];\n cc: Address[];\n subject: string;\n snippet: string;\n date: string; // ISO\n labelIds: string[];\n hasAttachments: boolean;\n}\n\nexport interface MailEvent {\n id: string; // evt_…\n seq: number; // monotonic, used as cursor\n type: EventType;\n accountId: string;\n accountEmail: string;\n message: MessageSummary | null;\n /** For label.added / label.removed. */\n labelIds?: string[];\n /** Free-form detail for account/sync events. */\n detail?: Record<string, unknown>;\n createdAt: string;\n}\n\n// ───────────────────────────── Webhooks ─────────────────────────────\n\nexport interface WebhookFilter {\n accountIds?: string[];\n /** Match if the message has any of these label ids. */\n labelIds?: string[];\n /** Glob on sender address, e.g. \"*@stripe.com\". Any match passes. */\n from?: string[];\n /** Glob on any to/cc address. */\n to?: string[];\n /** Case-insensitive regular expression tested against the subject. */\n subject?: string;\n hasAttachments?: boolean;\n}\n\nexport interface Webhook {\n id: string; // whk_…\n agentId: string;\n name: string;\n url: string;\n eventTypes: EventType[];\n filter: WebhookFilter;\n /** Include the cleaned message body (markdown) in the payload. */\n includeBody: boolean;\n enabled: boolean;\n /** Shown once at creation and on rotation (\"whsec_…\", Standard Webhooks format). */\n secretPrefix: string;\n createdAt: string;\n stats: { succeeded24h: number; failed24h: number; lastDeliveryAt: string | null };\n}\n\nexport type WebhookInput = Omit<Webhook, \"id\" | \"secretPrefix\" | \"createdAt\" | \"stats\">;\n\nexport interface WebhookWithSecret {\n webhook: Webhook;\n secret: string;\n}\n\nexport interface Delivery {\n id: string; // dlv_…\n webhookId: string;\n eventId: string;\n eventType: EventType;\n status: DeliveryStatus;\n attempts: number;\n nextAttemptAt: string | null;\n lastStatusCode: number | null;\n lastError: string | null;\n /** First ~500 chars of the receiver's response body. */\n lastResponse: string | null;\n durationMs: number | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Webhook HTTP request body. Signed per https://www.standardwebhooks.com:\n * headers webhook-id, webhook-timestamp, webhook-signature (\"v1,<base64 hmac-sha256>\").\n */\nexport interface WebhookPayload {\n type: EventType;\n timestamp: string;\n data: MailEvent & {\n /** Present only when the webhook has includeBody and the event concerns a message. */\n content?: MessageContent;\n /** Set instead of `content` when includeBody was requested but the body could not be fetched. */\n contentError?: string;\n };\n}\n\n// ───────────────────────────── Message content (returned to agents) ─────────────────────────────\n\n/** Signals attached to content. Informational: the content itself is still delivered (cleaned, not censored). */\nexport interface ContentSignals {\n /** Invisible content (display:none, zero-size/transparent text, HTML comments, zero-width/tag unicode) was removed. */\n hiddenContentRemoved: boolean;\n /** Phrases that commonly appear in prompt-injection attempts (\"ignore previous instructions\", …). */\n suspiciousInstructions: string[];\n /** SPF/DKIM/DMARC results parsed from Authentication-Results. null when the header is absent. */\n auth: { spf: string | null; dkim: string | null; dmarc: string | null } | null;\n /** Sender is outside the account's own domain. */\n external: boolean;\n}\n\nexport interface Attachment {\n id: string; // Gmail attachment id\n filename: string;\n mimeType: string;\n size: number;\n}\n\nexport interface MessageContent {\n /** Body converted to markdown (from HTML) or the plain-text part. */\n body: string;\n truncated: boolean;\n attachments: Attachment[];\n signals: ContentSignals;\n}\n\n// ───────────────────────────── Approvals ─────────────────────────────\n\nexport interface Approval {\n id: string; // apr_…\n agentId: string;\n agentName: string;\n accountId: string;\n accountEmail: string;\n kind: \"send\" | \"reply\" | \"forward\";\n status: ApprovalStatus;\n draftId: string;\n to: Address[];\n cc: Address[];\n bcc: Address[];\n subject: string;\n /** Full body as the agent wrote it (markdown/plain). */\n body: string;\n threadId: string | null;\n attachments: { filename: string; size: number }[];\n /** Why the agent says it is sending (optional `reason` tool arg). */\n reason: string | null;\n decidedAt: string | null;\n decisionNote: string | null;\n sentMessageId: string | null;\n error: string | null;\n createdAt: string;\n expiresAt: string;\n}\n\n// ───────────────────────────── Audit ─────────────────────────────\n\nexport interface AuditEntry {\n id: number;\n ts: string;\n actorType: ActorType;\n actorId: string | null;\n actorName: string | null;\n action: string; // MCP tool name or console action, e.g. \"send_message\", \"agent.create\"\n accountId: string | null;\n target: string | null; // message/thread/draft id\n outcome: AuditOutcome;\n /** For denials: the guardrail that blocked it, e.g. \"allow_to\", \"label_scope\", \"rate_limit\". */\n reason: string | null;\n durationMs: number | null;\n detail: Record<string, unknown> | null;\n}\n\n// ───────────────────────────── Overview ─────────────────────────────\n\nexport interface OverviewStats {\n accounts: { total: number; active: number; needsAttention: number };\n agents: { total: number; activeLast24h: number };\n events24h: number;\n deliveries24h: { succeeded: number; failed: number };\n pendingApprovals: number;\n toolCalls24h: number;\n denied24h: number;\n /** Hourly buckets for the last 24h, oldest first. */\n activity: { hour: string; events: number; toolCalls: number }[];\n}\n\n// ───────────────────────────── Pagination & errors ─────────────────────────────\n\nexport interface Page<T> {\n items: T[];\n /** Opaque cursor for the next page; null when exhausted. */\n nextCursor: string | null;\n}\n\n/** All non-2xx console API responses have this body. */\nexport interface ApiError {\n error: { code: string; message: string; hint?: string };\n}\n\n// ───────────────────────────── Console API route map ─────────────────────────────\n// Session: cookie `aib_session` (HttpOnly, SameSite=Lax). Mutating requests must send header\n// `x-agentinbox-csrf: 1` (simple CSRF guard; the console fetch wrapper always sets it).\n//\n// Unauthenticated:\n// GET /api/health → HealthResponse\n// GET /api/setup/status → SetupStatus\n// POST /api/setup/claim {token,password} → 204 + session cookie (only while unclaimed)\n// POST /api/auth/login {password} → 204 + session cookie (5 failures/IP → 15 min lockout)\n// POST /api/auth/logout → 204\n// GET /oauth/callback?code&state → HTML (redirects to console /accounts?connected=…)\n//\n// Authenticated (admin session):\n// GET /api/setup/state → SetupState\n// GET /api/system → SystemInfo\n// GET /api/system/latest → UpdateInfo (npm registry check, cached 30 min)\n// GET /api/settings → Settings\n// PATCH /api/settings Partial<Settings> → Settings\n// POST /api/auth/password {current,next} → 204\n// GET /api/google/client → GoogleClientView\n// PUT /api/google/client GoogleClientConfig → GoogleClientView\n// POST /api/google/pubsub/provision → PubsubState (creates topic + IAM + subscription using a connected account's token)\n// POST /api/google/pubsub/test → PubsubTestResult\n// GET /api/accounts → Account[]\n// POST /api/accounts/connect → ConnectAccountResponse\n// POST /api/accounts/connect/complete {url} → Account (paste mode)\n// POST /api/accounts/:id/resync → Account\n// POST /api/accounts/:id/pause | /resume → Account\n// DELETE /api/accounts/:id → 204 (stops watch, revokes token, deletes grants)\n// GET /api/accounts/:id/labels → GmailLabel[]\n// GET /api/agents → Agent[]\n// POST /api/agents CreateAgentInput → AgentWithKey\n// GET /api/agents/:id → Agent\n// PATCH /api/agents/:id {name?,description?,disabled?} → Agent\n// POST /api/agents/:id/rotate-key → AgentWithKey\n// DELETE /api/agents/:id → 204\n// PUT /api/agents/:id/grants/:accountId GrantInput → Grant (upsert; one grant per agent×account)\n// DELETE /api/agents/:id/grants/:accountId → 204\n// POST /api/agents/:id/test → AgentTestResult (runs MCP initialize+tools/list as the agent)\n// GET /api/webhooks?agentId → Webhook[]\n// POST /api/webhooks WebhookInput → WebhookWithSecret\n// PATCH /api/webhooks/:id Partial<WebhookInput> → Webhook\n// POST /api/webhooks/:id/rotate-secret → WebhookWithSecret\n// POST /api/webhooks/:id/test → Delivery (sends a synthetic event synchronously)\n// DELETE /api/webhooks/:id → 204\n// GET /api/deliveries?webhookId&status&cursor → Page<Delivery>\n// POST /api/deliveries/:id/replay → Delivery\n// GET /api/events?accountId&type&cursor → Page<MailEvent>\n// GET /api/events/stream → text/event-stream of ConsoleStreamMessage (SSE `data:` = JSON)\n// GET /api/approvals?status → Approval[]\n// POST /api/approvals/:id/approve {note?} → Approval\n// POST /api/approvals/:id/reject {note?} → Approval\n// GET /api/audit?agentId&outcome&cursor → Page<AuditEntry>\n// GET /api/overview → OverviewStats\n//\n// Agent-facing (bearer = agent API key):\n// POST/GET/DELETE /mcp MCP streamable HTTP (stateless)\n// GET /skill.md the agent skill (public, no secrets)\n// GET /health HealthResponse (alias of /api/health for uptime monitors; 503 when degraded)\n\nexport type ConsoleStreamMessage =\n | { kind: \"event\"; data: MailEvent }\n | { kind: \"delivery\"; data: Delivery }\n | { kind: \"approval\"; data: Approval }\n | { kind: \"audit\"; data: AuditEntry }\n | { kind: \"account\"; data: Account };\n","import { execFileSync } from \"node:child_process\";\nimport { existsSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { NPM_PACKAGE } from \"../shared/types.ts\";\nimport type { Config } from \"./config.ts\";\nimport { Crypto } from \"./crypto.ts\";\n\n/**\n * How to run this CLI in printed hints: `npx agentinbox-cli` when run through npx (and in service\n * logs, read by someone who most likely installed that way), else the installed `agentinbox`.\n */\nexport function cliCommand(args: string): string {\n const viaNpx = process.env.npm_command === \"exec\" || /[\\\\/]_npx[\\\\/]/.test(process.argv[1] ?? \"\") || process.env.AGENTINBOX_SERVICE === \"1\";\n return `${viaNpx ? `npx ${NPM_PACKAGE}` : \"agentinbox\"} ${args}`;\n}\n\n// ───────────── Single instance per data directory ─────────────\n\nexport interface InstanceInfo {\n pid: number;\n host: string;\n port: number;\n startedAt: string;\n /** Kernel boot id (Linux), so a lock from before a reboot is never mistaken for a live one. */\n bootId?: string | null;\n /** The process's own start time as the OS reports it, so a reused pid doesn't look alive. */\n processStart?: string | null;\n /** Started by the background service (AGENTINBOX_SERVICE=1). */\n service?: boolean;\n}\n\nexport const lockPath = (dataDir: string) => join(dataDir, \"agentinbox.lock\");\n\n/** Data dirs locked by this process: a lock naming our own pid is live only if it's in here. */\nconst heldByUs = new Set<string>();\n\nexport function bootId(): string | null {\n try {\n return readFileSync(\"/proc/sys/kernel/random/boot_id\", \"utf8\").trim() || null;\n } catch {\n return null;\n }\n}\n\n/** Process start time: /proc (Linux, in clock ticks since boot) or `ps` elsewhere; null if unknown. */\nexport function processStartTime(pid: number): string | null {\n try {\n const stat = readFileSync(`/proc/${pid}/stat`, \"utf8\");\n // Fields after the parenthesized command name; starttime is field 22, i.e. index 19 here.\n return stat.slice(stat.lastIndexOf(\")\") + 2).split(\" \")[19] ?? null;\n } catch {\n // No /proc (macOS) or no such process.\n }\n if (process.platform === \"linux\" || process.platform === \"win32\") return null;\n try {\n return execFileSync(\"ps\", [\"-o\", \"lstart=\", \"-p\", String(pid)], { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] }).trim() || null;\n } catch {\n return null;\n }\n}\n\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * Whether the process a lock names is still the one that wrote it. Our own pid is live only if we\n * hold the lock (in containers every start is pid 1); otherwise the pid must exist, on the same\n * boot, with the same start time (when both are known).\n */\nfunction isLive(info: InstanceInfo, dataDir: string): boolean {\n if (info.pid === process.pid) return heldByUs.has(dataDir);\n if (!isAlive(info.pid)) return false;\n const boot = bootId();\n if (info.bootId && boot && info.bootId !== boot) return false;\n if (info.processStart) {\n const start = processStartTime(info.pid);\n if (start && start !== info.processStart) return false;\n }\n return true;\n}\n\nfunction readLock(dataDir: string): InstanceInfo | null {\n try {\n const info = JSON.parse(readFileSync(lockPath(dataDir), \"utf8\")) as InstanceInfo;\n return Number.isInteger(info.pid) && info.pid > 0 ? info : null;\n } catch {\n return null;\n }\n}\n\n/** The running instance for this data directory, if any. */\nexport function runningInstance(dataDir: string): InstanceInfo | null {\n const info = readLock(dataDir);\n return info && isLive(info, dataDir) ? info : null;\n}\n\nexport interface InstanceLock {\n /** Records the bound port (for `reset-password` and error messages). */\n update(port: number): void;\n release(): void;\n}\n\n/**\n * Takes `<data>/agentinbox.lock`. Two processes on one data directory would both run ingest and\n * deliver every webhook twice, even on different ports. A lock whose process is gone (dead,\n * rebooted, or its pid reused by something else) is replaced.\n */\nexport function acquireInstanceLock(config: Pick<Config, \"dataDir\" | \"host\" | \"port\">): InstanceLock {\n const dataDir = config.dataDir;\n const file = lockPath(dataDir);\n const info: InstanceInfo = {\n pid: process.pid,\n host: config.host,\n port: config.port,\n startedAt: new Date().toISOString(),\n bootId: bootId(),\n processStart: processStartTime(process.pid),\n service: process.env.AGENTINBOX_SERVICE === \"1\",\n };\n for (let attempt = 0; ; attempt++) {\n try {\n writeFileSync(file, JSON.stringify(info), { flag: \"wx\", mode: 0o600 });\n break;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt > 0) throw err;\n const other = runningInstance(dataDir);\n if (other) {\n const where = `(pid ${other.pid}${other.port ? `, port ${other.port}` : \"\"})`;\n throw new Error(\n other.service\n ? `agentinbox is already running as a background service with the data directory ${dataDir} ${where}. ` +\n `Use that one, stop it (\\`${cliCommand(\"service uninstall\")}\\` keeps your data), or use another --data directory.`\n : `agentinbox is already running with the data directory ${dataDir} ${where}. ` +\n `Stop it first, or use another --data directory. If no such process exists, delete ${file}.`,\n );\n }\n rmSync(file, { force: true });\n }\n }\n heldByUs.add(dataDir);\n const ours = () => {\n const current = readLock(dataDir);\n return current?.pid === process.pid && current.startedAt === info.startedAt;\n };\n return {\n update(port) {\n info.port = port;\n if (ours()) writeFileSync(file, JSON.stringify(info), { mode: 0o600 });\n },\n release() {\n if (ours()) rmSync(file, { force: true });\n heldByUs.delete(dataDir);\n },\n };\n}\n\n// ───────────── Master key guard ─────────────\n\n/** One ciphertext from the database, or null when nothing encrypted is stored. */\nfunction sampleCiphertext(dbPath: string): string | null {\n if (!existsSync(dbPath)) return null;\n const db = new DatabaseSync(dbPath);\n try {\n const queries = [\n \"SELECT refresh_token_enc AS v FROM accounts WHERE refresh_token_enc LIKE 'v1:%' LIMIT 1\",\n \"SELECT secret_enc AS v FROM webhooks WHERE secret_enc LIKE 'v1:%' LIMIT 1\",\n \"SELECT json_extract(value, '$.clientSecretEnc') AS v FROM settings WHERE key = 'google.client'\",\n ];\n for (const sql of queries) {\n try {\n const v = (db.prepare(sql).get() as { v?: unknown } | undefined)?.v;\n if (typeof v === \"string\" && v.startsWith(\"v1:\")) return v;\n } catch {\n // Table not created yet.\n }\n }\n return null;\n } finally {\n db.close();\n }\n}\n\n/**\n * Refuses to start with a master key that can't read the stored secrets: a missing master.key would\n * otherwise be regenerated, silently making every refresh token and webhook secret unreadable.\n * `envKey` overrides AGENTINBOX_MASTER_KEY (used by `service install` to check the key it will install).\n */\nexport function assertMasterKeyUsable(config: Pick<Config, \"dbPath\" | \"masterKeyPath\">, envKey = process.env.AGENTINBOX_MASTER_KEY): void {\n const sample = sampleCiphertext(config.dbPath);\n if (!sample) return;\n let raw: string | null = envKey || null;\n const source = raw ? \"AGENTINBOX_MASTER_KEY\" : config.masterKeyPath;\n if (!raw && existsSync(config.masterKeyPath)) raw = readFileSync(config.masterKeyPath, \"utf8\").trim();\n if (!raw) {\n throw new Error(\n `${config.dbPath} holds encrypted secrets, but there is no master key: ${config.masterKeyPath} is missing and AGENTINBOX_MASTER_KEY is not set. ` +\n \"Restore master.key from your backup (or set AGENTINBOX_MASTER_KEY to the key used before). \" +\n \"agentinbox won't create a new key, because the stored Google tokens and secrets could never be read again.\",\n );\n }\n try {\n new Crypto(Buffer.from(raw, \"base64\")).decrypt(sample);\n } catch {\n throw new Error(\n `The master key from ${source} can't decrypt the secrets stored in ${config.dbPath}. ` +\n \"Use the key this data directory was created with (master.key from your backup, or the previous AGENTINBOX_MASTER_KEY).\",\n );\n }\n}\n"],"mappings":"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"}
|