agentinbox-cli 0.0.0-stage → 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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -2
  3. package/dist/audit-CQe-_dPQ.js +2 -0
  4. package/dist/audit-DETqygzG.js +65 -0
  5. package/dist/audit-DETqygzG.js.map +1 -0
  6. package/dist/auth-BniddcRD.js +447 -0
  7. package/dist/auth-BniddcRD.js.map +1 -0
  8. package/dist/auth-C2v7bEmQ.js +2 -0
  9. package/dist/bus-Cz0flS37.js +12 -0
  10. package/dist/bus-Cz0flS37.js.map +1 -0
  11. package/dist/bus-OKIGut72.js +2 -0
  12. package/dist/cli.js +141 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config-CIvgMqLx.js +2 -0
  15. package/dist/config-v975gxv4.js +44 -0
  16. package/dist/config-v975gxv4.js.map +1 -0
  17. package/dist/crypto-DYZ4R7c2.js +74 -0
  18. package/dist/crypto-DYZ4R7c2.js.map +1 -0
  19. package/dist/db-CfoMO5zW.js +2 -0
  20. package/dist/db-DdagFmUY.js +247 -0
  21. package/dist/db-DdagFmUY.js.map +1 -0
  22. package/dist/errors-CJLUPllS.js +38 -0
  23. package/dist/errors-CJLUPllS.js.map +1 -0
  24. package/dist/instance-DZMV78rI.js +2 -0
  25. package/dist/instance-Dy2jeyZc.js +174 -0
  26. package/dist/instance-Dy2jeyZc.js.map +1 -0
  27. package/dist/server-CWqKeCGn.js +981 -0
  28. package/dist/server-CWqKeCGn.js.map +1 -0
  29. package/dist/service-63pdSmhO.js +332 -0
  30. package/dist/service-63pdSmhO.js.map +1 -0
  31. package/dist/settings-Dq2ETFsd.js +98 -0
  32. package/dist/settings-Dq2ETFsd.js.map +1 -0
  33. package/dist/settings-qtn5WTBM.js +2 -0
  34. package/dist/setup-token-Bkwk84l7.js +47 -0
  35. package/dist/setup-token-Bkwk84l7.js.map +1 -0
  36. package/dist/skill/SKILL.md +226 -0
  37. package/dist/skill-BWWtinsE.js +2 -0
  38. package/dist/skill-WKbeyNct.js +43 -0
  39. package/dist/skill-WKbeyNct.js.map +1 -0
  40. package/dist/urls-CNYhbeUQ.js +2 -0
  41. package/dist/urls-DmnonkJo.js +22 -0
  42. package/dist/urls-DmnonkJo.js.map +1 -0
  43. package/dist/web/assets/_agentId-il4y7OCG.js +1 -0
  44. package/dist/web/assets/_app-BIO8WIYB.js +1 -0
  45. package/dist/web/assets/_app-wlLL4MrD.js +63 -0
  46. package/dist/web/assets/_webhookId-CiwDrkY4.js +1 -0
  47. package/dist/web/assets/account-status-at9Bv3_T.js +1 -0
  48. package/dist/web/assets/accounts-B4rfPwCV.js +1 -0
  49. package/dist/web/assets/accounts-list-e--JWxHH.js +1 -0
  50. package/dist/web/assets/activity-zrckFhPk.js +1 -0
  51. package/dist/web/assets/agent-create-form-DaWPR6kx.js +1 -0
  52. package/dist/web/assets/agents-D4gPq4dt.js +1 -0
  53. package/dist/web/assets/approvals-DaoZ6wuf.js +2 -0
  54. package/dist/web/assets/arrow-left-DVJV4Zx7.js +1 -0
  55. package/dist/web/assets/arrow-right-4PFX6MSS.js +1 -0
  56. package/dist/web/assets/audit-bAdLe-px.js +1 -0
  57. package/dist/web/assets/audit-bits-DQHo74i6.js +1 -0
  58. package/dist/web/assets/bot-DVw15lBn.js +1 -0
  59. package/dist/web/assets/card-EQTpqntn.js +1 -0
  60. package/dist/web/assets/checkbox-DDZZurgK.js +1 -0
  61. package/dist/web/assets/chevron-down-892ylB4R.js +1 -0
  62. package/dist/web/assets/chevron-right-CbBKS6-T.js +1 -0
  63. package/dist/web/assets/chips-input-Btdc_f7b.js +1 -0
  64. package/dist/web/assets/circle-x-Bi4IGwky.js +1 -0
  65. package/dist/web/assets/common-h6596gEC.js +1 -0
  66. package/dist/web/assets/connect-host-Co31qIYr.js +1 -0
  67. package/dist/web/assets/data-table-DsKrM62X.js +1 -0
  68. package/dist/web/assets/dialog-CgZr27yw.js +1 -0
  69. package/dist/web/assets/dist-B0e3Ju3r.js +1 -0
  70. package/dist/web/assets/empty-CE_WZGEC.js +1 -0
  71. package/dist/web/assets/es2015-DPaldoxG.js +41 -0
  72. package/dist/web/assets/event-row-C0qRfWuV.js +1 -0
  73. package/dist/web/assets/events-Svyij7mp.js +1 -0
  74. package/dist/web/assets/eye-off-DjpaihHf.js +1 -0
  75. package/dist/web/assets/field-CcyMn3ey.js +1 -0
  76. package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  77. package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  78. package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  79. package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  80. package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  81. package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  82. package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  83. package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  84. package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  85. package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  86. package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  87. package/dist/web/assets/get-started-Cxfw97sJ.js +1 -0
  88. package/dist/web/assets/grants-BilXCDps.js +1 -0
  89. package/dist/web/assets/index-DgIgw66p.css +2 -0
  90. package/dist/web/assets/index-TbxC5ZgP.js +10 -0
  91. package/dist/web/assets/input-CMhmmHiH.js +1 -0
  92. package/dist/web/assets/input-group-DRs3TNaT.js +1 -0
  93. package/dist/web/assets/key-round-CVIZRCLM.js +1 -0
  94. package/dist/web/assets/labels-C_Pwium9.js +1 -0
  95. package/dist/web/assets/login-DUJia7lg.js +1 -0
  96. package/dist/web/assets/mail-CAVmuESo.js +1 -0
  97. package/dist/web/assets/mutations-D_GluJ0j.js +1 -0
  98. package/dist/web/assets/paperclip-QFjM4y6n.js +1 -0
  99. package/dist/web/assets/password-input-_m9Ycphv.js +1 -0
  100. package/dist/web/assets/pencil-Bf1JhsG0.js +1 -0
  101. package/dist/web/assets/play-DiPk49JH.js +1 -0
  102. package/dist/web/assets/plus-CETmK0SB.js +1 -0
  103. package/dist/web/assets/preload-helper-BFuRUfdt.js +1 -0
  104. package/dist/web/assets/progress-3ZOsbLSK.js +6 -0
  105. package/dist/web/assets/queries-a3M5_loa.js +2 -0
  106. package/dist/web/assets/refresh-cw-DOn9kD7g.js +1 -0
  107. package/dist/web/assets/search-gcYjitzV.js +1 -0
  108. package/dist/web/assets/select-_tULk-vs.js +1 -0
  109. package/dist/web/assets/send-BhQwhuJo.js +1 -0
  110. package/dist/web/assets/settings-Dl4wb4f0.js +1 -0
  111. package/dist/web/assets/settings-form-D21eKBh-.js +1 -0
  112. package/dist/web/assets/setup-DYnNhnfH.js +1 -0
  113. package/dist/web/assets/sheet--2lKU0jN.js +1 -0
  114. package/dist/web/assets/shield-alert-CbFgd06X.js +1 -0
  115. package/dist/web/assets/stamp-CH2OvjH2.js +1 -0
  116. package/dist/web/assets/stream-6OwrgVZw.js +1 -0
  117. package/dist/web/assets/tabs-CiAUEj_y.js +1 -0
  118. package/dist/web/assets/tag-CP72Kk6Y.js +1 -0
  119. package/dist/web/assets/test-connection-F2tMowbv.js +1 -0
  120. package/dist/web/assets/textarea-CCokBZIw.js +1 -0
  121. package/dist/web/assets/toggle-group-D1pC7sJr.js +1 -0
  122. package/dist/web/assets/trash-DxdMNg8B.js +1 -0
  123. package/dist/web/assets/use-sign-out-BuzlTb0Q.js +1 -0
  124. package/dist/web/assets/useInfiniteQuery-DE6WChlb.js +1 -0
  125. package/dist/web/assets/useMatch-BKUnm54_.js +1 -0
  126. package/dist/web/assets/useMutation-DGl4PaZ1.js +1 -0
  127. package/dist/web/assets/useQuery-DAhf1YDH.js +1 -0
  128. package/dist/web/assets/useSelector-D70eKNYs.js +1 -0
  129. package/dist/web/assets/webhook-BWT75M78.js +1 -0
  130. package/dist/web/assets/webhook-sheet-4U_6Rw8I.js +34 -0
  131. package/dist/web/assets/webhooks-C1JvTI88.js +1 -0
  132. package/dist/web/assets/with-selector-CkNyXYMW.js +1 -0
  133. package/dist/web/assets/x-BHjDLXNO.js +1 -0
  134. package/dist/web/favicon.svg +1 -0
  135. package/dist/web/index.html +31 -0
  136. package/package.json +92 -4
