@ingram-tech/bot-protection 0.3.2 → 0.4.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.
package/README.md CHANGED
@@ -17,7 +17,10 @@ bun add @ingram-tech/bot-protection
17
17
  bun add botid
18
18
  ```
19
19
 
20
- Set `BOT_PROTECTION_SECRET` (e.g. `openssl rand -hex 32`).
20
+ Set `BOT_PROTECTION_SECRET` (e.g. `openssl rand -hex 32`). To rotate it, set a
21
+ comma-separated list (`new,old`): tokens sign with the first secret and verify
22
+ against all of them, so in-flight forms keep working; drop the old one after
23
+ the token window (1h) has passed.
21
24
 
22
25
  ## Use
23
26
 
@@ -27,6 +30,12 @@ Set `BOT_PROTECTION_SECRET` (e.g. `openssl rand -hex 32`).
27
30
  import { createFormToken } from "@ingram-tech/bot-protection";
28
31
  import { HoneypotField } from "@ingram-tech/bot-protection/honeypot";
29
32
 
33
+ // The page that mints the token MUST render per-request. On a statically
34
+ // prerendered (or ISR/cached) page the timestamp is the BUILD time: once the
35
+ // deploy is older than the token window (default 1h), every real submission
36
+ // verifies as "expired" and is silently dropped.
37
+ export const dynamic = "force-dynamic";
38
+
30
39
  export default function ContactPage() {
31
40
  const token = createFormToken(); // server component / server-side
32
41
  return (
@@ -39,6 +48,9 @@ export default function ContactPage() {
39
48
  }
40
49
  ```
41
50
 
51
+ Prefer keeping the page static? Mint the token from a route handler instead
52
+ and fetch it client-side — that's exactly what the `/react` hook does (below).
53
+
42
54
  **Verify on submit** (API route or server action):
43
55
 
