@verivyx/paywall-next 0.1.0 → 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/dist/index.cjs +40 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +41 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var paywall = require('@verivyx/paywall');
|
|
4
|
+
var server = require('next/server');
|
|
4
5
|
|
|
5
6
|
// src/index.ts
|
|
6
7
|
function firstHop(xff) {
|
|
@@ -40,6 +41,14 @@ function resolveIp(req, trustProxy) {
|
|
|
40
41
|
const xri = req.headers.get("x-real-ip");
|
|
41
42
|
return xri !== null && xri !== "" ? xri : void 0;
|
|
42
43
|
}
|
|
44
|
+
function globToRegExp(glob) {
|
|
45
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
46
|
+
const pattern = escaped.replace(/\*\*/g, ".+").replace(/\*/g, "[^/]+");
|
|
47
|
+
return new RegExp(`^${pattern}$`);
|
|
48
|
+
}
|
|
49
|
+
function pathMatchesAny(pathname, globs) {
|
|
50
|
+
return globs.some((g) => globToRegExp(g).test(pathname));
|
|
51
|
+
}
|
|
43
52
|
function withAdvertiseHeaders(res, advertise) {
|
|
44
53
|
if (advertise === void 0) {
|
|
45
54
|
return res;
|
|
@@ -58,29 +67,21 @@ function verivyxNext(opts) {
|
|
|
58
67
|
const proc = globalThis.process;
|
|
59
68
|
const env = proc?.env ?? {};
|
|
60
69
|
const cfg = paywall.resolveConfig(opts, env);
|
|
61
|
-
|
|
70
|
+
function buildCoreRequest(req) {
|
|
71
|
+
const ip = resolveIp(req, trustProxy);
|
|
72
|
+
const headers = new Headers(req.headers);
|
|
73
|
+
if (ip !== void 0) {
|
|
74
|
+
headers.set("x-real-ip", ip);
|
|
75
|
+
} else {
|
|
76
|
+
headers.delete("x-real-ip");
|
|
77
|
+
headers.delete("x-forwarded-for");
|
|
78
|
+
}
|
|
79
|
+
return new Request(req.url, { method: req.method, headers });
|
|
80
|
+
}
|
|
62
81
|
return {
|
|
63
82
|
protect(handler, o) {
|
|
64
83
|
return async function verivyxNextGuard(req, ctx) {
|
|
65
|
-
const
|
|
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 coreReq = buildCoreRequest(req);
|
|
84
85
|
const resolvedSlug = o?.slug?.(req) ?? (ctx.params !== void 0 ? (await ctx.params).slug : void 0) ?? lastPathSegment(req.url);
|
|
85
86
|
const decision = await vx.protect(coreReq, { slug: resolvedSlug });
|
|
86
87
|
if (!decision.allowed) {
|
|
@@ -101,35 +102,37 @@ function verivyxNext(opts) {
|
|
|
101
102
|
};
|
|
102
103
|
},
|
|
103
104
|
proxy() {
|
|
104
|
-
return async function
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
105
|
+
return async function verivyxProxyHandler(req) {
|
|
106
|
+
const url = new URL(req.url);
|
|
107
|
+
if (cfg.match && cfg.match.length > 0 && !pathMatchesAny(url.pathname, cfg.match)) {
|
|
107
108
|
return void 0;
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
const coreReq = buildCoreRequest(req);
|
|
111
|
+
const slug = url.pathname.split("/").filter(Boolean).pop() ?? "";
|
|
112
|
+
let decision;
|
|
110
113
|
try {
|
|
111
|
-
|
|
112
|
-
verifyWebBotAuth: proxyVerifyWebBotAuth
|
|
113
|
-
});
|
|
114
|
-
classification = result.classification;
|
|
114
|
+
decision = await vx.protect(coreReq, { slug });
|
|
115
115
|
} catch {
|
|
116
116
|
return void 0;
|
|
117
117
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
118
|
+
if (decision.allowed) {
|
|
119
|
+
if (decision.paymentResponse) {
|
|
120
|
+
return server.NextResponse.next({
|
|
121
|
+
headers: { "PAYMENT-RESPONSE": decision.paymentResponse }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
126
125
|
}
|
|
127
|
-
return
|
|
126
|
+
return withAdvertiseHeaders(decision.response(), opts?.advertise);
|
|
128
127
|
};
|
|
129
128
|
}
|
|
130
129
|
};
|
|
131
130
|
}
|
|
131
|
+
function verivyxProxy(opts) {
|
|
132
|
+
return verivyxNext(opts).proxy();
|
|
133
|
+
}
|
|
132
134
|
|
|
133
135
|
exports.verivyxNext = verivyxNext;
|
|
136
|
+
exports.verivyxProxy = verivyxProxy;
|
|
134
137
|
//# sourceMappingURL=index.cjs.map
|
|
135
138
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","resolveConfig","buildSeoPreviewResponse","attachPaymentResponse","NextResponse"],"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 buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, GateDecision, DiscoveryOptions } from \"@verivyx/paywall\";\nimport { NextResponse } from \"next/server\";\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 * Convert a glob pattern (`/articles/*`, `/articles/**`) to a RegExp.\n * Rules:\n * `**` matches any characters including `/`.\n * `*` matches any characters except `/`.\n * All other regex metacharacters are escaped.\n */\nfunction globToRegExp(glob: string): RegExp {\n const escaped = glob.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Order matters: replace ** before *\n const pattern = escaped\n .replace(/\\*\\*/g, \".+\")\n .replace(/\\*/g, \"[^/]+\");\n return new RegExp(`^${pattern}$`);\n}\n\n/**\n * Return true when `pathname` matches at least one of the glob patterns.\n */\nfunction pathMatchesAny(pathname: string, globs: string[]): boolean {\n return globs.some((g) => globToRegExp(g).test(pathname));\n}\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()` — authoritative settling gate for `middleware.ts` / `proxy.ts`.\n * Runs the full pipeline (classify → authorize → verify+settle → failMode).\n * Use `verivyxProxy(opts)` as a one-line convenience instead of calling\n * `verivyxNext(opts).proxy()` directly.\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 path-match filter.\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 /**\n * Build a header-sanitised core Request from an incoming Next.js Request.\n *\n * Security invariant:\n * trustProxy !== false → resolve IP from proxy headers and set x-real-ip\n * on the cloned request (overrides any client value).\n * trustProxy === false → strip x-real-ip and x-forwarded-for so the\n * client cannot spoof an IP into the core classifier.\n *\n * The body is intentionally omitted — core classify/protect read headers\n * only and we must not consume the body here.\n */\n function buildCoreRequest(req: Request): Request {\n const ip = resolveIp(req, trustProxy);\n const headers = new Headers(req.headers);\n if (ip !== undefined) {\n headers.set(\"x-real-ip\", ip);\n } else {\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n }\n return new Request(req.url, { method: req.method, headers });\n }\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 // (Logic extracted into buildCoreRequest above.)\n const coreReq = buildCoreRequest(req);\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 * Authoritative settling gate for `middleware.ts` / `proxy.ts`.\n *\n * Runs the full core pipeline (classify → authorize → verify+settle →\n * failMode). This is the single source of truth for the whole Next app;\n * there is no need for a second gate on each individual route handler.\n *\n * Behaviour:\n * - Paths not in `cfg.match` (when match is set) → undefined (pass).\n * - `decision.allowed` + `paymentResponse` → NextResponse.next()\n * with PAYMENT-RESPONSE.\n * - `decision.allowed` (no receipt) → undefined (pass).\n * - `!decision.allowed` → `decision.response()`\n * (402 or preview),\n * wrapped in advertise\n * headers when set.\n * - Core throws → undefined (don't\n * hard-break the site).\n *\n * Use `verivyxProxy(opts)` as a shorthand for `verivyxNext(opts).proxy()`.\n */\n return async function verivyxProxyHandler(\n req: Request,\n ): Promise<Response | undefined> {\n const url = new URL(req.url);\n // Match filter: when cfg.match is non-empty, skip paths that don't match.\n if (cfg.match && cfg.match.length > 0 && !pathMatchesAny(url.pathname, cfg.match)) {\n return undefined;\n }\n const coreReq = buildCoreRequest(req);\n const slug = url.pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n let decision: GateDecision;\n try {\n decision = await vx.protect(coreReq, { slug });\n } catch {\n // Non-failMode error → don't hard-break the site.\n return undefined;\n }\n if (decision.allowed) {\n if (decision.paymentResponse) {\n // Pass through to the page while surfacing the settlement receipt.\n // NextResponse.next() is required here — a plain Response would\n // short-circuit the request and return an empty body to the agent.\n return NextResponse.next({\n headers: { \"PAYMENT-RESPONSE\": decision.paymentResponse },\n });\n }\n return undefined;\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n };\n },\n };\n}\n\n/**\n * Convenience export: create a single proxy middleware function for a Next.js\n * `middleware.ts` that acts as the authoritative Verivyx settling gate.\n *\n * Equivalent to `verivyxNext(opts).proxy()`.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { verivyxProxy } from \"@verivyx/paywall-next\";\n * export const middleware = verivyxProxy({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * export const config = { matcher: [\"/articles/:path*\"] };\n * ```\n */\nexport function verivyxProxy(opts?: NextAdapterOptions): (req: Request) => Promise<Response | undefined> {\n return verivyxNext(opts).proxy();\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -78,7 +78,10 @@ type RouteHandler = (req: Request, ctx: {
|
|
|
78
78
|
*
|
|
79
79
|
* Returns an object with:
|
|
80
80
|
* - `protect(handler, o)` — wrap a route handler behind the Verivyx gate.
|
|
81
|
-
* - `proxy()` —
|
|
81
|
+
* - `proxy()` — authoritative settling gate for `middleware.ts` / `proxy.ts`.
|
|
82
|
+
* Runs the full pipeline (classify → authorize → verify+settle → failMode).
|
|
83
|
+
* Use `verivyxProxy(opts)` as a one-line convenience instead of calling
|
|
84
|
+
* `verivyxNext(opts).proxy()` directly.
|
|
82
85
|
*
|
|
83
86
|
* ```ts
|
|
84
87
|
* const vx = verivyxNext({ domain: "example.com", token: "..." });
|
|
@@ -97,5 +100,20 @@ declare function verivyxNext(opts?: NextAdapterOptions): {
|
|
|
97
100
|
}): RouteHandler;
|
|
98
101
|
proxy(): (req: Request) => Promise<Response | undefined>;
|
|
99
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* Convenience export: create a single proxy middleware function for a Next.js
|
|
105
|
+
* `middleware.ts` that acts as the authoritative Verivyx settling gate.
|
|
106
|
+
*
|
|
107
|
+
* Equivalent to `verivyxNext(opts).proxy()`.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* // middleware.ts
|
|
112
|
+
* import { verivyxProxy } from "@verivyx/paywall-next";
|
|
113
|
+
* export const middleware = verivyxProxy({ domain: "example.com", token: process.env.VX_TOKEN });
|
|
114
|
+
* export const config = { matcher: ["/articles/:path*"] };
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
declare function verivyxProxy(opts?: NextAdapterOptions): (req: Request) => Promise<Response | undefined>;
|
|
100
118
|
|
|
101
|
-
export { type NextAdapterOptions, verivyxNext };
|
|
119
|
+
export { type NextAdapterOptions, verivyxNext, verivyxProxy };
|
package/dist/index.d.ts
CHANGED
|
@@ -78,7 +78,10 @@ type RouteHandler = (req: Request, ctx: {
|
|
|
78
78
|
*
|
|
79
79
|
* Returns an object with:
|
|
80
80
|
* - `protect(handler, o)` — wrap a route handler behind the Verivyx gate.
|
|
81
|
-
* - `proxy()` —
|
|
81
|
+
* - `proxy()` — authoritative settling gate for `middleware.ts` / `proxy.ts`.
|
|
82
|
+
* Runs the full pipeline (classify → authorize → verify+settle → failMode).
|
|
83
|
+
* Use `verivyxProxy(opts)` as a one-line convenience instead of calling
|
|
84
|
+
* `verivyxNext(opts).proxy()` directly.
|
|
82
85
|
*
|
|
83
86
|
* ```ts
|
|
84
87
|
* const vx = verivyxNext({ domain: "example.com", token: "..." });
|
|
@@ -97,5 +100,20 @@ declare function verivyxNext(opts?: NextAdapterOptions): {
|
|
|
97
100
|
}): RouteHandler;
|
|
98
101
|
proxy(): (req: Request) => Promise<Response | undefined>;
|
|
99
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* Convenience export: create a single proxy middleware function for a Next.js
|
|
105
|
+
* `middleware.ts` that acts as the authoritative Verivyx settling gate.
|
|
106
|
+
*
|
|
107
|
+
* Equivalent to `verivyxNext(opts).proxy()`.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* // middleware.ts
|
|
112
|
+
* import { verivyxProxy } from "@verivyx/paywall-next";
|
|
113
|
+
* export const middleware = verivyxProxy({ domain: "example.com", token: process.env.VX_TOKEN });
|
|
114
|
+
* export const config = { matcher: ["/articles/:path*"] };
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
declare function verivyxProxy(opts?: NextAdapterOptions): (req: Request) => Promise<Response | undefined>;
|
|
100
118
|
|
|
101
|
-
export { type NextAdapterOptions, verivyxNext };
|
|
119
|
+
export { type NextAdapterOptions, verivyxNext, verivyxProxy };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { verivyx, createSearchCrawlerVerifier, resolveConfig,
|
|
1
|
+
import { verivyx, createSearchCrawlerVerifier, resolveConfig, buildSeoPreviewResponse, attachPaymentResponse, rslLinkHeader, contentUsageHeader } from '@verivyx/paywall';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
2
3
|
|
|
3
4
|
// src/index.ts
|
|
4
5
|
function firstHop(xff) {
|
|
@@ -38,6 +39,14 @@ function resolveIp(req, trustProxy) {
|
|
|
38
39
|
const xri = req.headers.get("x-real-ip");
|
|
39
40
|
return xri !== null && xri !== "" ? xri : void 0;
|
|
40
41
|
}
|
|
42
|
+
function globToRegExp(glob) {
|
|
43
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
const pattern = escaped.replace(/\*\*/g, ".+").replace(/\*/g, "[^/]+");
|
|
45
|
+
return new RegExp(`^${pattern}$`);
|
|
46
|
+
}
|
|
47
|
+
function pathMatchesAny(pathname, globs) {
|
|
48
|
+
return globs.some((g) => globToRegExp(g).test(pathname));
|
|
49
|
+
}
|
|
41
50
|
function withAdvertiseHeaders(res, advertise) {
|
|
42
51
|
if (advertise === void 0) {
|
|
43
52
|
return res;
|
|
@@ -56,29 +65,21 @@ function verivyxNext(opts) {
|
|
|
56
65
|
const proc = globalThis.process;
|
|
57
66
|
const env = proc?.env ?? {};
|
|
58
67
|
const cfg = resolveConfig(opts, env);
|
|
59
|
-
|
|
68
|
+
function buildCoreRequest(req) {
|
|
69
|
+
const ip = resolveIp(req, trustProxy);
|
|
70
|
+
const headers = new Headers(req.headers);
|
|
71
|
+
if (ip !== void 0) {
|
|
72
|
+
headers.set("x-real-ip", ip);
|
|
73
|
+
} else {
|
|
74
|
+
headers.delete("x-real-ip");
|
|
75
|
+
headers.delete("x-forwarded-for");
|
|
76
|
+
}
|
|
77
|
+
return new Request(req.url, { method: req.method, headers });
|
|
78
|
+
}
|
|
60
79
|
return {
|
|
61
80
|
protect(handler, o) {
|
|
62
81
|
return async function verivyxNextGuard(req, ctx) {
|
|
63
|
-
const
|
|
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 coreReq = buildCoreRequest(req);
|
|
82
83
|
const resolvedSlug = o?.slug?.(req) ?? (ctx.params !== void 0 ? (await ctx.params).slug : void 0) ?? lastPathSegment(req.url);
|
|
83
84
|
const decision = await vx.protect(coreReq, { slug: resolvedSlug });
|
|
84
85
|
if (!decision.allowed) {
|
|
@@ -99,35 +100,36 @@ function verivyxNext(opts) {
|
|
|
99
100
|
};
|
|
100
101
|
},
|
|
101
102
|
proxy() {
|
|
102
|
-
return async function
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
103
|
+
return async function verivyxProxyHandler(req) {
|
|
104
|
+
const url = new URL(req.url);
|
|
105
|
+
if (cfg.match && cfg.match.length > 0 && !pathMatchesAny(url.pathname, cfg.match)) {
|
|
105
106
|
return void 0;
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
const coreReq = buildCoreRequest(req);
|
|
109
|
+
const slug = url.pathname.split("/").filter(Boolean).pop() ?? "";
|
|
110
|
+
let decision;
|
|
108
111
|
try {
|
|
109
|
-
|
|
110
|
-
verifyWebBotAuth: proxyVerifyWebBotAuth
|
|
111
|
-
});
|
|
112
|
-
classification = result.classification;
|
|
112
|
+
decision = await vx.protect(coreReq, { slug });
|
|
113
113
|
} catch {
|
|
114
114
|
return void 0;
|
|
115
115
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
);
|
|
116
|
+
if (decision.allowed) {
|
|
117
|
+
if (decision.paymentResponse) {
|
|
118
|
+
return NextResponse.next({
|
|
119
|
+
headers: { "PAYMENT-RESPONSE": decision.paymentResponse }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
124
123
|
}
|
|
125
|
-
return
|
|
124
|
+
return withAdvertiseHeaders(decision.response(), opts?.advertise);
|
|
126
125
|
};
|
|
127
126
|
}
|
|
128
127
|
};
|
|
129
128
|
}
|
|
129
|
+
function verivyxProxy(opts) {
|
|
130
|
+
return verivyxNext(opts).proxy();
|
|
131
|
+
}
|
|
130
132
|
|
|
131
|
-
export { verivyxNext };
|
|
133
|
+
export { verivyxNext, verivyxProxy };
|
|
132
134
|
//# sourceMappingURL=index.js.map
|
|
133
135
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"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 buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, GateDecision, DiscoveryOptions } from \"@verivyx/paywall\";\nimport { NextResponse } from \"next/server\";\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 * Convert a glob pattern (`/articles/*`, `/articles/**`) to a RegExp.\n * Rules:\n * `**` matches any characters including `/`.\n * `*` matches any characters except `/`.\n * All other regex metacharacters are escaped.\n */\nfunction globToRegExp(glob: string): RegExp {\n const escaped = glob.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Order matters: replace ** before *\n const pattern = escaped\n .replace(/\\*\\*/g, \".+\")\n .replace(/\\*/g, \"[^/]+\");\n return new RegExp(`^${pattern}$`);\n}\n\n/**\n * Return true when `pathname` matches at least one of the glob patterns.\n */\nfunction pathMatchesAny(pathname: string, globs: string[]): boolean {\n return globs.some((g) => globToRegExp(g).test(pathname));\n}\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()` — authoritative settling gate for `middleware.ts` / `proxy.ts`.\n * Runs the full pipeline (classify → authorize → verify+settle → failMode).\n * Use `verivyxProxy(opts)` as a one-line convenience instead of calling\n * `verivyxNext(opts).proxy()` directly.\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 path-match filter.\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 /**\n * Build a header-sanitised core Request from an incoming Next.js Request.\n *\n * Security invariant:\n * trustProxy !== false → resolve IP from proxy headers and set x-real-ip\n * on the cloned request (overrides any client value).\n * trustProxy === false → strip x-real-ip and x-forwarded-for so the\n * client cannot spoof an IP into the core classifier.\n *\n * The body is intentionally omitted — core classify/protect read headers\n * only and we must not consume the body here.\n */\n function buildCoreRequest(req: Request): Request {\n const ip = resolveIp(req, trustProxy);\n const headers = new Headers(req.headers);\n if (ip !== undefined) {\n headers.set(\"x-real-ip\", ip);\n } else {\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n }\n return new Request(req.url, { method: req.method, headers });\n }\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 // (Logic extracted into buildCoreRequest above.)\n const coreReq = buildCoreRequest(req);\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 * Authoritative settling gate for `middleware.ts` / `proxy.ts`.\n *\n * Runs the full core pipeline (classify → authorize → verify+settle →\n * failMode). This is the single source of truth for the whole Next app;\n * there is no need for a second gate on each individual route handler.\n *\n * Behaviour:\n * - Paths not in `cfg.match` (when match is set) → undefined (pass).\n * - `decision.allowed` + `paymentResponse` → NextResponse.next()\n * with PAYMENT-RESPONSE.\n * - `decision.allowed` (no receipt) → undefined (pass).\n * - `!decision.allowed` → `decision.response()`\n * (402 or preview),\n * wrapped in advertise\n * headers when set.\n * - Core throws → undefined (don't\n * hard-break the site).\n *\n * Use `verivyxProxy(opts)` as a shorthand for `verivyxNext(opts).proxy()`.\n */\n return async function verivyxProxyHandler(\n req: Request,\n ): Promise<Response | undefined> {\n const url = new URL(req.url);\n // Match filter: when cfg.match is non-empty, skip paths that don't match.\n if (cfg.match && cfg.match.length > 0 && !pathMatchesAny(url.pathname, cfg.match)) {\n return undefined;\n }\n const coreReq = buildCoreRequest(req);\n const slug = url.pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n let decision: GateDecision;\n try {\n decision = await vx.protect(coreReq, { slug });\n } catch {\n // Non-failMode error → don't hard-break the site.\n return undefined;\n }\n if (decision.allowed) {\n if (decision.paymentResponse) {\n // Pass through to the page while surfacing the settlement receipt.\n // NextResponse.next() is required here — a plain Response would\n // short-circuit the request and return an empty body to the agent.\n return NextResponse.next({\n headers: { \"PAYMENT-RESPONSE\": decision.paymentResponse },\n });\n }\n return undefined;\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n };\n },\n };\n}\n\n/**\n * Convenience export: create a single proxy middleware function for a Next.js\n * `middleware.ts` that acts as the authoritative Verivyx settling gate.\n *\n * Equivalent to `verivyxNext(opts).proxy()`.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { verivyxProxy } from \"@verivyx/paywall-next\";\n * export const middleware = verivyxProxy({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * export const config = { matcher: [\"/articles/:path*\"] };\n * ```\n */\nexport function verivyxProxy(opts?: NextAdapterOptions): (req: Request) => Promise<Response | undefined> {\n return verivyxNext(opts).proxy();\n}\n"]}
|