xaq-cli 0.0.0 → 0.1.0-nightly.71
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 +79 -0
- package/dist/admin-5gThRwgj.js +162 -0
- package/dist/admin-5gThRwgj.js.map +1 -0
- package/dist/admin-Btrm8mCd.js +218 -0
- package/dist/admin-Btrm8mCd.js.map +1 -0
- package/dist/admin-CWofnT_Q.js +2 -0
- package/dist/api-BFpElwqK.js +89 -0
- package/dist/api-BFpElwqK.js.map +1 -0
- package/dist/audit-BB7jK9Z5.js +2 -0
- package/dist/audit-BVYknW2i.js +79 -0
- package/dist/audit-BVYknW2i.js.map +1 -0
- package/dist/auth-CXmBdMqw.js +2 -0
- package/dist/auth-D-GmwNv-.js +550 -0
- package/dist/auth-D-GmwNv-.js.map +1 -0
- package/dist/brand--ZB8jTn1.js +32 -0
- package/dist/brand--ZB8jTn1.js.map +1 -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 +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-BNVLjYXA.js +167 -0
- package/dist/commands-BNVLjYXA.js.map +1 -0
- package/dist/commands-CdA6-WE7.js +112 -0
- package/dist/commands-CdA6-WE7.js.map +1 -0
- package/dist/config-C9XmYZWO.js +94 -0
- package/dist/config-C9XmYZWO.js.map +1 -0
- package/dist/config-CNVi0v5t.js +2 -0
- package/dist/connect-TtJ9n0Rw.js +362 -0
- package/dist/connect-TtJ9n0Rw.js.map +1 -0
- package/dist/content-C1lKMxFS.js +1170 -0
- package/dist/content-C1lKMxFS.js.map +1 -0
- package/dist/content-worker.js +28 -0
- package/dist/content-worker.js.map +1 -0
- package/dist/crypto-fv2fj3S8.js +75 -0
- package/dist/crypto-fv2fj3S8.js.map +1 -0
- package/dist/db-BEUEUPyP.js +561 -0
- package/dist/db-BEUEUPyP.js.map +1 -0
- package/dist/db-Q5BQuwga.js +2 -0
- package/dist/errors-FbMS3QBo.js +39 -0
- package/dist/errors-FbMS3QBo.js.map +1 -0
- package/dist/filter-Ck6OMxvu.js +158 -0
- package/dist/filter-Ck6OMxvu.js.map +1 -0
- package/dist/hermes-CKyzt1mH.js +3 -0
- package/dist/hermes-DHnDTYJp.js +1023 -0
- package/dist/hermes-DHnDTYJp.js.map +1 -0
- package/dist/import-DsN5NZT3.js +910 -0
- package/dist/import-DsN5NZT3.js.map +1 -0
- package/dist/instance-CwWbFbJ8.js +2 -0
- package/dist/instance-Dj9TpJB2.js +195 -0
- package/dist/instance-Dj9TpJB2.js.map +1 -0
- package/dist/listen-B9iO7HgQ.js +1568 -0
- package/dist/listen-B9iO7HgQ.js.map +1 -0
- package/dist/listen-CXVKG9XD.js +2 -0
- package/dist/manage-Bo6COB4p.js +477 -0
- package/dist/manage-Bo6COB4p.js.map +1 -0
- package/dist/manage-DoXqvjnU.js +2 -0
- package/dist/mcpheader-B8c5YO1B.js +2 -0
- package/dist/mcpheader-C9H4vudu.js +185 -0
- package/dist/mcpheader-C9H4vudu.js.map +1 -0
- package/dist/mcpsetup-BnHudwpq.js +2 -0
- package/dist/mcpsetup-DXXo2qjf.js +1053 -0
- package/dist/mcpsetup-DXXo2qjf.js.map +1 -0
- package/dist/node-check-CjTk6VNL.js +24 -0
- package/dist/node-check-CjTk6VNL.js.map +1 -0
- package/dist/paths-d-C2kdjo.js +2 -0
- package/dist/paths-yzOKD6ow.js +46 -0
- package/dist/paths-yzOKD6ow.js.map +1 -0
- package/dist/profile-CcMZ4hn7.js +2 -0
- package/dist/profile-NmMKAczz.js +222 -0
- package/dist/profile-NmMKAczz.js.map +1 -0
- package/dist/prompt-CKMQx_MJ.js +114 -0
- package/dist/prompt-CKMQx_MJ.js.map +1 -0
- package/dist/recipes-C7TIDlRj.js +3 -0
- package/dist/recipes-DG_-doGq.js +314 -0
- package/dist/recipes-DG_-doGq.js.map +1 -0
- package/dist/send-Dk-t8ia-.js +108 -0
- package/dist/send-Dk-t8ia-.js.map +1 -0
- package/dist/server-CDqDp1q3.js +20640 -0
- package/dist/server-CDqDp1q3.js.map +1 -0
- package/dist/service-BVn3soZZ.js +766 -0
- package/dist/service-BVn3soZZ.js.map +1 -0
- package/dist/service-BaWGPmSd.js +2 -0
- package/dist/service-C7jfo-Ex.js +457 -0
- package/dist/service-C7jfo-Ex.js.map +1 -0
- package/dist/service-DxDyKDQ-.js +2 -0
- package/dist/settings-B5qSHxRM.js +134 -0
- package/dist/settings-B5qSHxRM.js.map +1 -0
- package/dist/settings-DQvZaMN1.js +2 -0
- package/dist/setup-token-CwuSzfLK.js +48 -0
- package/dist/setup-token-CwuSzfLK.js.map +1 -0
- package/dist/skill/SKILL.md +445 -0
- package/dist/skill-1UQj3qgW.js +69 -0
- package/dist/skill-1UQj3qgW.js.map +1 -0
- package/dist/skill-pelq18JQ.js +2 -0
- package/dist/ssrf-BlVpGZ7C.js +181 -0
- package/dist/ssrf-BlVpGZ7C.js.map +1 -0
- package/dist/types-BhJDbSzE.js +135 -0
- package/dist/types-BhJDbSzE.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/version-BxH7OuWD.js +20 -0
- package/dist/version-BxH7OuWD.js.map +1 -0
- package/dist/web/assets/_agentId-DonOCOlC.js +1 -0
- package/dist/web/assets/_app-B7t5lEia.js +1 -0
- package/dist/web/assets/_app-DMTGqYpM.js +65 -0
- package/dist/web/assets/_connectionId-DR5vFXOg.js +1 -0
- package/dist/web/assets/_webhookId-LfUlsULp.js +1 -0
- package/dist/web/assets/account-status-CEszs13C.js +1 -0
- package/dist/web/assets/accounts-5m7aiXuZ.js +1 -0
- package/dist/web/assets/accounts-list-YfJzThZa.js +1 -0
- package/dist/web/assets/activity-DOIe7SAg.js +1 -0
- package/dist/web/assets/add-connection-C8JR-13R.js +1 -0
- package/dist/web/assets/agent-create-form-CPxnPYH4.js +1 -0
- package/dist/web/assets/agents-B2Qj5KxT.js +1 -0
- package/dist/web/assets/approvals-BGaVaoFy.js +1 -0
- package/dist/web/assets/approve._id-BLrQnzPM.js +1 -0
- package/dist/web/assets/arrow-left-DZVAXoKj.js +1 -0
- package/dist/web/assets/arrow-right-C2zcqRr2.js +1 -0
- package/dist/web/assets/attention-gO9Nw5uW.js +1 -0
- package/dist/web/assets/audit-BPXXre8S.js +1 -0
- package/dist/web/assets/audit-bits-CGyIprzK.js +1 -0
- package/dist/web/assets/bell-ring-ig3zn1iS.js +1 -0
- package/dist/web/assets/bot-Zk76q_39.js +1 -0
- package/dist/web/assets/calendar-notice-DsCfq3VJ.js +1 -0
- package/dist/web/assets/calendar-plus-Cycfkk0y.js +1 -0
- package/dist/web/assets/calendar-setup-Pn61Wj1n.js +2 -0
- package/dist/web/assets/card-GSO5o-Ug.js +1 -0
- package/dist/web/assets/check-check-Ds1bPIGv.js +1 -0
- package/dist/web/assets/checkbox-B937U-Fd.js +1 -0
- package/dist/web/assets/chevron-right-Cqh4mDk1.js +1 -0
- package/dist/web/assets/chips-input-2xWDofmE.js +1 -0
- package/dist/web/assets/choice-cards-CgAIqBGX.js +1 -0
- package/dist/web/assets/circle-x-DcU-0zVh.js +1 -0
- package/dist/web/assets/command-khhto70y.js +1 -0
- package/dist/web/assets/common-BPZV8TFb.js +1 -0
- package/dist/web/assets/connect-agent-guide-CJdw31p2.js +1 -0
- package/dist/web/assets/connect-host-lr2c3xWh.js +6 -0
- package/dist/web/assets/connection-row-DYx_5o0c.js +1 -0
- package/dist/web/assets/connections-C4NOFb5R.js +1 -0
- package/dist/web/assets/data-table-kW1pkRB5.js +1 -0
- package/dist/web/assets/empty-DzTDioBP.js +1 -0
- package/dist/web/assets/events-C9zJnPKK.js +1 -0
- package/dist/web/assets/eye-B9gT1QSq.js +1 -0
- package/dist/web/assets/eye-off-Diktqz_g.js +1 -0
- package/dist/web/assets/field-C6ejurju.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-C38bq1gZ.js +2 -0
- package/dist/web/assets/hidden-chars-Dr9TxfvF.js +1 -0
- package/dist/web/assets/index-DGzo3kGh.js +11 -0
- package/dist/web/assets/index-zKOCQLGX.css +2 -0
- package/dist/web/assets/input-CWwJgozn.js +1 -0
- package/dist/web/assets/input-group-HG8OXcJ4.js +1 -0
- package/dist/web/assets/label-multi-select-sH8mllw8.js +1 -0
- package/dist/web/assets/labels-B5UqVwFo.js +1 -0
- package/dist/web/assets/laptop-BxJ4Nbj7.js +1 -0
- package/dist/web/assets/last-call-D-O5O94z.js +1 -0
- package/dist/web/assets/listener-handled-HNCuVrQ7.js +1 -0
- package/dist/web/assets/login-DaeKclcf.js +1 -0
- package/dist/web/assets/mail-C5fm2pTC.js +1 -0
- package/dist/web/assets/mcp-call-UoOTPFyh.js +1 -0
- package/dist/web/assets/mcp-call-card-Cr-OEDX2.js +3 -0
- package/dist/web/assets/message-square-quote-se8Bf3RP.js +1 -0
- package/dist/web/assets/mutations-CLPqJ6cj.js +1 -0
- package/dist/web/assets/network-igNw0VVx.js +1 -0
- package/dist/web/assets/pairing-XjuaVr2u.js +1 -0
- package/dist/web/assets/pairing-notifier-KPzOEZSO.js +1 -0
- package/dist/web/assets/paperclip-znS4iTrg.js +1 -0
- package/dist/web/assets/password-input-DV3aqcNf.js +1 -0
- package/dist/web/assets/pause-o-dK3khK.js +1 -0
- package/dist/web/assets/pencil-DkPFAhEq.js +1 -0
- package/dist/web/assets/play-DvCHND93.js +1 -0
- package/dist/web/assets/plug-CXAniEiR.js +1 -0
- package/dist/web/assets/plug-zap-BwR-l5wb.js +1 -0
- package/dist/web/assets/plus-B_TUslu3.js +1 -0
- package/dist/web/assets/preload-helper-Cc_aKyUb.js +1 -0
- package/dist/web/assets/public-url-rF2Xjyel.js +1 -0
- package/dist/web/assets/queries-Cv3nXx0U.js +42 -0
- package/dist/web/assets/refresh-cw-Ch5FNrby.js +1 -0
- package/dist/web/assets/revealed-keys-C_9NtJPY.js +1 -0
- package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/web/assets/rotate-ccw-B-mzPDk0.js +1 -0
- package/dist/web/assets/search-CTVKGomS.js +1 -0
- package/dist/web/assets/select-Bb806bpq.js +1 -0
- package/dist/web/assets/send-DuMVuYs1.js +1 -0
- package/dist/web/assets/settings-BWauhfFf.js +1 -0
- package/dist/web/assets/setup-B7bNKQOp.js +1 -0
- package/dist/web/assets/sheet-CYPwpUsH.js +1 -0
- package/dist/web/assets/shield-alert-CSYB-ThV.js +1 -0
- package/dist/web/assets/shield-check-BulpsYqK.js +1 -0
- package/dist/web/assets/sparkles-CUTiob4K.js +1 -0
- package/dist/web/assets/stamp-BNXUGNa3.js +1 -0
- package/dist/web/assets/step-url-51aMNak-.js +1 -0
- package/dist/web/assets/stream-Du7fY5_Y.js +1 -0
- package/dist/web/assets/tag-I5_-1jFL.js +1 -0
- package/dist/web/assets/terminal-BvxDFTJc.js +1 -0
- package/dist/web/assets/test-email-button-DYi7V_6X.js +1 -0
- package/dist/web/assets/toggle-group-Bdo1gmQ1.js +1 -0
- package/dist/web/assets/trash-Ci0aICEl.js +1 -0
- package/dist/web/assets/use-delivery-speed-BBh7W2q6.js +1 -0
- package/dist/web/assets/use-label-names-BXicesnP.js +1 -0
- package/dist/web/assets/use-sign-out-CPrY_oVO.js +1 -0
- package/dist/web/assets/useInfiniteQuery-DJLIM8yJ.js +1 -0
- package/dist/web/assets/useQuery-MrTwwNy5.js +1 -0
- package/dist/web/assets/useRouterState-D4OBK8l5.js +1 -0
- package/dist/web/assets/webhook-DjeeZiNC.js +1 -0
- package/dist/web/assets/webhook-bits-jw-y2D3u.js +1 -0
- package/dist/web/assets/webhook-sheet-B8h0skDe.js +34 -0
- package/dist/web/assets/webhooks-Cfsdpoyd.js +1 -0
- package/dist/web/assets/x-MF2TZ9aG.js +1 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +27 -0
- package/package.json +98 -1
|
@@ -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-fv2fj3S8.js";
|
|
4
|
+
import { i as regenerateSetupToken, n as ensureSetupToken } from "./setup-token-CwuSzfLK.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(`xaq: 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
|
+
/** `xaq 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 xaq.
|
|
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(`xaq: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\n`);
|
|
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 xaq 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 { clientKey as S, sessionToken as _, adminAuth as a, BAN_MS as b, csrfGuard as c, hasSession as d, isClaimed as f, resetAdminPassword as g, requireAdmin as h, Semaphore as i, currentSetupToken as l, rejection as m, MAX_PASSWORD_LENGTH as n, checkSecret as o, lockedOut as p, SESSION_COOKIE as r, clientIp as s, CSRF_HEADER as t, endSession as u, startSession as v, LoginLimiter as x, SESSION_MAX_AGE_MS as y };
|
|
549
|
+
|
|
550
|
+
//# sourceMappingURL=auth-D-GmwNv-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-D-GmwNv-.js","names":[],"sources":["../server/auth/lockout.ts","../server/auth/proxy.ts","../server/auth/sessions.ts","../server/auth/index.ts"],"sourcesContent":["import { isIPv6 } from \"node:net\";\nimport { sha256 } from \"../crypto.ts\";\n\n/** Distinct wrong secrets per client within the window before a ban. */\nexport const MAX_FAILURES = 5;\n/** Raw failed attempts per client within the window (repeats of the same wrong secret included). */\nexport const MAX_ATTEMPTS = 30;\n/** Distinct wrong secrets across all clients within the window before every attempt is slowed down. */\nexport const GLOBAL_MAX_FAILURES = 100;\n/** Delay per attempt while the global budget is exceeded. */\nexport const GLOBAL_SLOWDOWN_MS = 2000;\nexport const WINDOW_MS = 15 * 60_000;\nexport const BAN_MS = 15 * 60_000;\n/** Tracked clients; the least recently seen is dropped beyond this. */\nexport const MAX_ENTRIES = 10_000;\n\ninterface Entry {\n tried: Set<string>;\n attempts: number;\n since: number;\n bannedUntil: number;\n}\n\n/**\n * Failed sign-in limiter. Per client (IPv4 address or IPv6 /64): 5 distinct wrong secrets or 30\n * attempts within 15 minutes ban it for 15 minutes. A secret already known to be wrong is\n * rejected without hashing (`knownBad`) and counts only as an attempt, so a client retrying a stale\n * password can't spend every attempt, and can't make the server hash it again either. Past a global\n * budget of distinct failures, every attempt is slowed down (never refused, so many clients can't\n * lock the owner out).\n */\nexport class LoginLimiter {\n private readonly entries = new Map<string, Entry>();\n private global = { failures: 0, since: 0 };\n\n constructor(private readonly clock: () => number = Date.now) {}\n\n /** Minutes until this client may try again, or 0. */\n bannedMinutes(ip: string): number {\n const t = this.clock();\n const entry = this.entries.get(clientKey(ip));\n const until = entry && entry.bannedUntil > t ? entry.bannedUntil : 0;\n return until > t ? Math.ceil((until - t) / 60_000) : 0;\n }\n\n /** The global failure budget is exhausted: slow every attempt down. */\n overGlobalBudget(): boolean {\n return this.global.failures >= GLOBAL_MAX_FAILURES && this.clock() - this.global.since < WINDOW_MS;\n }\n\n /** The client already failed with exactly this secret in the current window. */\n knownBad(ip: string, secret: string): boolean {\n const entry = this.current(clientKey(ip), this.clock());\n return !!entry?.tried.has(sha256(secret));\n }\n\n /** Records a failure; returns true when it started a ban for this client. `global: false` keeps it out of the global budget. */\n fail(ip: string, secret: string, opts: { global?: boolean } = {}): boolean {\n return this.record(ip, secret, opts).banned;\n }\n\n /**\n * Counts an attempt with this secret now, before it is verified, so concurrent requests can't\n * all pass the ban check first. `countedGlobally` says whether it used global budget (refund it\n * with `refundGlobal` when the secret turns out to be right).\n */\n record(ip: string, secret: string, opts: { global?: boolean } = {}): { banned: boolean; countedGlobally: boolean } {\n const t = this.clock();\n const key = clientKey(ip);\n const entry = this.current(key, t) ?? { tried: new Set<string>(), attempts: 0, since: t, bannedUntil: 0 };\n const hash = sha256(secret);\n let countedGlobally = false;\n if (!entry.tried.has(hash)) {\n entry.tried.add(hash);\n if (opts.global !== false) {\n if (t - this.global.since >= WINDOW_MS) this.global = { failures: 0, since: t };\n this.global.failures++;\n countedGlobally = true;\n }\n }\n entry.attempts++;\n const banned = entry.tried.size >= MAX_FAILURES || entry.attempts >= MAX_ATTEMPTS;\n if (banned) entry.bannedUntil = t + BAN_MS;\n this.touch(key, entry);\n return { banned, countedGlobally };\n }\n\n refundGlobal(): void {\n if (this.global.failures > 0) this.global.failures--;\n }\n\n clear(ip: string): void {\n this.entries.delete(clientKey(ip));\n }\n\n /** After the password changes, previously wrong secrets may be right. */\n reset(): void {\n this.entries.clear();\n this.global = { failures: 0, since: 0 };\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /** The client's entry if its window (or ban) is still running. */\n private current(key: string, t: number): Entry | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.bannedUntil > t || (!entry.bannedUntil && t - entry.since < WINDOW_MS)) return entry;\n this.entries.delete(key);\n return undefined;\n }\n\n /** Map order doubles as LRU order: re-insert on use, evict from the front. */\n private touch(key: string, entry: Entry): void {\n this.entries.delete(key);\n this.entries.set(key, entry);\n while (this.entries.size > MAX_ENTRIES) this.entries.delete(this.entries.keys().next().value!);\n }\n}\n\n/** IPv4 addresses as is (also when IPv4-mapped); IPv6 by /64, since one client usually owns the whole prefix. */\nexport function clientKey(ip: string): string {\n const addr = ip.replace(/%.*$/, \"\").toLowerCase();\n const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(addr);\n if (mapped) return mapped[1]!;\n if (!isIPv6(addr)) return addr;\n const [head = \"\", tail] = addr.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array(8 - left.length - right.length).fill(\"0\"), ...right];\n return `${groups\n .slice(0, 4)\n .map((g) => g.replace(/^0+(?=.)/, \"\"))\n .join(\":\")}::/64`;\n}\n","import { isIPv4, isIPv6 } from \"node:net\";\n\n/** An address as a family and a big integer, with IPv4-mapped IPv6 (::ffff:a.b.c.d) treated as IPv4. */\ninterface Addr {\n v6: boolean;\n n: bigint;\n}\n\nfunction parseV4(ip: string): bigint {\n return ip.split(\".\").reduce((acc, part) => (acc << 8n) | BigInt(Number(part)), 0n);\n}\n\nexport function parseAddress(raw: string): Addr | null {\n const ip = raw.trim().replace(/^\\[|\\]$/g, \"\").replace(/%.*$/, \"\").toLowerCase();\n if (isIPv4(ip)) return { v6: false, n: parseV4(ip) };\n if (!isIPv6(ip)) return null;\n const mapped = /^(?:0{0,4}:){0,5}:?ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(ip);\n if (mapped && isIPv4(mapped[1]!)) return { v6: false, n: parseV4(mapped[1]!) };\n // Expand an embedded IPv4 tail into two hextets, then \"::\".\n let text = ip;\n const v4Tail = /(\\d+\\.\\d+\\.\\d+\\.\\d+)$/.exec(text);\n if (v4Tail) {\n const n = parseV4(v4Tail[1]!);\n text = text.slice(0, -v4Tail[1]!.length) + `${(n >> 16n).toString(16)}:${(n & 0xffffn).toString(16)}`;\n }\n const [head = \"\", tail] = text.split(\"::\");\n const left = head ? head.split(\":\") : [];\n const right = tail ? tail.split(\":\") : [];\n const groups = tail === undefined ? left : [...left, ...Array<string>(8 - left.length - right.length).fill(\"0\"), ...right];\n if (groups.length !== 8) return null;\n return { v6: true, n: groups.reduce((acc, g) => (acc << 16n) | BigInt(Number.parseInt(g || \"0\", 16)), 0n) };\n}\n\ninterface Range {\n v6: boolean;\n base: bigint;\n bits: number;\n}\n\n/**\n * Parses \"10.0.0.5\", \"10.0.0.0/8\", \"fd00::/8\", \"::ffff:10.0.0.0/104\"; null when invalid. A /0\n * (trust everyone) is refused: it would let any client pick its own address.\n */\nexport function parseRange(entry: string): Range | null {\n const parts = entry.trim().split(\"/\");\n if (parts.length > 2) return null;\n const [ip = \"\", prefix] = parts;\n const addr = parseAddress(ip);\n if (!addr) return null;\n let bits = addr.v6 ? 128 : 32;\n if (prefix !== undefined) {\n if (!/^\\d{1,3}$/.test(prefix)) return null;\n bits = Number(prefix);\n // A mapped IPv4 range written in IPv6 form: drop the 96-bit ::ffff: prefix.\n if (!addr.v6 && ip.includes(\":\")) bits -= 96;\n if (bits <= 0 || bits > (addr.v6 ? 128 : 32)) return null;\n }\n const width = BigInt(addr.v6 ? 128 : 32);\n const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << (width - BigInt(bits));\n return { v6: addr.v6, base: addr.n & mask, bits };\n}\n\nfunction inRange(addr: Addr, r: Range): boolean {\n if (addr.v6 !== r.v6) return false;\n const width = BigInt(r.v6 ? 128 : 32);\n const mask = r.bits === 0 ? 0n : ((1n << BigInt(r.bits)) - 1n) << (width - BigInt(r.bits));\n return (addr.n & mask) === r.base;\n}\n\nconst LOOPBACK = [parseRange(\"127.0.0.0/8\")!, parseRange(\"::1/128\")!];\n\nlet cache: { raw: string; ranges: Range[] } | undefined;\n\n/** TRUST_PROXY_FROM as ranges. Invalid entries are reported once and ignored. */\nfunction trustedRanges(raw: string): Range[] {\n if (cache?.raw === raw) return cache.ranges;\n const ranges: Range[] = [];\n const invalid: string[] = [];\n for (const entry of raw.split(/[,\\s]+/).filter(Boolean)) {\n const r = parseRange(entry);\n if (r) ranges.push(r);\n else invalid.push(entry);\n }\n if (invalid.length) process.stderr.write(`xaq: ignoring invalid TRUST_PROXY_FROM entries: ${invalid.join(\", \")}\\n`);\n cache = { raw, ranges };\n return ranges;\n}\n\n/**\n * Whether forwarding headers from this socket peer may be believed: loopback always (a proxy on\n * the same host), anything else only when listed in TRUST_PROXY_FROM (IPs or CIDRs, comma-separated).\n * The daemon usually binds 0.0.0.0, so trusting whole private ranges would let any LAN or tailnet\n * peer forge its address.\n */\nexport function isTrustedProxy(peer: string, trustFrom = process.env.TRUST_PROXY_FROM ?? \"\"): boolean {\n const addr = parseAddress(peer);\n if (!addr) return false;\n return [...LOOPBACK, ...trustedRanges(trustFrom)].some((r) => inRange(addr, r));\n}\n","import { randomBytes } from \"node:crypto\";\nimport { sha256 } from \"../crypto.ts\";\nimport type { Db } from \"../db/index.ts\";\n\nexport const SESSION_TTL_MS = 30 * 24 * 3600_000;\n/** Sliding refresh never extends a session past this age: sign in again after 90 days. */\nexport const SESSION_MAX_AGE_MS = 90 * 24 * 3600_000;\n/** Sliding expiry is pushed forward at most once per this interval, to avoid a write per request. */\nconst REFRESH_AFTER_MS = 24 * 3600_000;\n\nexport interface SessionCheck {\n valid: boolean;\n /** The expiry was extended: re-send the cookie so the browser keeps it as long. */\n refreshed: boolean;\n}\n\n/** Admin console sessions. Only sha256(token) is stored. */\nexport class SessionStore {\n constructor(\n private readonly db: Db,\n private readonly clock: () => number = Date.now,\n ) {}\n\n create(meta: { ip?: string | null; userAgent?: string | null } = {}): string {\n const token = randomBytes(32).toString(\"base64url\");\n const t = this.clock();\n this.db.run(\n \"INSERT INTO admin_sessions (token_hash, created_at, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)\",\n sha256(token),\n new Date(t).toISOString(),\n new Date(t + SESSION_TTL_MS).toISOString(),\n meta.ip ?? null,\n meta.userAgent?.slice(0, 300) ?? null,\n );\n return token;\n }\n\n check(token: string | undefined | null): SessionCheck {\n if (!token) return { valid: false, refreshed: false };\n const hash = sha256(token);\n const row = this.db.get<{ created_at: string; expires_at: string }>(\n \"SELECT created_at, expires_at FROM admin_sessions WHERE token_hash = ?\",\n hash,\n );\n if (!row) return { valid: false, refreshed: false };\n const t = this.clock();\n const expires = Date.parse(row.expires_at);\n const hardLimit = Date.parse(row.created_at) + SESSION_MAX_AGE_MS;\n if (!(expires > t) || !(hardLimit > t)) {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", hash);\n return { valid: false, refreshed: false };\n }\n const next = Math.min(t + SESSION_TTL_MS, hardLimit);\n if (next - expires < REFRESH_AFTER_MS) return { valid: true, refreshed: false };\n this.db.run(\"UPDATE admin_sessions SET expires_at = ? WHERE token_hash = ?\", new Date(next).toISOString(), hash);\n return { valid: true, refreshed: true };\n }\n\n delete(token: string): void {\n this.db.run(\"DELETE FROM admin_sessions WHERE token_hash = ?\", sha256(token));\n }\n\n /** Revokes every session, optionally keeping one (the caller's own). */\n deleteAll(exceptToken?: string | null): number {\n if (exceptToken) return this.db.run(\"DELETE FROM admin_sessions WHERE token_hash != ?\", sha256(exceptToken)).changes;\n return this.db.run(\"DELETE FROM admin_sessions\").changes;\n }\n\n pruneExpired(): number {\n return this.db.run(\"DELETE FROM admin_sessions WHERE expires_at <= ?\", new Date(this.clock()).toISOString()).changes;\n }\n}\n","import { isIP } from \"node:net\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { AppEnv } from \"../api/util.ts\";\nimport type { CoreContext } from \"../context.ts\";\nimport { HttpError } from \"../util/errors.ts\";\nimport { clientKey, GLOBAL_SLOWDOWN_MS, LoginLimiter } from \"./lockout.ts\";\nimport { isTrustedProxy } from \"./proxy.ts\";\nimport { SESSION_TTL_MS, SessionStore } from \"./sessions.ts\";\nimport { ensureSetupToken, regenerateSetupToken } from \"./setup-token.ts\";\n\nexport const SESSION_COOKIE = \"aib_session\";\n// Header names keep the pre-rename spelling: a console tab or script from before the rename still sends it.\nexport const CSRF_HEADER = \"x-agentinbox-csrf\";\nexport const MIN_PASSWORD_LENGTH = 10;\nexport const MAX_PASSWORD_LENGTH = 1024;\n\n/** Secret checks (scrypt) allowed at once: libuv's default thread pool size. */\nexport const MAX_CONCURRENT_VERIFICATIONS = 4;\n/** Requests from one client that may wait behind its in-flight check; more get 429. */\nexport const MAX_QUEUED_PER_CLIENT = 4;\n\n/** Hands out at most `max` slots; waiters are served in order. */\nexport class Semaphore {\n private active = 0;\n private readonly waiters: (() => void)[] = [];\n\n constructor(private readonly max: number) {}\n\n get inUse(): number {\n return this.active;\n }\n\n async acquire(): Promise<() => void> {\n if (this.active < this.max) this.active++;\n else await new Promise<void>((resolve) => this.waiters.push(resolve));\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = this.waiters.shift();\n if (next) next();\n else this.active--;\n };\n }\n}\n\n/** Per-context admin auth state: the session store, the failed-login limiter and verification queues. */\nexport interface AdminAuth {\n sessions: SessionStore;\n limiter: LoginLimiter;\n /** One secret check per client at a time: the tail of each client's queue. */\n lanes: Map<string, { tail: Promise<void>; queued: number }>;\n verifySlots: Semaphore;\n}\n\n/** What admin auth needs from the context (the CLI builds this without loading the master key). */\nexport type AuthCore = Pick<CoreContext, \"db\" | \"settings\" | \"audit\" | \"config\">;\n\nconst instances = new WeakMap<AuthCore, AdminAuth>();\n\nexport function adminAuth(ctx: AuthCore): AdminAuth {\n let auth = instances.get(ctx);\n if (!auth) {\n auth = { sessions: new SessionStore(ctx.db), limiter: new LoginLimiter(), lanes: new Map(), verifySlots: new Semaphore(MAX_CONCURRENT_VERIFICATIONS) };\n instances.set(ctx, auth);\n }\n return auth;\n}\n\n/** Claimed = an admin password exists. Before that only the setup-token claim works. */\nexport const isClaimed = (ctx: AuthCore): boolean => !!ctx.settings.getState(\"admin.passwordHash\");\n\n/** The current setup token while unclaimed (created on demand), otherwise null. */\nexport function currentSetupToken(ctx: AuthCore): string | null {\n return isClaimed(ctx) ? null : ensureSetupToken(ctx.config.dataDir);\n}\n\n/** `xaq reset-password`: back to the unclaimed state with a fresh setup link. */\nexport function resetAdminPassword(ctx: AuthCore): string {\n const auth = adminAuth(ctx);\n ctx.db.tx(() => {\n ctx.settings.deleteState(\"admin.passwordHash\");\n auth.sessions.deleteAll();\n });\n auth.limiter.reset();\n ctx.audit.record({ actorType: \"system\", action: \"admin.password_reset\", outcome: \"ok\" });\n return regenerateSetupToken(ctx.config.dataDir);\n}\n\nfunction socketAddress(c: Context): string | null {\n try {\n return getConnInfo(c).remote.address ?? null;\n } catch {\n // No Node socket (e.g. app.request() without bindings).\n return null;\n }\n}\n\n/**\n * The client address. X-Forwarded-For is honored only with TRUST_PROXY and only when the\n * connection comes from a trusted proxy (loopback, or TRUST_PROXY_FROM); it is read from the\n * right, since proxies append the address they saw and entries further left are client-controlled.\n * TRUST_PROXY_HOPS (default 1) is the number of proxies in front of xaq.\n */\n/** Some proxies (Azure, IIS) append \":port\" to X-Forwarded-For entries. */\nfunction stripPort(entry: string): string {\n const v6 = /^\\[([^\\]]+)\\](?::\\d+)?$/.exec(entry);\n if (v6) return v6[1]!;\n const v4 = /^(\\d{1,3}(?:\\.\\d{1,3}){3}):\\d+$/.exec(entry);\n return v4 ? v4[1]! : entry;\n}\n\nlet warnedUntrustedProxy = false;\n\n/** Upgrades from the old private-range rule silently lost proxy trust; say so once. */\nfunction warnUntrustedProxy(peer: string): void {\n if (warnedUntrustedProxy) return;\n warnedUntrustedProxy = true;\n process.stderr.write(\n `xaq: ignoring X-Forwarded-For from ${peer}: TRUST_PROXY only trusts loopback proxies unless TRUST_PROXY_FROM lists them (e.g. TRUST_PROXY_FROM=${peer}).\\n`,\n );\n}\n\nexport function clientIp(c: Context, trustProxy: boolean): string {\n const peer = socketAddress(c);\n if (trustProxy && peer && !isTrustedProxy(peer) && c.req.header(\"x-forwarded-for\")) warnUntrustedProxy(peer);\n if (trustProxy && peer && isTrustedProxy(peer)) {\n const hops = Math.max(1, Number.parseInt(process.env.TRUST_PROXY_HOPS ?? \"1\", 10) || 1);\n const chain = (c.req.header(\"x-forwarded-for\") ?? \"\")\n .split(\",\")\n .map((s) => stripPort(s.trim()))\n .filter(Boolean);\n const forwarded = chain[chain.length - hops];\n if (forwarded && isIP(forwarded)) return forwarded;\n }\n return peer ?? \"unknown\";\n}\n\nfunction isSecure(c: Context, trustProxy: boolean): boolean {\n if (new URL(c.req.url).protocol === \"https:\") return true;\n const peer = socketAddress(c);\n return trustProxy && !!peer && isTrustedProxy(peer) && c.req.header(\"x-forwarded-proto\")?.split(\",\")[0]?.trim().toLowerCase() === \"https\";\n}\n\nexport function sessionToken(c: Context): string | undefined {\n return getCookie(c, SESSION_COOKIE) || undefined;\n}\n\nfunction writeSessionCookie(c: Context, token: string, trustProxy: boolean): void {\n setCookie(c, SESSION_COOKIE, token, {\n httpOnly: true,\n sameSite: \"Lax\",\n path: \"/\",\n secure: isSecure(c, trustProxy),\n maxAge: Math.floor(SESSION_TTL_MS / 1000),\n });\n}\n\n/** Creates a session for the requester and sets the cookie. */\nexport function startSession(c: Context, ctx: AuthCore): void {\n const token = adminAuth(ctx).sessions.create({ ip: clientIp(c, ctx.config.trustProxy), userAgent: c.req.header(\"user-agent\") });\n writeSessionCookie(c, token, ctx.config.trustProxy);\n}\n\nexport function endSession(c: Context, ctx: AuthCore): void {\n const token = sessionToken(c);\n if (token) adminAuth(ctx).sessions.delete(token);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n}\n\n/** Whether the request carries a valid session; refreshes the cookie when the expiry slid forward. */\nexport function hasSession(c: Context, ctx: AuthCore): boolean {\n const token = sessionToken(c);\n const check = adminAuth(ctx).sessions.check(token);\n if (check.refreshed && token) writeSessionCookie(c, token, ctx.config.trustProxy);\n return check.valid;\n}\n\n/** Requires an admin session on every route it guards. */\nexport function requireAdmin(ctx: AuthCore): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (!hasSession(c, ctx)) {\n if (!isClaimed(ctx)) {\n throw new HttpError(401, \"setup_required\", \"This instance has not been claimed yet\", \"Open the setup link printed by xaq on startup.\");\n }\n throw new HttpError(401, \"unauthorized\", \"Sign in to continue\");\n }\n c.set(\"admin\", true);\n await next();\n };\n}\n\nconst MUTATING = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * CSRF guard: a custom header can't be sent cross-site without a CORS preflight (which this server\n * never approves), and SameSite=Lax already keeps the cookie off cross-site POSTs.\n */\nexport function csrfGuard(): MiddlewareHandler<AppEnv> {\n return async (c, next) => {\n if (MUTATING.has(c.req.method) && c.req.header(CSRF_HEADER) !== \"1\") {\n throw new HttpError(403, \"csrf\", `Missing ${CSRF_HEADER} header`, `Send \"${CSRF_HEADER}: 1\" with every mutating /api request.`);\n }\n await next();\n };\n}\n\n/** 429 response for a locked-out address. */\nexport function lockedOut(c: Context, minutes: number): Response {\n c.header(\"Retry-After\", String(minutes * 60));\n const message = `Too many failed attempts. Try again in ${minutes} minute${minutes === 1 ? \"\" : \"s\"}.`;\n return c.json({ error: { code: \"locked_out\", message } }, 429);\n}\n\nexport type CheckResult =\n | { ok: true }\n | { ok: false; lockedMinutes: number; nowBanned: boolean; /** Too many concurrent attempts from this client. */ busy?: boolean };\n\nexport interface CheckOptions {\n /**\n * Count toward (and be slowed down by) the instance-wide failure budget. Off for the setup claim:\n * it's only open while unclaimed and needs the token.\n */\n global?: boolean;\n /** Test hook: how long to wait while over the global budget. */\n slowdownMs?: number;\n}\n\n/**\n * Verifies a secret under the failed-login limiter.\n * - Each client gets one check at a time (a few more may queue, the rest get `busy`), and at most\n * MAX_CONCURRENT_VERIFICATIONS run at once overall.\n * - The attempt is recorded before `verify` (scrypt) runs and refunded if it succeeds, so\n * concurrent requests can't all slip past the ban.\n * - Locked-out clients and secrets this client already failed with never reach `verify`.\n * - While the instance-wide budget is exhausted, each attempt waits a little first; it is never refused.\n */\nexport async function checkSecret(\n ctx: AuthCore,\n ip: string,\n secret: string,\n verify: () => boolean | Promise<boolean>,\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const auth = adminAuth(ctx);\n const key = clientKey(ip);\n const lane = auth.lanes.get(key) ?? { tail: Promise.resolve(), queued: 0 };\n if (lane.queued > MAX_QUEUED_PER_CLIENT) return { ok: false, lockedMinutes: 0, nowBanned: false, busy: true };\n lane.queued++;\n auth.lanes.set(key, lane);\n const previous = lane.tail;\n let done!: () => void;\n lane.tail = new Promise<void>((resolve) => (done = resolve));\n try {\n await previous;\n return await checkOnce(auth, ip, secret, verify, opts);\n } finally {\n done();\n if (--lane.queued === 0) auth.lanes.delete(key);\n }\n}\n\nasync function checkOnce(auth: AdminAuth, ip: string, secret: string, verify: () => boolean | Promise<boolean>, opts: CheckOptions): Promise<CheckResult> {\n const { limiter } = auth;\n const global = opts.global !== false;\n if (global && limiter.overGlobalBudget() && !limiter.bannedMinutes(ip)) {\n await new Promise((r) => setTimeout(r, opts.slowdownMs ?? GLOBAL_SLOWDOWN_MS));\n }\n // From here to the reservation everything is synchronous.\n const locked = limiter.bannedMinutes(ip);\n if (locked) return { ok: false, lockedMinutes: locked, nowBanned: false };\n if (limiter.knownBad(ip, secret)) return { ok: false, lockedMinutes: 0, nowBanned: limiter.fail(ip, secret, { global }) };\n const reservation = limiter.record(ip, secret, { global });\n const release = await auth.verifySlots.acquire();\n let ok = false;\n try {\n ok = await verify();\n } finally {\n release();\n }\n if (!ok) return { ok: false, lockedMinutes: 0, nowBanned: reservation.banned };\n limiter.clear(ip);\n if (reservation.countedGlobally) limiter.refundGlobal();\n return { ok: true };\n}\n\n/** The 429 for a rejected check (locked out or too many at once), or null to continue. */\nexport function rejection(c: Context, check: CheckResult): Response | null {\n if (check.ok) return null;\n if (check.busy) {\n c.header(\"Retry-After\", \"2\");\n return c.json({ error: { code: \"too_many_requests\", message: \"Another sign-in attempt from your address is still being checked. Try again in a moment.\" } }, 429);\n }\n return check.lockedMinutes ? lockedOut(c, check.lockedMinutes) : null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
//#region server/brand.ts
|
|
4
|
+
const LEGACY_PRODUCT = "agentinbox";
|
|
5
|
+
/**
|
|
6
|
+
* `XAQ_<name>`, else the pre-rename `AGENTINBOX_<name>`. A set `XAQ_` variable wins even when empty
|
|
7
|
+
* (an empty value is how some settings are switched off).
|
|
8
|
+
*/
|
|
9
|
+
function envVar(name, env = process.env) {
|
|
10
|
+
const value = env[`XAQ_${name}`];
|
|
11
|
+
return value !== void 0 ? value : env[`AGENTINBOX_${name}`];
|
|
12
|
+
}
|
|
13
|
+
/** Whether this process runs under `service install` (XAQ_SERVICE=1; AGENTINBOX_SERVICE=1 in units from before the rename). */
|
|
14
|
+
const underService = (env = process.env) => envVar("SERVICE", env) === "1";
|
|
15
|
+
/**
|
|
16
|
+
* The directory to use out of a new and a pre-rename location: the new one when it exists, else the old
|
|
17
|
+
* one when that exists, else the new one. Nothing is ever moved.
|
|
18
|
+
*/
|
|
19
|
+
function pickDir(next, legacy) {
|
|
20
|
+
if (!existsSync(next) && existsSync(legacy)) return {
|
|
21
|
+
dir: legacy,
|
|
22
|
+
legacy: true
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
dir: next,
|
|
26
|
+
legacy: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { underService as i, envVar as n, pickDir as r, LEGACY_PRODUCT as t };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=brand--ZB8jTn1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand--ZB8jTn1.js","names":[],"sources":["../server/brand.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\n\n/**\n * The product name, and the one it had before (agentinbox → xaq). Installs from before the rename keep\n * working: their environment variables, data directory, services and agent-side names are still found.\n */\nexport const PRODUCT = \"xaq\";\nexport const LEGACY_PRODUCT = \"agentinbox\";\n\n/**\n * `XAQ_<name>`, else the pre-rename `AGENTINBOX_<name>`. A set `XAQ_` variable wins even when empty\n * (an empty value is how some settings are switched off).\n */\nexport function envVar(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {\n const value = env[`XAQ_${name}`];\n return value !== undefined ? value : env[`AGENTINBOX_${name}`];\n}\n\n/** Whether this process runs under `service install` (XAQ_SERVICE=1; AGENTINBOX_SERVICE=1 in units from before the rename). */\nexport const underService = (env: NodeJS.ProcessEnv = process.env) => envVar(\"SERVICE\", env) === \"1\";\n\n/**\n * The directory to use out of a new and a pre-rename location: the new one when it exists, else the old\n * one when that exists, else the new one. Nothing is ever moved.\n */\nexport function pickDir(next: string, legacy: string): { dir: string; legacy: boolean } {\n if (!existsSync(next) && existsSync(legacy)) return { dir: legacy, legacy: true };\n return { dir: next, legacy: false };\n}\n"],"mappings":";;;AAOA,MAAa,iBAAiB;;;;;AAM9B,SAAgB,OAAO,MAAc,MAAyB,QAAQ,KAAyB;CAC7F,MAAM,QAAQ,IAAI,OAAO;CACzB,OAAO,UAAU,KAAA,IAAY,QAAQ,IAAI,cAAc;AACzD;;AAGA,MAAa,gBAAgB,MAAyB,QAAQ,QAAQ,OAAO,WAAW,GAAG,MAAM;;;;;AAMjG,SAAgB,QAAQ,MAAc,QAAkD;CACtF,IAAI,CAAC,WAAW,IAAI,KAAK,WAAW,MAAM,GAAG,OAAO;EAAE,KAAK;EAAQ,QAAQ;CAAK;CAChF,OAAO;EAAE,KAAK;EAAM,QAAQ;CAAM;AACpC"}
|