@osqd/bothandlerjs 0.3.0

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 (186) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/LICENSE +102 -0
  3. package/README.md +308 -0
  4. package/bin/bothandlerjs.mjs +14 -0
  5. package/dist/actions/index.d.ts +22 -0
  6. package/dist/actions/types.d.ts +59 -0
  7. package/dist/adapters/fastify.d.ts +43 -0
  8. package/dist/adapters/fetch.d.ts +51 -0
  9. package/dist/adapters/index.cjs +723 -0
  10. package/dist/adapters/index.cjs.map +1 -0
  11. package/dist/adapters/index.d.ts +8 -0
  12. package/dist/adapters/index.js +692 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/koa.d.ts +27 -0
  15. package/dist/adapters/node.d.ts +29 -0
  16. package/dist/adapters/shared.d.ts +39 -0
  17. package/dist/audit.d.ts +172 -0
  18. package/dist/challenge/index.d.ts +176 -0
  19. package/dist/challenge/language.d.ts +57 -0
  20. package/dist/challenge/page.d.ts +42 -0
  21. package/dist/challenge/pow.d.ts +33 -0
  22. package/dist/challenge/token.d.ts +72 -0
  23. package/dist/cli.cjs +13388 -0
  24. package/dist/cli.cjs.map +1 -0
  25. package/dist/cli.d.ts +5 -0
  26. package/dist/cli.js +13358 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/client/index.cjs +139 -0
  29. package/dist/client/index.cjs.map +1 -0
  30. package/dist/client/index.d.ts +63 -0
  31. package/dist/client/index.js +112 -0
  32. package/dist/client/index.js.map +1 -0
  33. package/dist/config.d.ts +316 -0
  34. package/dist/core.d.ts +443 -0
  35. package/dist/corpus/adversarial.d.ts +2 -0
  36. package/dist/corpus/advertising-email.d.ts +2 -0
  37. package/dist/corpus/ai-crawlers.d.ts +2 -0
  38. package/dist/corpus/benign-bots.d.ts +2 -0
  39. package/dist/corpus/cdn-gateways.d.ts +2 -0
  40. package/dist/corpus/cookies.d.ts +49 -0
  41. package/dist/corpus/crawlers-regional.d.ts +2 -0
  42. package/dist/corpus/crawlers-vertical.d.ts +2 -0
  43. package/dist/corpus/headers.d.ts +403 -0
  44. package/dist/corpus/humans-apps.d.ts +2 -0
  45. package/dist/corpus/humans-browsers.d.ts +2 -0
  46. package/dist/corpus/humans.d.ts +2 -0
  47. package/dist/corpus/index.cjs +4196 -0
  48. package/dist/corpus/index.cjs.map +1 -0
  49. package/dist/corpus/index.d.ts +37 -0
  50. package/dist/corpus/index.js +4132 -0
  51. package/dist/corpus/index.js.map +1 -0
  52. package/dist/corpus/infrastructure.d.ts +17 -0
  53. package/dist/corpus/libraries-extended.d.ts +2 -0
  54. package/dist/corpus/ranges.d.ts +31 -0
  55. package/dist/corpus/reputation.d.ts +21 -0
  56. package/dist/corpus/runner.d.ts +130 -0
  57. package/dist/corpus/schema.d.ts +185 -0
  58. package/dist/corpus/tooling.d.ts +2 -0
  59. package/dist/corpus/unwanted.d.ts +2 -0
  60. package/dist/crawler-ranges.d.ts +113 -0
  61. package/dist/dashboard/client.generated.d.ts +7 -0
  62. package/dist/dashboard/feed.d.ts +186 -0
  63. package/dist/dashboard/index.d.ts +5 -0
  64. package/dist/dashboard/page.d.ts +51 -0
  65. package/dist/dashboard/parse-request.d.ts +28 -0
  66. package/dist/dashboard/preview.d.ts +45 -0
  67. package/dist/dashboard/server.d.ts +70 -0
  68. package/dist/dashboard/types.d.ts +714 -0
  69. package/dist/detectors/accept-signature.d.ts +6 -0
  70. package/dist/detectors/browsing-coherence.d.ts +44 -0
  71. package/dist/detectors/cadence.d.ts +28 -0
  72. package/dist/detectors/clearance.d.ts +25 -0
  73. package/dist/detectors/client-hints.d.ts +19 -0
  74. package/dist/detectors/client-signals.d.ts +27 -0
  75. package/dist/detectors/crawl-breadth.d.ts +28 -0
  76. package/dist/detectors/crawler-verification.d.ts +39 -0
  77. package/dist/detectors/fetch-metadata.d.ts +20 -0
  78. package/dist/detectors/header-integrity.d.ts +26 -0
  79. package/dist/detectors/header-order.d.ts +27 -0
  80. package/dist/detectors/identity-rotation.d.ts +34 -0
  81. package/dist/detectors/index.d.ts +56 -0
  82. package/dist/detectors/ip-intelligence.d.ts +30 -0
  83. package/dist/detectors/known-bots.d.ts +94 -0
  84. package/dist/detectors/probe-signature.d.ts +50 -0
  85. package/dist/detectors/rate-anomaly.d.ts +36 -0
  86. package/dist/detectors/self-identified.d.ts +33 -0
  87. package/dist/detectors/session-integrity.d.ts +24 -0
  88. package/dist/detectors/tls-fingerprint.d.ts +34 -0
  89. package/dist/detectors/trap.d.ts +98 -0
  90. package/dist/detectors/types.d.ts +70 -0
  91. package/dist/detectors/ua-coherence.d.ts +32 -0
  92. package/dist/evidence.d.ts +45 -0
  93. package/dist/facts.d.ts +46 -0
  94. package/dist/index.cjs +9600 -0
  95. package/dist/index.cjs.map +1 -0
  96. package/dist/index.d.ts +63 -0
  97. package/dist/index.js +9444 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/internal/async.d.ts +16 -0
  100. package/dist/internal/clock.d.ts +17 -0
  101. package/dist/internal/crypto.d.ts +26 -0
  102. package/dist/internal/dns.d.ts +100 -0
  103. package/dist/internal/emitter.d.ts +18 -0
  104. package/dist/internal/http.d.ts +22 -0
  105. package/dist/internal/ip.d.ts +73 -0
  106. package/dist/internal/lru.d.ts +34 -0
  107. package/dist/internal/matcher.d.ts +41 -0
  108. package/dist/internal/pattern.d.ts +14 -0
  109. package/dist/internal/ua.d.ts +92 -0
  110. package/dist/metrics.d.ts +120 -0
  111. package/dist/notify/hub.d.ts +70 -0
  112. package/dist/notify/index.d.ts +7 -0
  113. package/dist/notify/redact.d.ts +48 -0
  114. package/dist/notify/sinks.d.ts +62 -0
  115. package/dist/notify/types.d.ts +56 -0
  116. package/dist/policy/index.d.ts +7 -0
  117. package/dist/policy/match.d.ts +20 -0
  118. package/dist/policy/policy.d.ts +123 -0
  119. package/dist/policy/presets.d.ts +161 -0
  120. package/dist/policy/types.d.ts +155 -0
  121. package/dist/robots.d.ts +83 -0
  122. package/dist/state.d.ts +161 -0
  123. package/dist/stores/index.d.ts +5 -0
  124. package/dist/stores/memory.d.ts +33 -0
  125. package/dist/stores/redis.d.ts +59 -0
  126. package/dist/stores/types.d.ts +37 -0
  127. package/dist/types.d.ts +250 -0
  128. package/docs/challenge/index.md +152 -0
  129. package/docs/challenge/localisation.md +105 -0
  130. package/docs/concepts/actors.md +113 -0
  131. package/docs/concepts/evidence.md +133 -0
  132. package/docs/concepts/the-guard.md +121 -0
  133. package/docs/concepts/threat-model.md +120 -0
  134. package/docs/concepts/verdicts.md +108 -0
  135. package/docs/course/01-first-assessment.md +178 -0
  136. package/docs/course/02-proof-and-suspicion.md +164 -0
  137. package/docs/course/03-verdicts-and-scores.md +160 -0
  138. package/docs/course/04-the-guard.md +200 -0
  139. package/docs/course/05-detectors.md +178 -0
  140. package/docs/course/06-identity.md +161 -0
  141. package/docs/course/07-actors.md +178 -0
  142. package/docs/course/08-traps.md +183 -0
  143. package/docs/course/09-rules.md +224 -0
  144. package/docs/course/10-actions-and-presets.md +216 -0
  145. package/docs/course/11-the-challenge.md +227 -0
  146. package/docs/course/12-going-live.md +211 -0
  147. package/docs/course/13-operating-it.md +238 -0
  148. package/docs/course/14-scaling.md +195 -0
  149. package/docs/course/15-extending.md +259 -0
  150. package/docs/course/16-proving-it.md +266 -0
  151. package/docs/course/index.md +112 -0
  152. package/docs/design/decisions.md +216 -0
  153. package/docs/detection/client-signals.md +96 -0
  154. package/docs/detection/detectors.md +376 -0
  155. package/docs/detection/index.md +134 -0
  156. package/docs/detection/signatures.md +111 -0
  157. package/docs/detection/verification.md +123 -0
  158. package/docs/detection/writing-a-detector.md +145 -0
  159. package/docs/index.md +100 -0
  160. package/docs/integration/adapters.md +138 -0
  161. package/docs/integration/client-ip.md +114 -0
  162. package/docs/integration/index.md +55 -0
  163. package/docs/integration/stores.md +76 -0
  164. package/docs/operations/audit.md +116 -0
  165. package/docs/operations/dashboard.md +725 -0
  166. package/docs/operations/index.md +101 -0
  167. package/docs/operations/metrics.md +74 -0
  168. package/docs/operations/notifications.md +72 -0
  169. package/docs/operations/runtime-changes.md +118 -0
  170. package/docs/policy/actions.md +196 -0
  171. package/docs/policy/index.md +84 -0
  172. package/docs/policy/presets.md +191 -0
  173. package/docs/policy/robots.md +128 -0
  174. package/docs/policy/rules.md +134 -0
  175. package/docs/reference/api.md +126 -0
  176. package/docs/reference/configuration.md +175 -0
  177. package/docs/start/choosing-a-policy.md +127 -0
  178. package/docs/start/first-integration.md +127 -0
  179. package/docs/start/installation.md +63 -0
  180. package/docs/start/upgrading.md +72 -0
  181. package/docs/testing/cli.md +119 -0
  182. package/docs/testing/corpus.md +129 -0
  183. package/docs/testing/index.md +61 -0
  184. package/docs/testing/replay.md +102 -0
  185. package/docs/testing/try-it.md +189 -0
  186. package/package.json +145 -0
