@verivyx/paywall-next 0.1.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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.cjs +135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +101 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Verivyx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @verivyx/paywall-next
|
|
2
|
+
|
|
3
|
+
Next.js (App Router) adapter for the Verivyx paywall SDK — gate content from AI bots and charge agents on-chain via x402, with Vercel-aware IP resolution and async `ctx.params` support (Next 15+).
|
|
4
|
+
|
|
5
|
+
Requires `@verivyx/paywall` (installed automatically as a dependency) and `next` + `react` (peer dependencies).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i @verivyx/paywall-next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// app/articles/[slug]/route.ts
|
|
17
|
+
import { verivyxNext } from "@verivyx/paywall-next";
|
|
18
|
+
|
|
19
|
+
// Create an adapter (reads VERIVYX_TOKEN + VERIVYX_DOMAIN from env)
|
|
20
|
+
const vx = verivyxNext();
|
|
21
|
+
|
|
22
|
+
async function handler(req: Request, ctx: { params?: Promise<Record<string, string>> }) {
|
|
23
|
+
return Response.json({ content: "..." });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Export as a Next.js route handler — verified/paid requests pass through; bots get a 402
|
|
27
|
+
export const GET = vx.protect(handler);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### With SEO preview
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export const GET = vx.protect(handler, {
|
|
34
|
+
seoPreview: ({ slug }) => ({
|
|
35
|
+
title: "Article title",
|
|
36
|
+
excerpt: "A short teaser visible to search crawlers.",
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Proxy pre-filter (optional, defense-in-depth)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// proxy.ts (Next 16; middleware.ts on Next <=15)
|
|
45
|
+
import { verivyxNext } from "@verivyx/paywall-next";
|
|
46
|
+
import type { NextRequest } from "next/server";
|
|
47
|
+
|
|
48
|
+
// reads VERIVYX_TOKEN + VERIVYX_DOMAIN from env (throws at startup if unset)
|
|
49
|
+
const vx = verivyxNext();
|
|
50
|
+
const preFilter = vx.proxy(); // coarse, network-free pre-filter — the route handler is the real gate
|
|
51
|
+
|
|
52
|
+
export async function proxy(req: NextRequest) {
|
|
53
|
+
return (await preFilter(req)) ?? undefined; // 402 for clear unpaid bots, else continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const config = { matcher: ["/articles/:path*", "/api/:path*"] };
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The proxy is defense-in-depth only — it sheds obviously-unpaid bot traffic early (no network call). The route handler (`vx.protect(handler)`) remains the authoritative gate and must always be present.
|
|
60
|
+
|
|
61
|
+
## Config
|
|
62
|
+
|
|
63
|
+
All options can be passed to `verivyxNext(opts)` or set via environment variables.
|
|
64
|
+
|
|
65
|
+
| Env var | Required | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `VERIVYX_TOKEN` | yes (server-only) | Domain provisioning token from the Verivyx dashboard |
|
|
68
|
+
| `VERIVYX_DOMAIN` | yes | Your site domain, e.g. `example.com` |
|
|
69
|
+
| `VERIVYX_MATCH` | no | Comma-separated glob patterns to gate (e.g. `/articles/**`). Empty = gate all routes. Also accepts `string[]` in code. |
|
|
70
|
+
| `VERIVYX_FAIL_MODE` | no | Behaviour when the Verivyx backend is unreachable: `teaser` (default) \| `open` \| `closed` |
|
|
71
|
+
| `VERIVYX_TIMEOUT_MS` | no | Backend request timeout in milliseconds (default `800`) |
|
|
72
|
+
|
|
73
|
+
Additional code-only options: `trustProxy` (default `true`), `advertise` (RSL/AIPREF discovery headers).
|
|
74
|
+
|
|
75
|
+
## Docs
|
|
76
|
+
|
|
77
|
+
[https://docs.verivyx.com/docs/sdk](https://docs.verivyx.com/docs/sdk)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var paywall = require('@verivyx/paywall');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
function firstHop(xff) {
|
|
7
|
+
if (xff === null || xff === "") {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
const first = xff.split(",")[0];
|
|
11
|
+
return first !== void 0 ? first.trim() || void 0 : void 0;
|
|
12
|
+
}
|
|
13
|
+
function lastPathSegment(url) {
|
|
14
|
+
let pathname;
|
|
15
|
+
try {
|
|
16
|
+
pathname = new URL(url).pathname;
|
|
17
|
+
} catch {
|
|
18
|
+
pathname = url;
|
|
19
|
+
}
|
|
20
|
+
const segments = pathname.split("/").filter((s) => s.length > 0);
|
|
21
|
+
const last = segments[segments.length - 1];
|
|
22
|
+
if (last === void 0) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(last);
|
|
27
|
+
} catch {
|
|
28
|
+
return last;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function resolveIp(req, trustProxy) {
|
|
32
|
+
if (!trustProxy) {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
const xff = req.headers.get("x-forwarded-for");
|
|
36
|
+
const hop = firstHop(xff);
|
|
37
|
+
if (hop !== void 0) {
|
|
38
|
+
return hop;
|
|
39
|
+
}
|
|
40
|
+
const xri = req.headers.get("x-real-ip");
|
|
41
|
+
return xri !== null && xri !== "" ? xri : void 0;
|
|
42
|
+
}
|
|
43
|
+
function withAdvertiseHeaders(res, advertise) {
|
|
44
|
+
if (advertise === void 0) {
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
const headers = new Headers(res.headers);
|
|
48
|
+
headers.append("Link", paywall.rslLinkHeader(advertise));
|
|
49
|
+
headers.set("Content-Usage", paywall.contentUsageHeader(advertise));
|
|
50
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
51
|
+
}
|
|
52
|
+
function verivyxNext(opts) {
|
|
53
|
+
const vx = opts?._core ?? paywall.verivyx(opts, {
|
|
54
|
+
verifyCrawlerDns: opts?.verifyCrawlerDns ?? paywall.createSearchCrawlerVerifier(),
|
|
55
|
+
...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
|
|
56
|
+
});
|
|
57
|
+
const trustProxy = opts?.trustProxy !== false;
|
|
58
|
+
const proc = globalThis.process;
|
|
59
|
+
const env = proc?.env ?? {};
|
|
60
|
+
const cfg = paywall.resolveConfig(opts, env);
|
|
61
|
+
const proxyVerifyWebBotAuth = opts?.verifyWebBotAuth ?? paywall.verifyWebBotAuth;
|
|
62
|
+
return {
|
|
63
|
+
protect(handler, o) {
|
|
64
|
+
return async function verivyxNextGuard(req, ctx) {
|
|
65
|
+
const ip = resolveIp(req, trustProxy);
|
|
66
|
+
let coreReq;
|
|
67
|
+
if (ip !== void 0) {
|
|
68
|
+
const headers = new Headers(req.headers);
|
|
69
|
+
headers.set("x-real-ip", ip);
|
|
70
|
+
coreReq = new Request(req.url, {
|
|
71
|
+
method: req.method,
|
|
72
|
+
headers
|
|
73
|
+
// Do not attach body to coreReq — core classify reads headers only.
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
const headers = new Headers(req.headers);
|
|
77
|
+
headers.delete("x-real-ip");
|
|
78
|
+
headers.delete("x-forwarded-for");
|
|
79
|
+
coreReq = new Request(req.url, {
|
|
80
|
+
method: req.method,
|
|
81
|
+
headers
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const resolvedSlug = o?.slug?.(req) ?? (ctx.params !== void 0 ? (await ctx.params).slug : void 0) ?? lastPathSegment(req.url);
|
|
85
|
+
const decision = await vx.protect(coreReq, { slug: resolvedSlug });
|
|
86
|
+
if (!decision.allowed) {
|
|
87
|
+
const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
|
|
88
|
+
if (isPreviewCandidate && o?.seoPreview !== void 0) {
|
|
89
|
+
return withAdvertiseHeaders(
|
|
90
|
+
paywall.buildSeoPreviewResponse(resolvedSlug, req.url, o.seoPreview),
|
|
91
|
+
opts?.advertise
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return withAdvertiseHeaders(decision.response(), opts?.advertise);
|
|
95
|
+
}
|
|
96
|
+
const res = await handler(req, ctx);
|
|
97
|
+
return withAdvertiseHeaders(
|
|
98
|
+
paywall.attachPaymentResponse(res, decision.paymentResponse),
|
|
99
|
+
opts?.advertise
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
proxy() {
|
|
104
|
+
return async function verivyxProxy(req) {
|
|
105
|
+
const hasPaymentHeader = req.headers.has("payment-signature") || req.headers.has("x-payment");
|
|
106
|
+
if (hasPaymentHeader) {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
let classification;
|
|
110
|
+
try {
|
|
111
|
+
const result = await paywall.classify(req, cfg, {
|
|
112
|
+
verifyWebBotAuth: proxyVerifyWebBotAuth
|
|
113
|
+
});
|
|
114
|
+
classification = result.classification;
|
|
115
|
+
} catch {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
if (classification === "ai-bot" || classification === "signed-agent") {
|
|
119
|
+
return new Response(
|
|
120
|
+
JSON.stringify({ error: "payment_required" }),
|
|
121
|
+
{
|
|
122
|
+
status: 402,
|
|
123
|
+
headers: { "content-type": "application/json" }
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
exports.verivyxNext = verivyxNext;
|
|
134
|
+
//# sourceMappingURL=index.cjs.map
|
|
135
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","resolveConfig","coreVerifyWebBotAuth","buildSeoPreviewResponse","attachPaymentResponse","classify"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @verivyx/paywall-next\n *\n * Next.js (App Router) adapter for the Verivyx paywall SDK.\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module handles:\n * 1. IP resolution from Next.js / Vercel proxy headers.\n * 2. Awaiting the Next 15+ async `ctx.params` Promise.\n * 3. Calling core `protect()` (decision overload).\n * 4. Returning `decision.response()` when denied (handler NOT called).\n * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.\n * 6. SEO preview: when the caller supplies `seoPreview`, the adapter builds\n * the preview HTML itself (using the core-exported `buildPreviewHtml` /\n * `buildPaywallJsonLd`) and returns it for crawler/human-unverified\n * decisions. This approach is used because the core's decision overload\n * (`protect(req, {slug})`) does not forward previewBuilders — only the\n * wrap overload (`protect(handler, {seoPreview})`) does. Using core\n * exports directly keeps this adapter on the decision overload while still\n * delivering the preview.\n *\n * @example\n * ```ts\n * import { verivyxNext } from \"@verivyx/paywall-next\";\n * const vx = verivyxNext({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * export const GET = vx.protect(myHandler, {\n * seoPreview: ({ slug }) => ({ title: \"Article\", excerpt: \"Read more...\" }),\n * });\n * ```\n */\n\nimport {\n verivyx,\n resolveConfig,\n createSearchCrawlerVerifier,\n classify,\n verifyWebBotAuth as coreVerifyWebBotAuth,\n buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Next.js adapter.\n * Extends core VerivyxOptions with Next-specific controls.\n */\nexport interface NextAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from `X-Forwarded-For` /\n * `X-Real-IP` headers (set by Vercel / a trusted reverse proxy).\n * Set to false if running without a proxy, to ignore those headers.\n */\n trustProxy?: boolean;\n\n /**\n * Override the reverse-DNS search-crawler verifier injected into the core.\n * When omitted, `createSearchCrawlerVerifier()` is used (the Node.js impl).\n */\n verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;\n\n /**\n * Override the Web Bot Auth verifier injected into the core.\n * When omitted, the core's bundled RFC 9421 verifier is used.\n */\n verifyWebBotAuth?: (req: Request) => Promise<boolean>;\n\n /**\n * @internal\n * Inject a pre-built `Verivyx` core instance. Used in tests via\n * `verivyx.mock({...})` to avoid any network access. Production code\n * should never set this; omit it and the adapter constructs the real core.\n */\n _core?: Verivyx;\n\n /**\n * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both\n * the denied (402) and allowed handler responses.\n * Default undefined = OFF (no headers added; existing behavior unchanged).\n */\n advertise?: DiscoveryOptions;\n}\n\n/**\n * A Next.js App Router route handler signature (Next 15+).\n * `ctx.params` is a Promise in Next 15+ (async route segments).\n */\ntype RouteHandler = (\n req: Request,\n ctx: { params?: Promise<Record<string, string>> },\n) => Promise<Response> | Response;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header.\n */\nfunction firstHop(xff: string | null): string | undefined {\n if (xff === null || xff === \"\") {\n return undefined;\n }\n const first = xff.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Extract the last non-empty path segment from a URL string.\n * Used as a fallback slug when `ctx.params.slug` is unavailable.\n */\nfunction lastPathSegment(url: string): string {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n pathname = url;\n }\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n if (last === undefined) {\n return \"\";\n }\n try {\n return decodeURIComponent(last);\n } catch {\n return last;\n }\n}\n\n/**\n * Resolve the client IP from a Web Request.\n *\n * Precedence (when trustProxy !== false):\n * 1. `X-Forwarded-For` first hop (Vercel / nginx upstream).\n * 2. `X-Real-IP` header.\n * Returns undefined when trustProxy === false or no header is present.\n */\nfunction resolveIp(\n req: Request,\n trustProxy: boolean,\n): string | undefined {\n if (!trustProxy) {\n return undefined;\n }\n const xff = req.headers.get(\"x-forwarded-for\");\n const hop = firstHop(xff);\n if (hop !== undefined) {\n return hop;\n }\n const xri = req.headers.get(\"x-real-ip\");\n return (xri !== null && xri !== \"\") ? xri : undefined;\n}\n\n// (buildSeoPreviewResponse and attachPaymentResponse are imported from @verivyx/paywall)\n\n/**\n * Attach RSL + AIPREF discovery headers to a Web Response by cloning it.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * Returns the same Response unchanged when `advertise` is undefined.\n */\nfunction withAdvertiseHeaders(res: Response, advertise: DiscoveryOptions | undefined): Response {\n if (advertise === undefined) {\n return res;\n }\n const headers = new Headers(res.headers);\n headers.append(\"Link\", rslLinkHeader(advertise));\n headers.set(\"Content-Usage\", contentUsageHeader(advertise));\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Next.js adapter.\n *\n * Returns an object with:\n * - `protect(handler, o)` — wrap a route handler behind the Verivyx gate.\n * - `proxy()` — a coarse pre-filter for `proxy.ts` (defense-in-depth only).\n *\n * ```ts\n * const vx = verivyxNext({ domain: \"example.com\", token: \"...\" });\n * export const GET = vx.protect(myHandler);\n * ```\n */\nexport function verivyxNext(opts?: NextAdapterOptions): {\n protect(\n handler: RouteHandler,\n o?: {\n seoPreview?: (c: { slug: string }) => { title: string; excerpt: string };\n slug?: (req: Request) => string;\n },\n ): RouteHandler;\n proxy(): (req: Request) => Promise<Response | undefined>;\n} {\n // Resolve the core: use the injected `_core` (tests) or build the real one.\n // Only pass `verifyWebBotAuth` to the core deps when the caller overrode it;\n // the core bundled default is correct otherwise.\n const vx: Verivyx =\n opts?._core ??\n verivyx(opts, {\n verifyCrawlerDns:\n opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),\n ...(opts?.verifyWebBotAuth !== undefined\n ? { verifyWebBotAuth: opts.verifyWebBotAuth }\n : {}),\n });\n\n const trustProxy = opts?.trustProxy !== false; // default true\n\n // Build a real resolved config once for use by proxy()'s classify call.\n // resolveConfig throws ConfigError when domain/token are absent — same\n // behaviour as verivyx() itself, so verivyxNext always requires them.\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n const env: Record<string, string | undefined> = proc?.env ?? {};\n const cfg = resolveConfig(opts, env);\n\n // The verifyWebBotAuth dep used by proxy() mirrors what the core uses:\n // caller override if supplied, otherwise the bundled RFC 9421 verifier.\n const proxyVerifyWebBotAuth = opts?.verifyWebBotAuth ?? coreVerifyWebBotAuth;\n\n return {\n protect(handler, o) {\n return async function verivyxNextGuard(\n req: Request,\n ctx: { params?: Promise<Record<string, string>> },\n ): Promise<Response> {\n // 1. Resolve trusted client IP and inject into a cloned request so the\n // core classifier reads a reliable address regardless of edge hop.\n //\n // Security invariant:\n // trustProxy !== false → resolve IP from proxy headers and set\n // x-real-ip on the cloned request (overrides any client value).\n // trustProxy === false → no trustworthy socket IP is available in\n // Next.js route handlers; strip both x-real-ip and x-forwarded-for\n // so a client cannot spoof an IP into the core classifier\n // (core sees no IP → safe default).\n const ip = resolveIp(req, trustProxy);\n let coreReq: Request;\n if (ip !== undefined) {\n const headers = new Headers(req.headers);\n headers.set(\"x-real-ip\", ip);\n // Clone the Request with updated headers. For GET/HEAD this is safe;\n // for bodies we do NOT re-attach a body here (the core classify path\n // reads headers only — body stays with the original `req`).\n coreReq = new Request(req.url, {\n method: req.method,\n headers,\n // Do not attach body to coreReq — core classify reads headers only.\n });\n } else {\n // trustProxy === false: strip forwarding headers so the client cannot\n // inject a spoofed IP into the core.\n const headers = new Headers(req.headers);\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n coreReq = new Request(req.url, {\n method: req.method,\n headers,\n });\n }\n\n // 2. Resolve slug.\n // Priority: caller override > ctx.params.slug > last URL segment.\n // ctx.params is a Promise in Next 15+ — always await it (guard undefined).\n const resolvedSlug: string =\n o?.slug?.(req) ??\n (ctx.params !== undefined ? (await ctx.params).slug : undefined) ??\n lastPathSegment(req.url);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug: resolvedSlug });\n\n // 4a. Denied — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402.\n if (!decision.allowed) {\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(resolvedSlug, req.url, o.seoPreview),\n opts?.advertise,\n );\n }\n // Handler is NOT called — return the gate response (402 or preview).\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 4b. Allowed — call the original handler.\n const res = await handler(req, ctx);\n\n // 5. Attach the settlement receipt header when a payment was processed,\n // then attach discovery headers (single clone when both apply).\n return withAdvertiseHeaders(\n attachPaymentResponse(res, decision.paymentResponse),\n opts?.advertise,\n );\n };\n },\n\n proxy() {\n /**\n * Coarse pre-filter for `proxy.ts` — defense-in-depth ONLY.\n *\n * proxy() uses the real core classify() function directly (no mock).\n * It is a coarse, network-free pre-filter: shed obviously-unpaid bot\n * traffic early before it reaches the route handler. The route handler\n * (via protect()) remains the authoritative gate.\n *\n * Returns a 402 Response only when the request looks like a clear\n * unpaid bot (ai-bot / signed-agent UA with no payment header).\n * Returns `undefined` in all other cases — let the request continue to\n * the route handler which will make the authoritative decision.\n */\n return async function verivyxProxy(\n req: Request,\n ): Promise<Response | undefined> {\n // Quick check: if there is any payment signal, skip the pre-filter\n // and let the route handler handle authorization properly.\n const hasPaymentHeader =\n req.headers.has(\"payment-signature\") || req.headers.has(\"x-payment\");\n if (hasPaymentHeader) {\n return undefined;\n }\n\n // Use the core's exported classify with a real resolved config.\n // No crawler DNS verification at this layer (proxy does NOT need it —\n // crawler → preview is the route handler's job, not the proxy's).\n let classification: string;\n try {\n const result = await classify(req, cfg, {\n verifyWebBotAuth: proxyVerifyWebBotAuth,\n });\n classification = result.classification;\n } catch {\n // classify error → pass through to route handler.\n return undefined;\n }\n\n if (classification === \"ai-bot\" || classification === \"signed-agent\") {\n // Return a minimal 402 — the route handler's full 402 body (with\n // payment requirements) is not built here to keep this lightweight.\n return new Response(\n JSON.stringify({ error: \"payment_required\" }),\n {\n status: 402,\n headers: { \"content-type\": \"application/json\" },\n },\n );\n }\n\n // All other classifications → pass through.\n return undefined;\n };\n },\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { VerivyxOptions, Verivyx, DiscoveryOptions } from '@verivyx/paywall';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @verivyx/paywall-next
|
|
5
|
+
*
|
|
6
|
+
* Next.js (App Router) adapter for the Verivyx paywall SDK.
|
|
7
|
+
*
|
|
8
|
+
* Thin layer — all gate logic lives in @verivyx/paywall core.
|
|
9
|
+
* This module handles:
|
|
10
|
+
* 1. IP resolution from Next.js / Vercel proxy headers.
|
|
11
|
+
* 2. Awaiting the Next 15+ async `ctx.params` Promise.
|
|
12
|
+
* 3. Calling core `protect()` (decision overload).
|
|
13
|
+
* 4. Returning `decision.response()` when denied (handler NOT called).
|
|
14
|
+
* 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.
|
|
15
|
+
* 6. SEO preview: when the caller supplies `seoPreview`, the adapter builds
|
|
16
|
+
* the preview HTML itself (using the core-exported `buildPreviewHtml` /
|
|
17
|
+
* `buildPaywallJsonLd`) and returns it for crawler/human-unverified
|
|
18
|
+
* decisions. This approach is used because the core's decision overload
|
|
19
|
+
* (`protect(req, {slug})`) does not forward previewBuilders — only the
|
|
20
|
+
* wrap overload (`protect(handler, {seoPreview})`) does. Using core
|
|
21
|
+
* exports directly keeps this adapter on the decision overload while still
|
|
22
|
+
* delivering the preview.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { verivyxNext } from "@verivyx/paywall-next";
|
|
27
|
+
* const vx = verivyxNext({ domain: "example.com", token: process.env.VX_TOKEN });
|
|
28
|
+
* export const GET = vx.protect(myHandler, {
|
|
29
|
+
* seoPreview: ({ slug }) => ({ title: "Article", excerpt: "Read more..." }),
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for the Next.js adapter.
|
|
36
|
+
* Extends core VerivyxOptions with Next-specific controls.
|
|
37
|
+
*/
|
|
38
|
+
interface NextAdapterOptions extends VerivyxOptions {
|
|
39
|
+
/**
|
|
40
|
+
* When true (default), read the client IP from `X-Forwarded-For` /
|
|
41
|
+
* `X-Real-IP` headers (set by Vercel / a trusted reverse proxy).
|
|
42
|
+
* Set to false if running without a proxy, to ignore those headers.
|
|
43
|
+
*/
|
|
44
|
+
trustProxy?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Override the reverse-DNS search-crawler verifier injected into the core.
|
|
47
|
+
* When omitted, `createSearchCrawlerVerifier()` is used (the Node.js impl).
|
|
48
|
+
*/
|
|
49
|
+
verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Override the Web Bot Auth verifier injected into the core.
|
|
52
|
+
* When omitted, the core's bundled RFC 9421 verifier is used.
|
|
53
|
+
*/
|
|
54
|
+
verifyWebBotAuth?: (req: Request) => Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* @internal
|
|
57
|
+
* Inject a pre-built `Verivyx` core instance. Used in tests via
|
|
58
|
+
* `verivyx.mock({...})` to avoid any network access. Production code
|
|
59
|
+
* should never set this; omit it and the adapter constructs the real core.
|
|
60
|
+
*/
|
|
61
|
+
_core?: Verivyx;
|
|
62
|
+
/**
|
|
63
|
+
* When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both
|
|
64
|
+
* the denied (402) and allowed handler responses.
|
|
65
|
+
* Default undefined = OFF (no headers added; existing behavior unchanged).
|
|
66
|
+
*/
|
|
67
|
+
advertise?: DiscoveryOptions;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A Next.js App Router route handler signature (Next 15+).
|
|
71
|
+
* `ctx.params` is a Promise in Next 15+ (async route segments).
|
|
72
|
+
*/
|
|
73
|
+
type RouteHandler = (req: Request, ctx: {
|
|
74
|
+
params?: Promise<Record<string, string>>;
|
|
75
|
+
}) => Promise<Response> | Response;
|
|
76
|
+
/**
|
|
77
|
+
* Create a Verivyx Next.js adapter.
|
|
78
|
+
*
|
|
79
|
+
* Returns an object with:
|
|
80
|
+
* - `protect(handler, o)` — wrap a route handler behind the Verivyx gate.
|
|
81
|
+
* - `proxy()` — a coarse pre-filter for `proxy.ts` (defense-in-depth only).
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const vx = verivyxNext({ domain: "example.com", token: "..." });
|
|
85
|
+
* export const GET = vx.protect(myHandler);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function verivyxNext(opts?: NextAdapterOptions): {
|
|
89
|
+
protect(handler: RouteHandler, o?: {
|
|
90
|
+
seoPreview?: (c: {
|
|
91
|
+
slug: string;
|
|
92
|
+
}) => {
|
|
93
|
+
title: string;
|
|
94
|
+
excerpt: string;
|
|
95
|
+
};
|
|
96
|
+
slug?: (req: Request) => string;
|
|
97
|
+
}): RouteHandler;
|
|
98
|
+
proxy(): (req: Request) => Promise<Response | undefined>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { type NextAdapterOptions, verivyxNext };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { VerivyxOptions, Verivyx, DiscoveryOptions } from '@verivyx/paywall';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @verivyx/paywall-next
|
|
5
|
+
*
|
|
6
|
+
* Next.js (App Router) adapter for the Verivyx paywall SDK.
|
|
7
|
+
*
|
|
8
|
+
* Thin layer — all gate logic lives in @verivyx/paywall core.
|
|
9
|
+
* This module handles:
|
|
10
|
+
* 1. IP resolution from Next.js / Vercel proxy headers.
|
|
11
|
+
* 2. Awaiting the Next 15+ async `ctx.params` Promise.
|
|
12
|
+
* 3. Calling core `protect()` (decision overload).
|
|
13
|
+
* 4. Returning `decision.response()` when denied (handler NOT called).
|
|
14
|
+
* 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.
|
|
15
|
+
* 6. SEO preview: when the caller supplies `seoPreview`, the adapter builds
|
|
16
|
+
* the preview HTML itself (using the core-exported `buildPreviewHtml` /
|
|
17
|
+
* `buildPaywallJsonLd`) and returns it for crawler/human-unverified
|
|
18
|
+
* decisions. This approach is used because the core's decision overload
|
|
19
|
+
* (`protect(req, {slug})`) does not forward previewBuilders — only the
|
|
20
|
+
* wrap overload (`protect(handler, {seoPreview})`) does. Using core
|
|
21
|
+
* exports directly keeps this adapter on the decision overload while still
|
|
22
|
+
* delivering the preview.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { verivyxNext } from "@verivyx/paywall-next";
|
|
27
|
+
* const vx = verivyxNext({ domain: "example.com", token: process.env.VX_TOKEN });
|
|
28
|
+
* export const GET = vx.protect(myHandler, {
|
|
29
|
+
* seoPreview: ({ slug }) => ({ title: "Article", excerpt: "Read more..." }),
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for the Next.js adapter.
|
|
36
|
+
* Extends core VerivyxOptions with Next-specific controls.
|
|
37
|
+
*/
|
|
38
|
+
interface NextAdapterOptions extends VerivyxOptions {
|
|
39
|
+
/**
|
|
40
|
+
* When true (default), read the client IP from `X-Forwarded-For` /
|
|
41
|
+
* `X-Real-IP` headers (set by Vercel / a trusted reverse proxy).
|
|
42
|
+
* Set to false if running without a proxy, to ignore those headers.
|
|
43
|
+
*/
|
|
44
|
+
trustProxy?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Override the reverse-DNS search-crawler verifier injected into the core.
|
|
47
|
+
* When omitted, `createSearchCrawlerVerifier()` is used (the Node.js impl).
|
|
48
|
+
*/
|
|
49
|
+
verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Override the Web Bot Auth verifier injected into the core.
|
|
52
|
+
* When omitted, the core's bundled RFC 9421 verifier is used.
|
|
53
|
+
*/
|
|
54
|
+
verifyWebBotAuth?: (req: Request) => Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* @internal
|
|
57
|
+
* Inject a pre-built `Verivyx` core instance. Used in tests via
|
|
58
|
+
* `verivyx.mock({...})` to avoid any network access. Production code
|
|
59
|
+
* should never set this; omit it and the adapter constructs the real core.
|
|
60
|
+
*/
|
|
61
|
+
_core?: Verivyx;
|
|
62
|
+
/**
|
|
63
|
+
* When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both
|
|
64
|
+
* the denied (402) and allowed handler responses.
|
|
65
|
+
* Default undefined = OFF (no headers added; existing behavior unchanged).
|
|
66
|
+
*/
|
|
67
|
+
advertise?: DiscoveryOptions;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A Next.js App Router route handler signature (Next 15+).
|
|
71
|
+
* `ctx.params` is a Promise in Next 15+ (async route segments).
|
|
72
|
+
*/
|
|
73
|
+
type RouteHandler = (req: Request, ctx: {
|
|
74
|
+
params?: Promise<Record<string, string>>;
|
|
75
|
+
}) => Promise<Response> | Response;
|
|
76
|
+
/**
|
|
77
|
+
* Create a Verivyx Next.js adapter.
|
|
78
|
+
*
|
|
79
|
+
* Returns an object with:
|
|
80
|
+
* - `protect(handler, o)` — wrap a route handler behind the Verivyx gate.
|
|
81
|
+
* - `proxy()` — a coarse pre-filter for `proxy.ts` (defense-in-depth only).
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const vx = verivyxNext({ domain: "example.com", token: "..." });
|
|
85
|
+
* export const GET = vx.protect(myHandler);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function verivyxNext(opts?: NextAdapterOptions): {
|
|
89
|
+
protect(handler: RouteHandler, o?: {
|
|
90
|
+
seoPreview?: (c: {
|
|
91
|
+
slug: string;
|
|
92
|
+
}) => {
|
|
93
|
+
title: string;
|
|
94
|
+
excerpt: string;
|
|
95
|
+
};
|
|
96
|
+
slug?: (req: Request) => string;
|
|
97
|
+
}): RouteHandler;
|
|
98
|
+
proxy(): (req: Request) => Promise<Response | undefined>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { type NextAdapterOptions, verivyxNext };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { verivyx, createSearchCrawlerVerifier, resolveConfig, verifyWebBotAuth, classify, buildSeoPreviewResponse, attachPaymentResponse, rslLinkHeader, contentUsageHeader } from '@verivyx/paywall';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function firstHop(xff) {
|
|
5
|
+
if (xff === null || xff === "") {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
const first = xff.split(",")[0];
|
|
9
|
+
return first !== void 0 ? first.trim() || void 0 : void 0;
|
|
10
|
+
}
|
|
11
|
+
function lastPathSegment(url) {
|
|
12
|
+
let pathname;
|
|
13
|
+
try {
|
|
14
|
+
pathname = new URL(url).pathname;
|
|
15
|
+
} catch {
|
|
16
|
+
pathname = url;
|
|
17
|
+
}
|
|
18
|
+
const segments = pathname.split("/").filter((s) => s.length > 0);
|
|
19
|
+
const last = segments[segments.length - 1];
|
|
20
|
+
if (last === void 0) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return decodeURIComponent(last);
|
|
25
|
+
} catch {
|
|
26
|
+
return last;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function resolveIp(req, trustProxy) {
|
|
30
|
+
if (!trustProxy) {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
const xff = req.headers.get("x-forwarded-for");
|
|
34
|
+
const hop = firstHop(xff);
|
|
35
|
+
if (hop !== void 0) {
|
|
36
|
+
return hop;
|
|
37
|
+
}
|
|
38
|
+
const xri = req.headers.get("x-real-ip");
|
|
39
|
+
return xri !== null && xri !== "" ? xri : void 0;
|
|
40
|
+
}
|
|
41
|
+
function withAdvertiseHeaders(res, advertise) {
|
|
42
|
+
if (advertise === void 0) {
|
|
43
|
+
return res;
|
|
44
|
+
}
|
|
45
|
+
const headers = new Headers(res.headers);
|
|
46
|
+
headers.append("Link", rslLinkHeader(advertise));
|
|
47
|
+
headers.set("Content-Usage", contentUsageHeader(advertise));
|
|
48
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
49
|
+
}
|
|
50
|
+
function verivyxNext(opts) {
|
|
51
|
+
const vx = opts?._core ?? verivyx(opts, {
|
|
52
|
+
verifyCrawlerDns: opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),
|
|
53
|
+
...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
|
|
54
|
+
});
|
|
55
|
+
const trustProxy = opts?.trustProxy !== false;
|
|
56
|
+
const proc = globalThis.process;
|
|
57
|
+
const env = proc?.env ?? {};
|
|
58
|
+
const cfg = resolveConfig(opts, env);
|
|
59
|
+
const proxyVerifyWebBotAuth = opts?.verifyWebBotAuth ?? verifyWebBotAuth;
|
|
60
|
+
return {
|
|
61
|
+
protect(handler, o) {
|
|
62
|
+
return async function verivyxNextGuard(req, ctx) {
|
|
63
|
+
const ip = resolveIp(req, trustProxy);
|
|
64
|
+
let coreReq;
|
|
65
|
+
if (ip !== void 0) {
|
|
66
|
+
const headers = new Headers(req.headers);
|
|
67
|
+
headers.set("x-real-ip", ip);
|
|
68
|
+
coreReq = new Request(req.url, {
|
|
69
|
+
method: req.method,
|
|
70
|
+
headers
|
|
71
|
+
// Do not attach body to coreReq — core classify reads headers only.
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
const headers = new Headers(req.headers);
|
|
75
|
+
headers.delete("x-real-ip");
|
|
76
|
+
headers.delete("x-forwarded-for");
|
|
77
|
+
coreReq = new Request(req.url, {
|
|
78
|
+
method: req.method,
|
|
79
|
+
headers
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const resolvedSlug = o?.slug?.(req) ?? (ctx.params !== void 0 ? (await ctx.params).slug : void 0) ?? lastPathSegment(req.url);
|
|
83
|
+
const decision = await vx.protect(coreReq, { slug: resolvedSlug });
|
|
84
|
+
if (!decision.allowed) {
|
|
85
|
+
const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
|
|
86
|
+
if (isPreviewCandidate && o?.seoPreview !== void 0) {
|
|
87
|
+
return withAdvertiseHeaders(
|
|
88
|
+
buildSeoPreviewResponse(resolvedSlug, req.url, o.seoPreview),
|
|
89
|
+
opts?.advertise
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return withAdvertiseHeaders(decision.response(), opts?.advertise);
|
|
93
|
+
}
|
|
94
|
+
const res = await handler(req, ctx);
|
|
95
|
+
return withAdvertiseHeaders(
|
|
96
|
+
attachPaymentResponse(res, decision.paymentResponse),
|
|
97
|
+
opts?.advertise
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
proxy() {
|
|
102
|
+
return async function verivyxProxy(req) {
|
|
103
|
+
const hasPaymentHeader = req.headers.has("payment-signature") || req.headers.has("x-payment");
|
|
104
|
+
if (hasPaymentHeader) {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
let classification;
|
|
108
|
+
try {
|
|
109
|
+
const result = await classify(req, cfg, {
|
|
110
|
+
verifyWebBotAuth: proxyVerifyWebBotAuth
|
|
111
|
+
});
|
|
112
|
+
classification = result.classification;
|
|
113
|
+
} catch {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
if (classification === "ai-bot" || classification === "signed-agent") {
|
|
117
|
+
return new Response(
|
|
118
|
+
JSON.stringify({ error: "payment_required" }),
|
|
119
|
+
{
|
|
120
|
+
status: 402,
|
|
121
|
+
headers: { "content-type": "application/json" }
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { verivyxNext };
|
|
132
|
+
//# sourceMappingURL=index.js.map
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["coreVerifyWebBotAuth"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @verivyx/paywall-next\n *\n * Next.js (App Router) adapter for the Verivyx paywall SDK.\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module handles:\n * 1. IP resolution from Next.js / Vercel proxy headers.\n * 2. Awaiting the Next 15+ async `ctx.params` Promise.\n * 3. Calling core `protect()` (decision overload).\n * 4. Returning `decision.response()` when denied (handler NOT called).\n * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.\n * 6. SEO preview: when the caller supplies `seoPreview`, the adapter builds\n * the preview HTML itself (using the core-exported `buildPreviewHtml` /\n * `buildPaywallJsonLd`) and returns it for crawler/human-unverified\n * decisions. This approach is used because the core's decision overload\n * (`protect(req, {slug})`) does not forward previewBuilders — only the\n * wrap overload (`protect(handler, {seoPreview})`) does. Using core\n * exports directly keeps this adapter on the decision overload while still\n * delivering the preview.\n *\n * @example\n * ```ts\n * import { verivyxNext } from \"@verivyx/paywall-next\";\n * const vx = verivyxNext({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * export const GET = vx.protect(myHandler, {\n * seoPreview: ({ slug }) => ({ title: \"Article\", excerpt: \"Read more...\" }),\n * });\n * ```\n */\n\nimport {\n verivyx,\n resolveConfig,\n createSearchCrawlerVerifier,\n classify,\n verifyWebBotAuth as coreVerifyWebBotAuth,\n buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Next.js adapter.\n * Extends core VerivyxOptions with Next-specific controls.\n */\nexport interface NextAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from `X-Forwarded-For` /\n * `X-Real-IP` headers (set by Vercel / a trusted reverse proxy).\n * Set to false if running without a proxy, to ignore those headers.\n */\n trustProxy?: boolean;\n\n /**\n * Override the reverse-DNS search-crawler verifier injected into the core.\n * When omitted, `createSearchCrawlerVerifier()` is used (the Node.js impl).\n */\n verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;\n\n /**\n * Override the Web Bot Auth verifier injected into the core.\n * When omitted, the core's bundled RFC 9421 verifier is used.\n */\n verifyWebBotAuth?: (req: Request) => Promise<boolean>;\n\n /**\n * @internal\n * Inject a pre-built `Verivyx` core instance. Used in tests via\n * `verivyx.mock({...})` to avoid any network access. Production code\n * should never set this; omit it and the adapter constructs the real core.\n */\n _core?: Verivyx;\n\n /**\n * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both\n * the denied (402) and allowed handler responses.\n * Default undefined = OFF (no headers added; existing behavior unchanged).\n */\n advertise?: DiscoveryOptions;\n}\n\n/**\n * A Next.js App Router route handler signature (Next 15+).\n * `ctx.params` is a Promise in Next 15+ (async route segments).\n */\ntype RouteHandler = (\n req: Request,\n ctx: { params?: Promise<Record<string, string>> },\n) => Promise<Response> | Response;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header.\n */\nfunction firstHop(xff: string | null): string | undefined {\n if (xff === null || xff === \"\") {\n return undefined;\n }\n const first = xff.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Extract the last non-empty path segment from a URL string.\n * Used as a fallback slug when `ctx.params.slug` is unavailable.\n */\nfunction lastPathSegment(url: string): string {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n pathname = url;\n }\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n if (last === undefined) {\n return \"\";\n }\n try {\n return decodeURIComponent(last);\n } catch {\n return last;\n }\n}\n\n/**\n * Resolve the client IP from a Web Request.\n *\n * Precedence (when trustProxy !== false):\n * 1. `X-Forwarded-For` first hop (Vercel / nginx upstream).\n * 2. `X-Real-IP` header.\n * Returns undefined when trustProxy === false or no header is present.\n */\nfunction resolveIp(\n req: Request,\n trustProxy: boolean,\n): string | undefined {\n if (!trustProxy) {\n return undefined;\n }\n const xff = req.headers.get(\"x-forwarded-for\");\n const hop = firstHop(xff);\n if (hop !== undefined) {\n return hop;\n }\n const xri = req.headers.get(\"x-real-ip\");\n return (xri !== null && xri !== \"\") ? xri : undefined;\n}\n\n// (buildSeoPreviewResponse and attachPaymentResponse are imported from @verivyx/paywall)\n\n/**\n * Attach RSL + AIPREF discovery headers to a Web Response by cloning it.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * Returns the same Response unchanged when `advertise` is undefined.\n */\nfunction withAdvertiseHeaders(res: Response, advertise: DiscoveryOptions | undefined): Response {\n if (advertise === undefined) {\n return res;\n }\n const headers = new Headers(res.headers);\n headers.append(\"Link\", rslLinkHeader(advertise));\n headers.set(\"Content-Usage\", contentUsageHeader(advertise));\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Next.js adapter.\n *\n * Returns an object with:\n * - `protect(handler, o)` — wrap a route handler behind the Verivyx gate.\n * - `proxy()` — a coarse pre-filter for `proxy.ts` (defense-in-depth only).\n *\n * ```ts\n * const vx = verivyxNext({ domain: \"example.com\", token: \"...\" });\n * export const GET = vx.protect(myHandler);\n * ```\n */\nexport function verivyxNext(opts?: NextAdapterOptions): {\n protect(\n handler: RouteHandler,\n o?: {\n seoPreview?: (c: { slug: string }) => { title: string; excerpt: string };\n slug?: (req: Request) => string;\n },\n ): RouteHandler;\n proxy(): (req: Request) => Promise<Response | undefined>;\n} {\n // Resolve the core: use the injected `_core` (tests) or build the real one.\n // Only pass `verifyWebBotAuth` to the core deps when the caller overrode it;\n // the core bundled default is correct otherwise.\n const vx: Verivyx =\n opts?._core ??\n verivyx(opts, {\n verifyCrawlerDns:\n opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),\n ...(opts?.verifyWebBotAuth !== undefined\n ? { verifyWebBotAuth: opts.verifyWebBotAuth }\n : {}),\n });\n\n const trustProxy = opts?.trustProxy !== false; // default true\n\n // Build a real resolved config once for use by proxy()'s classify call.\n // resolveConfig throws ConfigError when domain/token are absent — same\n // behaviour as verivyx() itself, so verivyxNext always requires them.\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n const env: Record<string, string | undefined> = proc?.env ?? {};\n const cfg = resolveConfig(opts, env);\n\n // The verifyWebBotAuth dep used by proxy() mirrors what the core uses:\n // caller override if supplied, otherwise the bundled RFC 9421 verifier.\n const proxyVerifyWebBotAuth = opts?.verifyWebBotAuth ?? coreVerifyWebBotAuth;\n\n return {\n protect(handler, o) {\n return async function verivyxNextGuard(\n req: Request,\n ctx: { params?: Promise<Record<string, string>> },\n ): Promise<Response> {\n // 1. Resolve trusted client IP and inject into a cloned request so the\n // core classifier reads a reliable address regardless of edge hop.\n //\n // Security invariant:\n // trustProxy !== false → resolve IP from proxy headers and set\n // x-real-ip on the cloned request (overrides any client value).\n // trustProxy === false → no trustworthy socket IP is available in\n // Next.js route handlers; strip both x-real-ip and x-forwarded-for\n // so a client cannot spoof an IP into the core classifier\n // (core sees no IP → safe default).\n const ip = resolveIp(req, trustProxy);\n let coreReq: Request;\n if (ip !== undefined) {\n const headers = new Headers(req.headers);\n headers.set(\"x-real-ip\", ip);\n // Clone the Request with updated headers. For GET/HEAD this is safe;\n // for bodies we do NOT re-attach a body here (the core classify path\n // reads headers only — body stays with the original `req`).\n coreReq = new Request(req.url, {\n method: req.method,\n headers,\n // Do not attach body to coreReq — core classify reads headers only.\n });\n } else {\n // trustProxy === false: strip forwarding headers so the client cannot\n // inject a spoofed IP into the core.\n const headers = new Headers(req.headers);\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n coreReq = new Request(req.url, {\n method: req.method,\n headers,\n });\n }\n\n // 2. Resolve slug.\n // Priority: caller override > ctx.params.slug > last URL segment.\n // ctx.params is a Promise in Next 15+ — always await it (guard undefined).\n const resolvedSlug: string =\n o?.slug?.(req) ??\n (ctx.params !== undefined ? (await ctx.params).slug : undefined) ??\n lastPathSegment(req.url);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug: resolvedSlug });\n\n // 4a. Denied — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402.\n if (!decision.allowed) {\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(resolvedSlug, req.url, o.seoPreview),\n opts?.advertise,\n );\n }\n // Handler is NOT called — return the gate response (402 or preview).\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 4b. Allowed — call the original handler.\n const res = await handler(req, ctx);\n\n // 5. Attach the settlement receipt header when a payment was processed,\n // then attach discovery headers (single clone when both apply).\n return withAdvertiseHeaders(\n attachPaymentResponse(res, decision.paymentResponse),\n opts?.advertise,\n );\n };\n },\n\n proxy() {\n /**\n * Coarse pre-filter for `proxy.ts` — defense-in-depth ONLY.\n *\n * proxy() uses the real core classify() function directly (no mock).\n * It is a coarse, network-free pre-filter: shed obviously-unpaid bot\n * traffic early before it reaches the route handler. The route handler\n * (via protect()) remains the authoritative gate.\n *\n * Returns a 402 Response only when the request looks like a clear\n * unpaid bot (ai-bot / signed-agent UA with no payment header).\n * Returns `undefined` in all other cases — let the request continue to\n * the route handler which will make the authoritative decision.\n */\n return async function verivyxProxy(\n req: Request,\n ): Promise<Response | undefined> {\n // Quick check: if there is any payment signal, skip the pre-filter\n // and let the route handler handle authorization properly.\n const hasPaymentHeader =\n req.headers.has(\"payment-signature\") || req.headers.has(\"x-payment\");\n if (hasPaymentHeader) {\n return undefined;\n }\n\n // Use the core's exported classify with a real resolved config.\n // No crawler DNS verification at this layer (proxy does NOT need it —\n // crawler → preview is the route handler's job, not the proxy's).\n let classification: string;\n try {\n const result = await classify(req, cfg, {\n verifyWebBotAuth: proxyVerifyWebBotAuth,\n });\n classification = result.classification;\n } catch {\n // classify error → pass through to route handler.\n return undefined;\n }\n\n if (classification === \"ai-bot\" || classification === \"signed-agent\") {\n // Return a minimal 402 — the route handler's full 402 body (with\n // payment requirements) is not built here to keep this lightweight.\n return new Response(\n JSON.stringify({ error: \"payment_required\" }),\n {\n status: 402,\n headers: { \"content-type\": \"application/json\" },\n },\n );\n }\n\n // All other classifications → pass through.\n return undefined;\n };\n },\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verivyx/paywall-next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Next.js adapter for the Verivyx paywall SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Verivyx",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"keywords": ["paywall", "x402", "ai", "bot", "stellar", "verivyx"],
|
|
10
|
+
"homepage": "https://docs.verivyx.com/docs/sdk",
|
|
11
|
+
"bugs": { "url": "https://github.com/VerivyX/verivyx-monorepo/issues" },
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/VerivyX/verivyx-monorepo.git",
|
|
15
|
+
"directory": "services/publisher-sdk/packages/next"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
21
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
28
|
+
"engines": { "node": ">=18" },
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"prepublishOnly": "npm run build",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"typecheck:test": "tsc -p tsconfig.test.json"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@verivyx/paywall": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"next": ">=15 <17",
|
|
41
|
+
"react": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"next": "^15.0.0",
|
|
45
|
+
"react": "^18.0.0",
|
|
46
|
+
"react-dom": "^18.0.0",
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"@types/react": "^18.0.0",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.5.0",
|
|
51
|
+
"vitest": "^2.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|