agentinbox-cli 0.0.0 → 0.1.0-nightly.1
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/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/audit-CQe-_dPQ.js +2 -0
- package/dist/audit-DETqygzG.js +65 -0
- package/dist/audit-DETqygzG.js.map +1 -0
- package/dist/auth-BniddcRD.js +447 -0
- package/dist/auth-BniddcRD.js.map +1 -0
- package/dist/auth-C2v7bEmQ.js +2 -0
- package/dist/bus-Cz0flS37.js +12 -0
- package/dist/bus-Cz0flS37.js.map +1 -0
- package/dist/bus-OKIGut72.js +2 -0
- package/dist/cli.js +141 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-CIvgMqLx.js +2 -0
- package/dist/config-v975gxv4.js +44 -0
- package/dist/config-v975gxv4.js.map +1 -0
- package/dist/crypto-DYZ4R7c2.js +74 -0
- package/dist/crypto-DYZ4R7c2.js.map +1 -0
- package/dist/db-CfoMO5zW.js +2 -0
- package/dist/db-DdagFmUY.js +247 -0
- package/dist/db-DdagFmUY.js.map +1 -0
- package/dist/errors-CJLUPllS.js +38 -0
- package/dist/errors-CJLUPllS.js.map +1 -0
- package/dist/instance-DZMV78rI.js +2 -0
- package/dist/instance-Dy2jeyZc.js +174 -0
- package/dist/instance-Dy2jeyZc.js.map +1 -0
- package/dist/server-CWqKeCGn.js +981 -0
- package/dist/server-CWqKeCGn.js.map +1 -0
- package/dist/service-63pdSmhO.js +332 -0
- package/dist/service-63pdSmhO.js.map +1 -0
- package/dist/settings-Dq2ETFsd.js +98 -0
- package/dist/settings-Dq2ETFsd.js.map +1 -0
- package/dist/settings-qtn5WTBM.js +2 -0
- package/dist/setup-token-Bkwk84l7.js +47 -0
- package/dist/setup-token-Bkwk84l7.js.map +1 -0
- package/dist/skill/SKILL.md +226 -0
- package/dist/skill-BWWtinsE.js +2 -0
- package/dist/skill-WKbeyNct.js +43 -0
- package/dist/skill-WKbeyNct.js.map +1 -0
- package/dist/urls-CNYhbeUQ.js +2 -0
- package/dist/urls-DmnonkJo.js +22 -0
- package/dist/urls-DmnonkJo.js.map +1 -0
- package/dist/web/assets/_agentId-il4y7OCG.js +1 -0
- package/dist/web/assets/_app-BIO8WIYB.js +1 -0
- package/dist/web/assets/_app-wlLL4MrD.js +63 -0
- package/dist/web/assets/_webhookId-CiwDrkY4.js +1 -0
- package/dist/web/assets/account-status-at9Bv3_T.js +1 -0
- package/dist/web/assets/accounts-B4rfPwCV.js +1 -0
- package/dist/web/assets/accounts-list-e--JWxHH.js +1 -0
- package/dist/web/assets/activity-zrckFhPk.js +1 -0
- package/dist/web/assets/agent-create-form-DaWPR6kx.js +1 -0
- package/dist/web/assets/agents-D4gPq4dt.js +1 -0
- package/dist/web/assets/approvals-DaoZ6wuf.js +2 -0
- package/dist/web/assets/arrow-left-DVJV4Zx7.js +1 -0
- package/dist/web/assets/arrow-right-4PFX6MSS.js +1 -0
- package/dist/web/assets/audit-bAdLe-px.js +1 -0
- package/dist/web/assets/audit-bits-DQHo74i6.js +1 -0
- package/dist/web/assets/bot-DVw15lBn.js +1 -0
- package/dist/web/assets/card-EQTpqntn.js +1 -0
- package/dist/web/assets/checkbox-DDZZurgK.js +1 -0
- package/dist/web/assets/chevron-down-892ylB4R.js +1 -0
- package/dist/web/assets/chevron-right-CbBKS6-T.js +1 -0
- package/dist/web/assets/chips-input-Btdc_f7b.js +1 -0
- package/dist/web/assets/circle-x-Bi4IGwky.js +1 -0
- package/dist/web/assets/common-h6596gEC.js +1 -0
- package/dist/web/assets/connect-host-Co31qIYr.js +1 -0
- package/dist/web/assets/data-table-DsKrM62X.js +1 -0
- package/dist/web/assets/dialog-CgZr27yw.js +1 -0
- package/dist/web/assets/dist-B0e3Ju3r.js +1 -0
- package/dist/web/assets/empty-CE_WZGEC.js +1 -0
- package/dist/web/assets/es2015-DPaldoxG.js +41 -0
- package/dist/web/assets/event-row-C0qRfWuV.js +1 -0
- package/dist/web/assets/events-Svyij7mp.js +1 -0
- package/dist/web/assets/eye-off-DjpaihHf.js +1 -0
- package/dist/web/assets/field-CcyMn3ey.js +1 -0
- package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/web/assets/get-started-Cxfw97sJ.js +1 -0
- package/dist/web/assets/grants-BilXCDps.js +1 -0
- package/dist/web/assets/index-DgIgw66p.css +2 -0
- package/dist/web/assets/index-TbxC5ZgP.js +10 -0
- package/dist/web/assets/input-CMhmmHiH.js +1 -0
- package/dist/web/assets/input-group-DRs3TNaT.js +1 -0
- package/dist/web/assets/key-round-CVIZRCLM.js +1 -0
- package/dist/web/assets/labels-C_Pwium9.js +1 -0
- package/dist/web/assets/login-DUJia7lg.js +1 -0
- package/dist/web/assets/mail-CAVmuESo.js +1 -0
- package/dist/web/assets/mutations-D_GluJ0j.js +1 -0
- package/dist/web/assets/paperclip-QFjM4y6n.js +1 -0
- package/dist/web/assets/password-input-_m9Ycphv.js +1 -0
- package/dist/web/assets/pencil-Bf1JhsG0.js +1 -0
- package/dist/web/assets/play-DiPk49JH.js +1 -0
- package/dist/web/assets/plus-CETmK0SB.js +1 -0
- package/dist/web/assets/preload-helper-BFuRUfdt.js +1 -0
- package/dist/web/assets/progress-3ZOsbLSK.js +6 -0
- package/dist/web/assets/queries-a3M5_loa.js +2 -0
- package/dist/web/assets/refresh-cw-DOn9kD7g.js +1 -0
- package/dist/web/assets/search-gcYjitzV.js +1 -0
- package/dist/web/assets/select-_tULk-vs.js +1 -0
- package/dist/web/assets/send-BhQwhuJo.js +1 -0
- package/dist/web/assets/settings-Dl4wb4f0.js +1 -0
- package/dist/web/assets/settings-form-D21eKBh-.js +1 -0
- package/dist/web/assets/setup-DYnNhnfH.js +1 -0
- package/dist/web/assets/sheet--2lKU0jN.js +1 -0
- package/dist/web/assets/shield-alert-CbFgd06X.js +1 -0
- package/dist/web/assets/stamp-CH2OvjH2.js +1 -0
- package/dist/web/assets/stream-6OwrgVZw.js +1 -0
- package/dist/web/assets/tabs-CiAUEj_y.js +1 -0
- package/dist/web/assets/tag-CP72Kk6Y.js +1 -0
- package/dist/web/assets/test-connection-F2tMowbv.js +1 -0
- package/dist/web/assets/textarea-CCokBZIw.js +1 -0
- package/dist/web/assets/toggle-group-D1pC7sJr.js +1 -0
- package/dist/web/assets/trash-DxdMNg8B.js +1 -0
- package/dist/web/assets/use-sign-out-BuzlTb0Q.js +1 -0
- package/dist/web/assets/useInfiniteQuery-DE6WChlb.js +1 -0
- package/dist/web/assets/useMatch-BKUnm54_.js +1 -0
- package/dist/web/assets/useMutation-DGl4PaZ1.js +1 -0
- package/dist/web/assets/useQuery-DAhf1YDH.js +1 -0
- package/dist/web/assets/useSelector-D70eKNYs.js +1 -0
- package/dist/web/assets/webhook-BWT75M78.js +1 -0
- package/dist/web/assets/webhook-sheet-4U_6Rw8I.js +34 -0
- package/dist/web/assets/webhooks-C1JvTI88.js +1 -0
- package/dist/web/assets/with-selector-CkNyXYMW.js +1 -0
- package/dist/web/assets/x-BHjDLXNO.js +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +31 -0
- package/package.json +94 -1
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as Db } from "./db-DdagFmUY.js";
|
|
3
|
+
import { t as AuditLog } from "./audit-DETqygzG.js";
|
|
4
|
+
import { a as errorMessage, i as conflict, n as PolicyDenied, r as badRequest, t as HttpError } from "./errors-CJLUPllS.js";
|
|
5
|
+
import { a as verifyPasswordAsync, n as hashPasswordAsync, r as safeEqual, t as Crypto } from "./crypto-DYZ4R7c2.js";
|
|
6
|
+
import { a as adminAuth, c as csrfGuard, d as hasSession, f as isClaimed, g as requireAdmin, h as rejection, l as currentSetupToken, m as lockedOut, n as MAX_PASSWORD_LENGTH, o as checkSecret, s as clientIp, u as endSession, v as sessionToken, y as startSession } from "./auth-BniddcRD.js";
|
|
7
|
+
import { a as setupTokenPath, r as readSetupToken, t as deleteSetupToken } from "./setup-token-Bkwk84l7.js";
|
|
8
|
+
import { t as createBus } from "./bus-Cz0flS37.js";
|
|
9
|
+
import { n as loadConfig } from "./config-v975gxv4.js";
|
|
10
|
+
import { c as NPM_PACKAGE, i as cliCommand, n as assertMasterKeyUsable, t as acquireInstanceLock } from "./instance-Dy2jeyZc.js";
|
|
11
|
+
import { r as loopbackUrl, t as consoleUrls } from "./urls-DmnonkJo.js";
|
|
12
|
+
import { i as renderSkill } from "./skill-WKbeyNct.js";
|
|
13
|
+
import { t as SettingsStore } from "./settings-Dq2ETFsd.js";
|
|
14
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { extname, join, resolve, sep } from "node:path";
|
|
17
|
+
import pc from "picocolors";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { serve } from "@hono/node-server";
|
|
20
|
+
import open from "open";
|
|
21
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
22
|
+
import { Hono } from "hono";
|
|
23
|
+
import { bodyLimit } from "hono/body-limit";
|
|
24
|
+
import { HTTPException } from "hono/http-exception";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
//#region server/api/util.ts
|
|
27
|
+
/**
|
|
28
|
+
* Console API routers are `(ctx) => Hono<AppEnv>` factories with paths relative to /api
|
|
29
|
+
* (e.g. "/accounts/:id"). server/app.ts mounts them behind the admin auth middleware and turns
|
|
30
|
+
* thrown HttpError / PolicyDenied into ApiError JSON responses.
|
|
31
|
+
*/
|
|
32
|
+
const router = () => new Hono();
|
|
33
|
+
/** Parses and validates a JSON body; throws HttpError(400) with a readable message. */
|
|
34
|
+
async function readBody(c, schema) {
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
const text = await c.req.text();
|
|
38
|
+
raw = text ? JSON.parse(text) : {};
|
|
39
|
+
} catch {
|
|
40
|
+
throw badRequest("Request body must be valid JSON");
|
|
41
|
+
}
|
|
42
|
+
const parsed = schema.safeParse(raw);
|
|
43
|
+
if (!parsed.success) throw badRequest(z.prettifyError(parsed.error));
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
/** Audit fields for console (admin) actions: spread into ctx.audit.record. */
|
|
47
|
+
const ADMIN_ACTOR = {
|
|
48
|
+
actorType: "admin",
|
|
49
|
+
actorId: null
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region server/api/accounts.ts
|
|
53
|
+
function accountsApi(_ctx) {
|
|
54
|
+
return router();
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region server/api/agents.ts
|
|
58
|
+
function agentsApi(_ctx) {
|
|
59
|
+
return router();
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region server/api/approvals.ts
|
|
63
|
+
function approvalsApi(_ctx) {
|
|
64
|
+
return router();
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region server/api/audit.ts
|
|
68
|
+
function auditApi(_ctx) {
|
|
69
|
+
return router();
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region server/api/setup.ts
|
|
73
|
+
const ClaimBody = z.object({
|
|
74
|
+
token: z.string().trim().min(1, "Setup token is required").max(200),
|
|
75
|
+
password: z.string().max(MAX_PASSWORD_LENGTH, `Password must be at most ${MAX_PASSWORD_LENGTH} characters`)
|
|
76
|
+
});
|
|
77
|
+
/** Unauthenticated: first-run status and the one-time claim. Mounted before the admin guard. */
|
|
78
|
+
function setupPublicApi(ctx) {
|
|
79
|
+
const auth = adminAuth(ctx);
|
|
80
|
+
return router().get("/setup/status", (c) => {
|
|
81
|
+
c.header("Cache-Control", "no-store");
|
|
82
|
+
return c.json({
|
|
83
|
+
claimed: isClaimed(ctx),
|
|
84
|
+
authenticated: hasSession(c, ctx)
|
|
85
|
+
});
|
|
86
|
+
}).post("/setup/claim", async (c) => {
|
|
87
|
+
const ip = clientIp(c, ctx.config.trustProxy);
|
|
88
|
+
const banned = auth.limiter.bannedMinutes(ip);
|
|
89
|
+
if (banned) return lockedOut(c, banned);
|
|
90
|
+
const body = await readBody(c, ClaimBody);
|
|
91
|
+
if (isClaimed(ctx)) throw conflict("This instance is already claimed", "Sign in with the admin password instead.");
|
|
92
|
+
const expected = readSetupToken(ctx.config.dataDir);
|
|
93
|
+
if (!expected) throw new HttpError(403, "invalid_token", "The setup link has expired", `Restart agentinbox or run \`${cliCommand("reset-password")}\` for a new link.`);
|
|
94
|
+
const check = await checkSecret(ctx, ip, `claim:${body.token}`, () => safeEqual(body.token, expected), { global: false });
|
|
95
|
+
if (!check.ok) {
|
|
96
|
+
const rejected = rejection(c, check);
|
|
97
|
+
if (rejected) return rejected;
|
|
98
|
+
ctx.audit.record({
|
|
99
|
+
actorType: "admin",
|
|
100
|
+
action: "admin.claim",
|
|
101
|
+
outcome: "denied",
|
|
102
|
+
reason: check.nowBanned ? "lockout" : "invalid_token",
|
|
103
|
+
detail: { ip }
|
|
104
|
+
});
|
|
105
|
+
throw new HttpError(403, "invalid_token", "Invalid setup token", "Use the exact setup link printed by agentinbox on startup.");
|
|
106
|
+
}
|
|
107
|
+
checkPassword(body.password);
|
|
108
|
+
const hash = await hashPasswordAsync(body.password);
|
|
109
|
+
if (isClaimed(ctx)) throw conflict("This instance is already claimed", "Sign in with the admin password instead.");
|
|
110
|
+
ctx.settings.setState("admin.passwordHash", hash);
|
|
111
|
+
deleteSetupToken(ctx.config.dataDir);
|
|
112
|
+
auth.limiter.reset();
|
|
113
|
+
startSession(c, ctx);
|
|
114
|
+
ctx.audit.record({
|
|
115
|
+
actorType: "admin",
|
|
116
|
+
action: "admin.claim",
|
|
117
|
+
outcome: "ok",
|
|
118
|
+
detail: { ip }
|
|
119
|
+
});
|
|
120
|
+
return c.body(null, 204);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Authenticated: the setup checklist. */
|
|
124
|
+
function setupApi(ctx) {
|
|
125
|
+
return router().get("/setup/state", (c) => c.json(setupState(ctx)));
|
|
126
|
+
}
|
|
127
|
+
function checkPassword(password) {
|
|
128
|
+
if (password.length < 10) throw badRequest(`Password must be at least 10 characters`);
|
|
129
|
+
if (password.length > 1024) throw badRequest(`Password must be at most ${MAX_PASSWORD_LENGTH} characters`);
|
|
130
|
+
}
|
|
131
|
+
/** Composes the setup checklist. Reads the stored state directly if a service is unavailable. */
|
|
132
|
+
function setupState(ctx) {
|
|
133
|
+
const claimed = isClaimed(ctx);
|
|
134
|
+
const client = attempt(ctx, () => ctx.google.clientView()) ?? storedClientView(ctx);
|
|
135
|
+
const pubsub = attempt(ctx, () => ctx.google.pubsubState()) ?? storedPubsub(ctx);
|
|
136
|
+
const count = (table) => ctx.db.get(`SELECT COUNT(*) AS n FROM ${table}`)?.n ?? 0;
|
|
137
|
+
const accountsConnected = count("accounts");
|
|
138
|
+
const googleClientConfigured = !!client.clientId && client.hasSecret;
|
|
139
|
+
return {
|
|
140
|
+
claimed,
|
|
141
|
+
googleClientConfigured,
|
|
142
|
+
gcpProjectId: client.projectId,
|
|
143
|
+
googleAppType: client.appType,
|
|
144
|
+
pubsub,
|
|
145
|
+
accountsConnected,
|
|
146
|
+
agentsCreated: count("agents"),
|
|
147
|
+
complete: claimed && googleClientConfigured && accountsConnected > 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function attempt(ctx, fn) {
|
|
151
|
+
try {
|
|
152
|
+
return fn();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
ctx.log.debug("setup state: falling back to stored state", { error: err instanceof Error ? err.message : String(err) });
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function storedClientView(ctx) {
|
|
159
|
+
const s = ctx.settings.getState("google.client");
|
|
160
|
+
return {
|
|
161
|
+
clientId: s?.clientId || null,
|
|
162
|
+
hasSecret: !!s?.clientSecretEnc,
|
|
163
|
+
projectId: s?.projectId || null,
|
|
164
|
+
appType: s?.appType ?? null
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function storedPubsub(ctx) {
|
|
168
|
+
const s = ctx.settings.getState("pubsub");
|
|
169
|
+
return {
|
|
170
|
+
mode: ctx.settings.get().ingestMode,
|
|
171
|
+
topic: s?.topic ?? null,
|
|
172
|
+
subscription: s?.subscription ?? null,
|
|
173
|
+
lastError: s?.lastError ?? null,
|
|
174
|
+
lastMessageAt: s?.lastMessageAt ?? null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region server/api/auth.ts
|
|
179
|
+
const password = (what) => z.string().max(MAX_PASSWORD_LENGTH, `${what} must be at most ${MAX_PASSWORD_LENGTH} characters`);
|
|
180
|
+
const LoginBody = z.object({ password: password("Password").min(1, "Password is required") });
|
|
181
|
+
const PasswordBody = z.object({
|
|
182
|
+
current: password("Current password"),
|
|
183
|
+
next: password("New password")
|
|
184
|
+
});
|
|
185
|
+
/** Unauthenticated: sign in and out. Mounted before the admin guard. */
|
|
186
|
+
function authPublicApi(ctx) {
|
|
187
|
+
const auth = adminAuth(ctx);
|
|
188
|
+
return router().post("/auth/login", async (c) => {
|
|
189
|
+
const ip = clientIp(c, ctx.config.trustProxy);
|
|
190
|
+
const banned = auth.limiter.bannedMinutes(ip);
|
|
191
|
+
if (banned) return lockedOut(c, banned);
|
|
192
|
+
const body = await readBody(c, LoginBody);
|
|
193
|
+
const hash = ctx.settings.getState("admin.passwordHash");
|
|
194
|
+
if (!hash) throw conflict("This instance has not been claimed yet", "Open the setup link printed by agentinbox on startup.");
|
|
195
|
+
const check = await checkSecret(ctx, ip, body.password, () => verifyPasswordAsync(body.password, hash));
|
|
196
|
+
if (!check.ok) {
|
|
197
|
+
const rejected = rejection(c, check);
|
|
198
|
+
if (rejected) return rejected;
|
|
199
|
+
ctx.audit.record({
|
|
200
|
+
...ADMIN_ACTOR,
|
|
201
|
+
action: "admin.login",
|
|
202
|
+
outcome: "denied",
|
|
203
|
+
reason: check.nowBanned ? "lockout" : "bad_password",
|
|
204
|
+
detail: { ip }
|
|
205
|
+
});
|
|
206
|
+
throw new HttpError(401, "invalid_password", "Wrong password");
|
|
207
|
+
}
|
|
208
|
+
startSession(c, ctx);
|
|
209
|
+
ctx.audit.record({
|
|
210
|
+
...ADMIN_ACTOR,
|
|
211
|
+
action: "admin.login",
|
|
212
|
+
outcome: "ok",
|
|
213
|
+
detail: { ip }
|
|
214
|
+
});
|
|
215
|
+
return c.body(null, 204);
|
|
216
|
+
}).post("/auth/logout", (c) => {
|
|
217
|
+
endSession(c, ctx);
|
|
218
|
+
return c.body(null, 204);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Authenticated: change the admin password. Revokes every other session. */
|
|
222
|
+
function authApi(ctx) {
|
|
223
|
+
const auth = adminAuth(ctx);
|
|
224
|
+
return router().post("/auth/password", async (c) => {
|
|
225
|
+
const ip = clientIp(c, ctx.config.trustProxy);
|
|
226
|
+
const banned = auth.limiter.bannedMinutes(ip);
|
|
227
|
+
if (banned) return lockedOut(c, banned);
|
|
228
|
+
const body = await readBody(c, PasswordBody);
|
|
229
|
+
const hash = ctx.settings.getState("admin.passwordHash");
|
|
230
|
+
const check = await checkSecret(ctx, ip, body.current, async () => !!hash && await verifyPasswordAsync(body.current, hash));
|
|
231
|
+
if (!check.ok) {
|
|
232
|
+
const rejected = rejection(c, check);
|
|
233
|
+
if (rejected) return rejected;
|
|
234
|
+
ctx.audit.record({
|
|
235
|
+
...ADMIN_ACTOR,
|
|
236
|
+
action: "admin.password_change",
|
|
237
|
+
outcome: "denied",
|
|
238
|
+
reason: check.nowBanned ? "lockout" : "bad_password",
|
|
239
|
+
detail: { ip }
|
|
240
|
+
});
|
|
241
|
+
throw new HttpError(400, "invalid_password", "Current password is wrong");
|
|
242
|
+
}
|
|
243
|
+
checkPassword(body.next);
|
|
244
|
+
ctx.settings.setState("admin.passwordHash", await hashPasswordAsync(body.next));
|
|
245
|
+
const revoked = auth.sessions.deleteAll(sessionToken(c));
|
|
246
|
+
auth.limiter.reset();
|
|
247
|
+
ctx.audit.record({
|
|
248
|
+
...ADMIN_ACTOR,
|
|
249
|
+
action: "admin.password_change",
|
|
250
|
+
outcome: "ok",
|
|
251
|
+
detail: {
|
|
252
|
+
ip,
|
|
253
|
+
revokedSessions: revoked
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return c.body(null, 204);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region server/api/events.ts
|
|
261
|
+
function eventsApi(_ctx) {
|
|
262
|
+
return router();
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region server/api/google.ts
|
|
266
|
+
function googleApi(_ctx) {
|
|
267
|
+
return router();
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region server/api/oauth.ts
|
|
271
|
+
function oauthRoutes(_ctx) {
|
|
272
|
+
return router();
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region server/api/overview.ts
|
|
276
|
+
function overviewApi(_ctx) {
|
|
277
|
+
return router();
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region server/api/settings.ts
|
|
281
|
+
/** Shape check only; ranges and URL rules are enforced by SettingsStore.update. */
|
|
282
|
+
const SettingsPatch = z.object({
|
|
283
|
+
publicUrl: z.string().trim(),
|
|
284
|
+
ingestMode: z.enum(["pubsub", "poll"]),
|
|
285
|
+
pollIntervalSeconds: z.number(),
|
|
286
|
+
maxBodyChars: z.number(),
|
|
287
|
+
retentionDays: z.number(),
|
|
288
|
+
approvalTtlHours: z.number(),
|
|
289
|
+
webhookAllowlist: z.array(z.string())
|
|
290
|
+
}).partial().strict();
|
|
291
|
+
function settingsApi(ctx) {
|
|
292
|
+
return router().get("/settings", (c) => c.json(ctx.settings.get())).patch("/settings", async (c) => {
|
|
293
|
+
const patch = await readBody(c, SettingsPatch);
|
|
294
|
+
const before = ctx.settings.get();
|
|
295
|
+
const next = ctx.settings.update(patch);
|
|
296
|
+
const changed = Object.keys(next).filter((k) => JSON.stringify(before[k]) !== JSON.stringify(next[k]));
|
|
297
|
+
if (changed.length) ctx.audit.record({
|
|
298
|
+
...ADMIN_ACTOR,
|
|
299
|
+
action: "settings.update",
|
|
300
|
+
outcome: "ok",
|
|
301
|
+
detail: { changed }
|
|
302
|
+
});
|
|
303
|
+
return c.json(next);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region server/api/system.ts
|
|
308
|
+
const DEFAULT_HOST = "0.0.0.0";
|
|
309
|
+
const LATEST_TTL_MS = 18e5;
|
|
310
|
+
const LATEST_RETRY_MS = 3e5;
|
|
311
|
+
/**
|
|
312
|
+
* Public health check for uptime monitors: 200 when healthy, 503 when degraded (an account needs
|
|
313
|
+
* re-auth or is erroring, or ingest reports an error). Only states and the version, never secrets.
|
|
314
|
+
*/
|
|
315
|
+
function healthRoutes(ctx) {
|
|
316
|
+
const handler = (c) => {
|
|
317
|
+
const body = health(ctx);
|
|
318
|
+
c.header("Cache-Control", "no-store");
|
|
319
|
+
return c.json(body, body.status === "ok" ? 200 : 503);
|
|
320
|
+
};
|
|
321
|
+
return router().get("/health", handler).get("/health/", handler).get("/api/health", handler).get("/api/health/", handler);
|
|
322
|
+
}
|
|
323
|
+
function health(ctx) {
|
|
324
|
+
const counts = ctx.db.get("SELECT COUNT(*) AS total, COALESCE(SUM(status IN ('needs_reauth', 'error')), 0) AS attention FROM accounts") ?? {
|
|
325
|
+
total: 0,
|
|
326
|
+
attention: 0
|
|
327
|
+
};
|
|
328
|
+
let ingestError = false;
|
|
329
|
+
if (counts.total > 0) try {
|
|
330
|
+
ingestError = !!ctx.ingest.status().lastError;
|
|
331
|
+
} catch {}
|
|
332
|
+
let setupComplete = false;
|
|
333
|
+
try {
|
|
334
|
+
setupComplete = setupState(ctx).complete;
|
|
335
|
+
} catch {}
|
|
336
|
+
return {
|
|
337
|
+
status: counts.attention > 0 || ingestError ? "degraded" : "ok",
|
|
338
|
+
version: ctx.config.version,
|
|
339
|
+
setupComplete
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/** Authenticated: instance facts and the update notice. */
|
|
343
|
+
function systemApi(ctx) {
|
|
344
|
+
return router().get("/system", (c) => {
|
|
345
|
+
let ingest;
|
|
346
|
+
try {
|
|
347
|
+
ingest = ctx.ingest.status();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
ingest = {
|
|
350
|
+
mode: ctx.settings.get().ingestMode,
|
|
351
|
+
running: false,
|
|
352
|
+
lastPullAt: null,
|
|
353
|
+
lastError: errorMessage(err)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return c.json({
|
|
357
|
+
version: ctx.config.version,
|
|
358
|
+
nodeVersion: process.versions.node,
|
|
359
|
+
dataDir: ctx.config.dataDir,
|
|
360
|
+
service: ctx.config.service,
|
|
361
|
+
addresses: consoleUrls(ctx.config.host, ctx.config.port),
|
|
362
|
+
publicUrl: ctx.settings.publicUrl(),
|
|
363
|
+
ingest
|
|
364
|
+
});
|
|
365
|
+
}).get("/system/latest", async (c) => {
|
|
366
|
+
const latest = await latestVersion(ctx);
|
|
367
|
+
const current = ctx.config.version;
|
|
368
|
+
return c.json({
|
|
369
|
+
current,
|
|
370
|
+
latest,
|
|
371
|
+
updateAvailable: !!latest && compareVersions(latest, current) > 0,
|
|
372
|
+
command: upgradeCommand(ctx.config)
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
let latestCache;
|
|
377
|
+
/** The npm `latest` dist-tag, cached for 30 minutes (5 after a failed check). */
|
|
378
|
+
async function latestVersion(ctx) {
|
|
379
|
+
if (latestCache && Date.now() - latestCache.at < (latestCache.version ? LATEST_TTL_MS : LATEST_RETRY_MS)) return latestCache.version;
|
|
380
|
+
let version = null;
|
|
381
|
+
try {
|
|
382
|
+
const res = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, { signal: AbortSignal.timeout(5e3) });
|
|
383
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
384
|
+
version = (await res.json()).version ?? null;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
ctx.log.debug("Update check failed", { error: errorMessage(err) });
|
|
387
|
+
}
|
|
388
|
+
latestCache = {
|
|
389
|
+
at: Date.now(),
|
|
390
|
+
version
|
|
391
|
+
};
|
|
392
|
+
return version;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Semver precedence: numeric core, then a pre-release sorts before its release, and pre-release
|
|
396
|
+
* identifiers compare numerically when both are numbers ("nightly.10" > "nightly.9").
|
|
397
|
+
*/
|
|
398
|
+
function compareVersions(a, b) {
|
|
399
|
+
const parse = (v) => {
|
|
400
|
+
const clean = v.trim().replace(/^v/, "").split("+")[0];
|
|
401
|
+
const dash = clean.indexOf("-");
|
|
402
|
+
return {
|
|
403
|
+
parts: (dash === -1 ? clean : clean.slice(0, dash)).split(".").map((n) => Number.parseInt(n, 10) || 0),
|
|
404
|
+
pre: dash === -1 ? null : clean.slice(dash + 1).split(".")
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
const x = parse(a);
|
|
408
|
+
const y = parse(b);
|
|
409
|
+
for (let i = 0; i < Math.max(x.parts.length, y.parts.length); i++) {
|
|
410
|
+
const d = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
|
|
411
|
+
if (d) return Math.sign(d);
|
|
412
|
+
}
|
|
413
|
+
if (!x.pre || !y.pre) return x.pre === y.pre ? 0 : x.pre ? -1 : 1;
|
|
414
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
415
|
+
const p = x.pre[i];
|
|
416
|
+
const q = y.pre[i];
|
|
417
|
+
if (p === void 0) return -1;
|
|
418
|
+
if (q === void 0) return 1;
|
|
419
|
+
const pn = /^\d+$/.test(p);
|
|
420
|
+
const qn = /^\d+$/.test(q);
|
|
421
|
+
if (pn && qn) {
|
|
422
|
+
const d = Number(p) - Number(q);
|
|
423
|
+
if (d) return Math.sign(d);
|
|
424
|
+
} else if (pn !== qn) return pn ? -1 : 1;
|
|
425
|
+
else if (p !== q) return p < q ? -1 : 1;
|
|
426
|
+
}
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
const DEFAULT_DATA_DIR = () => resolve(join(homedir(), ".agentinbox"));
|
|
430
|
+
/** Runs, or reinstalls as a service, the latest version with this instance's options. */
|
|
431
|
+
function upgradeCommand(config) {
|
|
432
|
+
const args = [
|
|
433
|
+
...config.trustProxy ? ["TRUST_PROXY=1"] : [],
|
|
434
|
+
"npx",
|
|
435
|
+
"-y",
|
|
436
|
+
NPM_PACKAGE
|
|
437
|
+
];
|
|
438
|
+
if (config.service) args.push("service", "install");
|
|
439
|
+
if (config.dataDir !== DEFAULT_DATA_DIR()) args.push("--data", config.dataDir);
|
|
440
|
+
if (config.port !== 8425) args.push("--port", String(config.port));
|
|
441
|
+
if (config.host !== DEFAULT_HOST) args.push("--host", config.host);
|
|
442
|
+
return args.map(shellQuote).join(" ");
|
|
443
|
+
}
|
|
444
|
+
const shellQuote = (a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`;
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region server/api/webhooks.ts
|
|
447
|
+
function webhooksApi(_ctx) {
|
|
448
|
+
return router();
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region server/csp.ts
|
|
452
|
+
/**
|
|
453
|
+
* Content Security Policy. Inline scripts run only when their exact content is known up front:
|
|
454
|
+
* the console's index.html (hashed when the file is read) and scripts registered here by routes
|
|
455
|
+
* that render HTML. Never hash scripts out of a response body: that would bless injected markup.
|
|
456
|
+
*
|
|
457
|
+
* A server-rendered page with an inline script (e.g. the OAuth callback page) must keep that
|
|
458
|
+
* script static, call `registerInlineScript(source)` once at module load with the exact source,
|
|
459
|
+
* and pass per-request data in a `<script type="application/json" id="…">` block, which CSP
|
|
460
|
+
* doesn't execute and the static script reads with JSON.parse.
|
|
461
|
+
*/
|
|
462
|
+
const registered = /* @__PURE__ */ new Set();
|
|
463
|
+
/** "sha256-…" source for a script's exact text (what goes between <script> and <\/script>). */
|
|
464
|
+
const scriptHash = (source) => `sha256-${createHash("sha256").update(source).digest("base64")}`;
|
|
465
|
+
function contentSecurityPolicy(extraHashes = []) {
|
|
466
|
+
return [
|
|
467
|
+
"default-src 'self'",
|
|
468
|
+
`script-src 'self'${[.../* @__PURE__ */ new Set([...registered, ...extraHashes])].map((h) => ` '${h}'`).join("")}`,
|
|
469
|
+
"style-src 'self' 'unsafe-inline'",
|
|
470
|
+
"img-src 'self' data:",
|
|
471
|
+
"connect-src 'self'",
|
|
472
|
+
"object-src 'none'",
|
|
473
|
+
"base-uri 'none'",
|
|
474
|
+
"frame-ancestors 'none'",
|
|
475
|
+
"form-action 'self'"
|
|
476
|
+
].join("; ");
|
|
477
|
+
}
|
|
478
|
+
/** Hashes of the executable inline scripts in a trusted document (the built index.html). */
|
|
479
|
+
function inlineScriptHashes(html) {
|
|
480
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
481
|
+
for (const m of html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi)) {
|
|
482
|
+
const attrs = m[1];
|
|
483
|
+
if (/\bsrc\s*=/i.test(attrs) || !m[2]) continue;
|
|
484
|
+
const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs)?.[1]?.toLowerCase();
|
|
485
|
+
if (type && type !== "module" && type !== "text/javascript" && type !== "application/javascript") continue;
|
|
486
|
+
hashes.add(scriptHash(m[2]));
|
|
487
|
+
}
|
|
488
|
+
return [...hashes];
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region server/mcp/index.ts
|
|
492
|
+
function mcpRoutes(_ctx) {
|
|
493
|
+
return router();
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region server/app.ts
|
|
497
|
+
/**
|
|
498
|
+
* dist/web relative to the running module: the bundle (dist/cli.js) and source runs
|
|
499
|
+
* (server/app.ts via tsx) both resolve to <package root>/dist/web.
|
|
500
|
+
*/
|
|
501
|
+
const DEFAULT_WEB_DIR = resolve(import.meta.dirname, "..", "dist", "web");
|
|
502
|
+
const API_BODY_LIMIT = 1048576;
|
|
503
|
+
const AUTH_BODY_LIMIT = 16384;
|
|
504
|
+
const AUTH_PATHS = [
|
|
505
|
+
"/api/setup/claim",
|
|
506
|
+
"/api/auth/login",
|
|
507
|
+
"/api/auth/password"
|
|
508
|
+
];
|
|
509
|
+
const MCP_BODY_LIMIT = 4194304;
|
|
510
|
+
const AGENT_BODY_LIMIT = 65536;
|
|
511
|
+
/** Extensions served as files; any other path (even with a dot, e.g. /accounts/me@example.com) is a console route. */
|
|
512
|
+
const ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
513
|
+
".js",
|
|
514
|
+
".mjs",
|
|
515
|
+
".css",
|
|
516
|
+
".map",
|
|
517
|
+
".json",
|
|
518
|
+
".webmanifest",
|
|
519
|
+
".txt",
|
|
520
|
+
".xml",
|
|
521
|
+
".png",
|
|
522
|
+
".jpg",
|
|
523
|
+
".jpeg",
|
|
524
|
+
".gif",
|
|
525
|
+
".webp",
|
|
526
|
+
".avif",
|
|
527
|
+
".svg",
|
|
528
|
+
".ico",
|
|
529
|
+
".woff",
|
|
530
|
+
".woff2",
|
|
531
|
+
".ttf",
|
|
532
|
+
".otf",
|
|
533
|
+
".wasm"
|
|
534
|
+
]);
|
|
535
|
+
/** Paths the console's index.html fallback must never answer. */
|
|
536
|
+
const SERVER_PREFIXES = [
|
|
537
|
+
"/api",
|
|
538
|
+
"/mcp",
|
|
539
|
+
"/oauth",
|
|
540
|
+
"/skill.md",
|
|
541
|
+
"/health"
|
|
542
|
+
];
|
|
543
|
+
const isServerPath = (path) => SERVER_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
544
|
+
function createApp(ctx, opts = {}) {
|
|
545
|
+
const app = new Hono();
|
|
546
|
+
currentSetupToken(ctx);
|
|
547
|
+
let consoleHashes = () => [];
|
|
548
|
+
app.use("*", async (c, next) => {
|
|
549
|
+
await next();
|
|
550
|
+
try {
|
|
551
|
+
c.res.headers.set("Content-Security-Policy", contentSecurityPolicy(consoleHashes()));
|
|
552
|
+
c.res.headers.set("X-Frame-Options", "DENY");
|
|
553
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
554
|
+
c.res.headers.set("Referrer-Policy", "no-referrer");
|
|
555
|
+
} catch {}
|
|
556
|
+
});
|
|
557
|
+
app.onError((err, c) => errorResponse(ctx, err, c));
|
|
558
|
+
app.notFound((c) => {
|
|
559
|
+
if (c.req.path === "/api" || c.req.path.startsWith("/api/")) return apiError(c, 404, "not_found", "No such API route");
|
|
560
|
+
return c.text("Not found", 404);
|
|
561
|
+
});
|
|
562
|
+
const tooLarge = (c) => apiError(c, 413, "payload_too_large", "Request body is too large");
|
|
563
|
+
const limit = (maxSize) => bodyLimit({
|
|
564
|
+
maxSize,
|
|
565
|
+
onError: tooLarge
|
|
566
|
+
});
|
|
567
|
+
for (const path of AUTH_PATHS) app.use(path, limit(AUTH_BODY_LIMIT));
|
|
568
|
+
app.use("/api/*", limit(API_BODY_LIMIT));
|
|
569
|
+
app.use("/mcp", limit(MCP_BODY_LIMIT));
|
|
570
|
+
app.use("/mcp/*", limit(MCP_BODY_LIMIT));
|
|
571
|
+
app.use("/agent/*", limit(AGENT_BODY_LIMIT));
|
|
572
|
+
app.use("/api/*", csrfGuard());
|
|
573
|
+
app.route("/", healthRoutes(ctx));
|
|
574
|
+
app.route("/api", setupPublicApi(ctx));
|
|
575
|
+
app.route("/api", authPublicApi(ctx));
|
|
576
|
+
app.get("/skill.md", (c) => {
|
|
577
|
+
c.header("Content-Type", "text/markdown; charset=utf-8");
|
|
578
|
+
c.header("Cache-Control", "no-cache");
|
|
579
|
+
return c.body(renderSkill(ctx.settings.publicUrl()));
|
|
580
|
+
});
|
|
581
|
+
app.route("/", oauthRoutes(ctx));
|
|
582
|
+
app.route("/", mcpRoutes(ctx));
|
|
583
|
+
app.use("/api/*", requireAdmin(ctx));
|
|
584
|
+
for (const api of [
|
|
585
|
+
setupApi,
|
|
586
|
+
authApi,
|
|
587
|
+
settingsApi,
|
|
588
|
+
systemApi,
|
|
589
|
+
googleApi,
|
|
590
|
+
accountsApi,
|
|
591
|
+
agentsApi,
|
|
592
|
+
approvalsApi,
|
|
593
|
+
webhooksApi,
|
|
594
|
+
eventsApi,
|
|
595
|
+
auditApi,
|
|
596
|
+
overviewApi
|
|
597
|
+
]) app.route("/api", api(ctx));
|
|
598
|
+
if (ctx.config.dev) app.get("*", (c) => isServerPath(c.req.path) ? c.notFound() : devPage(c));
|
|
599
|
+
else consoleHashes = serveConsole(app, opts.webDir ?? DEFAULT_WEB_DIR);
|
|
600
|
+
return app;
|
|
601
|
+
}
|
|
602
|
+
/** Serves the built console; returns the CSP hashes of index.html's inline scripts (re-read when the file changes). */
|
|
603
|
+
function serveConsole(app, webDir) {
|
|
604
|
+
const index = join(webDir, "index.html");
|
|
605
|
+
if (!existsSync(index)) {
|
|
606
|
+
app.get("*", (c) => isServerPath(c.req.path) ? c.notFound() : missingBuildPage(c, webDir));
|
|
607
|
+
return () => [];
|
|
608
|
+
}
|
|
609
|
+
let cached = {
|
|
610
|
+
mtimeMs: -1,
|
|
611
|
+
hashes: []
|
|
612
|
+
};
|
|
613
|
+
const hashes = () => {
|
|
614
|
+
try {
|
|
615
|
+
const { mtimeMs } = statSync(index);
|
|
616
|
+
if (mtimeMs !== cached.mtimeMs) cached = {
|
|
617
|
+
mtimeMs,
|
|
618
|
+
hashes: inlineScriptHashes(readFileSync(index, "utf8"))
|
|
619
|
+
};
|
|
620
|
+
} catch {}
|
|
621
|
+
return cached.hashes;
|
|
622
|
+
};
|
|
623
|
+
const assets = serveStatic({ root: webDir });
|
|
624
|
+
const assetsDir = join(webDir, "assets") + sep;
|
|
625
|
+
app.use("*", async (c, next) => {
|
|
626
|
+
if (isServerPath(c.req.path)) return next();
|
|
627
|
+
const res = await assets(c, next);
|
|
628
|
+
if (res instanceof Response) {
|
|
629
|
+
const hashed = join(webDir, c.req.path).startsWith(assetsDir);
|
|
630
|
+
res.headers.set("Cache-Control", hashed ? "public, max-age=31536000, immutable" : "no-cache");
|
|
631
|
+
}
|
|
632
|
+
return res;
|
|
633
|
+
});
|
|
634
|
+
app.get("*", (c) => {
|
|
635
|
+
const path = c.req.path;
|
|
636
|
+
if (isServerPath(path) || path.startsWith("/assets/") || ASSET_EXTENSIONS.has(extname(path).toLowerCase())) return c.notFound();
|
|
637
|
+
c.header("Cache-Control", "no-cache");
|
|
638
|
+
return c.html(readFileSync(index, "utf8"));
|
|
639
|
+
});
|
|
640
|
+
return hashes;
|
|
641
|
+
}
|
|
642
|
+
function errorResponse(ctx, err, c) {
|
|
643
|
+
if (err instanceof HttpError) return c.json(err.toJSON(), err.status);
|
|
644
|
+
if (err instanceof PolicyDenied) return apiError(c, 403, "policy_denied", err.message);
|
|
645
|
+
if (err instanceof z.ZodError) return apiError(c, 400, "bad_request", z.prettifyError(err));
|
|
646
|
+
if (err instanceof HTTPException) return err.res ? err.getResponse() : apiError(c, err.status, "http_error", err.message || "Request failed");
|
|
647
|
+
if (err instanceof SyntaxError && /JSON/i.test(err.message)) return apiError(c, 400, "bad_request", "Request body must be valid JSON");
|
|
648
|
+
ctx.log.error(`${c.req.method} ${c.req.path} failed`, {
|
|
649
|
+
error: err.message,
|
|
650
|
+
stack: err.stack
|
|
651
|
+
});
|
|
652
|
+
return apiError(c, 500, "internal", "Something went wrong. Check the agentinbox logs for details.");
|
|
653
|
+
}
|
|
654
|
+
function apiError(c, status, code, message) {
|
|
655
|
+
return c.json({ error: {
|
|
656
|
+
code,
|
|
657
|
+
message
|
|
658
|
+
} }, status);
|
|
659
|
+
}
|
|
660
|
+
const page = (title, body) => `<!doctype html>
|
|
661
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
662
|
+
<title>${title}</title>
|
|
663
|
+
<style>
|
|
664
|
+
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
665
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: Canvas; color: CanvasText; }
|
|
666
|
+
main { max-width: 34rem; padding: 2rem; line-height: 1.55; }
|
|
667
|
+
h1 { font-size: 1.25rem; margin: 0 0 .75rem; }
|
|
668
|
+
code { font-family: ui-monospace, monospace; padding: .1rem .35rem; border-radius: .3rem; background: color-mix(in srgb, CanvasText 10%, transparent); }
|
|
669
|
+
p { margin: .5rem 0; opacity: .85; }
|
|
670
|
+
</style></head><body><main>${body}</main></body></html>`;
|
|
671
|
+
const escapeHtml = (s) => s.replace(/[&<>"]/g, (ch) => `&#${ch.charCodeAt(0)};`);
|
|
672
|
+
function missingBuildPage(c, webDir) {
|
|
673
|
+
c.header("Cache-Control", "no-store");
|
|
674
|
+
return c.html(page("agentinbox: console not built", `<h1>The web console hasn't been built</h1>
|
|
675
|
+
<p>agentinbox is running, but there is no console build in <code>${escapeHtml(webDir)}</code>.</p>
|
|
676
|
+
<p>From a source checkout, run <code>pnpm build</code> and restart, or use <code>pnpm dev</code> for development.</p>
|
|
677
|
+
<p>The API (<code>/api</code>), MCP endpoint (<code>/mcp</code>) and <code>/health</code> work as usual.</p>`), 503);
|
|
678
|
+
}
|
|
679
|
+
function devPage(c) {
|
|
680
|
+
const url = new URL(c.req.url);
|
|
681
|
+
const target = `http://localhost:5173${url.pathname}${url.search}`;
|
|
682
|
+
return c.html(page("agentinbox (dev)", `<h1>agentinbox is running in dev mode</h1>
|
|
683
|
+
<p>The console is served by Vite: open <a href="${escapeHtml(target)}">${escapeHtml(target)}</a>.</p>`));
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region server/maintenance.ts
|
|
687
|
+
const INTERVAL_MS = 36e5;
|
|
688
|
+
const FIRST_RUN_MS = 6e4;
|
|
689
|
+
/** Prunes data older than Settings.retentionDays and expired admin sessions. Never throws. */
|
|
690
|
+
function runMaintenance(ctx) {
|
|
691
|
+
const log = ctx.log.child("maintenance");
|
|
692
|
+
const before = (/* @__PURE__ */ new Date(Date.now() - ctx.settings.get().retentionDays * 864e5)).toISOString();
|
|
693
|
+
const step = (name, fn) => {
|
|
694
|
+
try {
|
|
695
|
+
return fn();
|
|
696
|
+
} catch (err) {
|
|
697
|
+
log.warn(`Pruning ${name} failed`, { error: errorMessage(err) });
|
|
698
|
+
return 0;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
const result = {
|
|
702
|
+
deliveries: step("deliveries", () => ctx.webhooks.prune(before)),
|
|
703
|
+
events: step("events", () => ctx.events.prune(before)),
|
|
704
|
+
audit: step("audit log", () => ctx.audit.prune(before)),
|
|
705
|
+
sessions: step("sessions", () => adminAuth(ctx).sessions.pruneExpired())
|
|
706
|
+
};
|
|
707
|
+
if (Object.values(result).some((n) => n > 0)) log.info("Pruned old data", { ...result });
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
/** Runs maintenance shortly after start, then hourly. Timers are unref'd. Returns a stop function. */
|
|
711
|
+
function startMaintenance(ctx) {
|
|
712
|
+
const run = () => {
|
|
713
|
+
try {
|
|
714
|
+
runMaintenance(ctx);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
ctx.log.warn("Maintenance failed", { error: errorMessage(err) });
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
const first = setTimeout(run, FIRST_RUN_MS).unref();
|
|
720
|
+
const timer = setInterval(run, INTERVAL_MS).unref();
|
|
721
|
+
return () => {
|
|
722
|
+
clearTimeout(first);
|
|
723
|
+
clearInterval(timer);
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
727
|
+
//#region server/util/log.ts
|
|
728
|
+
const order = {
|
|
729
|
+
debug: 10,
|
|
730
|
+
info: 20,
|
|
731
|
+
warn: 30,
|
|
732
|
+
error: 40
|
|
733
|
+
};
|
|
734
|
+
const min = order[process.env.AGENTINBOX_LOG_LEVEL ?? "info"] ?? 20;
|
|
735
|
+
const color = {
|
|
736
|
+
debug: pc.gray,
|
|
737
|
+
info: pc.cyan,
|
|
738
|
+
warn: pc.yellow,
|
|
739
|
+
error: pc.red
|
|
740
|
+
};
|
|
741
|
+
function createLogger(scope = "agentinbox") {
|
|
742
|
+
const emit = (level, msg, meta) => {
|
|
743
|
+
if (order[level] < min) return;
|
|
744
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
745
|
+
const extra = meta && Object.keys(meta).length ? " " + pc.gray(JSON.stringify(meta)) : "";
|
|
746
|
+
const line = `${pc.gray(ts)} ${color[level](level.padEnd(5))} ${pc.bold(scope)} ${msg}${extra}`;
|
|
747
|
+
(level === "error" || level === "warn" ? process.stderr : process.stdout).write(line + "\n");
|
|
748
|
+
};
|
|
749
|
+
return {
|
|
750
|
+
debug: (m, x) => emit("debug", m, x),
|
|
751
|
+
info: (m, x) => emit("info", m, x),
|
|
752
|
+
warn: (m, x) => emit("warn", m, x),
|
|
753
|
+
error: (m, x) => emit("error", m, x),
|
|
754
|
+
child: (s) => createLogger(`${scope}:${s}`)
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region server/context.ts
|
|
759
|
+
/** Start order. Policy and events first (no I/O), ingest last so everything it emits into is running. */
|
|
760
|
+
const SERVICE_ORDER = [
|
|
761
|
+
"policy",
|
|
762
|
+
"accounts",
|
|
763
|
+
"events",
|
|
764
|
+
"webhooks",
|
|
765
|
+
"mail",
|
|
766
|
+
"google",
|
|
767
|
+
"ingest"
|
|
768
|
+
];
|
|
769
|
+
function createCore(config) {
|
|
770
|
+
const db = new Db(config.dbPath);
|
|
771
|
+
const bus = createBus();
|
|
772
|
+
return {
|
|
773
|
+
config,
|
|
774
|
+
db,
|
|
775
|
+
crypto: Crypto.load(config.masterKeyPath),
|
|
776
|
+
log: createLogger(),
|
|
777
|
+
bus,
|
|
778
|
+
settings: new SettingsStore(db, bus, config),
|
|
779
|
+
audit: new AuditLog(db, bus)
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
//#endregion
|
|
783
|
+
//#region server/util/stub.ts
|
|
784
|
+
/**
|
|
785
|
+
* TEMPORARY scaffolding: a service whose methods throw "not implemented". Lets every domain compile
|
|
786
|
+
* and run its own tests before the others land. Removed at integration.
|
|
787
|
+
*/
|
|
788
|
+
function stubService(name) {
|
|
789
|
+
return new Proxy({}, { get(_target, prop) {
|
|
790
|
+
if (prop === "start" || prop === "stop" || prop === "then" || typeof prop === "symbol") return void 0;
|
|
791
|
+
return () => {
|
|
792
|
+
throw new Error(`${name}.${String(prop)} is not implemented yet`);
|
|
793
|
+
};
|
|
794
|
+
} });
|
|
795
|
+
}
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region server/events/index.ts
|
|
798
|
+
function createEventsService(_ctx) {
|
|
799
|
+
return stubService("events");
|
|
800
|
+
}
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region server/google/index.ts
|
|
803
|
+
function createGoogleService(_ctx) {
|
|
804
|
+
return stubService("google");
|
|
805
|
+
}
|
|
806
|
+
function createAccountsService(_ctx) {
|
|
807
|
+
return stubService("accounts");
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region server/ingest/index.ts
|
|
811
|
+
function createIngestService(_ctx) {
|
|
812
|
+
return stubService("ingest");
|
|
813
|
+
}
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region server/mail/index.ts
|
|
816
|
+
function createMailService(_ctx) {
|
|
817
|
+
return stubService("mail");
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region server/policy/index.ts
|
|
821
|
+
function createPolicyService(_ctx) {
|
|
822
|
+
return stubService("policy");
|
|
823
|
+
}
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region server/webhooks/index.ts
|
|
826
|
+
function createWebhooksService(_ctx) {
|
|
827
|
+
return stubService("webhooks");
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region server/wire.ts
|
|
831
|
+
const FACTORIES = {
|
|
832
|
+
accounts: createAccountsService,
|
|
833
|
+
google: createGoogleService,
|
|
834
|
+
ingest: createIngestService,
|
|
835
|
+
events: createEventsService,
|
|
836
|
+
policy: createPolicyService,
|
|
837
|
+
mail: createMailService,
|
|
838
|
+
webhooks: createWebhooksService
|
|
839
|
+
};
|
|
840
|
+
/** Builds the full context. Nothing runs until `startServices`. */
|
|
841
|
+
function createAppContext(config, overrides = {}) {
|
|
842
|
+
const ctx = createCore(config);
|
|
843
|
+
for (const key of SERVICE_ORDER) ctx[key] = (overrides[key] ?? FACTORIES[key])(ctx);
|
|
844
|
+
return ctx;
|
|
845
|
+
}
|
|
846
|
+
async function startServices(ctx) {
|
|
847
|
+
for (const key of SERVICE_ORDER) await ctx[key].start?.();
|
|
848
|
+
}
|
|
849
|
+
/** Stops services in reverse order, then closes the database. Errors are logged, not thrown. */
|
|
850
|
+
async function stopServices(ctx) {
|
|
851
|
+
for (const key of [...SERVICE_ORDER].reverse()) try {
|
|
852
|
+
await ctx[key].stop?.();
|
|
853
|
+
} catch (err) {
|
|
854
|
+
ctx.log.error(`Failed to stop ${key}`, { error: err instanceof Error ? err.message : String(err) });
|
|
855
|
+
}
|
|
856
|
+
ctx.db.close();
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region server/server.ts
|
|
860
|
+
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
861
|
+
/**
|
|
862
|
+
* Takes the data directory's instance lock, checks the master key against stored secrets, builds
|
|
863
|
+
* the context, binds the port, then starts services (so a second instance never runs ingest
|
|
864
|
+
* against the same data). Use `port: 0` in tests; `config.port` is updated to the bound port.
|
|
865
|
+
*/
|
|
866
|
+
async function startServer(opts) {
|
|
867
|
+
const { config } = opts;
|
|
868
|
+
const lock = acquireInstanceLock(config);
|
|
869
|
+
let ctx;
|
|
870
|
+
let server;
|
|
871
|
+
try {
|
|
872
|
+
assertMasterKeyUsable(config);
|
|
873
|
+
ctx = createAppContext(config, opts.overrides);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
lock.release();
|
|
876
|
+
throw err;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
server = await listen(createApp(ctx, { webDir: opts.webDir }).fetch, config.host, config.port);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
ctx.db.close();
|
|
882
|
+
lock.release();
|
|
883
|
+
throw friendlyListenError(err, config.port);
|
|
884
|
+
}
|
|
885
|
+
config.port = server.address().port;
|
|
886
|
+
lock.update(config.port);
|
|
887
|
+
let stopMaintenance = () => {};
|
|
888
|
+
let closing;
|
|
889
|
+
const close = () => closing ??= (async () => {
|
|
890
|
+
stopMaintenance();
|
|
891
|
+
await new Promise((resolve) => {
|
|
892
|
+
server.close(() => resolve());
|
|
893
|
+
server.closeAllConnections?.();
|
|
894
|
+
});
|
|
895
|
+
await stopServices(ctx);
|
|
896
|
+
lock.release();
|
|
897
|
+
})();
|
|
898
|
+
try {
|
|
899
|
+
await startServices(ctx);
|
|
900
|
+
} catch (err) {
|
|
901
|
+
await close();
|
|
902
|
+
throw err;
|
|
903
|
+
}
|
|
904
|
+
stopMaintenance = startMaintenance(ctx);
|
|
905
|
+
return {
|
|
906
|
+
url: loopbackUrl(config.host, config.port),
|
|
907
|
+
ctx,
|
|
908
|
+
server,
|
|
909
|
+
close
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function listen(fetch, hostname, port) {
|
|
913
|
+
return new Promise((resolve, reject) => {
|
|
914
|
+
const server = serve({
|
|
915
|
+
fetch,
|
|
916
|
+
hostname,
|
|
917
|
+
port
|
|
918
|
+
}, () => {
|
|
919
|
+
server.off("error", reject);
|
|
920
|
+
resolve(server);
|
|
921
|
+
});
|
|
922
|
+
server.once("error", reject);
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function friendlyListenError(err, port) {
|
|
926
|
+
const code = err?.code;
|
|
927
|
+
if (code === "EADDRINUSE") return /* @__PURE__ */ new Error(`Port ${port} is already in use (is agentinbox already running?). Try --port <port>.`);
|
|
928
|
+
if (code === "EACCES") return /* @__PURE__ */ new Error(`Not allowed to listen on port ${port}. Use a port above 1024, e.g. --port 8425.`);
|
|
929
|
+
if (code === "EADDRNOTAVAIL") return /* @__PURE__ */ new Error(`Can't bind to that address. Check --host.`);
|
|
930
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
931
|
+
}
|
|
932
|
+
/** `agentinbox start`: runs in the foreground until SIGINT/SIGTERM. */
|
|
933
|
+
async function runForeground(opts) {
|
|
934
|
+
const config = loadConfig(opts, opts.version);
|
|
935
|
+
const running = await startServer({ config });
|
|
936
|
+
printBanner(running.ctx);
|
|
937
|
+
const token = currentSetupToken(running.ctx);
|
|
938
|
+
if (token && opts.open && process.stdout.isTTY && !config.dev && !config.service) openBrowser(`${consoleUrls(config.host, config.port)[0]}/setup?token=${token}`);
|
|
939
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
|
|
940
|
+
console.log(pc.dim(`\n Stopping agentinbox…`));
|
|
941
|
+
setTimeout(() => {
|
|
942
|
+
console.error(`agentinbox did not stop within ${SHUTDOWN_TIMEOUT_MS / 1e3} s; exiting anyway.`);
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}, SHUTDOWN_TIMEOUT_MS).unref();
|
|
945
|
+
running.close().then(() => process.exit(0), (err) => {
|
|
946
|
+
console.error(errorMessage(err));
|
|
947
|
+
process.exit(1);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function printBanner(ctx) {
|
|
952
|
+
const { config } = ctx;
|
|
953
|
+
const urls = config.dev ? ["http://localhost:5173"] : consoleUrls(config.host, config.port);
|
|
954
|
+
const token = currentSetupToken(ctx);
|
|
955
|
+
const out = [
|
|
956
|
+
"",
|
|
957
|
+
` ${pc.bold("agentinbox")} ${pc.dim(`v${config.version} · data in ${config.dataDir}`)}`,
|
|
958
|
+
""
|
|
959
|
+
];
|
|
960
|
+
if (token && config.service) out.push(` ${pc.yellow("Not claimed yet.")} The setup link is ${urls[0]}/setup?token=<token>, with the token in ${setupTokenPath(config.dataDir)}.`, ` ${pc.dim(`\`${cliCommand("service install")}\` prints the full link.`)}`);
|
|
961
|
+
else if (token) {
|
|
962
|
+
out.push(` ${pc.yellow("First run:")} open this link to claim the instance and finish setup:`, "");
|
|
963
|
+
for (const url of urls) out.push(` ${pc.cyan(`${url}/setup?token=${token}`)}`);
|
|
964
|
+
} else {
|
|
965
|
+
out.push(` ${pc.dim("Console")} ${pc.cyan(urls[0])}`);
|
|
966
|
+
for (const url of urls.slice(1)) out.push(` ${pc.cyan(url)}`);
|
|
967
|
+
out.push(` ${pc.dim("MCP")} ${pc.cyan(`${ctx.settings.publicUrl()}/mcp`)}`);
|
|
968
|
+
}
|
|
969
|
+
if (config.dev) out.push("", pc.dim(` Dev mode: API on :${config.port}, console via Vite on :5173.`));
|
|
970
|
+
console.log(out.join("\n") + "\n");
|
|
971
|
+
}
|
|
972
|
+
function openBrowser(url) {
|
|
973
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return;
|
|
974
|
+
try {
|
|
975
|
+
open(url).then((child) => child.on("error", () => {}), () => {});
|
|
976
|
+
} catch {}
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
export { runForeground };
|
|
980
|
+
|
|
981
|
+
//# sourceMappingURL=server-CWqKeCGn.js.map
|