@@ -0,0 +1,723 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/index.ts
21
+ var adapters_exports = {};
22
+ __export(adapters_exports, {
23
+ botHandler: () => botHandler,
24
+ createFetchAdapter: () => createFetchAdapter,
25
+ fastifyBotHandler: () => fastifyBotHandler,
26
+ koaBotHandler: () => koaBotHandler,
27
+ withBotHandler: () => withBotHandler
28
+ });
29
+ module.exports = __toCommonJS(adapters_exports);
30
+
31
+ // src/internal/http.ts
32
+ var MAX_COOKIE_HEADER = 8192;
33
+ var MAX_COOKIES = 64;
34
+ function parseCookies(header) {
35
+ const cookies = /* @__PURE__ */ Object.create(null);
36
+ if (header === void 0 || header.length === 0) return cookies;
37
+ const source = header.length > MAX_COOKIE_HEADER ? header.slice(0, MAX_COOKIE_HEADER) : header;
38
+ let count = 0;
39
+ for (const pair of source.split(";")) {
40
+ if (count >= MAX_COOKIES) break;
41
+ const equals = pair.indexOf("=");
42
+ if (equals <= 0) continue;
43
+ const name = pair.slice(0, equals).trim();
44
+ if (name.length === 0) continue;
45
+ const rawValue = pair.slice(equals + 1).trim();
46
+ const value = rawValue.startsWith('"') && rawValue.endsWith('"') ? rawValue.slice(1, -1) : rawValue;
47
+ try {
48
+ cookies[name] = decodeURIComponent(value);
49
+ } catch {
50
+ cookies[name] = value;
51
+ }
52
+ count++;
53
+ }
54
+ return cookies;
55
+ }
56
+ function joinHeaderValue(value) {
57
+ if (value === void 0) return void 0;
58
+ return Array.isArray(value) ? value.join(", ") : value;
59
+ }
60
+
61
+ // src/internal/ip.ts
62
+ function parseIp(value) {
63
+ const input = value.trim();
64
+ if (input.length === 0 || input.length > 45) return null;
65
+ const bare = input.startsWith("[") && input.endsWith("]") ? input.slice(1, -1) : input;
66
+ if (bare.includes(":")) return parseIpv6(bare);
67
+ return parseIpv4(bare);
68
+ }
69
+ function parseIpv4(value) {
70
+ const parts = value.split(".");
71
+ if (parts.length !== 4) return null;
72
+ const bytes = new Uint8Array(4);
73
+ for (let i = 0; i < 4; i++) {
74
+ const part = parts[i];
75
+ if (part.length === 0 || part.length > 3) return null;
76
+ if (!/^\d+$/.test(part)) return null;
77
+ if (part.length > 1 && part[0] === "0") return null;
78
+ const n = Number(part);
79
+ if (n > 255) return null;
80
+ bytes[i] = n;
81
+ }
82
+ return bytes;
83
+ }
84
+ function parseIpv6(value) {
85
+ const withoutZone = value.split("%")[0];
86
+ const doubleColon = withoutZone.indexOf("::");
87
+ if (doubleColon !== withoutZone.lastIndexOf("::")) return null;
88
+ const [headText, tailText] = doubleColon === -1 ? [withoutZone, ""] : [withoutZone.slice(0, doubleColon), withoutZone.slice(doubleColon + 2)];
89
+ const head = headText.length > 0 ? headText.split(":") : [];
90
+ const tail = tailText.length > 0 ? tailText.split(":") : [];
91
+ let embedded = null;
92
+ const groups = [...head, ...tail];
93
+ const last = groups[groups.length - 1];
94
+ if (last !== void 0 && last.includes(".")) {
95
+ embedded = parseIpv4(last);
96
+ if (!embedded) return null;
97
+ if (tail.length > 0) tail.pop();
98
+ else head.pop();
99
+ }
100
+ const groupCount = head.length + tail.length + (embedded ? 2 : 0);
101
+ if (doubleColon === -1 ? groupCount !== 8 : groupCount > 7) return null;
102
+ const bytes = new Uint8Array(16);
103
+ let offset = 0;
104
+ for (const group of head) {
105
+ if (!writeGroup(bytes, offset, group)) return null;
106
+ offset += 2;
107
+ }
108
+ offset = 16 - tail.length * 2 - (embedded ? 4 : 0);
109
+ for (const group of tail) {
110
+ if (!writeGroup(bytes, offset, group)) return null;
111
+ offset += 2;
112
+ }
113
+ if (embedded) bytes.set(embedded, 12);
114
+ if (isIpv4Mapped(bytes)) return bytes.slice(12, 16);
115
+ return bytes;
116
+ }
117
+ function writeGroup(bytes, offset, group) {
118
+ if (group.length === 0 || group.length > 4 || !/^[0-9a-fA-F]+$/.test(group)) return false;
119
+ const n = Number.parseInt(group, 16);
120
+ bytes[offset] = n >> 8;
121
+ bytes[offset + 1] = n & 255;
122
+ return true;
123
+ }
124
+ function isIpv4Mapped(bytes) {
125
+ for (let i = 0; i < 10; i++) if (bytes[i] !== 0) return false;
126
+ return bytes[10] === 255 && bytes[11] === 255;
127
+ }
128
+ function formatIp(bytes) {
129
+ if (bytes.length === 4) return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`;
130
+ const groups = [];
131
+ for (let i = 0; i < 16; i += 2) groups.push((bytes[i] << 8 | bytes[i + 1]).toString(16));
132
+ let bestStart = -1;
133
+ let bestLen = 0;
134
+ for (let i = 0; i < 8; i++) {
135
+ if (groups[i] !== "0") continue;
136
+ let j = i;
137
+ while (j < 8 && groups[j] === "0") j++;
138
+ if (j - i > bestLen) {
139
+ bestLen = j - i;
140
+ bestStart = i;
141
+ }
142
+ i = j;
143
+ }
144
+ if (bestLen < 2) return groups.join(":");
145
+ return `${groups.slice(0, bestStart).join(":")}::${groups.slice(bestStart + bestLen).join(":")}`;
146
+ }
147
+ function normalizeIp(value) {
148
+ const bytes = parseIp(value);
149
+ return bytes ? formatIp(bytes) : null;
150
+ }
151
+ function parseCidr(value) {
152
+ const slash = value.lastIndexOf("/");
153
+ const addressText = slash === -1 ? value : value.slice(0, slash);
154
+ const bytes = parseIp(addressText);
155
+ if (!bytes) return null;
156
+ const maxPrefix = bytes.length * 8;
157
+ if (slash === -1) return { bytes, prefix: maxPrefix, source: value };
158
+ const prefixText = value.slice(slash + 1);
159
+ if (!/^\d{1,3}$/.test(prefixText)) return null;
160
+ const prefix = Number(prefixText);
161
+ if (prefix > maxPrefix) return null;
162
+ return { bytes, prefix, source: value };
163
+ }
164
+ function cidrContains(cidr, ip) {
165
+ if (cidr.bytes.length !== ip.length) return false;
166
+ const fullBytes = cidr.prefix >> 3;
167
+ for (let i = 0; i < fullBytes; i++) if (cidr.bytes[i] !== ip[i]) return false;
168
+ const remainder = cidr.prefix & 7;
169
+ if (remainder === 0) return true;
170
+ const mask = 255 << 8 - remainder;
171
+ return (cidr.bytes[fullBytes] & mask) === (ip[fullBytes] & mask);
172
+ }
173
+ var FamilyIndex = class {
174
+ /** Prefix < 8. Spans first bytes, so it is always scanned. Realistically empty. */
175
+ wide = [];
176
+ buckets = /* @__PURE__ */ new Map();
177
+ add(entry) {
178
+ const { cidr } = entry;
179
+ if (cidr.prefix < 8) {
180
+ this.wide.push(entry);
181
+ return;
182
+ }
183
+ const first = cidr.bytes[0];
184
+ let bucket = this.buckets.get(first);
185
+ if (bucket === void 0) {
186
+ bucket = { mid: [], deep: /* @__PURE__ */ new Map() };
187
+ this.buckets.set(first, bucket);
188
+ }
189
+ if (cidr.prefix < 16) {
190
+ bucket.mid.push(entry);
191
+ return;
192
+ }
193
+ const second = cidr.bytes[1];
194
+ let deep = bucket.deep.get(second);
195
+ if (deep === void 0) {
196
+ deep = [];
197
+ bucket.deep.set(second, deep);
198
+ }
199
+ deep.push(entry);
200
+ }
201
+ /** The earliest-added range containing `ip`, or `undefined`. */
202
+ find(ip) {
203
+ let best = scan(this.wide, ip, void 0);
204
+ const bucket = this.buckets.get(ip[0]);
205
+ if (bucket !== void 0) {
206
+ best = scan(bucket.mid, ip, best);
207
+ const deep = bucket.deep.get(ip[1]);
208
+ if (deep !== void 0) best = scan(deep, ip, best);
209
+ }
210
+ return best;
211
+ }
212
+ };
213
+ function scan(entries, ip, best) {
214
+ let winner = best;
215
+ for (let i = 0; i < entries.length; i++) {
216
+ const entry = entries[i];
217
+ if (winner !== void 0 && entry.order > winner.order) continue;
218
+ if (cidrContains(entry.cidr, ip)) winner = entry;
219
+ }
220
+ return winner;
221
+ }
222
+ var IpRangeSet = class {
223
+ v4 = new FamilyIndex();
224
+ v6 = new FamilyIndex();
225
+ count = 0;
226
+ /**
227
+ * Every range that parsed, in the order it was added.
228
+ *
229
+ * Kept as plain text beside the index rather than reconstructed from it. The index
230
+ * is shaped for lookups — bucketed by leading byte, prefix-split — and walking it
231
+ * back into a list would be both slower and, for anything that wants to *show* the
232
+ * set, wrong: what an operator recognises is the string they wrote, not a normalised
233
+ * form of it. These sets are configuration-sized, so the array costs nothing.
234
+ */
235
+ sources = [];
236
+ /** Ranges that failed to parse, surfaced so a typo in config is loud rather than silent. */
237
+ invalid = [];
238
+ constructor(ranges = []) {
239
+ for (const range of ranges) this.add(range);
240
+ }
241
+ add(range) {
242
+ const cidr = parseCidr(range);
243
+ if (!cidr) {
244
+ this.invalid.push(range);
245
+ return;
246
+ }
247
+ this.sources.push(cidr.source);
248
+ (cidr.bytes.length === 4 ? this.v4 : this.v6).add({ cidr, order: this.count++ });
249
+ }
250
+ get size() {
251
+ return this.count;
252
+ }
253
+ /** The ranges in this set, as written, oldest first. What a reader can act on. */
254
+ entries() {
255
+ return this.sources;
256
+ }
257
+ /**
258
+ * Returns the matching range's original text, or `undefined`. Useful for explaining
259
+ * a decision. When several ranges match, the one added first wins, so the
260
+ * explanation does not depend on the index's internal layout.
261
+ */
262
+ match(ip) {
263
+ const bytes = typeof ip === "string" ? parseIp(ip) : ip;
264
+ if (!bytes) return void 0;
265
+ return (bytes.length === 4 ? this.v4 : this.v6).find(bytes)?.cidr.source;
266
+ }
267
+ contains(ip) {
268
+ const bytes = typeof ip === "string" ? parseIp(ip) : ip;
269
+ if (!bytes) return false;
270
+ return (bytes.length === 4 ? this.v4 : this.v6).find(bytes) !== void 0;
271
+ }
272
+ };
273
+ var SPECIAL_USE_RANGES = [
274
+ "0.0.0.0/8",
275
+ "10.0.0.0/8",
276
+ "100.64.0.0/10",
277
+ "127.0.0.0/8",
278
+ "169.254.0.0/16",
279
+ "172.16.0.0/12",
280
+ "192.0.0.0/24",
281
+ "192.0.2.0/24",
282
+ "192.168.0.0/16",
283
+ "198.18.0.0/15",
284
+ "198.51.100.0/24",
285
+ "203.0.113.0/24",
286
+ "224.0.0.0/4",
287
+ "240.0.0.0/4",
288
+ "::1/128",
289
+ "fc00::/7",
290
+ "fe80::/10",
291
+ "2001:db8::/32"
292
+ ];
293
+ var SPECIAL_USE = new IpRangeSet(SPECIAL_USE_RANGES);
294
+
295
+ // src/facts.ts
296
+ var MAX_URL_LENGTH = 8192;
297
+ var MAX_QUERY_PARAMS = 64;
298
+ function createFacts(input) {
299
+ const rawUrl = input.url ?? "/";
300
+ const url = rawUrl.length > MAX_URL_LENGTH ? rawUrl.slice(0, MAX_URL_LENGTH) : rawUrl;
301
+ const queryStart = url.indexOf("?");
302
+ const rawPath = queryStart === -1 ? url : url.slice(0, queryStart);
303
+ const headers = /* @__PURE__ */ Object.create(null);
304
+ for (const [name, value] of Object.entries(input.headers)) {
305
+ const joined = joinHeaderValue(value);
306
+ if (joined !== void 0) headers[name.toLowerCase()] = joined;
307
+ }
308
+ const facts = {
309
+ method: (input.method ?? "GET").toUpperCase(),
310
+ path: normalizePath(rawPath),
311
+ query: parseQuery(queryStart === -1 ? "" : url.slice(queryStart + 1)),
312
+ headers,
313
+ headerOrder: extractOrder(input.rawHeaders, headers),
314
+ ip: normalizeIp(input.ip) ?? input.ip,
315
+ timestamp: input.timestamp ?? Date.now()
316
+ };
317
+ const cookieHeader = headers["cookie"];
318
+ if (cookieHeader !== void 0) facts.cookies = parseCookies(cookieHeader);
319
+ if (input.protocol !== void 0) facts.protocol = input.protocol;
320
+ if (input.httpVersion !== void 0) facts.httpVersion = input.httpVersion;
321
+ if (input.tlsFingerprint !== void 0) facts.tlsFingerprint = input.tlsFingerprint;
322
+ if (input.partialHeaders === true) facts.partialHeaders = true;
323
+ if (input.extra !== void 0) facts.extra = input.extra;
324
+ return facts;
325
+ }
326
+ function normalizePath(rawPath) {
327
+ let path = rawPath;
328
+ try {
329
+ path = decodeURIComponent(rawPath);
330
+ } catch {
331
+ }
332
+ path = path.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
333
+ if (!path.startsWith("/")) path = `/${path}`;
334
+ if (path.includes("./")) {
335
+ const resolved = [];
336
+ for (const segment of path.split("/")) {
337
+ if (segment === "" || segment === ".") continue;
338
+ if (segment === "..") resolved.pop();
339
+ else resolved.push(segment);
340
+ }
341
+ path = `/${resolved.join("/")}`;
342
+ }
343
+ return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
344
+ }
345
+ function parseQuery(search) {
346
+ const query = /* @__PURE__ */ Object.create(null);
347
+ if (search.length === 0) return query;
348
+ let count = 0;
349
+ for (const [key, value] of new URLSearchParams(search)) {
350
+ if (count++ >= MAX_QUERY_PARAMS) break;
351
+ query[key] = value.length > 1024 ? value.slice(0, 1024) : value;
352
+ }
353
+ return query;
354
+ }
355
+ var MAX_ORDERED_HEADERS = 64;
356
+ function extractOrder(rawHeaders, headers) {
357
+ if (!rawHeaders || rawHeaders.length === 0) return EMPTY_ORDER;
358
+ let isNodeStyle = rawHeaders.length % 2 === 0;
359
+ if (isNodeStyle) {
360
+ let everyEntryIsAHeader = true;
361
+ for (let i = 0; i < rawHeaders.length; i++) {
362
+ const entry = rawHeaders[i];
363
+ const known = headers[entry.toLowerCase()] !== void 0;
364
+ if (i % 2 === 0 && !known && !isHeaderName(entry)) {
365
+ isNodeStyle = false;
366
+ break;
367
+ }
368
+ if (!known) everyEntryIsAHeader = false;
369
+ }
370
+ if (everyEntryIsAHeader) isNodeStyle = false;
371
+ }
372
+ const step = isNodeStyle ? 2 : 1;
373
+ const order = [];
374
+ for (let i = 0; i < rawHeaders.length && order.length < MAX_ORDERED_HEADERS; i += step) {
375
+ order.push(rawHeaders[i].toLowerCase());
376
+ }
377
+ return order;
378
+ }
379
+ var EMPTY_ORDER = Object.freeze([]);
380
+ function isHeaderName(value) {
381
+ if (value.length === 0) return false;
382
+ for (let i = 0; i < value.length; i++) {
383
+ const code = value.charCodeAt(i);
384
+ const ok = code >= 48 && code <= 57 || // 0-9
385
+ code >= 65 && code <= 90 || // A-Z
386
+ code >= 97 && code <= 122 || // a-z
387
+ code === 33 || code >= 35 && code <= 39 || code === 42 || code === 43 || code === 45 || code === 46 || code === 94 || code === 95 || code === 96 || code === 124 || code === 126;
388
+ if (!ok) return false;
389
+ }
390
+ return true;
391
+ }
392
+
393
+ // src/adapters/shared.ts
394
+ var MAX_VERIFY_BODY = 4096;
395
+ function pause(ms) {
396
+ return new Promise((resolve) => {
397
+ const timer = setTimeout(resolve, ms);
398
+ timer.unref?.();
399
+ });
400
+ }
401
+ function parseJson(text) {
402
+ if (text.length === 0 || text.length > MAX_VERIFY_BODY) return void 0;
403
+ try {
404
+ return JSON.parse(text);
405
+ } catch {
406
+ return void 0;
407
+ }
408
+ }
409
+ function readBoundedBody(request) {
410
+ return new Promise((resolve) => {
411
+ let data = "";
412
+ let bytes = 0;
413
+ request.on("data", (chunk) => {
414
+ bytes += chunk.length;
415
+ if (bytes > MAX_VERIFY_BODY) {
416
+ request.destroy();
417
+ return;
418
+ }
419
+ data += chunk.toString("utf8");
420
+ });
421
+ request.on("end", () => resolve(data));
422
+ request.on("error", () => resolve(""));
423
+ });
424
+ }
425
+ function verificationBody(outcome) {
426
+ return JSON.stringify(outcome.ok ? { ok: true } : { ok: false, error: outcome.reason });
427
+ }
428
+
429
+ // src/adapters/node.ts
430
+ function botHandler(handler, options = {}) {
431
+ const mountChallenge = options.mountChallengeEndpoint ?? true;
432
+ return function botHandlerMiddleware(request, response, next) {
433
+ let handedOff = false;
434
+ void (async () => {
435
+ try {
436
+ const headers = {};
437
+ for (const [name, value] of Object.entries(request.headers)) {
438
+ headers[name] = Array.isArray(value) ? value.join(", ") : value;
439
+ }
440
+ const ip = handler.resolveIp(request.socket.remoteAddress, headers);
441
+ const fingerprint = options.tlsFingerprintHeader !== void 0 ? headers[options.tlsFingerprintHeader.toLowerCase()] : void 0;
442
+ let facts = createFacts({
443
+ method: request.method,
444
+ url: request.url,
445
+ headers: request.headers,
446
+ rawHeaders: request.rawHeaders,
447
+ ip,
448
+ httpVersion: request.httpVersion,
449
+ // `encrypted` is present only on a TLS socket. Behind a terminating proxy
450
+ // the forwarded protocol header is the only truth available.
451
+ protocol: request.socket.encrypted === true || headers["x-forwarded-proto"] === "https" ? "https" : "http",
452
+ ...fingerprint !== void 0 ? { tlsFingerprint: fingerprint } : {}
453
+ });
454
+ if (options.enrich) facts = options.enrich(request, facts);
455
+ if (mountChallenge && handler.isChallengeEndpoint(facts)) {
456
+ await serveVerification(handler, request, response, facts);
457
+ return;
458
+ }
459
+ const { outcome } = await handler.handle(facts);
460
+ if (outcome.kind === "drop") {
461
+ request.socket.destroy();
462
+ return;
463
+ }
464
+ if (outcome.kind === "respond") {
465
+ response.statusCode = outcome.status;
466
+ for (const [name, value] of Object.entries(outcome.headers)) response.setHeader(name, value);
467
+ response.end(outcome.body);
468
+ return;
469
+ }
470
+ for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {
471
+ request.headers[name] = value;
472
+ }
473
+ if (outcome.responseHeaders) {
474
+ for (const [name, value] of Object.entries(outcome.responseHeaders)) response.setHeader(name, value);
475
+ }
476
+ if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
477
+ handedOff = true;
478
+ next();
479
+ } catch (error) {
480
+ failOpen(handler, error, "adapter:node");
481
+ if (!handedOff && !response.headersSent) next();
482
+ }
483
+ })();
484
+ };
485
+ }
486
+ function failOpen(handler, error, source) {
487
+ handler.config.onError(error, { source });
488
+ }
489
+ async function serveVerification(handler, request, response, facts) {
490
+ const outcome = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(request)));
491
+ response.setHeader("content-type", "application/json; charset=utf-8");
492
+ response.setHeader("cache-control", "no-store");
493
+ if (outcome.ok) response.setHeader("set-cookie", outcome.setCookie);
494
+ response.statusCode = outcome.ok ? 200 : outcome.status;
495
+ response.end(verificationBody(outcome));
496
+ }
497
+
498
+ // src/adapters/fetch.ts
499
+ var DEFAULT_IP_HEADERS = ["cf-connecting-ip", "x-real-ip"];
500
+ function createFetchAdapter(handler, options = {}) {
501
+ const ipHeaders = options.ipHeaders ?? DEFAULT_IP_HEADERS;
502
+ const mountChallenge = options.mountChallengeEndpoint ?? true;
503
+ let warnedAboutAddress = false;
504
+ return async function evaluate3(request, context = void 0) {
505
+ try {
506
+ return await decide(request, context);
507
+ } catch (error) {
508
+ handler.config.onError(error, { source: "adapter:fetch" });
509
+ return { request };
510
+ }
511
+ };
512
+ async function decide(request, context) {
513
+ const headers = {};
514
+ const order = [];
515
+ for (const [name, value] of request.headers) {
516
+ headers[name] = value;
517
+ order.push(name);
518
+ }
519
+ const url = new URL(request.url);
520
+ const ip = options.clientIp?.(request, context) ?? firstHeaderAddress(headers, ipHeaders) ?? "";
521
+ if (ip === "" && !warnedAboutAddress) {
522
+ warnedAboutAddress = true;
523
+ handler.warn(
524
+ `The fetch adapter could not determine a client address: none of ${ipHeaders.join(", ")} is present and no \`clientIp\` was supplied. Every request will be tracked under one empty actor key, so rate limits and behavioural detection apply to your whole site at once. Pass \`clientIp\` to read the address from your platform's connection info, or list the header your edge sets.`
525
+ );
526
+ }
527
+ const fingerprint = options.tlsFingerprintHeader !== void 0 ? headers[options.tlsFingerprintHeader.toLowerCase()] : void 0;
528
+ let facts = createFacts({
529
+ method: request.method,
530
+ url: `${url.pathname}${url.search}`,
531
+ headers,
532
+ // The Fetch API normalises and sorts headers, so wire order is genuinely
533
+ // unavailable here. Passing the sorted order would be worse than passing none:
534
+ // the header-order detector would compare against an ordering no client chose.
535
+ rawHeaders: [],
536
+ ip,
537
+ protocol: url.protocol === "https:" ? "https" : "http",
538
+ ...fingerprint !== void 0 ? { tlsFingerprint: fingerprint } : {}
539
+ });
540
+ if (options.enrich) facts = options.enrich(request, facts);
541
+ if (mountChallenge && handler.isChallengeEndpoint(facts)) {
542
+ const body = await readBounded(request);
543
+ const outcome2 = await handler.verifyChallenge(facts, parseJson(body));
544
+ const responseHeaders = new Headers({ "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
545
+ if (outcome2.ok) responseHeaders.set("set-cookie", outcome2.setCookie);
546
+ return {
547
+ response: new Response(JSON.stringify(outcome2.ok ? { ok: true } : { ok: false, error: outcome2.reason }), {
548
+ status: outcome2.ok ? 200 : outcome2.status,
549
+ headers: responseHeaders
550
+ }),
551
+ request
552
+ };
553
+ }
554
+ const result = await handler.handle(facts);
555
+ const { outcome } = result;
556
+ if (outcome.kind === "drop") {
557
+ return { response: new Response(null, { status: 444 }), request, result };
558
+ }
559
+ if (outcome.kind === "respond") {
560
+ return { response: new Response(outcome.body, { status: outcome.status, headers: outcome.headers }), request, result };
561
+ }
562
+ if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
563
+ const tagged = outcome.requestHeaders ? new Request(request, { headers: mergeHeaders(request.headers, outcome.requestHeaders) }) : request;
564
+ return { request: tagged, result };
565
+ }
566
+ }
567
+ function withBotHandler(handler, next, options = {}) {
568
+ const evaluate3 = createFetchAdapter(handler, options);
569
+ return async (request, context = void 0) => {
570
+ const decision = await evaluate3(request, context);
571
+ if (decision.response) return decision.response;
572
+ const response = await next(decision.request, context);
573
+ const extra = decision.result?.outcome.kind === "continue" ? decision.result.outcome.responseHeaders : void 0;
574
+ if (!extra) return response;
575
+ const merged = new Response(response.body, response);
576
+ for (const [name, value] of Object.entries(extra)) merged.headers.set(name, value);
577
+ return merged;
578
+ };
579
+ }
580
+ function firstHeaderAddress(headers, names) {
581
+ for (const name of names) {
582
+ const value = headers[name];
583
+ if (value === void 0) continue;
584
+ const first = value.split(",")[0]?.trim();
585
+ if (first !== void 0 && first.length > 0) return first;
586
+ }
587
+ return void 0;
588
+ }
589
+ function mergeHeaders(original, additions) {
590
+ const merged = new Headers(original);
591
+ for (const [name, value] of Object.entries(additions)) merged.set(name, value);
592
+ return merged;
593
+ }
594
+ async function readBounded(request) {
595
+ const text = await request.clone().text();
596
+ return text.length > MAX_VERIFY_BODY ? "" : text;
597
+ }
598
+
599
+ // src/adapters/fastify.ts
600
+ function fastifyBotHandler(handler, options = {}) {
601
+ const mountChallenge = options.mountChallengeEndpoint ?? true;
602
+ return async function onRequest(request, reply) {
603
+ try {
604
+ await evaluate(handler, options, mountChallenge, request, reply);
605
+ } catch (error) {
606
+ handler.config.onError(error, { source: "adapter:fastify" });
607
+ }
608
+ };
609
+ }
610
+ async function evaluate(handler, options, mountChallenge, request, reply) {
611
+ const headers = {};
612
+ for (const [name, value] of Object.entries(request.headers)) {
613
+ headers[name] = Array.isArray(value) ? value.join(", ") : value;
614
+ }
615
+ const ip = handler.resolveIp(request.raw.socket.remoteAddress, headers);
616
+ const fingerprint = options.tlsFingerprintHeader !== void 0 ? headers[options.tlsFingerprintHeader.toLowerCase()] : void 0;
617
+ let facts = createFacts({
618
+ method: request.method,
619
+ url: request.url,
620
+ headers: request.headers,
621
+ rawHeaders: request.raw.rawHeaders,
622
+ ip,
623
+ httpVersion: request.raw.httpVersion,
624
+ protocol: request.raw.socket.encrypted === true || headers["x-forwarded-proto"] === "https" ? "https" : "http",
625
+ ...fingerprint !== void 0 ? { tlsFingerprint: fingerprint } : {}
626
+ });
627
+ if (options.enrich) facts = options.enrich(request, facts);
628
+ if (mountChallenge && handler.isChallengeEndpoint(facts)) {
629
+ const verification = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(request.raw)));
630
+ reply.code(verification.ok ? 200 : verification.status);
631
+ reply.header("content-type", "application/json; charset=utf-8");
632
+ reply.header("cache-control", "no-store");
633
+ if (verification.ok) reply.header("set-cookie", verification.setCookie);
634
+ reply.send(verificationBody(verification));
635
+ return;
636
+ }
637
+ const { outcome } = await handler.handle(facts);
638
+ if (outcome.kind === "drop") {
639
+ reply.hijack?.();
640
+ request.raw.socket.destroy();
641
+ return;
642
+ }
643
+ if (outcome.kind === "respond") {
644
+ reply.code(outcome.status);
645
+ for (const [name, value] of Object.entries(outcome.headers)) reply.header(name, value);
646
+ reply.send(outcome.body);
647
+ return;
648
+ }
649
+ for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {
650
+ request.headers[name] = value;
651
+ }
652
+ if (outcome.responseHeaders) {
653
+ for (const [name, value] of Object.entries(outcome.responseHeaders)) reply.header(name, value);
654
+ }
655
+ if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
656
+ }
657
+
658
+ // src/adapters/koa.ts
659
+ function koaBotHandler(handler, options = {}) {
660
+ const mountChallenge = options.mountChallengeEndpoint ?? true;
661
+ return async function botHandlerMiddleware(context, next) {
662
+ let proceed;
663
+ try {
664
+ proceed = await evaluate2(handler, mountChallenge, context);
665
+ } catch (error) {
666
+ handler.config.onError(error, { source: "adapter:koa" });
667
+ proceed = true;
668
+ }
669
+ if (proceed) await next();
670
+ };
671
+ }
672
+ async function evaluate2(handler, mountChallenge, context) {
673
+ const headers = {};
674
+ for (const [name, value] of Object.entries(context.headers)) {
675
+ headers[name] = Array.isArray(value) ? value.join(", ") : value;
676
+ }
677
+ const facts = createFacts({
678
+ method: context.method,
679
+ url: context.url,
680
+ headers: context.headers,
681
+ rawHeaders: context.req.rawHeaders,
682
+ ip: handler.resolveIp(context.req.socket.remoteAddress, headers),
683
+ httpVersion: context.req.httpVersion,
684
+ protocol: context.secure === true || context.req.socket.encrypted === true || headers["x-forwarded-proto"] === "https" ? "https" : "http"
685
+ });
686
+ if (mountChallenge && handler.isChallengeEndpoint(facts)) {
687
+ const verification = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(context.req)));
688
+ context.status = verification.ok ? 200 : verification.status;
689
+ context.set("content-type", "application/json; charset=utf-8");
690
+ context.set("cache-control", "no-store");
691
+ if (verification.ok) context.set("set-cookie", verification.setCookie);
692
+ context.body = verificationBody(verification);
693
+ return false;
694
+ }
695
+ const { outcome } = await handler.handle(facts);
696
+ if (outcome.kind === "drop") {
697
+ context.req.socket.destroy();
698
+ return false;
699
+ }
700
+ if (outcome.kind === "respond") {
701
+ context.status = outcome.status;
702
+ for (const [name, value] of Object.entries(outcome.headers)) context.set(name, value);
703
+ context.body = outcome.body;
704
+ return false;
705
+ }
706
+ for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {
707
+ context.req.headers[name] = value;
708
+ }
709
+ if (outcome.responseHeaders) {
710
+ for (const [name, value] of Object.entries(outcome.responseHeaders)) context.set(name, value);
711
+ }
712
+ if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
713
+ return true;
714
+ }
715
+ // Annotate the CommonJS export names for ESM import in node:
716
+ 0 && (module.exports = {
717
+ botHandler,
718
+ createFetchAdapter,
719
+ fastifyBotHandler,
720
+ koaBotHandler,
721
+ withBotHandler
722
+ });
723
+ //# sourceMappingURL=index.cjs.map