@verivyx/paywall-next 0.4.0 → 0.5.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 CHANGED
@@ -41,6 +41,11 @@ function resolveIp(req, trustProxy) {
41
41
  const xri = req.headers.get("x-real-ip");
42
42
  return xri !== null && xri !== "" ? xri : void 0;
43
43
  }
44
+ function isBrowserNavigation(req) {
45
+ const secFetchMode = req.headers.get("sec-fetch-mode") ?? "";
46
+ const accept = req.headers.get("accept") ?? "";
47
+ return secFetchMode === "navigate" || accept.includes("text/html");
48
+ }
44
49
  function publicUrl(req, trustProxy) {
45
50
  if (!trustProxy) return req.url;
46
51
  const u = new URL(req.url);
@@ -110,8 +115,8 @@ function verivyxNext(opts) {
110
115
  const resolvedSlug = o?.slug?.(req) ?? (ctx.params !== void 0 ? (await ctx.params).slug : void 0) ?? lastPathSegment(req.url);
111
116
  const decision = await vx.protect(coreReq, { slug: resolvedSlug });
112
117
  if (!decision.allowed) {
113
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
114
- if (isPreviewCandidate && o?.seoPreview !== void 0) {
118
+ const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(req);
119
+ if (previewable && o?.seoPreview !== void 0) {
115
120
  return withAdvertiseHeaders(
116
121
  paywall.buildSeoPreviewResponse(resolvedSlug, publicUrl(req, trustProxy), o.seoPreview),
117
122
  opts?.advertise
@@ -148,8 +153,8 @@ function verivyxNext(opts) {
148
153
  }
149
154
  return void 0;
150
155
  }
151
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
152
- if (isPreviewCandidate && opts?.seoPreview !== void 0) {
156
+ const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(req);
157
+ if (previewable && opts?.seoPreview !== void 0) {
153
158
  return withAdvertiseHeaders(
154
159
  paywall.buildSeoPreviewResponse(slug, publicUrl(req, trustProxy), opts.seoPreview),
155
160
  opts.advertise
@@ -1 +1 @@
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 * When set, unverified humans and search crawlers (reason: \"human-unverified\"\n * or \"crawler\") receive a 200 HTML teaser page instead of a bare 402.\n * Bots / agents (reason: \"bot-unpaid\") still get the 402 x402 response.\n *\n * Used by both `protect()` (when set on the factory opts) and `proxy()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\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 * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw `req.url` is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\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(publicUrl(req, trustProxy), { 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, publicUrl(req, trustProxy), 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 // Denied — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402.\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(req, trustProxy), opts.seoPreview),\n opts.advertise,\n );\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"]}
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 * When set, search crawlers (reason: \"crawler\") always receive a 200 HTML\n * teaser page. Unverified humans (reason: \"human-unverified\") also receive\n * the teaser — but ONLY when the request is a real browser top-level\n * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).\n * Machine clients and x402 payment agents that lack those browser headers\n * receive the 402 x402 response so they can pay.\n *\n * Used by both `protect()` (when set on the factory opts) and `proxy()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\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 * Return true when the request looks like a real top-level browser navigation.\n *\n * Real browsers send `Sec-Fetch-Mode: navigate` on top-level page loads AND/OR\n * an `Accept` header that includes `text/html`. Machine clients (undici, fetch,\n * x402 payment agents) send neither — they must receive the 402 so they can pay.\n *\n * Crawlers (search bots) are handled separately: they always get the SEO teaser\n * regardless of this check, so this function is only consulted for\n * `reason === \"human-unverified\"`.\n */\nfunction isBrowserNavigation(req: Request): boolean {\n const secFetchMode = req.headers.get(\"sec-fetch-mode\") ?? \"\";\n const accept = req.headers.get(\"accept\") ?? \"\";\n return secFetchMode === \"navigate\" || accept.includes(\"text/html\");\n}\n\n/**\n * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw `req.url` is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\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(publicUrl(req, trustProxy), { 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 // Crawlers always get the teaser (verified search bots need the\n // SEO preview + JSON-LD). human-unverified gets the teaser ONLY\n // when this is a real browser navigation (Sec-Fetch-Mode:navigate\n // or Accept includes text/html). Machine clients / x402 agents\n // (no browser headers) must receive the 402 so they can pay.\n if (!decision.allowed) {\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(req));\n if (previewable && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(resolvedSlug, publicUrl(req, trustProxy), 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 // Denied — crawlers always get the SEO teaser; human-unverified only\n // gets the teaser when this is a real browser navigation (Sec-Fetch-Mode\n // or Accept:text/html). Machine clients / x402 agents must get the 402.\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(req));\n if (previewable && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(req, trustProxy), opts.seoPreview),\n opts.advertise,\n );\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
@@ -66,9 +66,12 @@ interface NextAdapterOptions extends VerivyxOptions {
66
66
  */
67
67
  advertise?: DiscoveryOptions;
68
68
  /**
69
- * When set, unverified humans and search crawlers (reason: "human-unverified"
70
- * or "crawler") receive a 200 HTML teaser page instead of a bare 402.
71
- * Bots / agents (reason: "bot-unpaid") still get the 402 x402 response.
69
+ * When set, search crawlers (reason: "crawler") always receive a 200 HTML
70
+ * teaser page. Unverified humans (reason: "human-unverified") also receive
71
+ * the teaser but ONLY when the request is a real browser top-level
72
+ * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).
73
+ * Machine clients and x402 payment agents that lack those browser headers
74
+ * receive the 402 x402 response so they can pay.
72
75
  *
73
76
  * Used by both `protect()` (when set on the factory opts) and `proxy()`.
74
77
  * `protect()` also accepts `seoPreview` in its per-call options `o`; if both
package/dist/index.d.ts CHANGED
@@ -66,9 +66,12 @@ interface NextAdapterOptions extends VerivyxOptions {
66
66
  */
67
67
  advertise?: DiscoveryOptions;
68
68
  /**
69
- * When set, unverified humans and search crawlers (reason: "human-unverified"
70
- * or "crawler") receive a 200 HTML teaser page instead of a bare 402.
71
- * Bots / agents (reason: "bot-unpaid") still get the 402 x402 response.
69
+ * When set, search crawlers (reason: "crawler") always receive a 200 HTML
70
+ * teaser page. Unverified humans (reason: "human-unverified") also receive
71
+ * the teaser but ONLY when the request is a real browser top-level
72
+ * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).
73
+ * Machine clients and x402 payment agents that lack those browser headers
74
+ * receive the 402 x402 response so they can pay.
72
75
  *
73
76
  * Used by both `protect()` (when set on the factory opts) and `proxy()`.
74
77
  * `protect()` also accepts `seoPreview` in its per-call options `o`; if both
package/dist/index.js CHANGED
@@ -39,6 +39,11 @@ function resolveIp(req, trustProxy) {
39
39
  const xri = req.headers.get("x-real-ip");
40
40
  return xri !== null && xri !== "" ? xri : void 0;
41
41
  }
42
+ function isBrowserNavigation(req) {
43
+ const secFetchMode = req.headers.get("sec-fetch-mode") ?? "";
44
+ const accept = req.headers.get("accept") ?? "";
45
+ return secFetchMode === "navigate" || accept.includes("text/html");
46
+ }
42
47
  function publicUrl(req, trustProxy) {
43
48
  if (!trustProxy) return req.url;
44
49
  const u = new URL(req.url);
@@ -108,8 +113,8 @@ function verivyxNext(opts) {
108
113
  const resolvedSlug = o?.slug?.(req) ?? (ctx.params !== void 0 ? (await ctx.params).slug : void 0) ?? lastPathSegment(req.url);
109
114
  const decision = await vx.protect(coreReq, { slug: resolvedSlug });
110
115
  if (!decision.allowed) {
111
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
112
- if (isPreviewCandidate && o?.seoPreview !== void 0) {
116
+ const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(req);
117
+ if (previewable && o?.seoPreview !== void 0) {
113
118
  return withAdvertiseHeaders(
114
119
  buildSeoPreviewResponse(resolvedSlug, publicUrl(req, trustProxy), o.seoPreview),
115
120
  opts?.advertise
@@ -146,8 +151,8 @@ function verivyxNext(opts) {
146
151
  }
147
152
  return void 0;
148
153
  }
149
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
150
- if (isPreviewCandidate && opts?.seoPreview !== void 0) {
154
+ const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(req);
155
+ if (previewable && opts?.seoPreview !== void 0) {
151
156
  return withAdvertiseHeaders(
152
157
  buildSeoPreviewResponse(slug, publicUrl(req, trustProxy), opts.seoPreview),
153
158
  opts.advertise
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 * When set, unverified humans and search crawlers (reason: \"human-unverified\"\n * or \"crawler\") receive a 200 HTML teaser page instead of a bare 402.\n * Bots / agents (reason: \"bot-unpaid\") still get the 402 x402 response.\n *\n * Used by both `protect()` (when set on the factory opts) and `proxy()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\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 * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw `req.url` is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\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(publicUrl(req, trustProxy), { 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, publicUrl(req, trustProxy), 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 // Denied — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402.\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(req, trustProxy), opts.seoPreview),\n opts.advertise,\n );\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"]}
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 * When set, search crawlers (reason: \"crawler\") always receive a 200 HTML\n * teaser page. Unverified humans (reason: \"human-unverified\") also receive\n * the teaser — but ONLY when the request is a real browser top-level\n * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).\n * Machine clients and x402 payment agents that lack those browser headers\n * receive the 402 x402 response so they can pay.\n *\n * Used by both `protect()` (when set on the factory opts) and `proxy()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\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 * Return true when the request looks like a real top-level browser navigation.\n *\n * Real browsers send `Sec-Fetch-Mode: navigate` on top-level page loads AND/OR\n * an `Accept` header that includes `text/html`. Machine clients (undici, fetch,\n * x402 payment agents) send neither — they must receive the 402 so they can pay.\n *\n * Crawlers (search bots) are handled separately: they always get the SEO teaser\n * regardless of this check, so this function is only consulted for\n * `reason === \"human-unverified\"`.\n */\nfunction isBrowserNavigation(req: Request): boolean {\n const secFetchMode = req.headers.get(\"sec-fetch-mode\") ?? \"\";\n const accept = req.headers.get(\"accept\") ?? \"\";\n return secFetchMode === \"navigate\" || accept.includes(\"text/html\");\n}\n\n/**\n * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw `req.url` is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\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(publicUrl(req, trustProxy), { 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 // Crawlers always get the teaser (verified search bots need the\n // SEO preview + JSON-LD). human-unverified gets the teaser ONLY\n // when this is a real browser navigation (Sec-Fetch-Mode:navigate\n // or Accept includes text/html). Machine clients / x402 agents\n // (no browser headers) must receive the 402 so they can pay.\n if (!decision.allowed) {\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(req));\n if (previewable && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(resolvedSlug, publicUrl(req, trustProxy), 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 // Denied — crawlers always get the SEO teaser; human-unverified only\n // gets the teaser when this is a real browser navigation (Sec-Fetch-Mode\n // or Accept:text/html). Machine clients / x402 agents must get the 402.\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(req));\n if (previewable && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(req, trustProxy), opts.seoPreview),\n opts.advertise,\n );\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verivyx/paywall-next",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Next.js adapter for the Verivyx paywall SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",