@ingram-tech/bot-protection 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/botid.d.ts +5 -0
- package/dist/botid.d.ts.map +1 -1
- package/dist/botid.js +22 -2
- package/dist/botid.js.map +1 -1
- package/dist/hidden-style.d.ts +9 -0
- package/dist/hidden-style.d.ts.map +1 -0
- package/dist/hidden-style.js +15 -0
- package/dist/hidden-style.js.map +1 -0
- package/dist/honeypot.d.ts +14 -0
- package/dist/honeypot.d.ts.map +1 -1
- package/dist/honeypot.js +2 -9
- package/dist/honeypot.js.map +1 -1
- package/dist/keys.d.ts +8 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +13 -1
- package/dist/keys.js.map +1 -1
- package/dist/react.d.ts +13 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +45 -16
- package/dist/react.js.map +1 -1
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +16 -5
- package/dist/token.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +4 -3
- package/dist/verify.js.map +1 -1
- package/package.json +7 -7
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
|
-
|
|
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
|
*/
|
package/dist/botid.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"botid.d.ts","sourceRoot":"","sources":["../src/botid.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
|
|
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
|
|
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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Shared visually-hidden style for the honeypot trap: pushed off-screen but
|
|
4
|
+
* still present and fillable in the DOM, so bots that fill every field give
|
|
5
|
+
* themselves away while real users never see it. Kept in one place so the SSR
|
|
6
|
+
* ({@link HoneypotField}) and client ({@link HoneypotInput}) traps can't drift.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VISUALLY_HIDDEN: CSSProperties;
|
|
9
|
+
//# sourceMappingURL=hidden-style.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hidden-style.d.ts","sourceRoot":"","sources":["../src/hidden-style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,aAO7B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared visually-hidden style for the honeypot trap: pushed off-screen but
|
|
3
|
+
* still present and fillable in the DOM, so bots that fill every field give
|
|
4
|
+
* themselves away while real users never see it. Kept in one place so the SSR
|
|
5
|
+
* ({@link HoneypotField}) and client ({@link HoneypotInput}) traps can't drift.
|
|
6
|
+
*/
|
|
7
|
+
export const VISUALLY_HIDDEN = {
|
|
8
|
+
position: "absolute",
|
|
9
|
+
left: "-9999px",
|
|
10
|
+
top: "-9999px",
|
|
11
|
+
width: "1px",
|
|
12
|
+
height: "1px",
|
|
13
|
+
overflow: "hidden",
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=hidden-style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hidden-style.js","sourceRoot":"","sources":["../src/hidden-style.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAkB;IAC7C,QAAQ,EAAE,UAAU;IACpB,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,SAAS;IACd,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,KAAK;IACb,QAAQ,EAAE,QAAQ;CAClB,CAAC"}
|
package/dist/honeypot.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visually-hidden honeypot + signed timing token, dropped inside any <form>.
|
|
3
|
+
*
|
|
4
|
+
* Presentational only (no client JS), so it renders fine in server or client
|
|
5
|
+
* components. Pass a `token` minted server-side with `createFormToken()`.
|
|
6
|
+
*
|
|
7
|
+
* Real users never see or fill the honeypot (off-screen, aria-hidden,
|
|
8
|
+
* tabIndex -1, autocomplete off); bots that fill every field give themselves
|
|
9
|
+
* away.
|
|
10
|
+
*
|
|
11
|
+
* Pass `field` to use a custom honeypot name — it must match the
|
|
12
|
+
* `honeypotField` you pass to `verifyHuman()`, or the trap reads the wrong
|
|
13
|
+
* field and fails open.
|
|
14
|
+
*/
|
|
1
15
|
export declare const HoneypotField: ({ token, field, }: {
|
|
2
16
|
token: string;
|
|
3
17
|
field?: string;
|
package/dist/honeypot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"honeypot.d.ts","sourceRoot":"","sources":["../src/honeypot.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"honeypot.d.ts","sourceRoot":"","sources":["../src/honeypot.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,aAAa,sBAGvB;IACF,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,gCAoBA,CAAC"}
|
package/dist/honeypot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
3
|
+
import { VISUALLY_HIDDEN } from "./hidden-style.js";
|
|
3
4
|
/**
|
|
4
5
|
* Visually-hidden honeypot + signed timing token, dropped inside any <form>.
|
|
5
6
|
*
|
|
@@ -14,13 +15,5 @@ import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
|
14
15
|
* `honeypotField` you pass to `verifyHuman()`, or the trap reads the wrong
|
|
15
16
|
* field and fails open.
|
|
16
17
|
*/
|
|
17
|
-
const
|
|
18
|
-
position: "absolute",
|
|
19
|
-
left: "-9999px",
|
|
20
|
-
top: "-9999px",
|
|
21
|
-
width: "1px",
|
|
22
|
-
height: "1px",
|
|
23
|
-
overflow: "hidden",
|
|
24
|
-
};
|
|
25
|
-
export const HoneypotField = ({ token, field = HONEYPOT_FIELD, }) => (_jsxs("div", { "aria-hidden": "true", style: hidden, children: [_jsxs("label", { children: ["Leave this field empty", _jsx("input", { type: "text", name: field, tabIndex: -1, autoComplete: "off", defaultValue: "", "data-1p-ignore": true, "data-lpignore": "true", "data-form-type": "other" })] }), _jsx("input", { type: "hidden", name: TOKEN_FIELD, defaultValue: token })] }));
|
|
18
|
+
export const HoneypotField = ({ token, field = HONEYPOT_FIELD, }) => (_jsxs("div", { "aria-hidden": "true", style: VISUALLY_HIDDEN, children: [_jsxs("label", { children: ["Leave this field empty", _jsx("input", { type: "text", name: field, tabIndex: -1, autoComplete: "off", defaultValue: "", "data-1p-ignore": true, "data-lpignore": "true", "data-form-type": "other" })] }), _jsx("input", { type: "hidden", name: TOKEN_FIELD, defaultValue: token })] }));
|
|
26
19
|
//# sourceMappingURL=honeypot.js.map
|
package/dist/honeypot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"honeypot.js","sourceRoot":"","sources":["../src/honeypot.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"honeypot.js","sourceRoot":"","sources":["../src/honeypot.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC7B,KAAK,EACL,KAAK,GAAG,cAAc,GAItB,EAAE,EAAE,CAAC,CACL,8BAAiB,MAAM,EAAC,KAAK,EAAE,eAAe,aAC7C,sDAGC,gBACC,IAAI,EAAC,MAAM,EACX,IAAI,EAAE,KAAK,EACX,QAAQ,EAAE,CAAC,CAAC,EACZ,YAAY,EAAC,KAAK,EAClB,YAAY,EAAC,EAAE,2CAID,MAAM,oBACL,OAAO,GACrB,IACK,EACR,gBAAO,IAAI,EAAC,QAAQ,EAAC,IAAI,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,GAAI,IAC1D,CACN,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
|
package/dist/keys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA
|
|
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 =
|
|
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
|
|
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
|
package/dist/react.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAC;AAIpE;;;;;;;;;;;;;;;;;;;;;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,+BAiBA"}
|
package/dist/react.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
5
|
+
import { VISUALLY_HIDDEN } from "./hidden-style.js";
|
|
5
6
|
/**
|
|
6
7
|
* Client-side bot protection for forms that POST JSON to your own route.
|
|
7
8
|
*
|
|
@@ -13,39 +14,67 @@ import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
|
13
14
|
* Pair with {@link HoneypotInput}, which renders the trap and wires the ref.
|
|
14
15
|
*
|
|
15
16
|
* @param tokenEndpoint - GET route that mints the token (e.g. "/api/contact").
|
|
17
|
+
* @param options.honeypotField - Override the trap field name; must match the
|
|
18
|
+
* server's `verifyHuman({ honeypotField })` and the `<HoneypotInput name>`.
|
|
16
19
|
*
|
|
17
20
|
* @example
|
|
18
|
-
* const { honeypotRef, botFields } = useBotProtection("/api/contact");
|
|
21
|
+
* const { honeypotRef, botFields, ready } = useBotProtection("/api/contact");
|
|
19
22
|
* // ...in the form: <HoneypotInput inputRef={honeypotRef} />
|
|
20
23
|
* // ...on submit: body: JSON.stringify({ ...values, ...botFields() })
|
|
24
|
+
* // Optionally gate the submit button on `ready` — an empty token is
|
|
25
|
+
* // silently dropped server-side, so submitting before the token resolves
|
|
26
|
+
* // loses the message.
|
|
21
27
|
*/
|
|
22
|
-
export function useBotProtection(tokenEndpoint) {
|
|
28
|
+
export function useBotProtection(tokenEndpoint, options) {
|
|
23
29
|
const [token, setToken] = useState("");
|
|
30
|
+
const honeypotField = options?.honeypotField ?? HONEYPOT_FIELD;
|
|
24
31
|
const honeypotRef = useRef(null);
|
|
25
32
|
useEffect(() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
let cancelled = false;
|
|
34
|
+
const load = async () => {
|
|
35
|
+
// One retry: a transiently failed fetch would otherwise leave the token
|
|
36
|
+
// empty forever, and the server then silently drops a REAL user's
|
|
37
|
+
// submission — the exact failure mode this package promises to avoid.
|
|
38
|
+
for (let attempt = 0; attempt < 2 && !cancelled; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(tokenEndpoint);
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
throw new Error(`HTTP ${res.status}`);
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
if (typeof data === "object" &&
|
|
45
|
+
data !== null &&
|
|
46
|
+
"token" in data &&
|
|
47
|
+
typeof data.token === "string" &&
|
|
48
|
+
data.token !== "") {
|
|
49
|
+
if (!cancelled)
|
|
50
|
+
setToken(data.token);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// fall through to retry
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
void load();
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
};
|
|
30
63
|
}, [tokenEndpoint]);
|
|
31
64
|
const botFields = () => ({
|
|
32
|
-
[
|
|
65
|
+
[honeypotField]: honeypotRef.current?.value ?? "",
|
|
33
66
|
[TOKEN_FIELD]: token,
|
|
34
67
|
});
|
|
35
|
-
|
|
68
|
+
// `ready` = the timing token resolved. Not required — the server treats a
|
|
69
|
+
// missing token as bot-ish — but lets forms wait instead of losing input.
|
|
70
|
+
return { honeypotRef, botFields, ready: token !== "" };
|
|
36
71
|
}
|
|
37
72
|
/**
|
|
38
73
|
* Visually-hidden honeypot input for {@link useBotProtection}. Real users never
|
|
39
74
|
* see or fill it (off-screen, aria-hidden, tabIndex -1, autofill opted out);
|
|
40
75
|
* bots that fill every field give themselves away.
|
|
41
76
|
*/
|
|
42
|
-
export function HoneypotInput({ inputRef, }) {
|
|
43
|
-
return (_jsx("input", { ref: inputRef, type: "text", name:
|
|
44
|
-
position: "absolute",
|
|
45
|
-
left: "-9999px",
|
|
46
|
-
width: "1px",
|
|
47
|
-
height: "1px",
|
|
48
|
-
overflow: "hidden",
|
|
49
|
-
} }));
|
|
77
|
+
export function HoneypotInput({ inputRef, name = HONEYPOT_FIELD, }) {
|
|
78
|
+
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: VISUALLY_HIDDEN }));
|
|
50
79
|
}
|
|
51
80
|
//# sourceMappingURL=react.js.map
|
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;
|
|
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;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;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,eAAe,GACrB,CACF,CAAC;AACH,CAAC"}
|
package/dist/token.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAuBA;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAO,MAYlC,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,UACpB,MAAM,GAAG,SAAS,GAAG,IAAI,qBACU,UAAU,KAClD,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
|
|
@@ -13,6 +13,9 @@ import { getSecret, isConfigured } from "./keys.js";
|
|
|
13
13
|
* the token to a server-stored, one-time value (e.g. a session/CSRF nonce).
|
|
14
14
|
*/
|
|
15
15
|
const sign = (ts, secret) => createHmac("sha256", secret).update(ts).digest("hex");
|
|
16
|
+
// Warn at most once: createFormToken runs on every form render, so an
|
|
17
|
+
// unconfigured secret would otherwise spam the logs on every request.
|
|
18
|
+
let warnedUnconfigured = false;
|
|
16
19
|
/**
|
|
17
20
|
* Mint a token. Call server-side when rendering the form. If
|
|
18
21
|
* BOT_PROTECTION_SECRET isn't configured, returns "" (the timing layer is
|
|
@@ -20,7 +23,10 @@ const sign = (ts, secret) => createHmac("sha256", secret).update(ts).digest("hex
|
|
|
20
23
|
*/
|
|
21
24
|
export const createFormToken = () => {
|
|
22
25
|
if (!isConfigured()) {
|
|
23
|
-
|
|
26
|
+
if (!warnedUnconfigured) {
|
|
27
|
+
warnedUnconfigured = true;
|
|
28
|
+
console.warn("@ingram-tech/bot-protection: BOT_PROTECTION_SECRET not set — timing token disabled.");
|
|
29
|
+
}
|
|
24
30
|
return "";
|
|
25
31
|
}
|
|
26
32
|
const ts = Date.now().toString();
|
|
@@ -35,10 +41,15 @@ export const verifyFormToken = (token, { minMs = 2000, maxMs = 60 * 60 * 1000 }
|
|
|
35
41
|
return { ok: false, reason: "malformed-token" };
|
|
36
42
|
const ts = token.slice(0, dot);
|
|
37
43
|
const sig = token.slice(dot + 1);
|
|
38
|
-
|
|
44
|
+
// Verify against every configured secret (signing uses the first) so a
|
|
45
|
+
// rotation doesn't invalidate in-flight forms. Every candidate is compared
|
|
46
|
+
// timing-safely; the count of secrets is not attacker-relevant.
|
|
39
47
|
const a = Buffer.from(sig);
|
|
40
|
-
const
|
|
41
|
-
|
|
48
|
+
const valid = getSecrets().some((secret) => {
|
|
49
|
+
const b = Buffer.from(sign(ts, secret));
|
|
50
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
51
|
+
});
|
|
52
|
+
if (!valid) {
|
|
42
53
|
return { ok: false, reason: "bad-signature" };
|
|
43
54
|
}
|
|
44
55
|
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;
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAOD;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,YAAmB,aAAa,KAAG,OAAO,CAAC,YAAY,CAyB9E,CAAC"}
|
package/dist/verify.js
CHANGED
|
@@ -3,9 +3,7 @@ import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
|
3
3
|
import { isConfigured } from "./keys.js";
|
|
4
4
|
import { verifyFormToken } from "./token.js";
|
|
5
5
|
const getField = (formData, name) => {
|
|
6
|
-
const value = formData instanceof FormData
|
|
7
|
-
? formData.get(name)
|
|
8
|
-
: formData[name];
|
|
6
|
+
const value = formData instanceof FormData ? formData.get(name) : formData[name];
|
|
9
7
|
return typeof value === "string" ? value : "";
|
|
10
8
|
};
|
|
11
9
|
/**
|
|
@@ -20,6 +18,9 @@ const getField = (formData, name) => {
|
|
|
20
18
|
*/
|
|
21
19
|
export const verifyHuman = async (options) => {
|
|
22
20
|
const honeypotField = options.honeypotField ?? HONEYPOT_FIELD;
|
|
21
|
+
// trim(): whitespace-only values pass the trap on purpose — some browser
|
|
22
|
+
// autofill/extension quirks insert stray spaces, and per the "never punish
|
|
23
|
+
// real users" ethos that's a weaker trap, not a false positive.
|
|
23
24
|
if (getField(options.formData, honeypotField).trim() !== "") {
|
|
24
25
|
return { ok: false, reason: "honeypot" };
|
|
25
26
|
}
|
package/dist/verify.js.map
CHANGED
|
@@ -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,
|
|
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,GAAG,QAAQ,YAAY,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjF,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
|
+
"version": "0.4.1",
|
|
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.
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"@types/react": "^19.
|
|
45
|
-
"react": "^19.
|
|
46
|
-
"typescript": "^
|
|
47
|
-
"vitest": "^4.1.
|
|
42
|
+
"@ingram-tech/nk-dev": "0.5.0",
|
|
43
|
+
"@types/node": "^26.1.1",
|
|
44
|
+
"@types/react": "^19.2.17",
|
|
45
|
+
"react": "^19.2.7",
|
|
46
|
+
"typescript": "^7.0.2",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"botid": ">=1.0.0",
|