@ingram-tech/nk-forms 0.2.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 +106 -0
- package/dist/email.d.ts +30 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +47 -0
- package/dist/email.js.map +1 -0
- package/dist/handler.d.ts +58 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +77 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +30 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +58 -0
- package/dist/react.js.map +1 -0
- package/dist/token.d.ts +13 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +16 -0
- package/dist/token.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @ingram-tech/nk-forms
|
|
2
|
+
|
|
3
|
+
The contact/signup **submission pipeline** for Ingram's Next.js sites: bot
|
|
4
|
+
protection, validation, rate-limit hook, and escaped email notifications behind
|
|
5
|
+
one server handler and one client hook.
|
|
6
|
+
|
|
7
|
+
It is a thin layer over [`@ingram-tech/bot-protection`](../bot-protection)
|
|
8
|
+
(the security primitive) and [`@ingram-tech/nk-email`](../nk-email) (sending +
|
|
9
|
+
`escapeHtml`). Reach for those directly when you need bot detection somewhere
|
|
10
|
+
that isn't a public form (a checkout, an authed endpoint) — this package is for
|
|
11
|
+
the "validate a public submission and notify a human / subscribe an address"
|
|
12
|
+
shape, and deliberately does **not** try to own Stripe checkout or auth flows.
|
|
13
|
+
|
|
14
|
+
## What it gives you
|
|
15
|
+
|
|
16
|
+
- `handleFormSubmission(request, options)` — the fixed pipeline: rate-limit →
|
|
17
|
+
parse → bot gate → validate → deliver → uniform response. Bot hits and honest
|
|
18
|
+
submissions return the **same** 200 body, so a bot never learns it was
|
|
19
|
+
dropped. Returns a web-standard `Response` (no `next` dependency).
|
|
20
|
+
- `renderNotificationEmail({ heading, fields, message, footer })` — builds
|
|
21
|
+
`{ html, text }` with **every** value escaped. Use it so escaping is the
|
|
22
|
+
default, not a per-site coin flip.
|
|
23
|
+
- `mintFormToken()` — a ready-made GET handler for the signed timing token.
|
|
24
|
+
- `useFormSubmit(endpoint)` / re-exported `useBotProtection` + `HoneypotInput`
|
|
25
|
+
from `@ingram-tech/nk-forms/react`.
|
|
26
|
+
|
|
27
|
+
You still own your **schema** (a Zod schema — accepted structurally, no Zod
|
|
28
|
+
version pin), your **fields/branding**, and your **delivery** (`onSubmit`).
|
|
29
|
+
|
|
30
|
+
## Route
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// app/api/contact/route.ts
|
|
34
|
+
import { handleFormSubmission, renderNotificationEmail } from "@ingram-tech/nk-forms";
|
|
35
|
+
import { fromAddress, sendEmail } from "@ingram-tech/nk-email";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
|
|
38
|
+
export { mintFormToken as GET } from "@ingram-tech/nk-forms";
|
|
39
|
+
|
|
40
|
+
const schema = z.object({
|
|
41
|
+
name: z.string().trim().min(1).max(200),
|
|
42
|
+
email: z.string().trim().email().max(320),
|
|
43
|
+
message: z.string().trim().min(1).max(5000),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const POST = (request: Request) =>
|
|
47
|
+
handleFormSubmission(request, {
|
|
48
|
+
schema,
|
|
49
|
+
label: "contact",
|
|
50
|
+
onSubmit: async ({ name, email, message }) => {
|
|
51
|
+
const { html, text } = renderNotificationEmail({
|
|
52
|
+
heading: "New contact form submission",
|
|
53
|
+
fields: [
|
|
54
|
+
{ label: "Name", value: name },
|
|
55
|
+
{ label: "Email", value: email },
|
|
56
|
+
],
|
|
57
|
+
message,
|
|
58
|
+
footer: "Sent from the acme.test contact form.",
|
|
59
|
+
});
|
|
60
|
+
await sendEmail({
|
|
61
|
+
to: "studio@acme.test",
|
|
62
|
+
from: fromAddress("Acme"),
|
|
63
|
+
replyTo: email,
|
|
64
|
+
subject: `[Contact] ${name}`,
|
|
65
|
+
html,
|
|
66
|
+
text,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Add a rate limiter by passing `rateLimit: () => ({ ok, retryAfterMs })` — plug in
|
|
73
|
+
whatever store you already use; nk-forms owns none.
|
|
74
|
+
|
|
75
|
+
## Client
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
"use client";
|
|
79
|
+
import { HoneypotInput, useFormSubmit } from "@ingram-tech/nk-forms/react";
|
|
80
|
+
|
|
81
|
+
export function ContactForm() {
|
|
82
|
+
const { honeypotRef, submit, status, error } = useFormSubmit("/api/contact");
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<form
|
|
86
|
+
onSubmit={async (e) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const data = new FormData(e.currentTarget);
|
|
89
|
+
await submit(Object.fromEntries(data));
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<HoneypotInput inputRef={honeypotRef} />
|
|
93
|
+
{/* your fields */}
|
|
94
|
+
<button disabled={status === "submitting"}>Send</button>
|
|
95
|
+
{status === "success" && <p>Thanks — we'll be in touch.</p>}
|
|
96
|
+
{error && <p role="alert">{error}</p>}
|
|
97
|
+
</form>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Environment
|
|
103
|
+
|
|
104
|
+
Inherits `BOT_PROTECTION_SECRET` from `@ingram-tech/bot-protection` (the timing
|
|
105
|
+
layer; honeypot + BotID run without it) and the mail transport env from
|
|
106
|
+
`@ingram-tech/nk-email`. This package reads no env of its own.
|
package/dist/email.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** One labelled row in a notification email. Empty/blank values are dropped. */
|
|
2
|
+
export interface NotificationField {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string | null | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface NotificationEmailOptions {
|
|
7
|
+
/** The email's heading, e.g. "New contact form submission". */
|
|
8
|
+
heading: string;
|
|
9
|
+
/** Labelled key/value rows rendered as a table. */
|
|
10
|
+
fields?: NotificationField[];
|
|
11
|
+
/** Free-text body rendered in a pre-wrap block (the user's message). */
|
|
12
|
+
message?: string | null;
|
|
13
|
+
/** Small footer line, e.g. "Sent from the acme.test contact form.". */
|
|
14
|
+
footer?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface RenderedEmail {
|
|
17
|
+
html: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Render a submission into `{ html, text }` for `sendEmail`, with **every**
|
|
22
|
+
* interpolated value escaped via `@ingram-tech/nk-email`'s `escapeHtml`.
|
|
23
|
+
*
|
|
24
|
+
* This exists because each site hand-rolled its own notification HTML and most
|
|
25
|
+
* interpolated `name`/`email`/`message` straight into the markup unescaped — an
|
|
26
|
+
* HTML-injection footgun in the inbox. Rendering through here makes escaping the
|
|
27
|
+
* default instead of a per-site coin flip.
|
|
28
|
+
*/
|
|
29
|
+
export declare const renderNotificationEmail: (options: NotificationEmailOptions) => RenderedEmail;
|
|
30
|
+
//# sourceMappingURL=email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACxC,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC7B,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,YAC1B,wBAAwB,KAC/B,aAgDF,CAAC"}
|
package/dist/email.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { escapeHtml } from "@ingram-tech/nk-email";
|
|
2
|
+
/**
|
|
3
|
+
* Render a submission into `{ html, text }` for `sendEmail`, with **every**
|
|
4
|
+
* interpolated value escaped via `@ingram-tech/nk-email`'s `escapeHtml`.
|
|
5
|
+
*
|
|
6
|
+
* This exists because each site hand-rolled its own notification HTML and most
|
|
7
|
+
* interpolated `name`/`email`/`message` straight into the markup unescaped — an
|
|
8
|
+
* HTML-injection footgun in the inbox. Rendering through here makes escaping the
|
|
9
|
+
* default instead of a per-site coin flip.
|
|
10
|
+
*/
|
|
11
|
+
export const renderNotificationEmail = (options) => {
|
|
12
|
+
const fields = (options.fields ?? []).filter((field) => typeof field.value === "string" && field.value.trim() !== "");
|
|
13
|
+
const rows = fields
|
|
14
|
+
.map((field) => `<tr><td style="padding:4px 12px 4px 0;color:#666;">${escapeHtml(field.label)}</td><td style="padding:4px 0;">${escapeHtml(field.value)}</td></tr>`)
|
|
15
|
+
.join("");
|
|
16
|
+
const table = rows
|
|
17
|
+
? `<table style="border-collapse:collapse;font-size:14px;">${rows}</table>`
|
|
18
|
+
: "";
|
|
19
|
+
const messageBlock = options.message
|
|
20
|
+
? `<div style="background:#f5f5f5;padding:16px;border-radius:8px;margin:16px 0;"><p style="white-space:pre-wrap;margin:0;">${escapeHtml(options.message)}</p></div>`
|
|
21
|
+
: "";
|
|
22
|
+
const footerBlock = options.footer
|
|
23
|
+
? `<p style="color:#888;font-size:13px;margin-top:20px;">${escapeHtml(options.footer)}</p>`
|
|
24
|
+
: "";
|
|
25
|
+
const html = [
|
|
26
|
+
`<div style="font-family:sans-serif;max-width:600px;margin:0 auto;">`,
|
|
27
|
+
`<h2 style="color:#333;">${escapeHtml(options.heading)}</h2>`,
|
|
28
|
+
table,
|
|
29
|
+
messageBlock,
|
|
30
|
+
footerBlock,
|
|
31
|
+
`</div>`,
|
|
32
|
+
]
|
|
33
|
+
.filter((part) => part !== "")
|
|
34
|
+
.join("\n");
|
|
35
|
+
const textLines = [options.heading, ""];
|
|
36
|
+
for (const field of fields) {
|
|
37
|
+
textLines.push(`${field.label}: ${field.value}`);
|
|
38
|
+
}
|
|
39
|
+
if (options.message) {
|
|
40
|
+
textLines.push("", "Message:", options.message);
|
|
41
|
+
}
|
|
42
|
+
if (options.footer) {
|
|
43
|
+
textLines.push("", "---", options.footer);
|
|
44
|
+
}
|
|
45
|
+
return { html, text: textLines.join("\n") };
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAwBnD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACtC,OAAiC,EACjB,EAAE;IAClB,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAC3C,CAAC,KAAK,EAA6C,EAAE,CACpD,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAC7D,CAAC;IAEF,MAAM,IAAI,GAAG,MAAM;SACjB,GAAG,CACH,CAAC,KAAK,EAAE,EAAE,CACT,sDAAsD,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,mCAAmC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CACpJ;SACA,IAAI,CAAC,EAAE,CAAC,CAAC;IAEX,MAAM,KAAK,GAAG,IAAI;QACjB,CAAC,CAAC,2DAA2D,IAAI,UAAU;QAC3E,CAAC,CAAC,EAAE,CAAC;IAEN,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO;QACnC,CAAC,CAAC,2HAA2H,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY;QACpK,CAAC,CAAC,EAAE,CAAC;IAEN,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM;QACjC,CAAC,CAAC,yDAAyD,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM;QAC3F,CAAC,CAAC,EAAE,CAAC;IAEN,MAAM,IAAI,GAAG;QACZ,qEAAqE;QACrE,2BAA2B,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO;QAC7D,KAAK;QACL,YAAY;QACZ,WAAW;QACX,QAAQ;KACR;SACC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;SAC7B,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,MAAM,SAAS,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,SAAS,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7C,CAAC,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type VerifyOptions } from "@ingram-tech/bot-protection";
|
|
2
|
+
/**
|
|
3
|
+
* Decision returned by a caller-supplied rate limiter. nk-forms owns no store —
|
|
4
|
+
* you plug in your own (per-IP, per-user, Redis, in-memory) and just tell the
|
|
5
|
+
* handler whether this request may proceed.
|
|
6
|
+
*/
|
|
7
|
+
export interface RateLimitDecision {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
/** Milliseconds until the caller may retry; surfaced as a Retry-After header. */
|
|
10
|
+
retryAfterMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The one method the handler needs from a validator. Any Zod schema satisfies
|
|
14
|
+
* this structurally, so nk-forms never depends on (or pins) a Zod version.
|
|
15
|
+
*/
|
|
16
|
+
export interface FormSchema<T> {
|
|
17
|
+
safeParse(input: unknown): {
|
|
18
|
+
success: true;
|
|
19
|
+
data: T;
|
|
20
|
+
} | {
|
|
21
|
+
success: false;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface Loggerish {
|
|
25
|
+
warn(message: string): void;
|
|
26
|
+
error(message: string, error?: unknown): void;
|
|
27
|
+
}
|
|
28
|
+
export interface FormHandlerOptions<T> {
|
|
29
|
+
/** Validates (and narrows) the submission. Zod strips bot-protection fields. */
|
|
30
|
+
schema: FormSchema<T>;
|
|
31
|
+
/** Deliver the validated submission: send an email, subscribe an address, … */
|
|
32
|
+
onSubmit: (data: T) => void | Promise<void>;
|
|
33
|
+
/** Optional gate run before the body is read. Return `ok: false` to 429. */
|
|
34
|
+
rateLimit?: () => RateLimitDecision | Promise<RateLimitDecision>;
|
|
35
|
+
/** Bot-protection layers, forwarded to `verifyHuman` (minus `formData`). */
|
|
36
|
+
verify?: Omit<VerifyOptions, "formData">;
|
|
37
|
+
/** Where dropped/failed submissions are logged. Defaults to no logging. */
|
|
38
|
+
logger?: Loggerish;
|
|
39
|
+
/** Label used in log lines (e.g. "contact", "newsletter"). */
|
|
40
|
+
label?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The contact/signup submission pipeline, in fixed order:
|
|
44
|
+
*
|
|
45
|
+
* 1. rate limit → 429 (optional; cheapest, no body needed)
|
|
46
|
+
* 2. parse JSON body → 400 on malformed input
|
|
47
|
+
* 3. bot gate → silent 200 (honeypot + timing token + Vercel BotID)
|
|
48
|
+
* 4. schema validate → 400 on invalid fields
|
|
49
|
+
* 5. deliver → 500 if `onSubmit` throws
|
|
50
|
+
* 6. success → 200 { success: true }
|
|
51
|
+
*
|
|
52
|
+
* Returns a web-standard `Response`, which Next.js route handlers accept
|
|
53
|
+
* directly — nk-forms takes no `next` dependency. Pair with `mintFormToken` as
|
|
54
|
+
* the route's GET and `useFormSubmit`/`useBotProtection` on the client.
|
|
55
|
+
*/
|
|
56
|
+
export declare const handleFormSubmission: <T>(request: Request, options: FormHandlerOptions<T>) => Promise<Response>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE9E;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC5B,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,KAAK,CAAA;KAAE,CAAC;CAC3E;AAED,UAAU,SAAS;IAClB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9C;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACpC,gFAAgF;IAChF,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjE,4EAA4E;IAC5E,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IACzC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAYD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAAU,CAAC,WAClC,OAAO,WACP,kBAAkB,CAAC,CAAC,CAAC,KAC5B,OAAO,CAAC,QAAQ,CA8DlB,CAAC"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { verifyHuman } from "@ingram-tech/bot-protection";
|
|
2
|
+
const json = (data, status = 200) => new Response(JSON.stringify(data), {
|
|
3
|
+
status,
|
|
4
|
+
headers: { "content-type": "application/json" },
|
|
5
|
+
});
|
|
6
|
+
// Bot hits and honest submissions return the SAME 200 body — a dropped bot must
|
|
7
|
+
// never be able to tell it was dropped, and a real user is never shown an error.
|
|
8
|
+
const SUCCESS = { success: true };
|
|
9
|
+
/**
|
|
10
|
+
* The contact/signup submission pipeline, in fixed order:
|
|
11
|
+
*
|
|
12
|
+
* 1. rate limit → 429 (optional; cheapest, no body needed)
|
|
13
|
+
* 2. parse JSON body → 400 on malformed input
|
|
14
|
+
* 3. bot gate → silent 200 (honeypot + timing token + Vercel BotID)
|
|
15
|
+
* 4. schema validate → 400 on invalid fields
|
|
16
|
+
* 5. deliver → 500 if `onSubmit` throws
|
|
17
|
+
* 6. success → 200 { success: true }
|
|
18
|
+
*
|
|
19
|
+
* Returns a web-standard `Response`, which Next.js route handlers accept
|
|
20
|
+
* directly — nk-forms takes no `next` dependency. Pair with `mintFormToken` as
|
|
21
|
+
* the route's GET and `useFormSubmit`/`useBotProtection` on the client.
|
|
22
|
+
*/
|
|
23
|
+
export const handleFormSubmission = async (request, options) => {
|
|
24
|
+
const label = options.label ?? "form";
|
|
25
|
+
const log = options.logger;
|
|
26
|
+
// 1. Rate limit — before touching the body so floods are cheap to reject.
|
|
27
|
+
if (options.rateLimit) {
|
|
28
|
+
const decision = await options.rateLimit();
|
|
29
|
+
if (!decision.ok) {
|
|
30
|
+
const headers = {
|
|
31
|
+
"content-type": "application/json",
|
|
32
|
+
};
|
|
33
|
+
if (decision.retryAfterMs !== undefined) {
|
|
34
|
+
headers["retry-after"] = Math.ceil(decision.retryAfterMs / 1000).toString();
|
|
35
|
+
}
|
|
36
|
+
return new Response(JSON.stringify({
|
|
37
|
+
error: "Too many requests. Please try again later.",
|
|
38
|
+
}), { status: 429, headers });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 2. Read the JSON body.
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = await request.json();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return json({ error: "Invalid request." }, 400);
|
|
48
|
+
}
|
|
49
|
+
if (typeof body !== "object" || body === null) {
|
|
50
|
+
return json({ error: "Invalid request." }, 400);
|
|
51
|
+
}
|
|
52
|
+
// 3. Bot gate. On failure, *pretend success* — never tell a bot what tripped
|
|
53
|
+
// it, never punish a real user with a visible error.
|
|
54
|
+
const verdict = await verifyHuman({
|
|
55
|
+
...options.verify,
|
|
56
|
+
formData: body,
|
|
57
|
+
});
|
|
58
|
+
if (!verdict.ok) {
|
|
59
|
+
log?.warn(`${label}: dropped by bot protection (${verdict.reason})`);
|
|
60
|
+
return json(SUCCESS);
|
|
61
|
+
}
|
|
62
|
+
// 4. Validate. A well-behaved schema strips the bot-protection fields.
|
|
63
|
+
const parsed = options.schema.safeParse(body);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
return json({ error: "Invalid form data. Please check your input." }, 400);
|
|
66
|
+
}
|
|
67
|
+
// 5. Deliver.
|
|
68
|
+
try {
|
|
69
|
+
await options.onSubmit(parsed.data);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
log?.error(`${label}: delivery failed`, error);
|
|
73
|
+
return json({ error: "Failed to submit. Please try again later." }, 500);
|
|
74
|
+
}
|
|
75
|
+
return json(SUCCESS);
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type FormHandlerOptions, type FormSchema, handleFormSubmission, type RateLimitDecision, } from "./handler.js";
|
|
2
|
+
export { type NotificationEmailOptions, type NotificationField, type RenderedEmail, renderNotificationEmail, } from "./email.js";
|
|
3
|
+
export { mintFormToken } from "./token.js";
|
|
4
|
+
export { createFormToken, verifyHuman } from "@ingram-tech/bot-protection";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,oBAAoB,EACpB,KAAK,iBAAiB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,KAAK,wBAAwB,EAC7B,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,uBAAuB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Server entry — no React. The client hook and components live at
|
|
2
|
+
// "@ingram-tech/nk-forms/react" so importing the handler never pulls in React.
|
|
3
|
+
export { handleFormSubmission, } from "./handler.js";
|
|
4
|
+
export { renderNotificationEmail, } from "./email.js";
|
|
5
|
+
export { mintFormToken } from "./token.js";
|
|
6
|
+
// Re-export the underlying primitives so a form route is a single import.
|
|
7
|
+
export { createFormToken, verifyHuman } from "@ingram-tech/bot-protection";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,+EAA+E;AAC/E,OAAO,EAGN,oBAAoB,GAEpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAIN,uBAAuB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,0EAA0E;AAC1E,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { HoneypotInput, useBotProtection } from "@ingram-tech/bot-protection/react";
|
|
2
|
+
export type FormSubmitStatus = "idle" | "submitting" | "success" | "error";
|
|
3
|
+
export interface FormSubmitResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* One-import client wiring for a form that POSTs JSON to `endpoint`.
|
|
9
|
+
*
|
|
10
|
+
* Wraps {@link useBotProtection} (timing token + honeypot ref) and owns submit
|
|
11
|
+
* status so the component only renders markup. The route's GET mints the token
|
|
12
|
+
* (`mintFormToken`) and its POST runs `handleFormSubmission`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { honeypotRef, submit, status, error } = useFormSubmit("/api/contact");
|
|
16
|
+
* // in the form: <HoneypotInput inputRef={honeypotRef} />
|
|
17
|
+
* // on submit: const res = await submit({ name, email, message });
|
|
18
|
+
* // if (res.ok) setSent(true);
|
|
19
|
+
*/
|
|
20
|
+
export declare function useFormSubmit(endpoint: string, options?: {
|
|
21
|
+
honeypotField?: string;
|
|
22
|
+
}): {
|
|
23
|
+
honeypotRef: import("react").RefObject<HTMLInputElement | null>;
|
|
24
|
+
botFields: () => Record<string, string>;
|
|
25
|
+
ready: boolean;
|
|
26
|
+
status: FormSubmitStatus;
|
|
27
|
+
error: string | null;
|
|
28
|
+
submit: (values: Record<string, unknown>) => Promise<FormSubmitResult>;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAEpF,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAcD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;qBAM1E,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,OAAO,CAAC,gBAAgB,CAAC;EA6B5B"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useBotProtection } from "@ingram-tech/bot-protection/react";
|
|
4
|
+
// Re-export the primitives so a form component needs a single import.
|
|
5
|
+
export { HoneypotInput, useBotProtection } from "@ingram-tech/bot-protection/react";
|
|
6
|
+
const getErrorMessage = (data) => {
|
|
7
|
+
if (typeof data === "object" &&
|
|
8
|
+
data !== null &&
|
|
9
|
+
"error" in data &&
|
|
10
|
+
typeof data.error === "string") {
|
|
11
|
+
return data.error;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* One-import client wiring for a form that POSTs JSON to `endpoint`.
|
|
17
|
+
*
|
|
18
|
+
* Wraps {@link useBotProtection} (timing token + honeypot ref) and owns submit
|
|
19
|
+
* status so the component only renders markup. The route's GET mints the token
|
|
20
|
+
* (`mintFormToken`) and its POST runs `handleFormSubmission`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const { honeypotRef, submit, status, error } = useFormSubmit("/api/contact");
|
|
24
|
+
* // in the form: <HoneypotInput inputRef={honeypotRef} />
|
|
25
|
+
* // on submit: const res = await submit({ name, email, message });
|
|
26
|
+
* // if (res.ok) setSent(true);
|
|
27
|
+
*/
|
|
28
|
+
export function useFormSubmit(endpoint, options) {
|
|
29
|
+
const { honeypotRef, botFields, ready } = useBotProtection(endpoint, options);
|
|
30
|
+
const [status, setStatus] = useState("idle");
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const submit = async (values) => {
|
|
33
|
+
setStatus("submitting");
|
|
34
|
+
setError(null);
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(endpoint, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({ ...values, ...botFields() }),
|
|
40
|
+
});
|
|
41
|
+
const data = await res.json().catch(() => ({}));
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(getErrorMessage(data) ?? "Something went wrong. Please try again.");
|
|
44
|
+
}
|
|
45
|
+
setStatus("success");
|
|
46
|
+
return { ok: true, data };
|
|
47
|
+
}
|
|
48
|
+
catch (submitError) {
|
|
49
|
+
setStatus("error");
|
|
50
|
+
setError(submitError instanceof Error
|
|
51
|
+
? submitError.message
|
|
52
|
+
: "Something went wrong. Please try again.");
|
|
53
|
+
return { ok: false };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return { honeypotRef, botFields, ready, status, error, submit };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,sEAAsE;AACtE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AASpF,MAAM,eAAe,GAAG,CAAC,IAAa,EAAiB,EAAE;IACxD,IACC,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACb,OAAO,IAAI,IAAI;QACf,OAAQ,IAA2B,CAAC,KAAK,KAAK,QAAQ,EACrD,CAAC;QACF,OAAQ,IAA0B,CAAC,KAAK,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,OAAoC;IACnF,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9E,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAmB,MAAM,CAAC,CAAC;IAC/D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAExD,MAAM,MAAM,GAAG,KAAK,EACnB,MAA+B,EACH,EAAE;QAC9B,SAAS,CAAC,YAAY,CAAC,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE;gBACjC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,EAAE,CAAC;aACnD,CAAC,CAAC;YACH,MAAM,IAAI,GAAY,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACzD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACd,eAAe,CAAC,IAAI,CAAC,IAAI,yCAAyC,CAClE,CAAC;YACH,CAAC;YACD,SAAS,CAAC,SAAS,CAAC,CAAC;YACrB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,WAAW,EAAE,CAAC;YACtB,SAAS,CAAC,OAAO,CAAC,CAAC;YACnB,QAAQ,CACP,WAAW,YAAY,KAAK;gBAC3B,CAAC,CAAC,WAAW,CAAC,OAAO;gBACrB,CAAC,CAAC,yCAAyC,CAC5C,CAAC;YACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;QACtB,CAAC;IACF,CAAC,CAAC;IAEF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACjE,CAAC"}
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ready-made GET handler that mints the signed timing token the client hook
|
|
3
|
+
* fetches on mount. Wire it into any form route with one line:
|
|
4
|
+
*
|
|
5
|
+
* export { mintFormToken as GET } from "@ingram-tech/nk-forms";
|
|
6
|
+
*
|
|
7
|
+
* or alongside other GET logic: `export const GET = () => mintFormToken();`
|
|
8
|
+
*
|
|
9
|
+
* Returns `{ token: "" }` when `BOT_PROTECTION_SECRET` is unset (the timing
|
|
10
|
+
* layer is simply disabled — honeypot + BotID still run).
|
|
11
|
+
*/
|
|
12
|
+
export declare const mintFormToken: () => Response;
|
|
13
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,QAAO,QAG9B,CAAC"}
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFormToken } from "@ingram-tech/bot-protection";
|
|
2
|
+
/**
|
|
3
|
+
* Ready-made GET handler that mints the signed timing token the client hook
|
|
4
|
+
* fetches on mount. Wire it into any form route with one line:
|
|
5
|
+
*
|
|
6
|
+
* export { mintFormToken as GET } from "@ingram-tech/nk-forms";
|
|
7
|
+
*
|
|
8
|
+
* or alongside other GET logic: `export const GET = () => mintFormToken();`
|
|
9
|
+
*
|
|
10
|
+
* Returns `{ token: "" }` when `BOT_PROTECTION_SECRET` is unset (the timing
|
|
11
|
+
* layer is simply disabled — honeypot + BotID still run).
|
|
12
|
+
*/
|
|
13
|
+
export const mintFormToken = () => new Response(JSON.stringify({ token: createFormToken() }), {
|
|
14
|
+
headers: { "content-type": "application/json" },
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAa,EAAE,CAC3C,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE;IAC1D,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;CAC/C,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingram-tech/nk-forms",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Contact/signup form pipeline for Next.js sites — bot protection, validation, and escaped email notifications in one handler.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ingram-technologies/nextkit.git",
|
|
10
|
+
"directory": "packages/nk-forms"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./react": {
|
|
24
|
+
"types": "./dist/react.d.ts",
|
|
25
|
+
"import": "./dist/react.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@ingram-tech/bot-protection": "0.4.0",
|
|
35
|
+
"@ingram-tech/nk-email": "0.3.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@ingram-tech/nk-dev": "0.5.0",
|
|
39
|
+
"@types/node": "^26.1.1",
|
|
40
|
+
"@types/react": "^19.2.17",
|
|
41
|
+
"react": "^19.2.7",
|
|
42
|
+
"typescript": "^7.0.2",
|
|
43
|
+
"vitest": "^4.1.10"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|