@ingram-tech/bot-protection 0.3.1 → 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 +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/honeypot.js +1 -1
- package/dist/honeypot.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.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 -11
- package/dist/react.js.map +1 -1
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +9 -4
- package/dist/token.js.map +1 -1
- package/dist/verify.d.ts +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +7 -4
- package/dist/verify.js.map +1 -1
- package/package.json +6 -6
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"}
|
package/dist/honeypot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields";
|
|
2
|
+
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
3
3
|
/**
|
|
4
4
|
* Visually-hidden honeypot + signed timing token, dropped inside any <form>.
|
|
5
5
|
*
|
package/dist/honeypot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"honeypot.js","sourceRoot":"","sources":["../src/honeypot.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"honeypot.js","sourceRoot":"","sources":["../src/honeypot.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,GAAkB;IAC7B,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;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC7B,KAAK,EACL,KAAK,GAAG,cAAc,GAItB,EAAE,EAAE,CAAC,CACL,8BAAiB,MAAM,EAAC,KAAK,EAAE,MAAM,aACpC,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/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { checkBot } from "./botid";
|
|
2
|
-
export { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields";
|
|
3
|
-
export { isConfigured } from "./keys";
|
|
4
|
-
export { createFormToken, type TokenCheck, type TokenResult, verifyFormToken, } from "./token";
|
|
5
|
-
export { type VerifyOptions, type VerifyResult, verifyHuman } from "./verify";
|
|
1
|
+
export { checkBot } from "./botid.js";
|
|
2
|
+
export { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
3
|
+
export { isConfigured } from "./keys.js";
|
|
4
|
+
export { createFormToken, type TokenCheck, type TokenResult, verifyFormToken, } from "./token.js";
|
|
5
|
+
export { type VerifyOptions, type VerifyResult, verifyHuman } from "./verify.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,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,EACN,eAAe,EACf,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,eAAe,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Server entry. The honeypot component lives at "@ingram-tech/bot-protection/honeypot"
|
|
2
2
|
// so importing the verifier server-side never pulls in React.
|
|
3
|
-
export { checkBot } from "./botid";
|
|
4
|
-
export { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields";
|
|
5
|
-
export { isConfigured } from "./keys";
|
|
6
|
-
export { createFormToken, verifyFormToken, } from "./token";
|
|
7
|
-
export { verifyHuman } from "./verify";
|
|
3
|
+
export { checkBot } from "./botid.js";
|
|
4
|
+
export { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
5
|
+
export { isConfigured } from "./keys.js";
|
|
6
|
+
export { createFormToken, verifyFormToken, } from "./token.js";
|
|
7
|
+
export { verifyHuman } from "./verify.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,8DAA8D;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,8DAA8D;AAC9D,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,EACN,eAAe,EAGf,eAAe,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAyC,WAAW,EAAE,MAAM,aAAa,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;AAGpE
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields";
|
|
4
|
+
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
5
5
|
/**
|
|
6
6
|
* Client-side bot protection for forms that POST JSON to your own route.
|
|
7
7
|
*
|
|
@@ -13,34 +13,68 @@ import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields";
|
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
[
|
|
64
|
+
[honeypotField]: honeypotRef.current?.value ?? "",
|
|
33
65
|
[TOKEN_FIELD]: token,
|
|
34
66
|
});
|
|
35
|
-
|
|
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:
|
|
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,
|
|
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"}
|
package/dist/token.d.ts.map
CHANGED
|
@@ -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,
|
|
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";
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
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,
|
|
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"}
|
package/dist/verify.d.ts
CHANGED
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,
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { checkBot } from "./botid";
|
|
2
|
-
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields";
|
|
3
|
-
import { isConfigured } from "./keys";
|
|
4
|
-
import { verifyFormToken } from "./token";
|
|
1
|
+
import { checkBot } from "./botid.js";
|
|
2
|
+
import { HONEYPOT_FIELD, TOKEN_FIELD } from "./fields.js";
|
|
3
|
+
import { isConfigured } from "./keys.js";
|
|
4
|
+
import { verifyFormToken } from "./token.js";
|
|
5
5
|
const getField = (formData, name) => {
|
|
6
6
|
const value = formData instanceof FormData
|
|
7
7
|
? formData.get(name)
|
|
@@ -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
|
}
|
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,
|
|
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
|
+
"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/
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"@types/react": "^19.
|
|
45
|
-
"react": "^19.
|
|
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.
|
|
47
|
+
"vitest": "^4.1.9"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"botid": ">=1.0.0",
|