44
56
  ```ts
@@ -71,7 +83,9 @@ token; its `POST` verifies:
71
83
  import { HoneypotInput, useBotProtection } from "@ingram-tech/bot-protection/react";
72
84
 
73
85
  export function ContactForm() {
74
- const { honeypotRef, botFields } = useBotProtection("/api/contact");
86
+ // `ready` flips true once the token fetch resolves — gate the submit button
87
+ // on it if you don't want early submissions treated as bot-ish.
88
+ const { honeypotRef, botFields, ready } = useBotProtection("/api/contact");
75
89
 
76
90
  async function onSubmit(values: FormValues) {
77
91
  await fetch("/api/contact", {
@@ -93,6 +107,10 @@ export function ContactForm() {
93
107
  // app/api/contact/route.ts
94
108
  import { createFormToken, verifyHuman } from "@ingram-tech/bot-protection";
95
109
 
110
+ // Same freshness rule as above: the GET must not be cached, or the token
111
+ // ages with the cache entry and eventually expires every submission.
112
+ export const dynamic = "force-dynamic";
113
+
96
114
  export const GET = () => Response.json({ token: createFormToken() });
97
115
 
98
116
  export async function POST(request: Request) {
package/dist/botid.d.ts CHANGED
@@ -7,6 +7,11 @@
7
7
  * - if it isn't installed (or the call throws off-Vercel/in dev), we degrade
8
8
  * to "not a bot" rather than blocking real users.
9
9
  *
10
+ * The degrade is logged once per process: bundler file-tracing can exclude
11
+ * `botid/server` from a deployed function even when the package is installed
12
+ * (nothing imports it statically), and a silently disabled layer 3 is
13
+ * otherwise indistinguishable from "no bots today".
14
+ *
10
15
  * App-side wiring (per site, not in this package): add `withBotId(nextConfig)`
11
16
  * in next.config and `initBotId({ protect: [...] })` in instrumentation-client.
12
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"botid.d.ts","sourceRoot":"","sources":["../src/botid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAW3D,CAAC"}
1
+ {"version":3,"file":"botid.d.ts","sourceRoot":"","sources":["../src/botid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAYH,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAmB3D,CAAC"}
package/dist/botid.js CHANGED
@@ -7,17 +7,37 @@
7
7
  * - if it isn't installed (or the call throws off-Vercel/in dev), we degrade
8
8
  * to "not a bot" rather than blocking real users.
9
9
  *
10
+ * The degrade is logged once per process: bundler file-tracing can exclude
11
+ * `botid/server` from a deployed function even when the package is installed
12
+ * (nothing imports it statically), and a silently disabled layer 3 is
13
+ * otherwise indistinguishable from "no bots today".
14
+ *
10
15
  * App-side wiring (per site, not in this package): add `withBotId(nextConfig)`
11
16
  * in next.config and `initBotId({ protect: [...] })` in instrumentation-client.
12
17
  */
18
+ let warnedUnavailable = false;
19
+ const warnOnce = (detail) => {
20
+ if (warnedUnavailable)
21
+ return;
22
+ warnedUnavailable = true;
23
+ console.warn(`@ingram-tech/bot-protection: BotID layer unavailable, degrading to "not a bot" (${detail}).`);
24
+ };
13
25
  export const checkBot = async () => {
14
26
  try {
15
27
  // Non-literal specifier: keeps tsc from requiring `botid` at build time.
16
28
  const specifier = "botid/server";
29
+ // Shape-checked below rather than trusted — the cast only names what we
30
+ // probe for.
17
31
  const mod = (await import(specifier));
18
- return await mod.checkBotId();
32
+ if (typeof mod.checkBotId !== "function") {
33
+ warnOnce("botid/server has no checkBotId export");
34
+ return { isBot: false };
35
+ }
36
+ const result = await mod.checkBotId();
37
+ return { isBot: result?.isBot === true };
19
38
  }
20
- catch {
39
+ catch (error) {
40
+ warnOnce(error instanceof Error ? error.message : String(error));
21
41
  return { isBot: false };
22
42
  }
23
43
  };
package/dist/botid.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"botid.js","sourceRoot":"","sources":["../src/botid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAiC,EAAE;IAC/D,IAAI,CAAC;QACJ,yEAAyE;QACzE,MAAM,SAAS,GAAG,cAAc,CAAC;QACjC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAEnC,CAAC;QACF,OAAO,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC;AACF,CAAC,CAAC"}
1
+ {"version":3,"file":"botid.js","sourceRoot":"","sources":["../src/botid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAE9B,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAQ,EAAE;IACzC,IAAI,iBAAiB;QAAE,OAAO;IAC9B,iBAAiB,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,IAAI,CACX,mFAAmF,MAAM,IAAI,CAC7F,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAiC,EAAE;IAC/D,IAAI,CAAC;QACJ,yEAAyE;QACzE,MAAM,SAAS,GAAG,cAAc,CAAC;QACjC,wEAAwE;QACxE,aAAa;QACb,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAEnC,CAAC;QACF,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;YAC1C,QAAQ,CAAC,uCAAuC,CAAC,CAAC;YAClD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACzB,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,KAAK,IAAI,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,QAAQ,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACjE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC;AACF,CAAC,CAAC"}
package/dist/keys.d.ts CHANGED
@@ -4,8 +4,16 @@
4
4
  * BOT_PROTECTION_SECRET — secret used to HMAC-sign the timing token so bots
5
5
  * can't forge a "this form was rendered long enough ago" claim. Generate with
6
6
  * e.g. `openssl rand -hex 32`.
7
+ *
8
+ * Rotation: a comma-separated list is accepted — tokens are signed with the
9
+ * FIRST secret and verified against ALL of them, so `new,old` rotates without
10
+ * invalidating up-to-an-hour-old in-flight forms (which would silently drop
11
+ * every open form's submission).
7
12
  */
13
+ /** The signing secret (first of the list). */
8
14
  export declare const getSecret: () => string;
15
+ /** All accepted verification secrets, newest first. */
16
+ export declare const getSecrets: () => string[];
9
17
  /** Whether the signed-timing layer is configured. */
10
18
  export declare const isConfigured: () => boolean;
11
19
  //# sourceMappingURL=keys.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,SAAS,QAAO,MAQ5B,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,YAAY,QAAO,OAAqD,CAAC"}
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,8CAA8C;AAC9C,eAAO,MAAM,SAAS,QAAO,MAQ5B,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,UAAU,QAAO,MAAM,EACW,CAAC;AAEhD,qDAAqD;AACrD,eAAO,MAAM,YAAY,QAAO,OAAqD,CAAC"}
package/dist/keys.js CHANGED
@@ -4,14 +4,26 @@
4
4
  * BOT_PROTECTION_SECRET — secret used to HMAC-sign the timing token so bots
5
5
  * can't forge a "this form was rendered long enough ago" claim. Generate with
6
6
  * e.g. `openssl rand -hex 32`.
7
+ *
8
+ * Rotation: a comma-separated list is accepted — tokens are signed with the
9
+ * FIRST secret and verified against ALL of them, so `new,old` rotates without
10
+ * invalidating up-to-an-hour-old in-flight forms (which would silently drop
11
+ * every open form's submission).
7
12
  */
13
+ const parse = (raw) => raw
14
+ .split(",")
15
+ .map((s) => s.trim())
16
+ .filter((s) => s.length > 0);
17
+ /** The signing secret (first of the list). */
8
18
  export const getSecret = () => {
9
- const secret = process.env.BOT_PROTECTION_SECRET;
19
+ const secret = getSecrets()[0];
10
20
  if (!secret) {
11
21
  throw new Error("@ingram-tech/bot-protection: BOT_PROTECTION_SECRET is not configured");
12
22
  }
13
23
  return secret;
14
24
  };
25
+ /** All accepted verification secrets, newest first. */
26
+ export const getSecrets = () => parse(process.env.BOT_PROTECTION_SECRET ?? "");
15
27
  /** Whether the signed-timing layer is configured. */
16
28
  export const isConfigured = () => Boolean(process.env.BOT_PROTECTION_SECRET);
17
29
  //# sourceMappingURL=keys.js.map
package/dist/keys.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,GAAW,EAAE;IACrC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IACjD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACd,sEAAsE,CACtE,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,qDAAqD;AACrD,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC"}
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,KAAK,GAAG,CAAC,GAAW,EAAY,EAAE,CACvC,GAAG;KACD,KAAK,CAAC,GAAG,CAAC;KACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;KACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAE/B,8CAA8C;AAC9C,MAAM,CAAC,MAAM,SAAS,GAAG,GAAW,EAAE;IACrC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACd,sEAAsE,CACtE,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF,uDAAuD;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAa,EAAE,CACxC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;AAEhD,qDAAqD;AACrD,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC"}
package/dist/react.d.ts CHANGED
@@ -10,22 +10,32 @@ import { type RefObject } from "react";
10
10
  * Pair with {@link HoneypotInput}, which renders the trap and wires the ref.
11
11
  *
12
12
  * @param tokenEndpoint - GET route that mints the token (e.g. "/api/contact").
13
+ * @param options.honeypotField - Override the trap field name; must match the
14
+ * server's `verifyHuman({ honeypotField })` and the `<HoneypotInput name>`.
13
15
  *
14
16
  * @example
15
- * const { honeypotRef, botFields } = useBotProtection("/api/contact");
17
+ * const { honeypotRef, botFields, ready } = useBotProtection("/api/contact");
16
18
  * // ...in the form: <HoneypotInput inputRef={honeypotRef} />
17
19
  * // ...on submit: body: JSON.stringify({ ...values, ...botFields() })
20
+ * // Optionally gate the submit button on `ready` — an empty token is
21
+ * // silently dropped server-side, so submitting before the token resolves
22
+ * // loses the message.
18
23
  */
19
- export declare function useBotProtection(tokenEndpoint: string): {
24
+ export declare function useBotProtection(tokenEndpoint: string, options?: {
25
+ honeypotField?: string;
26
+ }): {
20
27
  honeypotRef: RefObject<HTMLInputElement | null>;
21
28
  botFields: () => Record<string, string>;
29
+ ready: boolean;
22
30
  };
23
31
  /**
24
32
  * Visually-hidden honeypot input for {@link useBotProtection}. Real users never
25
33
  * see or fill it (off-screen, aria-hidden, tabIndex -1, autofill opted out);
26
34
  * bots that fill every field give themselves away.
27
35
  */
28
- export declare function HoneypotInput({ inputRef, }: {
36
+ export declare function HoneypotInput({ inputRef, name, }: {
29
37
  inputRef: RefObject<HTMLInputElement | null>;
38
+ /** Must match `useBotProtection`'s `honeypotField` when overridden. */
39
+ name?: string;
30
40
  }): import("react").JSX.Element;
31
41
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AAGpE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM;;qBAW/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;EAM5C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC7B,QAAQ,GACR,EAAE;IACF,QAAQ,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAC7C,+BAuBA"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AAGpE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAC/B,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE;;qBAsCd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;;EAQ5C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC7B,QAAQ,EACR,IAAqB,GACrB,EAAE;IACF,QAAQ,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC7C,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,+BAuBA"}
package/dist/react.js CHANGED
@@ -13,34 +13,68 @@ import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
13
13
  * Pair with {@link HoneypotInput}, which renders the trap and wires the ref.
14
14
  *
15
15
  * @param tokenEndpoint - GET route that mints the token (e.g. "/api/contact").
16
+ * @param options.honeypotField - Override the trap field name; must match the
17
+ * server's `verifyHuman({ honeypotField })` and the `<HoneypotInput name>`.
16
18
  *
17
19
  * @example
18
- * const { honeypotRef, botFields } = useBotProtection("/api/contact");
20
+ * const { honeypotRef, botFields, ready } = useBotProtection("/api/contact");
19
21
  * // ...in the form: <HoneypotInput inputRef={honeypotRef} />
20
22
  * // ...on submit: body: JSON.stringify({ ...values, ...botFields() })
23
+ * // Optionally gate the submit button on `ready` — an empty token is
24
+ * // silently dropped server-side, so submitting before the token resolves
25
+ * // loses the message.
21
26
  */
22
- export function useBotProtection(tokenEndpoint) {
27
+ export function useBotProtection(tokenEndpoint, options) {
23
28
  const [token, setToken] = useState("");
29
+ const honeypotField = options?.honeypotField ?? HONEYPOT_FIELD;
24
30
  const honeypotRef = useRef(null);
25
31
  useEffect(() => {
26
- fetch(tokenEndpoint)
27
- .then((res) => res.json())
28
- .then((data) => setToken(data.token ?? ""))
29
- .catch(() => { });
32
+ let cancelled = false;
33
+ const load = async () => {
34
+ // One retry: a transiently failed fetch would otherwise leave the token
35
+ // empty forever, and the server then silently drops a REAL user's
36
+ // submission — the exact failure mode this package promises to avoid.
37
+ for (let attempt = 0; attempt < 2 && !cancelled; attempt++) {
38
+ try {
39
+ const res = await fetch(tokenEndpoint);
40
+ if (!res.ok)
41
+ throw new Error(`HTTP ${res.status}`);
42
+ const data = await res.json();
43
+ if (typeof data === "object" &&
44
+ data !== null &&
45
+ "token" in data &&
46
+ typeof data.token === "string" &&
47
+ data.token !== "") {
48
+ if (!cancelled)
49
+ setToken(data.token);
50
+ return;
51
+ }
52
+ }
53
+ catch {
54
+ // fall through to retry
55
+ }
56
+ }
57
+ };
58
+ void load();
59
+ return () => {
60
+ cancelled = true;
61
+ };
30
62
  }, [tokenEndpoint]);
31
63
  const botFields = () => ({
32
- [HONEYPOT_FIELD]: honeypotRef.current?.value ?? "",
64
+ [honeypotField]: honeypotRef.current?.value ?? "",
33
65
  [TOKEN_FIELD]: token,
34
66
  });
35
- return { honeypotRef, botFields };
67
+ // `ready` = the timing token resolved. Not required — the server treats a
68
+ // missing token as bot-ish — but lets forms wait instead of losing input.
69
+ return { honeypotRef, botFields, ready: token !== "" };
36
70
  }
37
71
  /**
38
72
  * Visually-hidden honeypot input for {@link useBotProtection}. Real users never
39
73
  * see or fill it (off-screen, aria-hidden, tabIndex -1, autofill opted out);
40
74
  * bots that fill every field give themselves away.
41
75
  */
42
- export function HoneypotInput({ inputRef, }) {
43
- return (_jsx("input", { ref: inputRef, type: "text", name: HONEYPOT_FIELD, tabIndex: -1, autoComplete: "off", "aria-hidden": "true", defaultValue: "", "data-1p-ignore": true, "data-lpignore": "true", "data-form-type": "other", style: {
76
+ export function HoneypotInput({ inputRef, name = HONEYPOT_FIELD, }) {
77
+ return (_jsx("input", { ref: inputRef, type: "text", name: name, tabIndex: -1, autoComplete: "off", "aria-hidden": "true", defaultValue: "", "data-1p-ignore": true, "data-lpignore": "true", "data-form-type": "other", style: {
44
78
  position: "absolute",
45
79
  left: "-9999px",
46
80
  width: "1px",
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAAC,aAAqB;IACrD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,WAAW,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAC;IAEnD,SAAS,CAAC,GAAG,EAAE;QACd,KAAK,CAAC,aAAa,CAAC;aAClB,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;aACzB,IAAI,CAAC,CAAC,IAAwB,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;aAC9D,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACnB,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,MAAM,SAAS,GAAG,GAA2B,EAAE,CAAC,CAAC;QAChD,CAAC,cAAc,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QAClD,CAAC,WAAW,CAAC,EAAE,KAAK;KACpB,CAAC,CAAC;IAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,EAC7B,QAAQ,GAGR;IACA,OAAO,CACN,gBACC,GAAG,EAAE,QAAQ,EACb,IAAI,EAAC,MAAM,EACX,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,CAAC,CAAC,EACZ,YAAY,EAAC,KAAK,iBACN,MAAM,EAClB,YAAY,EAAC,EAAE,2CAGD,MAAM,oBACL,OAAO,EACtB,KAAK,EAAE;YACN,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,QAAQ;SAClB,GACA,CACF,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,gBAAgB,CAC/B,aAAqB,EACrB,OAAoC;IAEpC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,aAAa,GAAG,OAAO,EAAE,aAAa,IAAI,cAAc,CAAC;IAC/D,MAAM,WAAW,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAC;IAEnD,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;YACtC,wEAAwE;YACxE,kEAAkE;YAClE,sEAAsE;YACtE,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,CAAC;gBAC5D,IAAI,CAAC;oBACJ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,CAAC;oBACvC,IAAI,CAAC,GAAG,CAAC,EAAE;wBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;oBACnD,MAAM,IAAI,GAAY,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;oBACvC,IACC,OAAO,IAAI,KAAK,QAAQ;wBACxB,IAAI,KAAK,IAAI;wBACb,OAAO,IAAI,IAAI;wBACf,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;wBAC9B,IAAI,CAAC,KAAK,KAAK,EAAE,EAChB,CAAC;wBACF,IAAI,CAAC,SAAS;4BAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;wBACrC,OAAO;oBACR,CAAC;gBACF,CAAC;gBAAC,MAAM,CAAC;oBACR,wBAAwB;gBACzB,CAAC;YACF,CAAC;QACF,CAAC,CAAC;QACF,KAAK,IAAI,EAAE,CAAC;QACZ,OAAO,GAAG,EAAE;YACX,SAAS,GAAG,IAAI,CAAC;QAClB,CAAC,CAAC;IACH,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,MAAM,SAAS,GAAG,GAA2B,EAAE,CAAC,CAAC;QAChD,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QACjD,CAAC,WAAW,CAAC,EAAE,KAAK;KACpB,CAAC,CAAC;IAEH,0EAA0E;IAC1E,0EAA0E;IAC1E,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,EAAE,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,EAC7B,QAAQ,EACR,IAAI,GAAG,cAAc,GAKrB;IACA,OAAO,CACN,gBACC,GAAG,EAAE,QAAQ,EACb,IAAI,EAAC,MAAM,EACX,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,CAAC,CAAC,EACZ,YAAY,EAAC,KAAK,iBACN,MAAM,EAClB,YAAY,EAAC,EAAE,2CAGD,MAAM,oBACL,OAAO,EACtB,KAAK,EAAE;YACN,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,QAAQ;SAClB,GACA,CACF,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAO,MASlC,CAAC;AAEF,MAAM,WAAW,UAAU;IAC1B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,GAC3B,OAAO,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,mBAA0C,UAAe,KACvD,WAqBF,CAAC"}
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAO,MASlC,CAAC;AAEF,MAAM,WAAW,UAAU;IAC1B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,GAC3B,OAAO,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,mBAA0C,UAAe,KACvD,WA0BF,CAAC"}
package/dist/token.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
- import { getSecret, isConfigured } from "./keys.js";
2
+ import { getSecret, getSecrets, isConfigured } from "./keys.js";
3
3
  /**
4
4
  * Signed timing token: `<timestamp>.<hmac>`. The timestamp records when the
5
5
  * form was rendered; the HMAC (keyed by BOT_PROTECTION_SECRET) prevents a bot
@@ -35,10 +35,15 @@ export const verifyFormToken = (token, { minMs = 2000, maxMs = 60 * 60 * 1000 }
35
35
  return { ok: false, reason: "malformed-token" };
36
36
  const ts = token.slice(0, dot);
37
37
  const sig = token.slice(dot + 1);
38
- const expected = sign(ts, getSecret());
38
+ // Verify against every configured secret (signing uses the first) so a
39
+ // rotation doesn't invalidate in-flight forms. Every candidate is compared
40
+ // timing-safely; the count of secrets is not attacker-relevant.
39
41
  const a = Buffer.from(sig);
40
- const b = Buffer.from(expected);
41
- if (a.length !== b.length || !timingSafeEqual(a, b)) {
42
+ const valid = getSecrets().some((secret) => {
43
+ const b = Buffer.from(sign(ts, secret));
44
+ return a.length === b.length && timingSafeEqual(a, b);
45
+ });
46
+ if (!valid) {
42
47
  return { ok: false, reason: "bad-signature" };
43
48
  }
44
49
  const elapsed = Date.now() - Number(ts);
package/dist/token.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEpD;;;;;;;;;;;GAWG;AAEH,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,MAAc,EAAU,EAAE,CACnD,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEvD;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,GAAW,EAAE;IAC3C,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CACX,qFAAqF,CACrF,CAAC;QACF,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IACjC,OAAO,GAAG,EAAE,IAAI,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AACzC,CAAC,CAAC;AAcF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,KAAgC,EAChC,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,KAAiB,EAAE,EAC3C,EAAE;IAChB,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAE1D,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC9D,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAEjC,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IACvC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QACrD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC7E,IAAI,OAAO,GAAG,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC9D,IAAI,OAAO,GAAG,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAE7D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACrB,CAAC,CAAC"}
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEhE;;;;;;;;;;;GAWG;AAEH,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,MAAc,EAAU,EAAE,CACnD,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEvD;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,GAAW,EAAE;IAC3C,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CACX,qFAAqF,CACrF,CAAC;QACF,OAAO,EAAE,CAAC;IACX,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IACjC,OAAO,GAAG,EAAE,IAAI,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AACzC,CAAC,CAAC;AAcF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,KAAgC,EAChC,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,KAAiB,EAAE,EAC3C,EAAE;IAChB,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAE1D,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC9D,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAEjC,uEAAuE;IACvE,2EAA2E;IAC3E,gEAAgE;IAChE,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QACxC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAC7E,IAAI,OAAO,GAAG,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC9D,IAAI,OAAO,GAAG,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAE7D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACrB,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAmB,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC7B,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,0CAA0C;IAC1C,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAUD;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAU,SAAS,aAAa,KAAG,OAAO,CAAC,YAAY,CAsB9E,CAAC"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAmB,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC7B,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,0CAA0C;IAC1C,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAUD;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAU,SAAS,aAAa,KAAG,OAAO,CAAC,YAAY,CAyB9E,CAAC"}
package/dist/verify.js CHANGED
@@ -20,6 +20,9 @@ const getField = (formData, name) => {
20
20
  */
21
21
  export const verifyHuman = async (options) => {
22
22
  const honeypotField = options.honeypotField ?? HONEYPOT_FIELD;
23
+ // trim(): whitespace-only values pass the trap on purpose — some browser
24
+ // autofill/extension quirks insert stray spaces, and per the "never punish
25
+ // real users" ethos that's a weaker trap, not a false positive.
23
26
  if (getField(options.formData, honeypotField).trim() !== "") {
24
27
  return { ok: false, reason: "honeypot" };
25
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAmB,eAAe,EAAE,MAAM,YAAY,CAAC;AAsB9D,MAAM,QAAQ,GAAG,CAAC,QAAmC,EAAE,IAAY,EAAU,EAAE;IAC9E,MAAM,KAAK,GACV,QAAQ,YAAY,QAAQ;QAC3B,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;QACpB,CAAC,CAAE,QAAoC,CAAC,IAAI,CAAC,CAAC;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/C,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAsB,EAAyB,EAAE;IAClF,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,cAAc,CAAC;IAC9D,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,wEAAwE;IACxE,+EAA+E;IAC/E,IAAI,YAAY,EAAE,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,eAAe,CAC5B,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,EACvC,OAAO,CAAC,MAAM,CACd,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;IAC7B,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QAC7B,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;QACnC,IAAI,KAAK;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACrB,CAAC,CAAC"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAmB,eAAe,EAAE,MAAM,YAAY,CAAC;AAsB9D,MAAM,QAAQ,GAAG,CAAC,QAAmC,EAAE,IAAY,EAAU,EAAE;IAC9E,MAAM,KAAK,GACV,QAAQ,YAAY,QAAQ;QAC3B,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;QACpB,CAAC,CAAE,QAAoC,CAAC,IAAI,CAAC,CAAC;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/C,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAsB,EAAyB,EAAE;IAClF,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,cAAc,CAAC;IAC9D,yEAAyE;IACzE,2EAA2E;IAC3E,gEAAgE;IAChE,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,wEAAwE;IACxE,+EAA+E;IAC/E,IAAI,YAAY,EAAE,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,eAAe,CAC5B,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,EACvC,OAAO,CAAC,MAAM,CACd,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;IAC7B,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QAC7B,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;QACnC,IAAI,KAAK;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACrB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/bot-protection",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Invisible, layered bot protection for forms — honeypot + signed timing token + Vercel BotID.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,12 +39,12 @@
39
39
  "test": "vitest run"
40
40
  },
41
41
  "devDependencies": {
42
- "@ingram-tech/nk-dev": "0.1.0",
43
- "@types/node": "^25.0.0",
44
- "@types/react": "^19.0.0",
45
- "react": "^19.0.0",
42
+ "@ingram-tech/nk-dev": "0.2.5",
43
+ "@types/node": "^26.0.1",
44
+ "@types/react": "^19.2.17",
45
+ "react": "^19.2.7",
46
46
  "typescript": "^6.0.3",
47
- "vitest": "^4.1.6"
47
+ "vitest": "^4.1.9"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "botid": ">=1.0.0",