@@ -0,0 +1,447 @@
1
+ #!/usr/bin/env node
2
+ import { t as HttpError } from "./errors-CJLUPllS.js";
3
+ import { i as sha256 } from "./crypto-DYZ4R7c2.js";
4
+ import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-Bkwk84l7.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/sessions.ts
133
+ const SESSION_TTL_MS = 2592e6;
134
+ /** Sliding refresh never extends a session past this age: sign in again after 90 days. */
135
+ const SESSION_MAX_AGE_MS = 7776e6;
136
+ /** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */
137
+ const REFRESH_AFTER_MS = 864e5;
138
+ /** Admin console sessions. Only sha256(token) is stored. */
139
+ var SessionStore = class {
140
+ db;
141
+ clock;
142
+ constructor(db, clock = Date.now) {
143
+ this.db = db;
144
+ this.clock = clock;
145
+ }
146
+ create(meta = {}) {
147
+ const token = randomBytes(32).toString("base64url");
148
+ const t = this.clock();
149
+ 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);
150
+ return token;
151
+ }
152
+ check(token) {
153
+ if (!token) return {
154
+ valid: false,
155
+ refreshed: false
156
+ };
157
+ const hash = sha256(token);
158
+ const row = this.db.get("SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?", hash);
159
+ if (!row) return {
160
+ valid: false,
161
+ refreshed: false
162
+ };
163
+ const t = this.clock();
164
+ const expires = Date.parse(row.expires_at);
165
+ const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;
166
+ if (!(expires > t) || !(hardLimit > t)) {
167
+ this.db.run("DELETE FROM admin_sessions WHERE token_hash = ?", hash);
168
+ return {
169
+ valid: false,
170
+ refreshed: false
171
+ };
172
+ }
173
+ const next = Math.min(t + SESSION_TTL_MS, hardLimit);
174
+ if (next - expires < REFRESH_AFTER_MS) return {
175
+ valid: true,
176
+ refreshed: false
177
+ };
178
+ this.db.run("UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?", new Date(next).toISOString(), hash);
179
+ return {
180
+ valid: true,
181
+ refreshed: true
182
+ };
183
+ }
184
+ delete(token) {
185
+ this.db.run("DELETE FROM admin_sessions WHERE token_hash = ?", sha256(token));
186
+ }
187
+ /** Revokes every session, optionally keeping one (the caller's own). */
188
+ deleteAll(exceptToken) {
189
+ if (exceptToken) return this.db.run("DELETE FROM admin_sessions WHERE token_hash != ?", sha256(exceptToken)).changes;
190
+ return this.db.run("DELETE FROM admin_sessions").changes;
191
+ }
192
+ pruneExpired() {
193
+ return this.db.run("DELETE FROM admin_sessions WHERE expires_at <= ?", new Date(this.clock()).toISOString()).changes;
194
+ }
195
+ };
196
+ //#endregion
197
+ //#region server/auth/index.ts
198
+ const SESSION_COOKIE = "aib_session";
199
+ const CSRF_HEADER = "x-agentinbox-csrf";
200
+ const MAX_PASSWORD_LENGTH = 1024;
201
+ /** Hands out at most `max` slots; waiters are served in order. */
202
+ var Semaphore = class {
203
+ max;
204
+ active = 0;
205
+ waiters = [];
206
+ constructor(max) {
207
+ this.max = max;
208
+ }
209
+ get inUse() {
210
+ return this.active;
211
+ }
212
+ async acquire() {
213
+ if (this.active < this.max) this.active++;
214
+ else await new Promise((resolve) => this.waiters.push(resolve));
215
+ let released = false;
216
+ return () => {
217
+ if (released) return;
218
+ released = true;
219
+ const next = this.waiters.shift();
220
+ if (next) next();
221
+ else this.active--;
222
+ };
223
+ }
224
+ };
225
+ const instances = /* @__PURE__ */ new WeakMap();
226
+ function adminAuth(ctx) {
227
+ let auth = instances.get(ctx);
228
+ if (!auth) {
229
+ auth = {
230
+ sessions: new SessionStore(ctx.db),
231
+ limiter: new LoginLimiter(),
232
+ lanes: /* @__PURE__ */ new Map(),
233
+ verifySlots: new Semaphore(4)
234
+ };
235
+ instances.set(ctx, auth);
236
+ }
237
+ return auth;
238
+ }
239
+ /** Claimed = an admin password exists. Before that only the setup-token claim works. */
240
+ const isClaimed = (ctx) => !!ctx.settings.getState("admin.passwordHash");
241
+ /** The current setup token while unclaimed (created on demand), otherwise null. */
242
+ function currentSetupToken(ctx) {
243
+ return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);
244
+ }
245
+ /** `agentinbox reset-password`: back to the unclaimed state with a fresh setup link. */
246
+ function resetAdminPassword(ctx) {
247
+ const auth = adminAuth(ctx);
248
+ ctx.db.tx(() => {
249
+ ctx.settings.deleteState("admin.passwordHash");
250
+ auth.sessions.deleteAll();
251
+ });
252
+ auth.limiter.reset();
253
+ ctx.audit.record({
254
+ actorType: "system",
255
+ action: "admin.password_reset",
256
+ outcome: "ok"
257
+ });
258
+ return regenerateSetupToken(ctx.config.dataDir);
259
+ }
260
+ /** Loopback, RFC 1918, CGNAT (100.64/10) and IPv6 ULA (fc00::/7): where a reverse proxy can be. */
261
+ function isPrivateAddress(ip) {
262
+ const addr = ip.replace(/%.*$/, "").toLowerCase().replace(/^::ffff:(?=\d+\.\d+\.\d+\.\d+$)/, "");
263
+ if (isIPv4(addr)) {
264
+ const [a = 0, b = 0] = addr.split(".").map(Number);
265
+ return a === 127 || a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
266
+ }
267
+ if (isIPv6(addr)) return addr === "::1" || /^f[cd][0-9a-f]{0,2}:/.test(addr);
268
+ return false;
269
+ }
270
+ function socketAddress(c) {
271
+ try {
272
+ return getConnInfo(c).remote.address ?? null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+ /**
278
+ * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the
279
+ * connection comes from a loopback or private address (where the proxy runs); it is read from the
280
+ * right, since proxies append the address they saw and entries further left are client-controlled.
281
+ * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of agentinbox.
282
+ */
283
+ function clientIp(c, trustProxy) {
284
+ const peer = socketAddress(c);
285
+ if (trustProxy && peer && isPrivateAddress(peer)) {
286
+ const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? "1", 10) || 1);
287
+ const chain = (c.req.header("x-forwarded-for") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
288
+ const forwarded = chain[chain.length - hops];
289
+ if (forwarded && isIP(forwarded)) return forwarded;
290
+ }
291
+ return peer ?? "unknown";
292
+ }
293
+ function isSecure(c, trustProxy) {
294
+ if (new URL(c.req.url).protocol === "https:") return true;
295
+ const peer = socketAddress(c);
296
+ return trustProxy && !!peer && isPrivateAddress(peer) && c.req.header("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase() === "https";
297
+ }
298
+ function sessionToken(c) {
299
+ return getCookie(c, "aib_session") || void 0;
300
+ }
301
+ function writeSessionCookie(c, token, trustProxy) {
302
+ setCookie(c, SESSION_COOKIE, token, {
303
+ httpOnly: true,
304
+ sameSite: "Lax",
305
+ path: "/",
306
+ secure: isSecure(c, trustProxy),
307
+ maxAge: Math.floor(SESSION_TTL_MS / 1e3)
308
+ });
309
+ }
310
+ /** Creates a session for the requester and sets the cookie. */
311
+ function startSession(c, ctx) {
312
+ writeSessionCookie(c, adminAuth(ctx).sessions.create({
313
+ ip: clientIp(c, ctx.config.trustProxy),
314
+ userAgent: c.req.header("user-agent")
315
+ }), ctx.config.trustProxy);
316
+ }
317
+ function endSession(c, ctx) {
318
+ const token = sessionToken(c);
319
+ if (token) adminAuth(ctx).sessions.delete(token);
320
+ deleteCookie(c, SESSION_COOKIE, { path: "/" });
321
+ }
322
+ /** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */
323
+ function hasSession(c, ctx) {
324
+ const token = sessionToken(c);
325
+ const check = adminAuth(ctx).sessions.check(token);
326
+ if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);
327
+ return check.valid;
328
+ }
329
+ /** Requires an admin session on every route it guards. */
330
+ function requireAdmin(ctx) {
331
+ return async (c, next) => {
332
+ if (!hasSession(c, ctx)) {
333
+ 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.");
334
+ throw new HttpError(401, "unauthorized", "Sign in to continue");
335
+ }
336
+ c.set("admin", true);
337
+ await next();
338
+ };
339
+ }
340
+ const MUTATING = /* @__PURE__ */ new Set([
341
+ "POST",
342
+ "PUT",
343
+ "PATCH",
344
+ "DELETE"
345
+ ]);
346
+ /**
347
+ * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server
348
+ * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.
349
+ */
350
+ function csrfGuard() {
351
+ return async (c, next) => {
352
+ 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.`);
353
+ await next();
354
+ };
355
+ }
356
+ /** 429 response for a locked-out address. */
357
+ function lockedOut(c, minutes) {
358
+ c.header("Retry-After", String(minutes * 60));
359
+ const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? "" : "s"}.`;
360
+ return c.json({ error: {
361
+ code: "locked_out",
362
+ message
363
+ } }, 429);
364
+ }
365
+ /**
366
+ * Verifies a secret under the failed-login limiter.
367
+ * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most
368
+ * MAX_CONCURRENT_VERIFICATIONS run at once overall.
369
+ * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so
370
+ * concurrent requests can't all slip past the ban.
371
+ * - Locked-out clients and secrets this client already failed with never reach `verify`.
372
+ * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.
373
+ */
374
+ async function checkSecret(ctx, ip, secret, verify, opts = {}) {
375
+ const auth = adminAuth(ctx);
376
+ const key = clientKey(ip);
377
+ const lane = auth.lanes.get(key) ?? {
378
+ tail: Promise.resolve(),
379
+ queued: 0
380
+ };
381
+ if (lane.queued > 4) return {
382
+ ok: false,
383
+ lockedMinutes: 0,
384
+ nowBanned: false,
385
+ busy: true
386
+ };
387
+ lane.queued++;
388
+ auth.lanes.set(key, lane);
389
+ const previous = lane.tail;
390
+ let done;
391
+ lane.tail = new Promise((resolve) => done = resolve);
392
+ try {
393
+ await previous;
394
+ return await checkOnce(auth, ip, secret, verify, opts);
395
+ } finally {
396
+ done();
397
+ if (--lane.queued === 0) auth.lanes.delete(key);
398
+ }
399
+ }
400
+ async function checkOnce(auth, ip, secret, verify, opts) {
401
+ const { limiter } = auth;
402
+ const global = opts.global !== false;
403
+ if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) await new Promise((r) => setTimeout(r, opts.slowdownMs ?? 2e3));
404
+ const locked = limiter.bannedMinutes(ip);
405
+ if (locked) return {
406
+ ok: false,
407
+ lockedMinutes: locked,
408
+ nowBanned: false
409
+ };
410
+ if (limiter.knownBad(ip, secret)) return {
411
+ ok: false,
412
+ lockedMinutes: 0,
413
+ nowBanned: limiter.fail(ip, secret, { global })
414
+ };
415
+ const reservation = limiter.record(ip, secret, { global });
416
+ const release = await auth.verifySlots.acquire();
417
+ let ok = false;
418
+ try {
419
+ ok = await verify();
420
+ } finally {
421
+ release();
422
+ }
423
+ if (!ok) return {
424
+ ok: false,
425
+ lockedMinutes: 0,
426
+ nowBanned: reservation.banned
427
+ };
428
+ limiter.clear(ip);
429
+ if (reservation.countedGlobally) limiter.refundGlobal();
430
+ return { ok: true };
431
+ }
432
+ /** The 429 for a rejected check (locked out or too many at once), or null to continue. */
433
+ function rejection(c, check) {
434
+ if (check.ok) return null;
435
+ if (check.busy) {
436
+ c.header("Retry-After", "2");
437
+ return c.json({ error: {
438
+ code: "too_many_requests",
439
+ message: "Another sign-in attempt from your address is still being checked. Try again in a moment."
440
+ } }, 429);
441
+ }
442
+ return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;
443
+ }
444
+ //#endregion
445
+ export { resetAdminPassword as _, adminAuth as a, csrfGuard as c, hasSession as d, isClaimed as f, requireAdmin as g, rejection as h, Semaphore as i, currentSetupToken as l, lockedOut as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, isPrivateAddress as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, sessionToken as v, startSession as y };
446
+
447
+ //# sourceMappingURL=auth-BniddcRD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-BniddcRD.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/sessions.ts","../server/auth/index.ts"],"sourcesContent":["import { isIPv6 } from \"node:net\";\nimport { sha256 } from \"../crypto.ts\";\n\n/** Distinct wrong secrets per client within the window before a ban. */\nexport const MAX_FAILURES = 5;\n/** Raw failed attempts per client within the window (repeats of the same wrong secret included). */\nexport const MAX_ATTEMPTS = 30;\n/** Distinct wrong secrets across all clients within the window before every attempt is slowed down. */\nexport const GLOBAL_MAX_FAILURES = 100;\n/** Delay per attempt while the global budget is exceeded. */\nexport const GLOBAL_SLOWDOWN_MS = 2000;\nexport const WINDOW_MS = 15 * 60_000;\nexport const BAN_MS = 15 * 60_000;\n/** Tracked clients; the least recently seen is dropped beyond this. */\nexport const MAX_ENTRIES = 10_000;\n\ninterface Entry {\n tried: Set<string>;\n attempts: number;\n since: number;\n bannedUntil: number;\n}\n\n/**\n * Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30\n * attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is\n * rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale\n * password can't spend every attempt, and can't make the server hash it again either. Past a global\n * budget of distinct failures, every attempt is slowed down (never refused, so many clients can't\n * lock the owner out).\n */\nexport class LoginLimiter {\n private readonly entries = new Map<string, Entry>();\n private global = { failures: 0, since: 0 };\n\n constructor(private readonly clock: () => number = Date.now) {}\n\n /** Minutes until this client may try again, or 0. */\n bannedMinutes(ip: string): number {\n const t = this.clock();\n const entry = this.entries.get(clientKey(ip));\n const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;\n return until > t ? Math.ceil((until - t) / 60_000) : 0;\n }\n\n /** The global failure budget is exhausted: slow every attempt down. */\n overGlobalBudget(): boolean {\n return this.global.failures >= GLOBAL_MAX_FAILURES && this.clock() - this.global.since < WINDOW_MS;\n }\n\n /** The client already failed with exactly this secret in the current window. */\n knownBad(ip: string, secret: string): boolean {\n const entry = this.current(clientKey(ip), this.clock());\n return !!entry?.tried.has(sha256(secret));\n }\n\n /** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */\n fail(ip: string, secret: string, opts: { global?: boolean } = {}): boolean {\n return this.record(ip, secret, opts).banned;\n }\n\n /**\n * Counts an attempt with this secret now, before it is verified, so concurrent requests can't\n * all pass the ban check first. `countedGlobally` says whether it used global budget (refund it\n * with `refundGlobal` when the secret turns out to be right).\n */\n record(ip: string, secret: string, opts: { global?: boolean } = {}): { banned: boolean; countedGlobally: boolean } {\n const t = this.clock();\n const key = clientKey(ip);\n const entry = this.current(key, t) ?? { tried: new Set<string>(), attempts: 0, since: t, bannedUntil: 0 };\n const hash = sha256(secret);\n let countedGlobally = false;\n if (!entry.tried.has(hash)) {\n entry.tried.add(hash);\n if (opts.global !== false) {\n if (t - this.global.since >= WINDOW_MS) this.global = { failures: 0, since: t };\n this.global.failures++;\n countedGlobally = true;\n }\n }\n entry.attempts++;\n const banned = entry.tried.size >= MAX_FAILURES || entry.attempts >= MAX_ATTEMPTS;\n if (banned) entry.bannedUntil = t + BAN_MS;\n this.touch(key, entry);\n return { banned, countedGlobally };\n }\n\n refundGlobal(): void {\n if (this.global.failures > 0) this.global.failures--;\n }\n\n clear(ip: string): void {\n this.entries.delete(clientKey(ip));\n }\n\n /** After the password changes, previously wrong secrets may be right. */\n reset(): void {\n this.entries.clear();\n this.global = { failures: 0, since: 0 };\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The client's entry if its window (or ban) is still running. */\n private current(key: string, t: number): Entry | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.bannedUntil > t || (!entry.bannedUntil && t - entry.since < WINDOW_MS)) return entry;\n this.entries.delete(key);\n return undefined;\n }\n\n /** Map order doubles as LRU order: re-insert on use, evict from the front. */\n private touch(key: string, entry: Entry): void {\n this.entries.delete(key);\n this.entries.set(key, entry);\n while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value!);\n }\n}\n\n/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */\nexport function clientKey(ip: string): string {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase();\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(addr);\n if (mapped) return mapped[1]!;\n if (!isIPv6(addr)) return addr;\n const [head = \"\", tail] = addr.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill(\"0\"), ...right];\n return `${groups\n .slice(0, 4)\n .map((g) => g.replace(/^0+(?=.)/, \"\"))\n .join(\":\")}::/64`;\n}\n","import { randomBytes } from \"node:crypto\";\nimport { sha256 } from \"../crypto.ts\";\nimport type { Db } from \"../db/index.ts\";\n\nexport const SESSION_TTL_MS = 30 * 24 * 3600_000;\n/** Sliding refresh never extends a session past this age: sign in again after 90 days. */\nexport const SESSION_MAX_AGE_MS = 90 * 24 * 3600_000;\n/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */\nconst REFRESH_AFTER_MS = 24 * 3600_000;\n\nexport interface SessionCheck {\n valid: boolean;\n /** The expiry was extended: re-send the cookie so the browser keeps it as long. */\n refreshed: boolean;\n}\n\n/** Admin console sessions. Only sha256(token) is stored. */\nexport class SessionStore {\n constructor(\n private readonly db: Db,\n private readonly clock: () => number = Date.now,\n ) {}\n\n create(meta: { ip?: string | null; userAgent?: string | null } = {}): string {\n const token = randomBytes(32).toString(\"base64url\");\n const t = this.clock();\n this.db.run(\n \"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\",\n sha256(token),\n new Date(t).toISOString(),\n new Date(t + SESSION_TTL_MS).toISOString(),\n meta.ip ?? null,\n meta.userAgent?.slice(0, 300) ?? null,\n );\n return token;\n }\n\n check(token: string | undefined | null): SessionCheck {\n if (!token) return { valid: false, refreshed: false };\n const hash = sha256(token);\n const row = this.db.get<{ created_at: string; expires_at: string }>(\n \"SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?\",\n hash,\n );\n if (!row) return { valid: false, refreshed: false };\n const t = this.clock();\n const expires = Date.parse(row.expires_at);\n const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;\n if (!(expires > t) || !(hardLimit > t)) {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", hash);\n return { valid: false, refreshed: false };\n }\n const next = Math.min(t + SESSION_TTL_MS, hardLimit);\n if (next - expires < REFRESH_AFTER_MS) return { valid: true, refreshed: false };\n this.db.run(\"UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?\", new Date(next).toISOString(), hash);\n return { valid: true, refreshed: true };\n }\n\n delete(token: string): void {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", sha256(token));\n }\n\n /** Revokes every session, optionally keeping one (the caller's own). */\n deleteAll(exceptToken?: string | null): number {\n if (exceptToken) return this.db.run(\"DELETE FROM admin_sessions WHERE token_hash != ?\", sha256(exceptToken)).changes;\n return this.db.run(\"DELETE FROM admin_sessions\").changes;\n }\n\n pruneExpired(): number {\n return this.db.run(\"DELETE FROM admin_sessions WHERE expires_at <= ?\", new Date(this.clock()).toISOString()).changes;\n }\n}\n","import { isIP, isIPv4, isIPv6 } from \"node:net\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { AppEnv } from \"../api/util.ts\";\nimport type { CoreContext } from \"../context.ts\";\nimport { HttpError } from \"../util/errors.ts\";\nimport { clientKey, GLOBAL_SLOWDOWN_MS, LoginLimiter } from \"./lockout.ts\";\nimport { SESSION_TTL_MS, SessionStore } from \"./sessions.ts\";\nimport { ensureSetupToken, regenerateSetupToken } from \"./setup-token.ts\";\n\nexport const SESSION_COOKIE = \"aib_session\";\nexport const CSRF_HEADER = \"x-agentinbox-csrf\";\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const MAX_PASSWORD_LENGTH = 1024;\n\n/** Secret checks (scrypt) allowed at once: libuv's default thread pool size. */\nexport const MAX_CONCURRENT_VERIFICATIONS = 4;\n/** Requests from one client that may wait behind its in-flight check; more get 429. */\nexport const MAX_QUEUED_PER_CLIENT = 4;\n\n/** Hands out at most `max` slots; waiters are served in order. */\nexport class Semaphore {\n private active = 0;\n private readonly waiters: (() => void)[] = [];\n\n constructor(private readonly max: number) {}\n\n get inUse(): number {\n return this.active;\n }\n\n async acquire(): Promise<() => void> {\n if (this.active < this.max) this.active++;\n else await new Promise<void>((resolve) => this.waiters.push(resolve));\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = this.waiters.shift();\n if (next) next();\n else this.active--;\n };\n }\n}\n\n/** Per-context admin auth state: the session store, the failed-login limiter and verification queues. */\nexport interface AdminAuth {\n sessions: SessionStore;\n limiter: LoginLimiter;\n /** One secret check per client at a time: the tail of each client's queue. */\n lanes: Map<string, { tail: Promise<void>; queued: number }>;\n verifySlots: Semaphore;\n}\n\n/** What admin auth needs from the context (the CLI builds this without loading the master key). */\nexport type AuthCore = Pick<CoreContext, \"db\" | \"settings\" | \"audit\" | \"config\">;\n\nconst instances = new WeakMap<AuthCore, AdminAuth>();\n\nexport function adminAuth(ctx: AuthCore): AdminAuth {\n let auth = instances.get(ctx);\n if (!auth) {\n auth = { sessions: new SessionStore(ctx.db), limiter: new LoginLimiter(), lanes: new Map(), verifySlots: new Semaphore(MAX_CONCURRENT_VERIFICATIONS) };\n instances.set(ctx, auth);\n }\n return auth;\n}\n\n/** Claimed = an admin password exists. Before that only the setup-token claim works. */\nexport const isClaimed = (ctx: AuthCore): boolean => !!ctx.settings.getState(\"admin.passwordHash\");\n\n/** The current setup token while unclaimed (created on demand), otherwise null. */\nexport function currentSetupToken(ctx: AuthCore): string | null {\n return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);\n}\n\n/** `agentinbox reset-password`: back to the unclaimed state with a fresh setup link. */\nexport function resetAdminPassword(ctx: AuthCore): string {\n const auth = adminAuth(ctx);\n ctx.db.tx(() => {\n ctx.settings.deleteState(\"admin.passwordHash\");\n auth.sessions.deleteAll();\n });\n auth.limiter.reset();\n ctx.audit.record({ actorType: \"system\", action: \"admin.password_reset\", outcome: \"ok\" });\n return regenerateSetupToken(ctx.config.dataDir);\n}\n\n/** Loopback, RFC 1918, CGNAT (100.64/10) and IPv6 ULA (fc00::/7): where a reverse proxy can be. */\nexport function isPrivateAddress(ip: string): boolean {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase().replace(/^::ffff:(?=\\d+\\.\\d+\\.\\d+\\.\\d+$)/, \"\");\n if (isIPv4(addr)) {\n const [a = 0, b = 0] = addr.split(\".\").map(Number);\n return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);\n }\n if (isIPv6(addr)) return addr === \"::1\" || /^f[cd][0-9a-f]{0,2}:/.test(addr);\n return false;\n}\n\nfunction socketAddress(c: Context): string | null {\n try {\n return getConnInfo(c).remote.address ?? null;\n } catch {\n // No Node socket (e.g. app.request() without bindings).\n return null;\n }\n}\n\n/**\n * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the\n * connection comes from a loopback or private address (where the proxy runs); it is read from the\n * right, since proxies append the address they saw and entries further left are client-controlled.\n * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of agentinbox.\n */\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && isPrivateAddress(peer)) {\n const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? \"1\", 10) || 1);\n const chain = (c.req.header(\"x-forwarded-for\") ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n const forwarded = chain[chain.length - hops];\n if (forwarded && isIP(forwarded)) return forwarded;\n }\n return peer ?? \"unknown\";\n}\n\nfunction isSecure(c: Context, trustProxy: boolean): boolean {\n if (new URL(c.req.url).protocol === \"https:\") return true;\n const peer = socketAddress(c);\n return trustProxy && !!peer && isPrivateAddress(peer) && c.req.header(\"x-forwarded-proto\")?.split(\",\")[0]?.trim().toLowerCase() === \"https\";\n}\n\nexport function sessionToken(c: Context): string | undefined {\n return getCookie(c, SESSION_COOKIE) || undefined;\n}\n\nfunction writeSessionCookie(c: Context, token: string, trustProxy: boolean): void {\n setCookie(c, SESSION_COOKIE, token, {\n httpOnly: true,\n sameSite: \"Lax\",\n path: \"/\",\n secure: isSecure(c, trustProxy),\n maxAge: Math.floor(SESSION_TTL_MS / 1000),\n });\n}\n\n/** Creates a session for the requester and sets the cookie. */\nexport function startSession(c: Context, ctx: AuthCore): void {\n const token = adminAuth(ctx).sessions.create({ ip: clientIp(c, ctx.config.trustProxy), userAgent: c.req.header(\"user-agent\") });\n writeSessionCookie(c, token, ctx.config.trustProxy);\n}\n\nexport function endSession(c: Context, ctx: AuthCore): void {\n const token = sessionToken(c);\n if (token) adminAuth(ctx).sessions.delete(token);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n}\n\n/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */\nexport function hasSession(c: Context, ctx: AuthCore): boolean {\n const token = sessionToken(c);\n const check = adminAuth(ctx).sessions.check(token);\n if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);\n return check.valid;\n}\n\n/** Requires an admin session on every route it guards. */\nexport function requireAdmin(ctx: AuthCore): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (!hasSession(c, ctx)) {\n if (!isClaimed(ctx)) {\n throw new HttpError(401, \"setup_required\", \"This instance has not been claimed yet\", \"Open the setup link printed by agentinbox on startup.\");\n }\n throw new HttpError(401, \"unauthorized\", \"Sign in to continue\");\n }\n c.set(\"admin\", true);\n await next();\n };\n}\n\nconst MUTATING = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server\n * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.\n */\nexport function csrfGuard(): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (MUTATING.has(c.req.method) && c.req.header(CSRF_HEADER) !== \"1\") {\n throw new HttpError(403, \"csrf\", `Missing ${CSRF_HEADER} header`, `Send \"${CSRF_HEADER}: 1\" with every mutating /api request.`);\n }\n await next();\n };\n}\n\n/** 429 response for a locked-out address. */\nexport function lockedOut(c: Context, minutes: number): Response {\n c.header(\"Retry-After\", String(minutes * 60));\n const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? \"\" : \"s\"}.`;\n return c.json({ error: { code: \"locked_out\", message } }, 429);\n}\n\nexport type CheckResult =\n | { ok: true }\n | { ok: false; lockedMinutes: number; nowBanned: boolean; /** Too many concurrent attempts from this client. */ busy?: boolean };\n\nexport interface CheckOptions {\n /**\n * Count toward (and be slowed down by) the instance-wide failure budget. Off for the setup claim:\n * it's only open while unclaimed and needs the token.\n */\n global?: boolean;\n /** Test hook: how long to wait while over the global budget. */\n slowdownMs?: number;\n}\n\n/**\n * Verifies a secret under the failed-login limiter.\n * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most\n * MAX_CONCURRENT_VERIFICATIONS run at once overall.\n * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so\n * concurrent requests can't all slip past the ban.\n * - Locked-out clients and secrets this client already failed with never reach `verify`.\n * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.\n */\nexport async function checkSecret(\n ctx: AuthCore,\n ip: string,\n secret: string,\n verify: () => boolean | Promise<boolean>,\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const auth = adminAuth(ctx);\n const key = clientKey(ip);\n const lane = auth.lanes.get(key) ?? { tail: Promise.resolve(), queued: 0 };\n if (lane.queued > MAX_QUEUED_PER_CLIENT) return { ok: false, lockedMinutes: 0, nowBanned: false, busy: true };\n lane.queued++;\n auth.lanes.set(key, lane);\n const previous = lane.tail;\n let done!: () => void;\n lane.tail = new Promise<void>((resolve) => (done = resolve));\n try {\n await previous;\n return await checkOnce(auth, ip, secret, verify, opts);\n } finally {\n done();\n if (--lane.queued === 0) auth.lanes.delete(key);\n }\n}\n\nasync function checkOnce(auth: AdminAuth, ip: string, secret: string, verify: () => boolean | Promise<boolean>, opts: CheckOptions): Promise<CheckResult> {\n const { limiter } = auth;\n const global = opts.global !== false;\n if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) {\n await new Promise((r) => setTimeout(r, opts.slowdownMs ?? GLOBAL_SLOWDOWN_MS));\n }\n // From here to the reservation everything is synchronous.\n const locked = limiter.bannedMinutes(ip);\n if (locked) return { ok: false, lockedMinutes: locked, nowBanned: false };\n if (limiter.knownBad(ip, secret)) return { ok: false, lockedMinutes: 0, nowBanned: limiter.fail(ip, secret, { global }) };\n const reservation = limiter.record(ip, secret, { global });\n const release = await auth.verifySlots.acquire();\n let ok = false;\n try {\n ok = await verify();\n } finally {\n release();\n }\n if (!ok) return { ok: false, lockedMinutes: 0, nowBanned: reservation.banned };\n limiter.clear(ip);\n if (reservation.countedGlobally) limiter.refundGlobal();\n return { ok: true };\n}\n\n/** The 429 for a rejected check (locked out or too many at once), or null to continue. */\nexport function rejection(c: Context, check: CheckResult): Response | null {\n if (check.ok) return null;\n if (check.busy) {\n c.header(\"Retry-After\", \"2\");\n return c.json({ error: { code: \"too_many_requests\", message: \"Another sign-in attempt from your address is still being checked. Try again in a moment.\" } }, 429);\n }\n return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { l as currentSetupToken, f as isClaimed, _ as resetAdminPassword } from "./auth-BniddcRD.js";
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { EventEmitter } from "node:events";
3
+ //#region server/bus.ts
4
+ function createBus() {
5
+ const bus = new EventEmitter();
6
+ bus.setMaxListeners(0);
7
+ return bus;
8
+ }
9
+ //#endregion
10
+ export { createBus as t };
11
+
12
+ //# sourceMappingURL=bus-Cz0flS37.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bus-Cz0flS37.js","names":[],"sources":["../server/bus.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\nimport type { Account, Approval, AuditEntry, Delivery, MailEvent, Settings } from \"../shared/types.ts\";\n\n/**\n * In-process notifications. Fired *after* the change is committed to the database, so listeners\n * can read it back. Used by the console SSE stream, `wait_for_events`, the webhook worker and the\n * ingest loop (settings changes). Listeners must not throw.\n */\nexport interface BusEvents {\n /** A new event row was committed (and its webhook deliveries enqueued). */\n event: [MailEvent];\n /** A delivery was created or changed state. */\n delivery: [Delivery];\n /** An approval was created or decided. */\n approval: [Approval];\n audit: [AuditEntry];\n /** An account was connected or its status/sync state changed. */\n account: [Account];\n settings: [Settings];\n}\n\nexport type Bus = EventEmitter<BusEvents>;\n\nexport function createBus(): Bus {\n const bus = new EventEmitter<BusEvents>();\n // Every SSE client and pending wait_for_events call adds a listener.\n bus.setMaxListeners(0);\n return bus;\n}\n"],"mappings":";;;AAuBA,SAAgB,YAAiB;CAC/B,MAAM,MAAM,IAAI,aAAwB;CAExC,IAAI,gBAAgB,CAAC;CACrB,OAAO;AACT"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { t as createBus } from "./bus-Cz0flS37.js";