agentinbox-cli 0.0.0 → 0.1.0-nightly.10
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 +337 -0
- package/dist/admin-CXkmdTYo.js +196 -0
- package/dist/admin-CXkmdTYo.js.map +1 -0
- package/dist/audit-bItbJYFP.js +65 -0
- package/dist/audit-bItbJYFP.js.map +1 -0
- package/dist/audit-gVdDzkAE.js +2 -0
- package/dist/auth-0NrMvHdE.js +550 -0
- package/dist/auth-0NrMvHdE.js.map +1 -0
- package/dist/auth-CoHvbWgO.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 +220 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-DZ60j8_y.js +103 -0
- package/dist/commands-DZ60j8_y.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/connect-BHC2trBi.js +292 -0
- package/dist/connect-BHC2trBi.js.map +1 -0
- package/dist/content-BXblC_Qs.js +1162 -0
- package/dist/content-BXblC_Qs.js.map +1 -0
- package/dist/content-worker.js +28 -0
- package/dist/content-worker.js.map +1 -0
- package/dist/crypto-DYZ4R7c2.js +74 -0
- package/dist/crypto-DYZ4R7c2.js.map +1 -0
- package/dist/db-B6gLQGBK.js +2 -0
- package/dist/db-D0gvshCc.js +282 -0
- package/dist/db-D0gvshCc.js.map +1 -0
- package/dist/errors-FbMS3QBo.js +39 -0
- package/dist/errors-FbMS3QBo.js.map +1 -0
- package/dist/filter-DGVz6-P1.js +140 -0
- package/dist/filter-DGVz6-P1.js.map +1 -0
- package/dist/instance-BBzEsYZO.js +175 -0
- package/dist/instance-BBzEsYZO.js.map +1 -0
- package/dist/instance-DLNTj9nm.js +2 -0
- package/dist/listen-D8DkEhqM.js +1724 -0
- package/dist/listen-D8DkEhqM.js.map +1 -0
- package/dist/listen-DT-omyD6.js +2 -0
- package/dist/mcpheader-By2dIyZq.js +2 -0
- package/dist/mcpheader-D9rrI-aa.js +65 -0
- package/dist/mcpheader-D9rrI-aa.js.map +1 -0
- package/dist/mcpsetup-7KxroOCp.js +2 -0
- package/dist/mcpsetup-DJ7ebFGJ.js +266 -0
- package/dist/mcpsetup-DJ7ebFGJ.js.map +1 -0
- package/dist/paths-BCBp8vrG.js +2 -0
- package/dist/paths-TLp8hiMt.js +40 -0
- package/dist/paths-TLp8hiMt.js.map +1 -0
- package/dist/profile-CgGgJ4Dm.js +181 -0
- package/dist/profile-CgGgJ4Dm.js.map +1 -0
- package/dist/profile-RbN2FRz7.js +2 -0
- package/dist/recipes-7d_Xws4M.js +2 -0
- package/dist/recipes-UalOc_LB.js +100 -0
- package/dist/recipes-UalOc_LB.js.map +1 -0
- package/dist/send-Dk-t8ia-.js +108 -0
- package/dist/send-Dk-t8ia-.js.map +1 -0
- package/dist/server-Hunjpt8n.js +8103 -0
- package/dist/server-Hunjpt8n.js.map +1 -0
- package/dist/service-BAgZDh22.js +2 -0
- package/dist/service-DYdD92CC.js +2 -0
- package/dist/service-D_ZEs3Zk.js +508 -0
- package/dist/service-D_ZEs3Zk.js.map +1 -0
- package/dist/service-whbugeQp.js +295 -0
- package/dist/service-whbugeQp.js.map +1 -0
- package/dist/settings-CeEsv4CE.js +2 -0
- package/dist/settings-DLUStIN7.js +109 -0
- package/dist/settings-DLUStIN7.js.map +1 -0
- package/dist/setup-token-BO89xnpb.js +48 -0
- package/dist/setup-token-BO89xnpb.js.map +1 -0
- package/dist/skill/SKILL.md +277 -0
- package/dist/skill-BJFn2bHK.js +2 -0
- package/dist/skill-WKbeyNct.js +43 -0
- package/dist/skill-WKbeyNct.js.map +1 -0
- package/dist/types-3eEsBFVj.js +37 -0
- package/dist/types-3eEsBFVj.js.map +1 -0
- package/dist/ui-B8Sd4Y7Y.js +81 -0
- package/dist/ui-B8Sd4Y7Y.js.map +1 -0
- package/dist/urls-Dp8pBGMU.js +2 -0
- package/dist/urls-U_LfYV61.js +27 -0
- package/dist/urls-U_LfYV61.js.map +1 -0
- package/dist/web/assets/_agentId-DAj08NDq.js +1 -0
- package/dist/web/assets/_app-B6q_HS9y.js +1 -0
- package/dist/web/assets/_app-CAQ6Mu1s.js +63 -0
- package/dist/web/assets/_webhookId-CMgBUN2Z.js +1 -0
- package/dist/web/assets/account-status-omoUlgBI.js +1 -0
- package/dist/web/assets/accounts-DyLfACj_.js +1 -0
- package/dist/web/assets/accounts-list-DiXACW0-.js +1 -0
- package/dist/web/assets/activity-zrckFhPk.js +1 -0
- package/dist/web/assets/agent-create-form-DKwaY1Dg.js +1 -0
- package/dist/web/assets/agents-Drvjccgt.js +1 -0
- package/dist/web/assets/approvals-4kd0_J8X.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-76H45orR.js +1 -0
- package/dist/web/assets/audit-bits-B1We-fQ3.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--xgMZpg2.js +1 -0
- package/dist/web/assets/circle-x-Bi4IGwky.js +1 -0
- package/dist/web/assets/common-PG6weTqi.js +1 -0
- package/dist/web/assets/connect-host-DdkOJnMu.js +1 -0
- package/dist/web/assets/data-table-Ca9aQZKR.js +1 -0
- package/dist/web/assets/dialog-CxNRCZdE.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-2xoyzgbP.js +1 -0
- package/dist/web/assets/events-Bizq3DPC.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-DXUVmyNX.js +1 -0
- package/dist/web/assets/grants-BhXj1Pew.js +1 -0
- package/dist/web/assets/index-Bd4Y5wSp.js +10 -0
- package/dist/web/assets/index-akvt_1UI.css +2 -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-aCr-cPqJ.js +1 -0
- package/dist/web/assets/mail-CAVmuESo.js +1 -0
- package/dist/web/assets/mutations-DVlbDx0M.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-C8tJBHb5.js +1 -0
- package/dist/web/assets/send-BhQwhuJo.js +1 -0
- package/dist/web/assets/settings-H7ExHD9L.js +1 -0
- package/dist/web/assets/settings-form-CNUionsI.js +1 -0
- package/dist/web/assets/setup-DLbH5Dvl.js +1 -0
- package/dist/web/assets/sheet-BQKpA2xa.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-C2Tr2d5O.js +1 -0
- package/dist/web/assets/tabs-ySiz7Mni.js +1 -0
- package/dist/web/assets/tag-CP72Kk6Y.js +1 -0
- package/dist/web/assets/test-connection-Bd2Gac0O.js +1 -0
- package/dist/web/assets/textarea-CCokBZIw.js +1 -0
- package/dist/web/assets/toggle-group-ne0pVDg6.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-DeWvLw1a.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-Cy_jHIqG.js +34 -0
- package/dist/web/assets/webhooks-CTAj5Ovp.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 +95 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as now, r as json } from "./db-D0gvshCc.js";
|
|
3
|
+
//#region server/audit.ts
|
|
4
|
+
const SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name
|
|
5
|
+
FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;
|
|
6
|
+
const toEntry = (r) => ({
|
|
7
|
+
id: r.id,
|
|
8
|
+
ts: r.ts,
|
|
9
|
+
actorType: r.actor_type,
|
|
10
|
+
actorId: r.actor_id,
|
|
11
|
+
actorName: r.actor_name,
|
|
12
|
+
action: r.action,
|
|
13
|
+
accountId: r.account_id,
|
|
14
|
+
target: r.target,
|
|
15
|
+
outcome: r.outcome,
|
|
16
|
+
reason: r.reason,
|
|
17
|
+
durationMs: r.duration_ms,
|
|
18
|
+
detail: json.parse(r.detail, null)
|
|
19
|
+
});
|
|
20
|
+
/** Append-only audit log. Every agent tool call and every admin mutation is recorded. */
|
|
21
|
+
var AuditLog = class {
|
|
22
|
+
db;
|
|
23
|
+
bus;
|
|
24
|
+
constructor(db, bus) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.bus = bus;
|
|
27
|
+
}
|
|
28
|
+
record(input) {
|
|
29
|
+
const { lastInsertRowid } = this.db.run(`INSERT INTO audit_log (ts, actor_type, actor_id, action, account_id, target, outcome, reason, duration_ms, detail)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, now(), input.actorType, input.actorId ?? null, input.action, input.accountId ?? null, input.target ?? null, input.outcome, input.reason ?? null, input.durationMs ?? null, input.detail ? json.str(input.detail) : null);
|
|
31
|
+
const entry = toEntry(this.db.get(`${SELECT} WHERE a.id = ?`, lastInsertRowid));
|
|
32
|
+
this.bus.emit("audit", entry);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
/** Newest first. */
|
|
36
|
+
list(q = {}) {
|
|
37
|
+
const where = [];
|
|
38
|
+
const params = [];
|
|
39
|
+
const add = (clause, value) => {
|
|
40
|
+
where.push(clause);
|
|
41
|
+
params.push(value);
|
|
42
|
+
};
|
|
43
|
+
if (q.agentId) add("a.actor_id = ?", q.agentId);
|
|
44
|
+
if (q.accountId) add("a.account_id = ?", q.accountId);
|
|
45
|
+
if (q.outcome) add("a.outcome = ?", q.outcome);
|
|
46
|
+
if (q.action) add("a.action = ?", q.action);
|
|
47
|
+
const before = Number(q.cursor);
|
|
48
|
+
if (q.cursor && Number.isInteger(before)) add("a.id < ?", before);
|
|
49
|
+
const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);
|
|
50
|
+
const rows = this.db.all(`${SELECT} ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY a.id DESC LIMIT ?`, ...params, limit + 1);
|
|
51
|
+
const items = rows.slice(0, limit).map(toEntry);
|
|
52
|
+
return {
|
|
53
|
+
items,
|
|
54
|
+
nextCursor: rows.length > limit ? String(items.at(-1).id) : null
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Deletes entries older than `beforeIso`. Returns the number removed. */
|
|
58
|
+
prune(beforeIso) {
|
|
59
|
+
return this.db.run("DELETE FROM audit_log WHERE ts < ?", beforeIso).changes;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { AuditLog as t };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=audit-bItbJYFP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-bItbJYFP.js","names":[],"sources":["../server/audit.ts"],"sourcesContent":["import type { ActorType, AuditEntry, AuditOutcome, Page } from \"../shared/types.ts\";\nimport type { Bus } from \"./bus.ts\";\nimport { type Db, json, now } from \"./db/index.ts\";\n\nexport interface AuditInput {\n actorType: ActorType;\n actorId?: string | null;\n /** MCP tool name or console action, e.g. \"send_message\", \"agent.create\", \"account.connect\". */\n action: string;\n accountId?: string | null;\n target?: string | null;\n outcome: AuditOutcome;\n /** Required for denials: the guardrail that blocked it (\"allow_to\", \"label_scope\", \"rate_limit\", …). */\n reason?: string | null;\n durationMs?: number | null;\n /** Small, non-sensitive context. Never message bodies, keys or tokens. */\n detail?: Record<string, unknown> | null;\n}\n\nexport interface AuditQuery {\n agentId?: string;\n accountId?: string;\n outcome?: AuditOutcome;\n action?: string;\n /** Opaque cursor from a previous page. */\n cursor?: string | null;\n limit?: number;\n}\n\ninterface AuditRow {\n id: number;\n ts: string;\n actor_type: ActorType;\n actor_id: string | null;\n actor_name: string | null;\n action: string;\n account_id: string | null;\n target: string | null;\n outcome: AuditOutcome;\n reason: string | null;\n duration_ms: number | null;\n detail: string | null;\n}\n\nconst SELECT = `SELECT a.*, CASE a.actor_type WHEN 'agent' THEN ag.name WHEN 'admin' THEN 'admin' ELSE NULL END AS actor_name\n FROM audit_log a LEFT JOIN agents ag ON a.actor_type = 'agent' AND ag.id = a.actor_id`;\n\nconst toEntry = (r: AuditRow): AuditEntry => ({\n id: r.id,\n ts: r.ts,\n actorType: r.actor_type,\n actorId: r.actor_id,\n actorName: r.actor_name,\n action: r.action,\n accountId: r.account_id,\n target: r.target,\n outcome: r.outcome,\n reason: r.reason,\n durationMs: r.duration_ms,\n detail: json.parse<Record<string, unknown> | null>(r.detail, null),\n});\n\n/** Append-only audit log. Every agent tool call and every admin mutation is recorded. */\nexport class AuditLog {\n constructor(\n private readonly db: Db,\n private readonly bus: Bus,\n ) {}\n\n record(input: AuditInput): AuditEntry {\n const { lastInsertRowid } = this.db.run(\n `INSERT INTO audit_log (ts, actor_type, actor_id, action, account_id, target, outcome, reason, duration_ms, detail)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n now(),\n input.actorType,\n input.actorId ?? null,\n input.action,\n input.accountId ?? null,\n input.target ?? null,\n input.outcome,\n input.reason ?? null,\n input.durationMs ?? null,\n input.detail ? json.str(input.detail) : null,\n );\n const entry = toEntry(this.db.get<AuditRow>(`${SELECT} WHERE a.id = ?`, lastInsertRowid)!);\n this.bus.emit(\"audit\", entry);\n return entry;\n }\n\n /** Newest first. */\n list(q: AuditQuery = {}): Page<AuditEntry> {\n const where: string[] = [];\n const params: (string | number)[] = [];\n const add = (clause: string, value: string | number) => {\n where.push(clause);\n params.push(value);\n };\n if (q.agentId) add(\"a.actor_id = ?\", q.agentId);\n if (q.accountId) add(\"a.account_id = ?\", q.accountId);\n if (q.outcome) add(\"a.outcome = ?\", q.outcome);\n if (q.action) add(\"a.action = ?\", q.action);\n const before = Number(q.cursor);\n if (q.cursor && Number.isInteger(before)) add(\"a.id < ?\", before);\n const limit = Math.min(Math.max(q.limit ?? 50, 1), 200);\n const rows = this.db.all<AuditRow>(\n `${SELECT} ${where.length ? \"WHERE \" + where.join(\" AND \") : \"\"} ORDER BY a.id DESC LIMIT ?`,\n ...params,\n limit + 1,\n );\n const items = rows.slice(0, limit).map(toEntry);\n return { items, nextCursor: rows.length > limit ? String(items.at(-1)!.id) : null };\n }\n\n /** Deletes entries older than `beforeIso`. Returns the number removed. */\n prune(beforeIso: string): number {\n return this.db.run(\"DELETE FROM audit_log WHERE ts < ?\", beforeIso).changes;\n }\n}\n"],"mappings":";;;AA4CA,MAAM,SAAS;;AAGf,MAAM,WAAW,OAA6B;CAC5C,IAAI,EAAE;CACN,IAAI,EAAE;CACN,WAAW,EAAE;CACb,SAAS,EAAE;CACX,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,WAAW,EAAE;CACb,QAAQ,EAAE;CACV,SAAS,EAAE;CACX,QAAQ,EAAE;CACV,YAAY,EAAE;CACd,QAAQ,KAAK,MAAsC,EAAE,QAAQ,IAAI;AACnE;;AAGA,IAAa,WAAb,MAAsB;CAED;CACA;CAFnB,YACE,IACA,KACA;EAFiB,KAAA,KAAA;EACA,KAAA,MAAA;CAChB;CAEH,OAAO,OAA+B;EACpC,MAAM,EAAE,oBAAoB,KAAK,GAAG,IAClC;+CAEA,IAAI,GACJ,MAAM,WACN,MAAM,WAAW,MACjB,MAAM,QACN,MAAM,aAAa,MACnB,MAAM,UAAU,MAChB,MAAM,SACN,MAAM,UAAU,MAChB,MAAM,cAAc,MACpB,MAAM,SAAS,KAAK,IAAI,MAAM,MAAM,IAAI,IAC1C;EACA,MAAM,QAAQ,QAAQ,KAAK,GAAG,IAAc,GAAG,OAAO,kBAAkB,eAAe,CAAE;EACzF,KAAK,IAAI,KAAK,SAAS,KAAK;EAC5B,OAAO;CACT;;CAGA,KAAK,IAAgB,CAAC,GAAqB;EACzC,MAAM,QAAkB,CAAC;EACzB,MAAM,SAA8B,CAAC;EACrC,MAAM,OAAO,QAAgB,UAA2B;GACtD,MAAM,KAAK,MAAM;GACjB,OAAO,KAAK,KAAK;EACnB;EACA,IAAI,EAAE,SAAS,IAAI,kBAAkB,EAAE,OAAO;EAC9C,IAAI,EAAE,WAAW,IAAI,oBAAoB,EAAE,SAAS;EACpD,IAAI,EAAE,SAAS,IAAI,iBAAiB,EAAE,OAAO;EAC7C,IAAI,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM;EAC1C,MAAM,SAAS,OAAO,EAAE,MAAM;EAC9B,IAAI,EAAE,UAAU,OAAO,UAAU,MAAM,GAAG,IAAI,YAAY,MAAM;EAChE,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,EAAE,SAAS,IAAI,CAAC,GAAG,GAAG;EACtD,MAAM,OAAO,KAAK,GAAG,IACnB,GAAG,OAAO,GAAG,MAAM,SAAS,WAAW,MAAM,KAAK,OAAO,IAAI,GAAG,8BAChE,GAAG,QACH,QAAQ,CACV;EACA,MAAM,QAAQ,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,IAAI,OAAO;EAC9C,OAAO;GAAE;GAAO,YAAY,KAAK,SAAS,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC,CAAE,EAAE,IAAI;EAAK;CACpF;;CAGA,MAAM,WAA2B;EAC/B,OAAO,KAAK,GAAG,IAAI,sCAAsC,SAAS,CAAC,CAAC;CACtE;AACF"}
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as HttpError } from "./errors-FbMS3QBo.js";
|
|
3
|
+
import { i as sha256 } from "./crypto-DYZ4R7c2.js";
|
|
4
|
+
import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-BO89xnpb.js";
|
|
5
|
+
import { isIP, isIPv4, isIPv6 } from "node:net";
|
|
6
|
+
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
7
|
+
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
const BAN_MS = 9e5;
|
|
10
|
+
/** Tracked clients; the least recently seen is dropped beyond this. */
|
|
11
|
+
const MAX_ENTRIES = 1e4;
|
|
12
|
+
/**
|
|
13
|
+
* Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30
|
|
14
|
+
* attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is
|
|
15
|
+
* rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale
|
|
16
|
+
* password can't spend every attempt, and can't make the server hash it again either. Past a global
|
|
17
|
+
* budget of distinct failures, every attempt is slowed down (never refused, so many clients can't
|
|
18
|
+
* lock the owner out).
|
|
19
|
+
*/
|
|
20
|
+
var LoginLimiter = class {
|
|
21
|
+
clock;
|
|
22
|
+
entries = /* @__PURE__ */ new Map();
|
|
23
|
+
global = {
|
|
24
|
+
failures: 0,
|
|
25
|
+
since: 0
|
|
26
|
+
};
|
|
27
|
+
constructor(clock = Date.now) {
|
|
28
|
+
this.clock = clock;
|
|
29
|
+
}
|
|
30
|
+
/** Minutes until this client may try again, or 0. */
|
|
31
|
+
bannedMinutes(ip) {
|
|
32
|
+
const t = this.clock();
|
|
33
|
+
const entry = this.entries.get(clientKey(ip));
|
|
34
|
+
const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;
|
|
35
|
+
return until > t ? Math.ceil((until - t) / 6e4) : 0;
|
|
36
|
+
}
|
|
37
|
+
/** The global failure budget is exhausted: slow every attempt down. */
|
|
38
|
+
overGlobalBudget() {
|
|
39
|
+
return this.global.failures >= 100 && this.clock() - this.global.since < 9e5;
|
|
40
|
+
}
|
|
41
|
+
/** The client already failed with exactly this secret in the current window. */
|
|
42
|
+
knownBad(ip, secret) {
|
|
43
|
+
return !!this.current(clientKey(ip), this.clock())?.tried.has(sha256(secret));
|
|
44
|
+
}
|
|
45
|
+
/** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */
|
|
46
|
+
fail(ip, secret, opts = {}) {
|
|
47
|
+
return this.record(ip, secret, opts).banned;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Counts an attempt with this secret now, before it is verified, so concurrent requests can't
|
|
51
|
+
* all pass the ban check first. `countedGlobally` says whether it used global budget (refund it
|
|
52
|
+
* with `refundGlobal` when the secret turns out to be right).
|
|
53
|
+
*/
|
|
54
|
+
record(ip, secret, opts = {}) {
|
|
55
|
+
const t = this.clock();
|
|
56
|
+
const key = clientKey(ip);
|
|
57
|
+
const entry = this.current(key, t) ?? {
|
|
58
|
+
tried: /* @__PURE__ */ new Set(),
|
|
59
|
+
attempts: 0,
|
|
60
|
+
since: t,
|
|
61
|
+
bannedUntil: 0
|
|
62
|
+
};
|
|
63
|
+
const hash = sha256(secret);
|
|
64
|
+
let countedGlobally = false;
|
|
65
|
+
if (!entry.tried.has(hash)) {
|
|
66
|
+
entry.tried.add(hash);
|
|
67
|
+
if (opts.global !== false) {
|
|
68
|
+
if (t - this.global.since >= 9e5) this.global = {
|
|
69
|
+
failures: 0,
|
|
70
|
+
since: t
|
|
71
|
+
};
|
|
72
|
+
this.global.failures++;
|
|
73
|
+
countedGlobally = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
entry.attempts++;
|
|
77
|
+
const banned = entry.tried.size >= 5 || entry.attempts >= 30;
|
|
78
|
+
if (banned) entry.bannedUntil = t + BAN_MS;
|
|
79
|
+
this.touch(key, entry);
|
|
80
|
+
return {
|
|
81
|
+
banned,
|
|
82
|
+
countedGlobally
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
refundGlobal() {
|
|
86
|
+
if (this.global.failures > 0) this.global.failures--;
|
|
87
|
+
}
|
|
88
|
+
clear(ip) {
|
|
89
|
+
this.entries.delete(clientKey(ip));
|
|
90
|
+
}
|
|
91
|
+
/** After the password changes, previously wrong secrets may be right. */
|
|
92
|
+
reset() {
|
|
93
|
+
this.entries.clear();
|
|
94
|
+
this.global = {
|
|
95
|
+
failures: 0,
|
|
96
|
+
since: 0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
get size() {
|
|
100
|
+
return this.entries.size;
|
|
101
|
+
}
|
|
102
|
+
/** The client's entry if its window (or ban) is still running. */
|
|
103
|
+
current(key, t) {
|
|
104
|
+
const entry = this.entries.get(key);
|
|
105
|
+
if (!entry) return void 0;
|
|
106
|
+
if (entry.bannedUntil > t || !entry.bannedUntil && t - entry.since < 9e5) return entry;
|
|
107
|
+
this.entries.delete(key);
|
|
108
|
+
}
|
|
109
|
+
/** Map order doubles as LRU order: re-insert on use, evict from the front. */
|
|
110
|
+
touch(key, entry) {
|
|
111
|
+
this.entries.delete(key);
|
|
112
|
+
this.entries.set(key, entry);
|
|
113
|
+
while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */
|
|
117
|
+
function clientKey(ip) {
|
|
118
|
+
const addr = ip.replace(/%.*$/, "").toLowerCase();
|
|
119
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(addr);
|
|
120
|
+
if (mapped) return mapped[1];
|
|
121
|
+
if (!isIPv6(addr)) return addr;
|
|
122
|
+
const [head = "", tail] = addr.split("::");
|
|
123
|
+
const left = head ? head.split(":") : [];
|
|
124
|
+
const right = tail ? tail.split(":") : [];
|
|
125
|
+
return `${(tail === void 0 ? left : [
|
|
126
|
+
...left,
|
|
127
|
+
...Array(8 - left.length - right.length).fill("0"),
|
|
128
|
+
...right
|
|
129
|
+
]).slice(0, 4).map((g) => g.replace(/^0+(?=.)/, "")).join(":")}::/64`;
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region server/auth/proxy.ts
|
|
133
|
+
function parseV4(ip) {
|
|
134
|
+
return ip.split(".").reduce((acc, part) => acc << 8n | BigInt(Number(part)), 0n);
|
|
135
|
+
}
|
|
136
|
+
function parseAddress(raw) {
|
|
137
|
+
const ip = raw.trim().replace(/^\[|\]$/g, "").replace(/%.*$/, "").toLowerCase();
|
|
138
|
+
if (isIPv4(ip)) return {
|
|
139
|
+
v6: false,
|
|
140
|
+
n: parseV4(ip)
|
|
141
|
+
};
|
|
142
|
+
if (!isIPv6(ip)) return null;
|
|
143
|
+
const mapped = /^(?:0{0,4}:){0,5}:?ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(ip);
|
|
144
|
+
if (mapped && isIPv4(mapped[1])) return {
|
|
145
|
+
v6: false,
|
|
146
|
+
n: parseV4(mapped[1])
|
|
147
|
+
};
|
|
148
|
+
let text = ip;
|
|
149
|
+
const v4Tail = /(\d+\.\d+\.\d+\.\d+)$/.exec(text);
|
|
150
|
+
if (v4Tail) {
|
|
151
|
+
const n = parseV4(v4Tail[1]);
|
|
152
|
+
text = text.slice(0, -v4Tail[1].length) + `${(n >> 16n).toString(16)}:${(n & 65535n).toString(16)}`;
|
|
153
|
+
}
|
|
154
|
+
const [head = "", tail] = text.split("::");
|
|
155
|
+
const left = head ? head.split(":") : [];
|
|
156
|
+
const right = tail ? tail.split(":") : [];
|
|
157
|
+
const groups = tail === void 0 ? left : [
|
|
158
|
+
...left,
|
|
159
|
+
...Array(8 - left.length - right.length).fill("0"),
|
|
160
|
+
...right
|
|
161
|
+
];
|
|
162
|
+
if (groups.length !== 8) return null;
|
|
163
|
+
return {
|
|
164
|
+
v6: true,
|
|
165
|
+
n: groups.reduce((acc, g) => acc << 16n | BigInt(Number.parseInt(g || "0", 16)), 0n)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Parses "10.0.0.5", "10.0.0.0/8", "fd00::/8", "::ffff:10.0.0.0/104"; null when invalid. A /0
|
|
170
|
+
* (trust everyone) is refused: it would let any client pick its own address.
|
|
171
|
+
*/
|
|
172
|
+
function parseRange(entry) {
|
|
173
|
+
const parts = entry.trim().split("/");
|
|
174
|
+
if (parts.length > 2) return null;
|
|
175
|
+
const [ip = "", prefix] = parts;
|
|
176
|
+
const addr = parseAddress(ip);
|
|
177
|
+
if (!addr) return null;
|
|
178
|
+
let bits = addr.v6 ? 128 : 32;
|
|
179
|
+
if (prefix !== void 0) {
|
|
180
|
+
if (!/^\d{1,3}$/.test(prefix)) return null;
|
|
181
|
+
bits = Number(prefix);
|
|
182
|
+
if (!addr.v6 && ip.includes(":")) bits -= 96;
|
|
183
|
+
if (bits <= 0 || bits > (addr.v6 ? 128 : 32)) return null;
|
|
184
|
+
}
|
|
185
|
+
const width = BigInt(addr.v6 ? 128 : 32);
|
|
186
|
+
const mask = bits === 0 ? 0n : (1n << BigInt(bits)) - 1n << width - BigInt(bits);
|
|
187
|
+
return {
|
|
188
|
+
v6: addr.v6,
|
|
189
|
+
base: addr.n & mask,
|
|
190
|
+
bits
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function inRange(addr, r) {
|
|
194
|
+
if (addr.v6 !== r.v6) return false;
|
|
195
|
+
const width = BigInt(r.v6 ? 128 : 32);
|
|
196
|
+
const mask = r.bits === 0 ? 0n : (1n << BigInt(r.bits)) - 1n << width - BigInt(r.bits);
|
|
197
|
+
return (addr.n & mask) === r.base;
|
|
198
|
+
}
|
|
199
|
+
const LOOPBACK = [parseRange("127.0.0.0/8"), parseRange("::1/128")];
|
|
200
|
+
let cache;
|
|
201
|
+
/** TRUST_PROXY_FROM as ranges. Invalid entries are reported once and ignored. */
|
|
202
|
+
function trustedRanges(raw) {
|
|
203
|
+
if (cache?.raw === raw) return cache.ranges;
|
|
204
|
+
const ranges = [];
|
|
205
|
+
const invalid = [];
|
|
206
|
+
for (const entry of raw.split(/[,\s]+/).filter(Boolean)) {
|
|
207
|
+
const r = parseRange(entry);
|
|
208
|
+
if (r) ranges.push(r);
|
|
209
|
+
else invalid.push(entry);
|
|
210
|
+
}
|
|
211
|
+
if (invalid.length) process.stderr.write(`agentinbox: ignoring invalid TRUST_PROXY_FROM entries: ${invalid.join(", ")}\n`);
|
|
212
|
+
cache = {
|
|
213
|
+
raw,
|
|
214
|
+
ranges
|
|
215
|
+
};
|
|
216
|
+
return ranges;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether forwarding headers from this socket peer may be believed: loopback always (a proxy on
|
|
220
|
+
* the same host), anything else only when listed in TRUST_PROXY_FROM (IPs or CIDRs, comma-separated).
|
|
221
|
+
* The daemon usually binds 0.0.0.0, so trusting whole private ranges would let any LAN or tailnet
|
|
222
|
+
* peer forge its address.
|
|
223
|
+
*/
|
|
224
|
+
function isTrustedProxy(peer, trustFrom = process.env.TRUST_PROXY_FROM ?? "") {
|
|
225
|
+
const addr = parseAddress(peer);
|
|
226
|
+
if (!addr) return false;
|
|
227
|
+
return [...LOOPBACK, ...trustedRanges(trustFrom)].some((r) => inRange(addr, r));
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region server/auth/sessions.ts
|
|
231
|
+
const SESSION_TTL_MS = 2592e6;
|
|
232
|
+
/** Sliding refresh never extends a session past this age: sign in again after 90 days. */
|
|
233
|
+
const SESSION_MAX_AGE_MS = 7776e6;
|
|
234
|
+
/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */
|
|
235
|
+
const REFRESH_AFTER_MS = 864e5;
|
|
236
|
+
/** Admin console sessions. Only sha256(token) is stored. */
|
|
237
|
+
var SessionStore = class {
|
|
238
|
+
db;
|
|
239
|
+
clock;
|
|
240
|
+
constructor(db, clock = Date.now) {
|
|
241
|
+
this.db = db;
|
|
242
|
+
this.clock = clock;
|
|
243
|
+
}
|
|
244
|
+
create(meta = {}) {
|
|
245
|
+
const token = randomBytes(32).toString("base64url");
|
|
246
|
+
const t = this.clock();
|
|
247
|
+
this.db.run("INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)", sha256(token), new Date(t).toISOString(), new Date(t + SESSION_TTL_MS).toISOString(), meta.ip ?? null, meta.userAgent?.slice(0, 300) ?? null);
|
|
248
|
+
return token;
|
|
249
|
+
}
|
|
250
|
+
check(token) {
|
|
251
|
+
if (!token) return {
|
|
252
|
+
valid: false,
|
|
253
|
+
refreshed: false
|
|
254
|
+
};
|
|
255
|
+
const hash = sha256(token);
|
|
256
|
+
const row = this.db.get("SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?", hash);
|
|
257
|
+
if (!row) return {
|
|
258
|
+
valid: false,
|
|
259
|
+
refreshed: false
|
|
260
|
+
};
|
|
261
|
+
const t = this.clock();
|
|
262
|
+
const expires = Date.parse(row.expires_at);
|
|
263
|
+
const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;
|
|
264
|
+
if (!(expires > t) || !(hardLimit > t)) {
|
|
265
|
+
this.db.run("DELETE FROM admin_sessions WHERE token_hash = ?", hash);
|
|
266
|
+
return {
|
|
267
|
+
valid: false,
|
|
268
|
+
refreshed: false
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const next = Math.min(t + SESSION_TTL_MS, hardLimit);
|
|
272
|
+
if (next - expires < REFRESH_AFTER_MS) return {
|
|
273
|
+
valid: true,
|
|
274
|
+
refreshed: false
|
|
275
|
+
};
|
|
276
|
+
this.db.run("UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?", new Date(next).toISOString(), hash);
|
|
277
|
+
return {
|
|
278
|
+
valid: true,
|
|
279
|
+
refreshed: true
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
delete(token) {
|
|
283
|
+
this.db.run("DELETE FROM admin_sessions WHERE token_hash = ?", sha256(token));
|
|
284
|
+
}
|
|
285
|
+
/** Revokes every session, optionally keeping one (the caller's own). */
|
|
286
|
+
deleteAll(exceptToken) {
|
|
287
|
+
if (exceptToken) return this.db.run("DELETE FROM admin_sessions WHERE token_hash != ?", sha256(exceptToken)).changes;
|
|
288
|
+
return this.db.run("DELETE FROM admin_sessions").changes;
|
|
289
|
+
}
|
|
290
|
+
pruneExpired() {
|
|
291
|
+
return this.db.run("DELETE FROM admin_sessions WHERE expires_at <= ?", new Date(this.clock()).toISOString()).changes;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region server/auth/index.ts
|
|
296
|
+
const SESSION_COOKIE = "aib_session";
|
|
297
|
+
const CSRF_HEADER = "x-agentinbox-csrf";
|
|
298
|
+
const MAX_PASSWORD_LENGTH = 1024;
|
|
299
|
+
/** Hands out at most `max` slots; waiters are served in order. */
|
|
300
|
+
var Semaphore = class {
|
|
301
|
+
max;
|
|
302
|
+
active = 0;
|
|
303
|
+
waiters = [];
|
|
304
|
+
constructor(max) {
|
|
305
|
+
this.max = max;
|
|
306
|
+
}
|
|
307
|
+
get inUse() {
|
|
308
|
+
return this.active;
|
|
309
|
+
}
|
|
310
|
+
async acquire() {
|
|
311
|
+
if (this.active < this.max) this.active++;
|
|
312
|
+
else await new Promise((resolve) => this.waiters.push(resolve));
|
|
313
|
+
let released = false;
|
|
314
|
+
return () => {
|
|
315
|
+
if (released) return;
|
|
316
|
+
released = true;
|
|
317
|
+
const next = this.waiters.shift();
|
|
318
|
+
if (next) next();
|
|
319
|
+
else this.active--;
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const instances = /* @__PURE__ */ new WeakMap();
|
|
324
|
+
function adminAuth(ctx) {
|
|
325
|
+
let auth = instances.get(ctx);
|
|
326
|
+
if (!auth) {
|
|
327
|
+
auth = {
|
|
328
|
+
sessions: new SessionStore(ctx.db),
|
|
329
|
+
limiter: new LoginLimiter(),
|
|
330
|
+
lanes: /* @__PURE__ */ new Map(),
|
|
331
|
+
verifySlots: new Semaphore(4)
|
|
332
|
+
};
|
|
333
|
+
instances.set(ctx, auth);
|
|
334
|
+
}
|
|
335
|
+
return auth;
|
|
336
|
+
}
|
|
337
|
+
/** Claimed = an admin password exists. Before that only the setup-token claim works. */
|
|
338
|
+
const isClaimed = (ctx) => !!ctx.settings.getState("admin.passwordHash");
|
|
339
|
+
/** The current setup token while unclaimed (created on demand), otherwise null. */
|
|
340
|
+
function currentSetupToken(ctx) {
|
|
341
|
+
return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);
|
|
342
|
+
}
|
|
343
|
+
/** `agentinbox reset-password`: back to the unclaimed state with a fresh setup link. */
|
|
344
|
+
function resetAdminPassword(ctx) {
|
|
345
|
+
const auth = adminAuth(ctx);
|
|
346
|
+
ctx.db.tx(() => {
|
|
347
|
+
ctx.settings.deleteState("admin.passwordHash");
|
|
348
|
+
auth.sessions.deleteAll();
|
|
349
|
+
});
|
|
350
|
+
auth.limiter.reset();
|
|
351
|
+
ctx.audit.record({
|
|
352
|
+
actorType: "system",
|
|
353
|
+
action: "admin.password_reset",
|
|
354
|
+
outcome: "ok"
|
|
355
|
+
});
|
|
356
|
+
return regenerateSetupToken(ctx.config.dataDir);
|
|
357
|
+
}
|
|
358
|
+
function socketAddress(c) {
|
|
359
|
+
try {
|
|
360
|
+
return getConnInfo(c).remote.address ?? null;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the
|
|
367
|
+
* connection comes from a trusted proxy (loopback, or TRUST_PROXY_FROM); it is read from the
|
|
368
|
+
* right, since proxies append the address they saw and entries further left are client-controlled.
|
|
369
|
+
* TRUST_PROXY_HOPS (default 1) is the number of proxies in front of agentinbox.
|
|
370
|
+
*/
|
|
371
|
+
/** Some proxies (Azure, IIS) append ":port" to X-Forwarded-For entries. */
|
|
372
|
+
function stripPort(entry) {
|
|
373
|
+
const v6 = /^\[([^\]]+)\](?::\d+)?$/.exec(entry);
|
|
374
|
+
if (v6) return v6[1];
|
|
375
|
+
const v4 = /^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/.exec(entry);
|
|
376
|
+
return v4 ? v4[1] : entry;
|
|
377
|
+
}
|
|
378
|
+
let warnedUntrustedProxy = false;
|
|
379
|
+
/** Upgrades from the old private-range rule silently lost proxy trust; say so once. */
|
|
380
|
+
function warnUntrustedProxy(peer) {
|
|
381
|
+
if (warnedUntrustedProxy) return;
|
|
382
|
+
warnedUntrustedProxy = true;
|
|
383
|
+
process.stderr.write(`agentinbox: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\n`);
|
|
384
|
+
}
|
|
385
|
+
function clientIp(c, trustProxy) {
|
|
386
|
+
const peer = socketAddress(c);
|
|
387
|
+
if (trustProxy && peer && !isTrustedProxy(peer) && c.req.header("x-forwarded-for")) warnUntrustedProxy(peer);
|
|
388
|
+
if (trustProxy && peer && isTrustedProxy(peer)) {
|
|
389
|
+
const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? "1", 10) || 1);
|
|
390
|
+
const chain = (c.req.header("x-forwarded-for") ?? "").split(",").map((s) => stripPort(s.trim())).filter(Boolean);
|
|
391
|
+
const forwarded = chain[chain.length - hops];
|
|
392
|
+
if (forwarded && isIP(forwarded)) return forwarded;
|
|
393
|
+
}
|
|
394
|
+
return peer ?? "unknown";
|
|
395
|
+
}
|
|
396
|
+
function isSecure(c, trustProxy) {
|
|
397
|
+
if (new URL(c.req.url).protocol === "https:") return true;
|
|
398
|
+
const peer = socketAddress(c);
|
|
399
|
+
return trustProxy && !!peer && isTrustedProxy(peer) && c.req.header("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase() === "https";
|
|
400
|
+
}
|
|
401
|
+
function sessionToken(c) {
|
|
402
|
+
return getCookie(c, "aib_session") || void 0;
|
|
403
|
+
}
|
|
404
|
+
function writeSessionCookie(c, token, trustProxy) {
|
|
405
|
+
setCookie(c, SESSION_COOKIE, token, {
|
|
406
|
+
httpOnly: true,
|
|
407
|
+
sameSite: "Lax",
|
|
408
|
+
path: "/",
|
|
409
|
+
secure: isSecure(c, trustProxy),
|
|
410
|
+
maxAge: Math.floor(SESSION_TTL_MS / 1e3)
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** Creates a session for the requester and sets the cookie. */
|
|
414
|
+
function startSession(c, ctx) {
|
|
415
|
+
writeSessionCookie(c, adminAuth(ctx).sessions.create({
|
|
416
|
+
ip: clientIp(c, ctx.config.trustProxy),
|
|
417
|
+
userAgent: c.req.header("user-agent")
|
|
418
|
+
}), ctx.config.trustProxy);
|
|
419
|
+
}
|
|
420
|
+
function endSession(c, ctx) {
|
|
421
|
+
const token = sessionToken(c);
|
|
422
|
+
if (token) adminAuth(ctx).sessions.delete(token);
|
|
423
|
+
deleteCookie(c, SESSION_COOKIE, { path: "/" });
|
|
424
|
+
}
|
|
425
|
+
/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */
|
|
426
|
+
function hasSession(c, ctx) {
|
|
427
|
+
const token = sessionToken(c);
|
|
428
|
+
const check = adminAuth(ctx).sessions.check(token);
|
|
429
|
+
if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);
|
|
430
|
+
return check.valid;
|
|
431
|
+
}
|
|
432
|
+
/** Requires an admin session on every route it guards. */
|
|
433
|
+
function requireAdmin(ctx) {
|
|
434
|
+
return async (c, next) => {
|
|
435
|
+
if (!hasSession(c, ctx)) {
|
|
436
|
+
if (!isClaimed(ctx)) throw new HttpError(401, "setup_required", "This instance has not been claimed yet", "Open the setup link printed by agentinbox on startup.");
|
|
437
|
+
throw new HttpError(401, "unauthorized", "Sign in to continue");
|
|
438
|
+
}
|
|
439
|
+
c.set("admin", true);
|
|
440
|
+
await next();
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const MUTATING = /* @__PURE__ */ new Set([
|
|
444
|
+
"POST",
|
|
445
|
+
"PUT",
|
|
446
|
+
"PATCH",
|
|
447
|
+
"DELETE"
|
|
448
|
+
]);
|
|
449
|
+
/**
|
|
450
|
+
* CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server
|
|
451
|
+
* never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.
|
|
452
|
+
*/
|
|
453
|
+
function csrfGuard() {
|
|
454
|
+
return async (c, next) => {
|
|
455
|
+
if (MUTATING.has(c.req.method) && c.req.header("x-agentinbox-csrf") !== "1") throw new HttpError(403, "csrf", `Missing ${CSRF_HEADER} header`, `Send "${CSRF_HEADER}: 1" with every mutating /api request.`);
|
|
456
|
+
await next();
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/** 429 response for a locked-out address. */
|
|
460
|
+
function lockedOut(c, minutes) {
|
|
461
|
+
c.header("Retry-After", String(minutes * 60));
|
|
462
|
+
const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? "" : "s"}.`;
|
|
463
|
+
return c.json({ error: {
|
|
464
|
+
code: "locked_out",
|
|
465
|
+
message
|
|
466
|
+
} }, 429);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Verifies a secret under the failed-login limiter.
|
|
470
|
+
* - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most
|
|
471
|
+
* MAX_CONCURRENT_VERIFICATIONS run at once overall.
|
|
472
|
+
* - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so
|
|
473
|
+
* concurrent requests can't all slip past the ban.
|
|
474
|
+
* - Locked-out clients and secrets this client already failed with never reach `verify`.
|
|
475
|
+
* - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.
|
|
476
|
+
*/
|
|
477
|
+
async function checkSecret(ctx, ip, secret, verify, opts = {}) {
|
|
478
|
+
const auth = adminAuth(ctx);
|
|
479
|
+
const key = clientKey(ip);
|
|
480
|
+
const lane = auth.lanes.get(key) ?? {
|
|
481
|
+
tail: Promise.resolve(),
|
|
482
|
+
queued: 0
|
|
483
|
+
};
|
|
484
|
+
if (lane.queued > 4) return {
|
|
485
|
+
ok: false,
|
|
486
|
+
lockedMinutes: 0,
|
|
487
|
+
nowBanned: false,
|
|
488
|
+
busy: true
|
|
489
|
+
};
|
|
490
|
+
lane.queued++;
|
|
491
|
+
auth.lanes.set(key, lane);
|
|
492
|
+
const previous = lane.tail;
|
|
493
|
+
let done;
|
|
494
|
+
lane.tail = new Promise((resolve) => done = resolve);
|
|
495
|
+
try {
|
|
496
|
+
await previous;
|
|
497
|
+
return await checkOnce(auth, ip, secret, verify, opts);
|
|
498
|
+
} finally {
|
|
499
|
+
done();
|
|
500
|
+
if (--lane.queued === 0) auth.lanes.delete(key);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function checkOnce(auth, ip, secret, verify, opts) {
|
|
504
|
+
const { limiter } = auth;
|
|
505
|
+
const global = opts.global !== false;
|
|
506
|
+
if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) await new Promise((r) => setTimeout(r, opts.slowdownMs ?? 2e3));
|
|
507
|
+
const locked = limiter.bannedMinutes(ip);
|
|
508
|
+
if (locked) return {
|
|
509
|
+
ok: false,
|
|
510
|
+
lockedMinutes: locked,
|
|
511
|
+
nowBanned: false
|
|
512
|
+
};
|
|
513
|
+
if (limiter.knownBad(ip, secret)) return {
|
|
514
|
+
ok: false,
|
|
515
|
+
lockedMinutes: 0,
|
|
516
|
+
nowBanned: limiter.fail(ip, secret, { global })
|
|
517
|
+
};
|
|
518
|
+
const reservation = limiter.record(ip, secret, { global });
|
|
519
|
+
const release = await auth.verifySlots.acquire();
|
|
520
|
+
let ok = false;
|
|
521
|
+
try {
|
|
522
|
+
ok = await verify();
|
|
523
|
+
} finally {
|
|
524
|
+
release();
|
|
525
|
+
}
|
|
526
|
+
if (!ok) return {
|
|
527
|
+
ok: false,
|
|
528
|
+
lockedMinutes: 0,
|
|
529
|
+
nowBanned: reservation.banned
|
|
530
|
+
};
|
|
531
|
+
limiter.clear(ip);
|
|
532
|
+
if (reservation.countedGlobally) limiter.refundGlobal();
|
|
533
|
+
return { ok: true };
|
|
534
|
+
}
|
|
535
|
+
/** The 429 for a rejected check (locked out or too many at once), or null to continue. */
|
|
536
|
+
function rejection(c, check) {
|
|
537
|
+
if (check.ok) return null;
|
|
538
|
+
if (check.busy) {
|
|
539
|
+
c.header("Retry-After", "2");
|
|
540
|
+
return c.json({ error: {
|
|
541
|
+
code: "too_many_requests",
|
|
542
|
+
message: "Another sign-in attempt from your address is still being checked. Try again in a moment."
|
|
543
|
+
} }, 429);
|
|
544
|
+
}
|
|
545
|
+
return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
export { sessionToken as _, adminAuth as a, LoginLimiter as b, csrfGuard as c, hasSession as d, isClaimed as f, resetAdminPassword as g, requireAdmin as h, Semaphore as i, currentSetupToken as l, rejection as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, lockedOut as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, startSession as v, clientKey as x, BAN_MS as y };
|
|
549
|
+
|
|
550
|
+
//# sourceMappingURL=auth-0NrMvHdE.js.map
|