@verivyx/paywall-express 0.3.0 → 0.6.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
@@ -100,6 +100,11 @@ async function buildWebRequest(req, opts) {
100
100
  ...rawBody !== void 0 ? { duplex: "half" } : {}
101
101
  });
102
102
  }
103
+ function isBrowserNavigation(req) {
104
+ const secFetchMode = req.headers.get("sec-fetch-mode") ?? "";
105
+ const accept = req.headers.get("accept") ?? "";
106
+ return secFetchMode === "navigate" || accept.includes("text/html");
107
+ }
103
108
  function attachAdvertiseHeaders(res, advertise) {
104
109
  if (advertise === void 0) {
105
110
  return;
@@ -124,6 +129,9 @@ function verivyxExpress(opts) {
124
129
  verifyCrawlerDns: opts?.verifyCrawlerDns ?? paywall.createSearchCrawlerVerifier(),
125
130
  ...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
126
131
  });
132
+ const proc = globalThis.process;
133
+ const env = proc?.env ?? {};
134
+ const cfg = paywall.resolveConfig(opts, env);
127
135
  return {
128
136
  protect(handler, o) {
129
137
  return async function verivyxGuard(req, res, next) {
@@ -132,8 +140,17 @@ function verivyxExpress(opts) {
132
140
  const slug = req.params.slug ?? lastPathSegment(req.path);
133
141
  const decision = await vx.protect(webReq, { slug });
134
142
  if (!decision.allowed) {
135
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
136
- if (isPreviewCandidate && o?.seoPreview !== void 0) {
143
+ const isHU = decision.reason === "human-unverified";
144
+ const previewable = decision.reason === "crawler" || isHU && isBrowserNavigation(webReq);
145
+ if (previewable && o?.seoPreview !== void 0) {
146
+ const seo = o.seoPreview({ slug });
147
+ if (isHU && opts?.humanUnlock !== void 0) {
148
+ const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;
149
+ const html = paywall.buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });
150
+ attachAdvertiseHeaders(res, opts?.advertise);
151
+ await sendWebResponse(res, new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }));
152
+ return;
153
+ }
137
154
  attachAdvertiseHeaders(res, opts?.advertise);
138
155
  await sendWebResponse(res, paywall.buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));
139
156
  return;
@@ -169,8 +186,17 @@ function verivyxExpress(opts) {
169
186
  }
170
187
  return next();
171
188
  }
172
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
173
- if (isPreviewCandidate && opts?.seoPreview !== void 0) {
189
+ const isHU = decision.reason === "human-unverified";
190
+ const previewable = decision.reason === "crawler" || isHU && isBrowserNavigation(webReq);
191
+ if (previewable && opts?.seoPreview !== void 0) {
192
+ const seo = opts.seoPreview({ slug });
193
+ if (isHU && opts?.humanUnlock !== void 0) {
194
+ const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;
195
+ const html = paywall.buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });
196
+ attachAdvertiseHeaders(res, opts?.advertise);
197
+ await sendWebResponse(res, new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }));
198
+ return;
199
+ }
174
200
  attachAdvertiseHeaders(res, opts?.advertise);
175
201
  await sendWebResponse(res, paywall.buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));
176
202
  return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","buildSeoPreviewResponse"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @verivyx/paywall-express\n *\n * Express adapter for the Verivyx paywall SDK.\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module only handles:\n * 1. IP resolution from Express request.\n * 2. Converting Express IncomingMessage → Web Request.\n * 3. Calling core `protect()` (decision overload).\n * 4. Converting the Web Response back to Express response, OR calling the\n * original Express handler when the request is allowed through.\n *\n * Mock-injection seam (for Tasks 18/19 to reuse):\n * Pass `_core: verivyx.mock({...})` in options to bypass network.\n * In production: omit `_core`; the adapter instantiates `verivyx(opts, deps)`.\n *\n * @example\n * ```ts\n * import { verivyxExpress } from \"@verivyx/paywall-express\";\n * const vx = verivyxExpress({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\n\nimport type { Request as ExpressRequest, RequestHandler, Response as ExpressResponse, NextFunction } from \"express\";\nimport {\n verivyx,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport type { Socket } from \"node:net\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Express adapter.\n * Extends core VerivyxOptions with Express-specific IP resolution controls.\n */\nexport interface ExpressAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from `X-Forwarded-For` /\n * `X-Real-IP` headers (set by a trusted reverse proxy). Set to false if\n * the Express app is not behind a proxy, to use the raw socket address.\n */\n trustProxy?: boolean;\n\n /**\n * Custom IP extractor. When provided, this overrides the built-in\n * `trustProxy` / `X-Forwarded-For` logic entirely.\n *\n * @param req - The Express request object.\n * @returns The resolved client IP, or undefined to fall back to socket.\n */\n clientIp?: (req: ExpressRequest) => string | undefined;\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 * Pattern for Next.js / Hono adapters to reuse:\n * `const vx = opts?._core ?? verivyx(opts, { verifyCrawlerDns });`\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 `middleware()`.\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// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | string[] | undefined): string | undefined {\n if (xff === undefined) {\n return undefined;\n }\n const raw = Array.isArray(xff) ? xff[0] : xff;\n if (!raw) {\n return undefined;\n }\n const first = raw.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Extract the last non-empty path segment from a URL path string.\n * Used as a fallback slug when `req.params.slug` is not available.\n */\nfunction lastPathSegment(path: string): string {\n const segments = path.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n return last !== undefined ? decodeURIComponent(last) : \"\";\n}\n\n/**\n * Build a `Headers` object from Node.js IncomingHttpHeaders, overriding\n * `x-real-ip` with the resolved (trusted) client IP so the core classifier\n * reads a reliable address.\n */\nfunction toWebHeaders(\n nodeHeaders: IncomingHttpHeaders,\n ip: string | undefined,\n): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value === undefined) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const v of value) {\n headers.append(key, v);\n }\n } else {\n headers.set(key, value);\n }\n }\n if (ip !== undefined) {\n headers.set(\"x-real-ip\", ip);\n }\n return headers;\n}\n\n/**\n * Resolve the client IP from an Express request.\n *\n * Precedence:\n * 1. `opts.clientIp(req)` — caller-supplied extractor (highest precedence).\n * 2. `X-Forwarded-For` first hop (when trustProxy !== false).\n * 3. `X-Real-IP` header (when trustProxy !== false).\n * 4. `req.socket.remoteAddress` — raw TCP peer (lowest precedence).\n */\nfunction resolveIp(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): string | undefined {\n if (opts?.clientIp) {\n return opts.clientIp(req);\n }\n if (opts?.trustProxy !== false) {\n const xff = req.headers[\"x-forwarded-for\"];\n const hop = firstHop(xff);\n if (hop) {\n return hop;\n }\n const xri = req.headers[\"x-real-ip\"];\n if (typeof xri === \"string\" && xri) {\n return xri;\n }\n }\n return (req.socket as Socket).remoteAddress;\n}\n\n/**\n * Collect the raw request body into a Uint8Array.\n * Returns undefined for requests with no body (GET / HEAD / OPTIONS).\n * Express may have already consumed the stream with a body parser — if\n * `req.body` is already populated as a Buffer we use that directly.\n */\nasync function readRawBody(\n req: ExpressRequest,\n): Promise<Uint8Array | undefined> {\n const method = req.method.toUpperCase();\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return undefined;\n }\n\n // Express body-parser may have already consumed and parsed the stream.\n // If `req.body` is a Buffer, use it directly.\n const body: unknown = (req as { body?: unknown }).body;\n if (Buffer.isBuffer(body)) {\n return new Uint8Array(body);\n }\n\n // Otherwise collect the raw stream.\n return new Promise<Uint8Array>((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(new Uint8Array(Buffer.concat(chunks))));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Check whether a pathname matches any of the given glob patterns.\n * Supports `*` (any segment chars except `/`) and `**` (any chars including `/`).\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Build a regex from the glob pattern:\n // 1. Escape all regex metacharacters except * and ?.\n // 2. Replace ** with a placeholder, then * with [^/]*, then restore **.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const regexStr =\n \"^\" +\n escaped\n .replace(/\\*\\*/g, \"\\x00\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\x00/g, \".*\") +\n \"$\";\n if (new RegExp(regexStr).test(pathname)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Build a Web API `Request` from an Express request.\n * Resolves the trusted client IP, builds absolute URL, and reads the raw body.\n */\nasync function buildWebRequest(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): Promise<Request> {\n const ip = resolveIp(req, opts);\n const host =\n (typeof req.get === \"function\" ? req.get(\"host\") : undefined) ??\n (typeof req.headers.host === \"string\" ? req.headers.host : undefined) ??\n \"localhost\";\n const protocol = req.protocol ?? \"http\";\n const absoluteUrl = `${protocol}://${host}${req.originalUrl}`;\n const webHeaders = toWebHeaders(req.headers, ip);\n\n let rawBody: Uint8Array | undefined;\n try {\n rawBody = await readRawBody(req);\n } catch {\n rawBody = undefined;\n }\n\n return new Request(absoluteUrl, {\n method: req.method,\n headers: webHeaders,\n body: rawBody !== undefined ? rawBody : undefined,\n ...(rawBody !== undefined ? { duplex: \"half\" } : {}),\n } as RequestInit);\n}\n\n/**\n * Attach RSL + AIPREF discovery headers to an Express response.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * No-op when `advertise` is undefined.\n */\nfunction attachAdvertiseHeaders(\n res: ExpressResponse,\n advertise: DiscoveryOptions | undefined,\n): void {\n if (advertise === undefined) {\n return;\n }\n res.append(\"Link\", rslLinkHeader(advertise));\n res.setHeader(\"Content-Usage\", contentUsageHeader(advertise));\n}\n\n/**\n * Convert a Web API `Response` to an Express response.\n * Copies status, all response headers, and the body.\n *\n * @internal Exported for unit-testing the Set-Cookie accumulation fix.\n * Production callers should use the `protect()` middleware instead.\n *\n * Set-Cookie is special: `Headers.forEach` emits each cookie as a separate\n * call with the same key, and `res.setHeader` REPLACES on repeated same-key\n * calls — so all but the last cookie would be silently dropped.\n * `res.append` accumulates values into an array, preserving every cookie.\n */\nexport async function sendWebResponse(\n res: ExpressResponse,\n webRes: Response,\n): Promise<void> {\n res.status(webRes.status);\n webRes.headers.forEach((value, key) => {\n if (key.toLowerCase() === \"set-cookie\") {\n res.append(key, value);\n } else {\n res.setHeader(key, value);\n }\n });\n const buf = Buffer.from(await webRes.arrayBuffer());\n res.send(buf);\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Express adapter.\n *\n * Returns an object with a single `protect(handler)` method that wraps an\n * Express `RequestHandler` behind the Verivyx paywall gate.\n *\n * ```ts\n * const vx = verivyxExpress({ domain: \"example.com\", token: \"...\" });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\nexport function verivyxExpress(opts?: ExpressAdapterOptions): {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler;\n middleware(): RequestHandler;\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 return {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler {\n // Return an async Express handler (req, res, next).\n return async function verivyxGuard(\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction,\n ): Promise<void> {\n try {\n // 1+2. Build a Web Request (resolves IP, builds absolute URL, reads body).\n const webReq = await buildWebRequest(req, opts);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const slug =\n (req.params as Record<string, string | undefined>).slug ??\n lastPathSegment(req.path);\n const decision = await vx.protect(webReq, { slug });\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 attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n return;\n }\n\n // 4b. Allowed — if a payment receipt was returned, attach it first.\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n\n // Delegate to the original Express handler.\n handler(req, res, next);\n } catch (err) {\n // Propagate to Express error-handling middleware.\n next(err);\n }\n };\n },\n\n middleware(): RequestHandler {\n return async (req, res, next) => {\n try {\n const rawPath = req.originalUrl ?? req.url ?? \"/\";\n const pathname = rawPath.split(\"?\")[0]!;\n\n // If match patterns are configured and this path doesn't match, pass through.\n if (\n opts?.match !== undefined &&\n opts.match.length > 0 &&\n !pathMatchesAny(pathname, opts.match)\n ) {\n return next();\n }\n\n const webReq = await buildWebRequest(req, opts);\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n const decision = await vx.protect(webReq, { slug });\n\n if (decision.allowed) {\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return next();\n }\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 attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n } catch (err) {\n next(err as Error);\n }\n };\n },\n };\n}\n\n/**\n * Convenience export: create a Verivyx Express app-level middleware in one call.\n *\n * ```ts\n * import { verivyxMiddleware } from \"@verivyx/paywall-express\";\n * app.use(verivyxMiddleware({ domain: \"example.com\", token: process.env.VX_TOKEN }));\n * ```\n */\nexport function verivyxMiddleware(opts?: ExpressAdapterOptions): RequestHandler {\n return verivyxExpress(opts).middleware();\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","resolveConfig","buildUnlockHtml","buildSeoPreviewResponse"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @verivyx/paywall-express\n *\n * Express adapter for the Verivyx paywall SDK.\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module only handles:\n * 1. IP resolution from Express request.\n * 2. Converting Express IncomingMessage → Web Request.\n * 3. Calling core `protect()` (decision overload).\n * 4. Converting the Web Response back to Express response, OR calling the\n * original Express handler when the request is allowed through.\n *\n * Mock-injection seam (for Tasks 18/19 to reuse):\n * Pass `_core: verivyx.mock({...})` in options to bypass network.\n * In production: omit `_core`; the adapter instantiates `verivyx(opts, deps)`.\n *\n * @example\n * ```ts\n * import { verivyxExpress } from \"@verivyx/paywall-express\";\n * const vx = verivyxExpress({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\n\nimport type { Request as ExpressRequest, RequestHandler, Response as ExpressResponse, NextFunction } from \"express\";\nimport {\n verivyx,\n resolveConfig,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n buildUnlockHtml,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport type { Socket } from \"node:net\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Express adapter.\n * Extends core VerivyxOptions with Express-specific IP resolution controls.\n */\nexport interface ExpressAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from `X-Forwarded-For` /\n * `X-Real-IP` headers (set by a trusted reverse proxy). Set to false if\n * the Express app is not behind a proxy, to use the raw socket address.\n */\n trustProxy?: boolean;\n\n /**\n * Custom IP extractor. When provided, this overrides the built-in\n * `trustProxy` / `X-Forwarded-For` logic entirely.\n *\n * @param req - The Express request object.\n * @returns The resolved client IP, or undefined to fall back to socket.\n */\n clientIp?: (req: ExpressRequest) => string | undefined;\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 * Pattern for Next.js / Hono adapters to reuse:\n * `const vx = opts?._core ?? verivyx(opts, { verifyCrawlerDns });`\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 `middleware()`.\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 * When set, human-unverified real-browser visitors receive an interactive\n * PoW unlock page (from core's `buildUnlockHtml`) instead of the static\n * teaser. Crawlers always get the static teaser. Machines still get 402.\n *\n * `authBase` overrides the API base used for the challenge/verify endpoints\n * (defaults to `cfg.apiBase` / VERIVYX_API_BASE).\n */\n humanUnlock?: { authBase?: string };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | string[] | undefined): string | undefined {\n if (xff === undefined) {\n return undefined;\n }\n const raw = Array.isArray(xff) ? xff[0] : xff;\n if (!raw) {\n return undefined;\n }\n const first = raw.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Extract the last non-empty path segment from a URL path string.\n * Used as a fallback slug when `req.params.slug` is not available.\n */\nfunction lastPathSegment(path: string): string {\n const segments = path.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n return last !== undefined ? decodeURIComponent(last) : \"\";\n}\n\n/**\n * Build a `Headers` object from Node.js IncomingHttpHeaders, overriding\n * `x-real-ip` with the resolved (trusted) client IP so the core classifier\n * reads a reliable address.\n */\nfunction toWebHeaders(\n nodeHeaders: IncomingHttpHeaders,\n ip: string | undefined,\n): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value === undefined) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const v of value) {\n headers.append(key, v);\n }\n } else {\n headers.set(key, value);\n }\n }\n if (ip !== undefined) {\n headers.set(\"x-real-ip\", ip);\n }\n return headers;\n}\n\n/**\n * Resolve the client IP from an Express request.\n *\n * Precedence:\n * 1. `opts.clientIp(req)` — caller-supplied extractor (highest precedence).\n * 2. `X-Forwarded-For` first hop (when trustProxy !== false).\n * 3. `X-Real-IP` header (when trustProxy !== false).\n * 4. `req.socket.remoteAddress` — raw TCP peer (lowest precedence).\n */\nfunction resolveIp(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): string | undefined {\n if (opts?.clientIp) {\n return opts.clientIp(req);\n }\n if (opts?.trustProxy !== false) {\n const xff = req.headers[\"x-forwarded-for\"];\n const hop = firstHop(xff);\n if (hop) {\n return hop;\n }\n const xri = req.headers[\"x-real-ip\"];\n if (typeof xri === \"string\" && xri) {\n return xri;\n }\n }\n return (req.socket as Socket).remoteAddress;\n}\n\n/**\n * Collect the raw request body into a Uint8Array.\n * Returns undefined for requests with no body (GET / HEAD / OPTIONS).\n * Express may have already consumed the stream with a body parser — if\n * `req.body` is already populated as a Buffer we use that directly.\n */\nasync function readRawBody(\n req: ExpressRequest,\n): Promise<Uint8Array | undefined> {\n const method = req.method.toUpperCase();\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return undefined;\n }\n\n // Express body-parser may have already consumed and parsed the stream.\n // If `req.body` is a Buffer, use it directly.\n const body: unknown = (req as { body?: unknown }).body;\n if (Buffer.isBuffer(body)) {\n return new Uint8Array(body);\n }\n\n // Otherwise collect the raw stream.\n return new Promise<Uint8Array>((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(new Uint8Array(Buffer.concat(chunks))));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Check whether a pathname matches any of the given glob patterns.\n * Supports `*` (any segment chars except `/`) and `**` (any chars including `/`).\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Build a regex from the glob pattern:\n // 1. Escape all regex metacharacters except * and ?.\n // 2. Replace ** with a placeholder, then * with [^/]*, then restore **.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const regexStr =\n \"^\" +\n escaped\n .replace(/\\*\\*/g, \"\\x00\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\x00/g, \".*\") +\n \"$\";\n if (new RegExp(regexStr).test(pathname)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Build a Web API `Request` from an Express request.\n * Resolves the trusted client IP, builds absolute URL, and reads the raw body.\n */\nasync function buildWebRequest(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): Promise<Request> {\n const ip = resolveIp(req, opts);\n const host =\n (typeof req.get === \"function\" ? req.get(\"host\") : undefined) ??\n (typeof req.headers.host === \"string\" ? req.headers.host : undefined) ??\n \"localhost\";\n const protocol = req.protocol ?? \"http\";\n const absoluteUrl = `${protocol}://${host}${req.originalUrl}`;\n const webHeaders = toWebHeaders(req.headers, ip);\n\n let rawBody: Uint8Array | undefined;\n try {\n rawBody = await readRawBody(req);\n } catch {\n rawBody = undefined;\n }\n\n return new Request(absoluteUrl, {\n method: req.method,\n headers: webHeaders,\n body: rawBody !== undefined ? rawBody : undefined,\n ...(rawBody !== undefined ? { duplex: \"half\" } : {}),\n } as RequestInit);\n}\n\n/**\n * Return true when the Web 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 * Attach RSL + AIPREF discovery headers to an Express response.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * No-op when `advertise` is undefined.\n */\nfunction attachAdvertiseHeaders(\n res: ExpressResponse,\n advertise: DiscoveryOptions | undefined,\n): void {\n if (advertise === undefined) {\n return;\n }\n res.append(\"Link\", rslLinkHeader(advertise));\n res.setHeader(\"Content-Usage\", contentUsageHeader(advertise));\n}\n\n/**\n * Convert a Web API `Response` to an Express response.\n * Copies status, all response headers, and the body.\n *\n * @internal Exported for unit-testing the Set-Cookie accumulation fix.\n * Production callers should use the `protect()` middleware instead.\n *\n * Set-Cookie is special: `Headers.forEach` emits each cookie as a separate\n * call with the same key, and `res.setHeader` REPLACES on repeated same-key\n * calls — so all but the last cookie would be silently dropped.\n * `res.append` accumulates values into an array, preserving every cookie.\n */\nexport async function sendWebResponse(\n res: ExpressResponse,\n webRes: Response,\n): Promise<void> {\n res.status(webRes.status);\n webRes.headers.forEach((value, key) => {\n if (key.toLowerCase() === \"set-cookie\") {\n res.append(key, value);\n } else {\n res.setHeader(key, value);\n }\n });\n const buf = Buffer.from(await webRes.arrayBuffer());\n res.send(buf);\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Express adapter.\n *\n * Returns an object with a single `protect(handler)` method that wraps an\n * Express `RequestHandler` behind the Verivyx paywall gate.\n *\n * ```ts\n * const vx = verivyxExpress({ domain: \"example.com\", token: \"...\" });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\nexport function verivyxExpress(opts?: ExpressAdapterOptions): {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler;\n middleware(): RequestHandler;\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 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 return {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler {\n // Return an async Express handler (req, res, next).\n return async function verivyxGuard(\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction,\n ): Promise<void> {\n try {\n // 1+2. Build a Web Request (resolves IP, builds absolute URL, reads body).\n const webReq = await buildWebRequest(req, opts);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const slug =\n (req.params as Record<string, string | undefined>).slug ??\n lastPathSegment(req.path);\n const decision = await vx.protect(webReq, { slug });\n\n // 4a. Denied — crawlers always get the SEO teaser (verified search bots\n // need the preview + JSON-LD). human-unverified gets the teaser\n // ONLY for real browser navigations (Sec-Fetch-Mode:navigate or\n // Accept includes text/html). Machine clients / x402 agents must\n // receive the 402 so they can pay.\n if (!decision.allowed) {\n const isHU = decision.reason === \"human-unverified\";\n const previewable =\n decision.reason === \"crawler\" ||\n (isHU && isBrowserNavigation(webReq));\n if (previewable && o?.seoPreview !== undefined) {\n const seo = o.seoPreview({ slug });\n if (isHU && opts?.humanUnlock !== undefined) {\n const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;\n const html = buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, new Response(html, { status: 200, headers: { \"content-type\": \"text/html; charset=utf-8\" } }));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n return;\n }\n\n // 4b. Allowed — if a payment receipt was returned, attach it first.\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n\n // Delegate to the original Express handler.\n handler(req, res, next);\n } catch (err) {\n // Propagate to Express error-handling middleware.\n next(err);\n }\n };\n },\n\n middleware(): RequestHandler {\n return async (req, res, next) => {\n try {\n const rawPath = req.originalUrl ?? req.url ?? \"/\";\n const pathname = rawPath.split(\"?\")[0]!;\n\n // If match patterns are configured and this path doesn't match, pass through.\n if (\n opts?.match !== undefined &&\n opts.match.length > 0 &&\n !pathMatchesAny(pathname, opts.match)\n ) {\n return next();\n }\n\n const webReq = await buildWebRequest(req, opts);\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n const decision = await vx.protect(webReq, { slug });\n\n if (decision.allowed) {\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return next();\n }\n\n // Denied — crawlers always get the SEO teaser; human-unverified only\n // gets the teaser for real browser navigations (Sec-Fetch-Mode or\n // Accept:text/html). Machine clients / x402 agents must get the 402.\n const isHU = decision.reason === \"human-unverified\";\n const previewable =\n decision.reason === \"crawler\" ||\n (isHU && isBrowserNavigation(webReq));\n if (previewable && opts?.seoPreview !== undefined) {\n const seo = opts.seoPreview({ slug });\n if (isHU && opts?.humanUnlock !== undefined) {\n const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;\n const html = buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, new Response(html, { status: 200, headers: { \"content-type\": \"text/html; charset=utf-8\" } }));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n } catch (err) {\n next(err as Error);\n }\n };\n },\n };\n}\n\n/**\n * Convenience export: create a Verivyx Express app-level middleware in one call.\n *\n * ```ts\n * import { verivyxMiddleware } from \"@verivyx/paywall-express\";\n * app.use(verivyxMiddleware({ domain: \"example.com\", token: process.env.VX_TOKEN }));\n * ```\n */\nexport function verivyxMiddleware(opts?: ExpressAdapterOptions): RequestHandler {\n return verivyxExpress(opts).middleware();\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -72,9 +72,12 @@ interface ExpressAdapterOptions extends VerivyxOptions {
72
72
  */
73
73
  advertise?: DiscoveryOptions;
74
74
  /**
75
- * When set, unverified humans and search crawlers (reason: "human-unverified"
76
- * or "crawler") receive a 200 HTML teaser page instead of a bare 402.
77
- * Bots / agents (reason: "bot-unpaid") still get the 402 x402 response.
75
+ * When set, search crawlers (reason: "crawler") always receive a 200 HTML
76
+ * teaser page. Unverified humans (reason: "human-unverified") also receive
77
+ * the teaser but ONLY when the request is a real browser top-level
78
+ * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).
79
+ * Machine clients and x402 payment agents that lack those browser headers
80
+ * receive the 402 x402 response so they can pay.
78
81
  *
79
82
  * Used by both `protect()` (when set on the factory opts) and `middleware()`.
80
83
  * `protect()` also accepts `seoPreview` in its per-call options `o`; if both
@@ -86,6 +89,17 @@ interface ExpressAdapterOptions extends VerivyxOptions {
86
89
  title: string;
87
90
  excerpt: string;
88
91
  };
92
+ /**
93
+ * When set, human-unverified real-browser visitors receive an interactive
94
+ * PoW unlock page (from core's `buildUnlockHtml`) instead of the static
95
+ * teaser. Crawlers always get the static teaser. Machines still get 402.
96
+ *
97
+ * `authBase` overrides the API base used for the challenge/verify endpoints
98
+ * (defaults to `cfg.apiBase` / VERIVYX_API_BASE).
99
+ */
100
+ humanUnlock?: {
101
+ authBase?: string;
102
+ };
89
103
  }
90
104
  /**
91
105
  * Convert a Web API `Response` to an Express response.
package/dist/index.d.ts CHANGED
@@ -72,9 +72,12 @@ interface ExpressAdapterOptions extends VerivyxOptions {
72
72
  */
73
73
  advertise?: DiscoveryOptions;
74
74
  /**
75
- * When set, unverified humans and search crawlers (reason: "human-unverified"
76
- * or "crawler") receive a 200 HTML teaser page instead of a bare 402.
77
- * Bots / agents (reason: "bot-unpaid") still get the 402 x402 response.
75
+ * When set, search crawlers (reason: "crawler") always receive a 200 HTML
76
+ * teaser page. Unverified humans (reason: "human-unverified") also receive
77
+ * the teaser but ONLY when the request is a real browser top-level
78
+ * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).
79
+ * Machine clients and x402 payment agents that lack those browser headers
80
+ * receive the 402 x402 response so they can pay.
78
81
  *
79
82
  * Used by both `protect()` (when set on the factory opts) and `middleware()`.
80
83
  * `protect()` also accepts `seoPreview` in its per-call options `o`; if both
@@ -86,6 +89,17 @@ interface ExpressAdapterOptions extends VerivyxOptions {
86
89
  title: string;
87
90
  excerpt: string;
88
91
  };
92
+ /**
93
+ * When set, human-unverified real-browser visitors receive an interactive
94
+ * PoW unlock page (from core's `buildUnlockHtml`) instead of the static
95
+ * teaser. Crawlers always get the static teaser. Machines still get 402.
96
+ *
97
+ * `authBase` overrides the API base used for the challenge/verify endpoints
98
+ * (defaults to `cfg.apiBase` / VERIVYX_API_BASE).
99
+ */
100
+ humanUnlock?: {
101
+ authBase?: string;
102
+ };
89
103
  }
90
104
  /**
91
105
  * Convert a Web API `Response` to an Express response.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { verivyx, createSearchCrawlerVerifier, buildSeoPreviewResponse, rslLinkHeader, contentUsageHeader } from '@verivyx/paywall';
1
+ import { verivyx, createSearchCrawlerVerifier, resolveConfig, buildUnlockHtml, buildSeoPreviewResponse, rslLinkHeader, contentUsageHeader } from '@verivyx/paywall';
2
2
 
3
3
  // src/index.ts
4
4
  function firstHop(xff) {
@@ -98,6 +98,11 @@ async function buildWebRequest(req, opts) {
98
98
  ...rawBody !== void 0 ? { duplex: "half" } : {}
99
99
  });
100
100
  }
101
+ function isBrowserNavigation(req) {
102
+ const secFetchMode = req.headers.get("sec-fetch-mode") ?? "";
103
+ const accept = req.headers.get("accept") ?? "";
104
+ return secFetchMode === "navigate" || accept.includes("text/html");
105
+ }
101
106
  function attachAdvertiseHeaders(res, advertise) {
102
107
  if (advertise === void 0) {
103
108
  return;
@@ -122,6 +127,9 @@ function verivyxExpress(opts) {
122
127
  verifyCrawlerDns: opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),
123
128
  ...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
124
129
  });
130
+ const proc = globalThis.process;
131
+ const env = proc?.env ?? {};
132
+ const cfg = resolveConfig(opts, env);
125
133
  return {
126
134
  protect(handler, o) {
127
135
  return async function verivyxGuard(req, res, next) {
@@ -130,8 +138,17 @@ function verivyxExpress(opts) {
130
138
  const slug = req.params.slug ?? lastPathSegment(req.path);
131
139
  const decision = await vx.protect(webReq, { slug });
132
140
  if (!decision.allowed) {
133
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
134
- if (isPreviewCandidate && o?.seoPreview !== void 0) {
141
+ const isHU = decision.reason === "human-unverified";
142
+ const previewable = decision.reason === "crawler" || isHU && isBrowserNavigation(webReq);
143
+ if (previewable && o?.seoPreview !== void 0) {
144
+ const seo = o.seoPreview({ slug });
145
+ if (isHU && opts?.humanUnlock !== void 0) {
146
+ const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;
147
+ const html = buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });
148
+ attachAdvertiseHeaders(res, opts?.advertise);
149
+ await sendWebResponse(res, new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }));
150
+ return;
151
+ }
135
152
  attachAdvertiseHeaders(res, opts?.advertise);
136
153
  await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));
137
154
  return;
@@ -167,8 +184,17 @@ function verivyxExpress(opts) {
167
184
  }
168
185
  return next();
169
186
  }
170
- const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
171
- if (isPreviewCandidate && opts?.seoPreview !== void 0) {
187
+ const isHU = decision.reason === "human-unverified";
188
+ const previewable = decision.reason === "crawler" || isHU && isBrowserNavigation(webReq);
189
+ if (previewable && opts?.seoPreview !== void 0) {
190
+ const seo = opts.seoPreview({ slug });
191
+ if (isHU && opts?.humanUnlock !== void 0) {
192
+ const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;
193
+ const html = buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });
194
+ attachAdvertiseHeaders(res, opts?.advertise);
195
+ await sendWebResponse(res, new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }));
196
+ return;
197
+ }
172
198
  attachAdvertiseHeaders(res, opts?.advertise);
173
199
  await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));
174
200
  return;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAgHA,SAAS,SAAS,GAAA,EAAwD;AACxE,EAAA,IAAI,QAAQ,MAAA,EAAW;AACrB,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAM,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,GAAA,CAAI,CAAC,CAAA,GAAI,GAAA;AAC1C,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AAC9B,EAAA,OAAO,KAAA,KAAU,MAAA,GAAY,KAAA,CAAM,IAAA,MAAU,MAAA,GAAY,MAAA;AAC3D;AAMA,SAAS,gBAAgB,IAAA,EAAsB;AAC7C,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,OAAO,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA;AAC3D,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,CAAC,CAAA;AACzC,EAAA,OAAO,IAAA,KAAS,MAAA,GAAY,kBAAA,CAAmB,IAAI,CAAA,GAAI,EAAA;AACzD;AAOA,SAAS,YAAA,CACP,aACA,EAAA,EACS;AACT,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,EAAQ;AAC5B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACtD,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA;AAAA,IACF;AACA,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,QAAA,OAAA,CAAQ,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,MACvB;AAAA,IACF,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,GAAA,CAAI,KAAK,KAAK,CAAA;AAAA,IACxB;AAAA,EACF;AACA,EAAA,IAAI,OAAO,MAAA,EAAW;AACpB,IAAA,OAAA,CAAQ,GAAA,CAAI,aAAa,EAAE,CAAA;AAAA,EAC7B;AACA,EAAA,OAAO,OAAA;AACT;AAWA,SAAS,SAAA,CACP,KACA,IAAA,EACoB;AACpB,EAAA,IAAI,MAAM,QAAA,EAAU;AAClB,IAAA,OAAO,IAAA,CAAK,SAAS,GAAG,CAAA;AAAA,EAC1B;AACA,EAAA,IAAI,IAAA,EAAM,eAAe,KAAA,EAAO;AAC9B,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,iBAAiB,CAAA;AACzC,IAAA,MAAM,GAAA,GAAM,SAAS,GAAG,CAAA;AACxB,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,OAAO,GAAA;AAAA,IACT;AACA,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,WAAW,CAAA;AACnC,IAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,EAAK;AAClC,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAQ,IAAI,MAAA,CAAkB,aAAA;AAChC;AAQA,eAAe,YACb,GAAA,EACiC;AACjC,EAAA,MAAM,MAAA,GAAS,GAAA,CAAI,MAAA,CAAO,WAAA,EAAY;AACtC,EAAA,IAAI,MAAA,KAAW,KAAA,IAAS,MAAA,KAAW,MAAA,IAAU,WAAW,SAAA,EAAW;AACjE,IAAA,OAAO,MAAA;AAAA,EACT;AAIA,EAAA,MAAM,OAAiB,GAAA,CAA2B,IAAA;AAClD,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA,EAAG;AACzB,IAAA,OAAO,IAAI,WAAW,IAAI,CAAA;AAAA,EAC5B;AAGA,EAAA,OAAO,IAAI,OAAA,CAAoB,CAAC,OAAA,EAAS,MAAA,KAAW;AAClD,IAAA,MAAM,SAAmB,EAAC;AAC1B,IAAA,GAAA,CAAI,GAAG,MAAA,EAAQ,CAAC,UAAkB,MAAA,CAAO,IAAA,CAAK,KAAK,CAAC,CAAA;AACpD,IAAA,GAAA,CAAI,EAAA,CAAG,KAAA,EAAO,MAAM,OAAA,CAAQ,IAAI,UAAA,CAAW,MAAA,CAAO,MAAA,CAAO,MAAM,CAAC,CAAC,CAAC,CAAA;AAClE,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACH;AAMA,SAAS,cAAA,CAAe,UAAkB,QAAA,EAA6B;AACrE,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAI9B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,mBAAA,EAAqB,MAAM,CAAA;AAC3D,IAAA,MAAM,QAAA,GACJ,GAAA,GACA,OAAA,CACG,OAAA,CAAQ,SAAS,IAAM,CAAA,CACvB,OAAA,CAAQ,KAAA,EAAO,OAAO,CAAA,CACtB,OAAA,CAAQ,OAAA,EAAS,IAAI,CAAA,GACxB,GAAA;AACF,IAAA,IAAI,IAAI,MAAA,CAAO,QAAQ,CAAA,CAAE,IAAA,CAAK,QAAQ,CAAA,EAAG;AACvC,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAMA,eAAe,eAAA,CACb,KACA,IAAA,EACkB;AAClB,EAAA,MAAM,EAAA,GAAK,SAAA,CAAU,GAAA,EAAK,IAAI,CAAA;AAC9B,EAAA,MAAM,QACH,OAAO,GAAA,CAAI,QAAQ,UAAA,GAAa,GAAA,CAAI,IAAI,MAAM,CAAA,GAAI,MAAA,MAClD,OAAO,IAAI,OAAA,CAAQ,IAAA,KAAS,WAAW,GAAA,CAAI,OAAA,CAAQ,OAAO,MAAA,CAAA,IAC3D,WAAA;AACF,EAAA,MAAM,QAAA,GAAW,IAAI,QAAA,IAAY,MAAA;AACjC,EAAA,MAAM,cAAc,CAAA,EAAG,QAAQ,MAAM,IAAI,CAAA,EAAG,IAAI,WAAW,CAAA,CAAA;AAC3D,EAAA,MAAM,UAAA,GAAa,YAAA,CAAa,GAAA,CAAI,OAAA,EAAS,EAAE,CAAA;AAE/C,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI;AACF,IAAA,OAAA,GAAU,MAAM,YAAY,GAAG,CAAA;AAAA,EACjC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAA,GAAU,MAAA;AAAA,EACZ;AAEA,EAAA,OAAO,IAAI,QAAQ,WAAA,EAAa;AAAA,IAC9B,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,OAAA,EAAS,UAAA;AAAA,IACT,IAAA,EAAM,OAAA,KAAY,MAAA,GAAY,OAAA,GAAU,MAAA;AAAA,IACxC,GAAI,OAAA,KAAY,MAAA,GAAY,EAAE,MAAA,EAAQ,MAAA,KAAW;AAAC,GACpC,CAAA;AAClB;AAOA,SAAS,sBAAA,CACP,KACA,SAAA,EACM;AACN,EAAA,IAAI,cAAc,MAAA,EAAW;AAC3B,IAAA;AAAA,EACF;AACA,EAAA,GAAA,CAAI,MAAA,CAAO,MAAA,EAAQ,aAAA,CAAc,SAAS,CAAC,CAAA;AAC3C,EAAA,GAAA,CAAI,SAAA,CAAU,eAAA,EAAiB,kBAAA,CAAmB,SAAS,CAAC,CAAA;AAC9D;AAcA,eAAsB,eAAA,CACpB,KACA,MAAA,EACe;AACf,EAAA,GAAA,CAAI,MAAA,CAAO,OAAO,MAAM,CAAA;AACxB,EAAA,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACrC,IAAA,IAAI,GAAA,CAAI,WAAA,EAAY,KAAM,YAAA,EAAc;AACtC,MAAA,GAAA,CAAI,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACvB,CAAA,MAAO;AACL,MAAA,GAAA,CAAI,SAAA,CAAU,KAAK,KAAK,CAAA;AAAA,IAC1B;AAAA,EACF,CAAC,CAAA;AACD,EAAA,MAAM,MAAM,MAAA,CAAO,IAAA,CAAK,MAAM,MAAA,CAAO,aAAa,CAAA;AAClD,EAAA,GAAA,CAAI,KAAK,GAAG,CAAA;AACd;AAiBO,SAAS,eAAe,IAAA,EAM7B;AAIA,EAAA,MAAM,EAAA,GACJ,IAAA,EAAM,KAAA,IACN,OAAA,CAAQ,IAAA,EAAM;AAAA,IACZ,gBAAA,EACE,IAAA,EAAM,gBAAA,IAAoB,2BAAA,EAA4B;AAAA,IACxD,GAAI,MAAM,gBAAA,KAAqB,MAAA,GAC3B,EAAE,gBAAA,EAAkB,IAAA,CAAK,gBAAA,EAAiB,GAC1C;AAAC,GACN,CAAA;AAEH,EAAA,OAAO;AAAA,IACL,OAAA,CACE,SACA,CAAA,EACgB;AAEhB,MAAA,OAAO,eAAe,YAAA,CACpB,GAAA,EACA,GAAA,EACA,IAAA,EACe;AACf,QAAA,IAAI;AAEF,UAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,GAAA,EAAK,IAAI,CAAA;AAG9C,UAAA,MAAM,OACH,GAAA,CAAI,MAAA,CAA8C,IAAA,IACnD,eAAA,CAAgB,IAAI,IAAI,CAAA;AAC1B,UAAA,MAAM,WAAW,MAAM,EAAA,CAAG,QAAQ,MAAA,EAAQ,EAAE,MAAM,CAAA;AAIlD,UAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACrB,YAAA,MAAM,kBAAA,GACJ,QAAA,CAAS,MAAA,KAAW,SAAA,IAAa,SAAS,MAAA,KAAW,kBAAA;AACvD,YAAA,IAAI,kBAAA,IAAsB,CAAA,EAAG,UAAA,KAAe,KAAA,CAAA,EAAW;AACrD,cAAA,sBAAA,CAAuB,GAAA,EAAK,MAAM,SAAS,CAAA;AAC3C,cAAA,MAAM,eAAA,CAAgB,KAAK,uBAAA,CAAwB,IAAA,EAAM,OAAO,GAAA,EAAK,CAAA,CAAE,UAAU,CAAC,CAAA;AAClF,cAAA;AAAA,YACF;AACA,YAAA,sBAAA,CAAuB,GAAA,EAAK,MAAM,SAAS,CAAA;AAC3C,YAAA,MAAM,eAAA,CAAgB,GAAA,EAAK,QAAA,CAAS,QAAA,EAAU,CAAA;AAC9C,YAAA;AAAA,UACF;AAGA,UAAA,IAAI,QAAA,CAAS,oBAAoB,KAAA,CAAA,EAAW;AAC1C,YAAA,GAAA,CAAI,SAAA,CAAU,kBAAA,EAAoB,QAAA,CAAS,eAAe,CAAA;AAAA,UAC5D;AACA,UAAA,sBAAA,CAAuB,GAAA,EAAK,MAAM,SAAS,CAAA;AAG3C,UAAA,OAAA,CAAQ,GAAA,EAAK,KAAK,IAAI,CAAA;AAAA,QACxB,SAAS,GAAA,EAAK;AAEZ,UAAA,IAAA,CAAK,GAAG,CAAA;AAAA,QACV;AAAA,MACF,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,UAAA,GAA6B;AAC3B,MAAA,OAAO,OAAO,GAAA,EAAK,GAAA,EAAK,IAAA,KAAS;AAC/B,QAAA,IAAI;AACF,UAAA,MAAM,OAAA,GAAU,GAAA,CAAI,WAAA,IAAe,GAAA,CAAI,GAAA,IAAO,GAAA;AAC9C,UAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AAGrC,UAAA,IACE,IAAA,EAAM,KAAA,KAAU,KAAA,CAAA,IAChB,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,CAAA,IACpB,CAAC,cAAA,CAAe,QAAA,EAAU,IAAA,CAAK,KAAK,CAAA,EACpC;AACA,YAAA,OAAO,IAAA,EAAK;AAAA,UACd;AAEA,UAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,GAAA,EAAK,IAAI,CAAA;AAC9C,UAAA,MAAM,IAAA,GAAO,SAAS,KAAA,CAAM,GAAG,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,GAAA,EAAI,IAAK,EAAA;AAC1D,UAAA,MAAM,WAAW,MAAM,EAAA,CAAG,QAAQ,MAAA,EAAQ,EAAE,MAAM,CAAA;AAElD,UAAA,IAAI,SAAS,OAAA,EAAS;AACpB,YAAA,IAAI,QAAA,CAAS,oBAAoB,KAAA,CAAA,EAAW;AAC1C,cAAA,GAAA,CAAI,SAAA,CAAU,kBAAA,EAAoB,QAAA,CAAS,eAAe,CAAA;AAAA,YAC5D;AACA,YAAA,OAAO,IAAA,EAAK;AAAA,UACd;AAIA,UAAA,MAAM,kBAAA,GACJ,QAAA,CAAS,MAAA,KAAW,SAAA,IAAa,SAAS,MAAA,KAAW,kBAAA;AACvD,UAAA,IAAI,kBAAA,IAAsB,IAAA,EAAM,UAAA,KAAe,KAAA,CAAA,EAAW;AACxD,YAAA,sBAAA,CAAuB,GAAA,EAAK,MAAM,SAAS,CAAA;AAC3C,YAAA,MAAM,eAAA,CAAgB,KAAK,uBAAA,CAAwB,IAAA,EAAM,OAAO,GAAA,EAAK,IAAA,CAAK,UAAU,CAAC,CAAA;AACrF,YAAA;AAAA,UACF;AACA,UAAA,sBAAA,CAAuB,GAAA,EAAK,MAAM,SAAS,CAAA;AAC3C,UAAA,MAAM,eAAA,CAAgB,GAAA,EAAK,QAAA,CAAS,QAAA,EAAU,CAAA;AAAA,QAChD,SAAS,GAAA,EAAK;AACZ,UAAA,IAAA,CAAK,GAAY,CAAA;AAAA,QACnB;AAAA,MACF,CAAA;AAAA,IACF;AAAA,GACF;AACF;AAUO,SAAS,kBAAkB,IAAA,EAA8C;AAC9E,EAAA,OAAO,cAAA,CAAe,IAAI,CAAA,CAAE,UAAA,EAAW;AACzC","file":"index.js","sourcesContent":["/**\n * @verivyx/paywall-express\n *\n * Express adapter for the Verivyx paywall SDK.\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module only handles:\n * 1. IP resolution from Express request.\n * 2. Converting Express IncomingMessage → Web Request.\n * 3. Calling core `protect()` (decision overload).\n * 4. Converting the Web Response back to Express response, OR calling the\n * original Express handler when the request is allowed through.\n *\n * Mock-injection seam (for Tasks 18/19 to reuse):\n * Pass `_core: verivyx.mock({...})` in options to bypass network.\n * In production: omit `_core`; the adapter instantiates `verivyx(opts, deps)`.\n *\n * @example\n * ```ts\n * import { verivyxExpress } from \"@verivyx/paywall-express\";\n * const vx = verivyxExpress({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\n\nimport type { Request as ExpressRequest, RequestHandler, Response as ExpressResponse, NextFunction } from \"express\";\nimport {\n verivyx,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport type { Socket } from \"node:net\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Express adapter.\n * Extends core VerivyxOptions with Express-specific IP resolution controls.\n */\nexport interface ExpressAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from `X-Forwarded-For` /\n * `X-Real-IP` headers (set by a trusted reverse proxy). Set to false if\n * the Express app is not behind a proxy, to use the raw socket address.\n */\n trustProxy?: boolean;\n\n /**\n * Custom IP extractor. When provided, this overrides the built-in\n * `trustProxy` / `X-Forwarded-For` logic entirely.\n *\n * @param req - The Express request object.\n * @returns The resolved client IP, or undefined to fall back to socket.\n */\n clientIp?: (req: ExpressRequest) => string | undefined;\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 * Pattern for Next.js / Hono adapters to reuse:\n * `const vx = opts?._core ?? verivyx(opts, { verifyCrawlerDns });`\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 `middleware()`.\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// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | string[] | undefined): string | undefined {\n if (xff === undefined) {\n return undefined;\n }\n const raw = Array.isArray(xff) ? xff[0] : xff;\n if (!raw) {\n return undefined;\n }\n const first = raw.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Extract the last non-empty path segment from a URL path string.\n * Used as a fallback slug when `req.params.slug` is not available.\n */\nfunction lastPathSegment(path: string): string {\n const segments = path.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n return last !== undefined ? decodeURIComponent(last) : \"\";\n}\n\n/**\n * Build a `Headers` object from Node.js IncomingHttpHeaders, overriding\n * `x-real-ip` with the resolved (trusted) client IP so the core classifier\n * reads a reliable address.\n */\nfunction toWebHeaders(\n nodeHeaders: IncomingHttpHeaders,\n ip: string | undefined,\n): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value === undefined) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const v of value) {\n headers.append(key, v);\n }\n } else {\n headers.set(key, value);\n }\n }\n if (ip !== undefined) {\n headers.set(\"x-real-ip\", ip);\n }\n return headers;\n}\n\n/**\n * Resolve the client IP from an Express request.\n *\n * Precedence:\n * 1. `opts.clientIp(req)` — caller-supplied extractor (highest precedence).\n * 2. `X-Forwarded-For` first hop (when trustProxy !== false).\n * 3. `X-Real-IP` header (when trustProxy !== false).\n * 4. `req.socket.remoteAddress` — raw TCP peer (lowest precedence).\n */\nfunction resolveIp(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): string | undefined {\n if (opts?.clientIp) {\n return opts.clientIp(req);\n }\n if (opts?.trustProxy !== false) {\n const xff = req.headers[\"x-forwarded-for\"];\n const hop = firstHop(xff);\n if (hop) {\n return hop;\n }\n const xri = req.headers[\"x-real-ip\"];\n if (typeof xri === \"string\" && xri) {\n return xri;\n }\n }\n return (req.socket as Socket).remoteAddress;\n}\n\n/**\n * Collect the raw request body into a Uint8Array.\n * Returns undefined for requests with no body (GET / HEAD / OPTIONS).\n * Express may have already consumed the stream with a body parser — if\n * `req.body` is already populated as a Buffer we use that directly.\n */\nasync function readRawBody(\n req: ExpressRequest,\n): Promise<Uint8Array | undefined> {\n const method = req.method.toUpperCase();\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return undefined;\n }\n\n // Express body-parser may have already consumed and parsed the stream.\n // If `req.body` is a Buffer, use it directly.\n const body: unknown = (req as { body?: unknown }).body;\n if (Buffer.isBuffer(body)) {\n return new Uint8Array(body);\n }\n\n // Otherwise collect the raw stream.\n return new Promise<Uint8Array>((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(new Uint8Array(Buffer.concat(chunks))));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Check whether a pathname matches any of the given glob patterns.\n * Supports `*` (any segment chars except `/`) and `**` (any chars including `/`).\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Build a regex from the glob pattern:\n // 1. Escape all regex metacharacters except * and ?.\n // 2. Replace ** with a placeholder, then * with [^/]*, then restore **.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const regexStr =\n \"^\" +\n escaped\n .replace(/\\*\\*/g, \"\\x00\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\x00/g, \".*\") +\n \"$\";\n if (new RegExp(regexStr).test(pathname)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Build a Web API `Request` from an Express request.\n * Resolves the trusted client IP, builds absolute URL, and reads the raw body.\n */\nasync function buildWebRequest(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): Promise<Request> {\n const ip = resolveIp(req, opts);\n const host =\n (typeof req.get === \"function\" ? req.get(\"host\") : undefined) ??\n (typeof req.headers.host === \"string\" ? req.headers.host : undefined) ??\n \"localhost\";\n const protocol = req.protocol ?? \"http\";\n const absoluteUrl = `${protocol}://${host}${req.originalUrl}`;\n const webHeaders = toWebHeaders(req.headers, ip);\n\n let rawBody: Uint8Array | undefined;\n try {\n rawBody = await readRawBody(req);\n } catch {\n rawBody = undefined;\n }\n\n return new Request(absoluteUrl, {\n method: req.method,\n headers: webHeaders,\n body: rawBody !== undefined ? rawBody : undefined,\n ...(rawBody !== undefined ? { duplex: \"half\" } : {}),\n } as RequestInit);\n}\n\n/**\n * Attach RSL + AIPREF discovery headers to an Express response.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * No-op when `advertise` is undefined.\n */\nfunction attachAdvertiseHeaders(\n res: ExpressResponse,\n advertise: DiscoveryOptions | undefined,\n): void {\n if (advertise === undefined) {\n return;\n }\n res.append(\"Link\", rslLinkHeader(advertise));\n res.setHeader(\"Content-Usage\", contentUsageHeader(advertise));\n}\n\n/**\n * Convert a Web API `Response` to an Express response.\n * Copies status, all response headers, and the body.\n *\n * @internal Exported for unit-testing the Set-Cookie accumulation fix.\n * Production callers should use the `protect()` middleware instead.\n *\n * Set-Cookie is special: `Headers.forEach` emits each cookie as a separate\n * call with the same key, and `res.setHeader` REPLACES on repeated same-key\n * calls — so all but the last cookie would be silently dropped.\n * `res.append` accumulates values into an array, preserving every cookie.\n */\nexport async function sendWebResponse(\n res: ExpressResponse,\n webRes: Response,\n): Promise<void> {\n res.status(webRes.status);\n webRes.headers.forEach((value, key) => {\n if (key.toLowerCase() === \"set-cookie\") {\n res.append(key, value);\n } else {\n res.setHeader(key, value);\n }\n });\n const buf = Buffer.from(await webRes.arrayBuffer());\n res.send(buf);\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Express adapter.\n *\n * Returns an object with a single `protect(handler)` method that wraps an\n * Express `RequestHandler` behind the Verivyx paywall gate.\n *\n * ```ts\n * const vx = verivyxExpress({ domain: \"example.com\", token: \"...\" });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\nexport function verivyxExpress(opts?: ExpressAdapterOptions): {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler;\n middleware(): RequestHandler;\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 return {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler {\n // Return an async Express handler (req, res, next).\n return async function verivyxGuard(\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction,\n ): Promise<void> {\n try {\n // 1+2. Build a Web Request (resolves IP, builds absolute URL, reads body).\n const webReq = await buildWebRequest(req, opts);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const slug =\n (req.params as Record<string, string | undefined>).slug ??\n lastPathSegment(req.path);\n const decision = await vx.protect(webReq, { slug });\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 attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n return;\n }\n\n // 4b. Allowed — if a payment receipt was returned, attach it first.\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n\n // Delegate to the original Express handler.\n handler(req, res, next);\n } catch (err) {\n // Propagate to Express error-handling middleware.\n next(err);\n }\n };\n },\n\n middleware(): RequestHandler {\n return async (req, res, next) => {\n try {\n const rawPath = req.originalUrl ?? req.url ?? \"/\";\n const pathname = rawPath.split(\"?\")[0]!;\n\n // If match patterns are configured and this path doesn't match, pass through.\n if (\n opts?.match !== undefined &&\n opts.match.length > 0 &&\n !pathMatchesAny(pathname, opts.match)\n ) {\n return next();\n }\n\n const webReq = await buildWebRequest(req, opts);\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n const decision = await vx.protect(webReq, { slug });\n\n if (decision.allowed) {\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return next();\n }\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 attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n } catch (err) {\n next(err as Error);\n }\n };\n },\n };\n}\n\n/**\n * Convenience export: create a Verivyx Express app-level middleware in one call.\n *\n * ```ts\n * import { verivyxMiddleware } from \"@verivyx/paywall-express\";\n * app.use(verivyxMiddleware({ domain: \"example.com\", token: process.env.VX_TOKEN }));\n * ```\n */\nexport function verivyxMiddleware(opts?: ExpressAdapterOptions): RequestHandler {\n return verivyxExpress(opts).middleware();\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @verivyx/paywall-express\n *\n * Express adapter for the Verivyx paywall SDK.\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module only handles:\n * 1. IP resolution from Express request.\n * 2. Converting Express IncomingMessage → Web Request.\n * 3. Calling core `protect()` (decision overload).\n * 4. Converting the Web Response back to Express response, OR calling the\n * original Express handler when the request is allowed through.\n *\n * Mock-injection seam (for Tasks 18/19 to reuse):\n * Pass `_core: verivyx.mock({...})` in options to bypass network.\n * In production: omit `_core`; the adapter instantiates `verivyx(opts, deps)`.\n *\n * @example\n * ```ts\n * import { verivyxExpress } from \"@verivyx/paywall-express\";\n * const vx = verivyxExpress({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\n\nimport type { Request as ExpressRequest, RequestHandler, Response as ExpressResponse, NextFunction } from \"express\";\nimport {\n verivyx,\n resolveConfig,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n buildUnlockHtml,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport type { Socket } from \"node:net\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Express adapter.\n * Extends core VerivyxOptions with Express-specific IP resolution controls.\n */\nexport interface ExpressAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from `X-Forwarded-For` /\n * `X-Real-IP` headers (set by a trusted reverse proxy). Set to false if\n * the Express app is not behind a proxy, to use the raw socket address.\n */\n trustProxy?: boolean;\n\n /**\n * Custom IP extractor. When provided, this overrides the built-in\n * `trustProxy` / `X-Forwarded-For` logic entirely.\n *\n * @param req - The Express request object.\n * @returns The resolved client IP, or undefined to fall back to socket.\n */\n clientIp?: (req: ExpressRequest) => string | undefined;\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 * Pattern for Next.js / Hono adapters to reuse:\n * `const vx = opts?._core ?? verivyx(opts, { verifyCrawlerDns });`\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 `middleware()`.\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 * When set, human-unverified real-browser visitors receive an interactive\n * PoW unlock page (from core's `buildUnlockHtml`) instead of the static\n * teaser. Crawlers always get the static teaser. Machines still get 402.\n *\n * `authBase` overrides the API base used for the challenge/verify endpoints\n * (defaults to `cfg.apiBase` / VERIVYX_API_BASE).\n */\n humanUnlock?: { authBase?: string };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | string[] | undefined): string | undefined {\n if (xff === undefined) {\n return undefined;\n }\n const raw = Array.isArray(xff) ? xff[0] : xff;\n if (!raw) {\n return undefined;\n }\n const first = raw.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Extract the last non-empty path segment from a URL path string.\n * Used as a fallback slug when `req.params.slug` is not available.\n */\nfunction lastPathSegment(path: string): string {\n const segments = path.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n return last !== undefined ? decodeURIComponent(last) : \"\";\n}\n\n/**\n * Build a `Headers` object from Node.js IncomingHttpHeaders, overriding\n * `x-real-ip` with the resolved (trusted) client IP so the core classifier\n * reads a reliable address.\n */\nfunction toWebHeaders(\n nodeHeaders: IncomingHttpHeaders,\n ip: string | undefined,\n): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value === undefined) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const v of value) {\n headers.append(key, v);\n }\n } else {\n headers.set(key, value);\n }\n }\n if (ip !== undefined) {\n headers.set(\"x-real-ip\", ip);\n }\n return headers;\n}\n\n/**\n * Resolve the client IP from an Express request.\n *\n * Precedence:\n * 1. `opts.clientIp(req)` — caller-supplied extractor (highest precedence).\n * 2. `X-Forwarded-For` first hop (when trustProxy !== false).\n * 3. `X-Real-IP` header (when trustProxy !== false).\n * 4. `req.socket.remoteAddress` — raw TCP peer (lowest precedence).\n */\nfunction resolveIp(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): string | undefined {\n if (opts?.clientIp) {\n return opts.clientIp(req);\n }\n if (opts?.trustProxy !== false) {\n const xff = req.headers[\"x-forwarded-for\"];\n const hop = firstHop(xff);\n if (hop) {\n return hop;\n }\n const xri = req.headers[\"x-real-ip\"];\n if (typeof xri === \"string\" && xri) {\n return xri;\n }\n }\n return (req.socket as Socket).remoteAddress;\n}\n\n/**\n * Collect the raw request body into a Uint8Array.\n * Returns undefined for requests with no body (GET / HEAD / OPTIONS).\n * Express may have already consumed the stream with a body parser — if\n * `req.body` is already populated as a Buffer we use that directly.\n */\nasync function readRawBody(\n req: ExpressRequest,\n): Promise<Uint8Array | undefined> {\n const method = req.method.toUpperCase();\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n return undefined;\n }\n\n // Express body-parser may have already consumed and parsed the stream.\n // If `req.body` is a Buffer, use it directly.\n const body: unknown = (req as { body?: unknown }).body;\n if (Buffer.isBuffer(body)) {\n return new Uint8Array(body);\n }\n\n // Otherwise collect the raw stream.\n return new Promise<Uint8Array>((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(new Uint8Array(Buffer.concat(chunks))));\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Check whether a pathname matches any of the given glob patterns.\n * Supports `*` (any segment chars except `/`) and `**` (any chars including `/`).\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Build a regex from the glob pattern:\n // 1. Escape all regex metacharacters except * and ?.\n // 2. Replace ** with a placeholder, then * with [^/]*, then restore **.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const regexStr =\n \"^\" +\n escaped\n .replace(/\\*\\*/g, \"\\x00\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\x00/g, \".*\") +\n \"$\";\n if (new RegExp(regexStr).test(pathname)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Build a Web API `Request` from an Express request.\n * Resolves the trusted client IP, builds absolute URL, and reads the raw body.\n */\nasync function buildWebRequest(\n req: ExpressRequest,\n opts: ExpressAdapterOptions | undefined,\n): Promise<Request> {\n const ip = resolveIp(req, opts);\n const host =\n (typeof req.get === \"function\" ? req.get(\"host\") : undefined) ??\n (typeof req.headers.host === \"string\" ? req.headers.host : undefined) ??\n \"localhost\";\n const protocol = req.protocol ?? \"http\";\n const absoluteUrl = `${protocol}://${host}${req.originalUrl}`;\n const webHeaders = toWebHeaders(req.headers, ip);\n\n let rawBody: Uint8Array | undefined;\n try {\n rawBody = await readRawBody(req);\n } catch {\n rawBody = undefined;\n }\n\n return new Request(absoluteUrl, {\n method: req.method,\n headers: webHeaders,\n body: rawBody !== undefined ? rawBody : undefined,\n ...(rawBody !== undefined ? { duplex: \"half\" } : {}),\n } as RequestInit);\n}\n\n/**\n * Return true when the Web 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 * Attach RSL + AIPREF discovery headers to an Express response.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * No-op when `advertise` is undefined.\n */\nfunction attachAdvertiseHeaders(\n res: ExpressResponse,\n advertise: DiscoveryOptions | undefined,\n): void {\n if (advertise === undefined) {\n return;\n }\n res.append(\"Link\", rslLinkHeader(advertise));\n res.setHeader(\"Content-Usage\", contentUsageHeader(advertise));\n}\n\n/**\n * Convert a Web API `Response` to an Express response.\n * Copies status, all response headers, and the body.\n *\n * @internal Exported for unit-testing the Set-Cookie accumulation fix.\n * Production callers should use the `protect()` middleware instead.\n *\n * Set-Cookie is special: `Headers.forEach` emits each cookie as a separate\n * call with the same key, and `res.setHeader` REPLACES on repeated same-key\n * calls — so all but the last cookie would be silently dropped.\n * `res.append` accumulates values into an array, preserving every cookie.\n */\nexport async function sendWebResponse(\n res: ExpressResponse,\n webRes: Response,\n): Promise<void> {\n res.status(webRes.status);\n webRes.headers.forEach((value, key) => {\n if (key.toLowerCase() === \"set-cookie\") {\n res.append(key, value);\n } else {\n res.setHeader(key, value);\n }\n });\n const buf = Buffer.from(await webRes.arrayBuffer());\n res.send(buf);\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Express adapter.\n *\n * Returns an object with a single `protect(handler)` method that wraps an\n * Express `RequestHandler` behind the Verivyx paywall gate.\n *\n * ```ts\n * const vx = verivyxExpress({ domain: \"example.com\", token: \"...\" });\n * app.get(\"/articles/:slug\", vx.protect(myHandler));\n * ```\n */\nexport function verivyxExpress(opts?: ExpressAdapterOptions): {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler;\n middleware(): RequestHandler;\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 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 return {\n protect(\n handler: RequestHandler,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): RequestHandler {\n // Return an async Express handler (req, res, next).\n return async function verivyxGuard(\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction,\n ): Promise<void> {\n try {\n // 1+2. Build a Web Request (resolves IP, builds absolute URL, reads body).\n const webReq = await buildWebRequest(req, opts);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const slug =\n (req.params as Record<string, string | undefined>).slug ??\n lastPathSegment(req.path);\n const decision = await vx.protect(webReq, { slug });\n\n // 4a. Denied — crawlers always get the SEO teaser (verified search bots\n // need the preview + JSON-LD). human-unverified gets the teaser\n // ONLY for real browser navigations (Sec-Fetch-Mode:navigate or\n // Accept includes text/html). Machine clients / x402 agents must\n // receive the 402 so they can pay.\n if (!decision.allowed) {\n const isHU = decision.reason === \"human-unverified\";\n const previewable =\n decision.reason === \"crawler\" ||\n (isHU && isBrowserNavigation(webReq));\n if (previewable && o?.seoPreview !== undefined) {\n const seo = o.seoPreview({ slug });\n if (isHU && opts?.humanUnlock !== undefined) {\n const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;\n const html = buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, new Response(html, { status: 200, headers: { \"content-type\": \"text/html; charset=utf-8\" } }));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n return;\n }\n\n // 4b. Allowed — if a payment receipt was returned, attach it first.\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n\n // Delegate to the original Express handler.\n handler(req, res, next);\n } catch (err) {\n // Propagate to Express error-handling middleware.\n next(err);\n }\n };\n },\n\n middleware(): RequestHandler {\n return async (req, res, next) => {\n try {\n const rawPath = req.originalUrl ?? req.url ?? \"/\";\n const pathname = rawPath.split(\"?\")[0]!;\n\n // If match patterns are configured and this path doesn't match, pass through.\n if (\n opts?.match !== undefined &&\n opts.match.length > 0 &&\n !pathMatchesAny(pathname, opts.match)\n ) {\n return next();\n }\n\n const webReq = await buildWebRequest(req, opts);\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n const decision = await vx.protect(webReq, { slug });\n\n if (decision.allowed) {\n if (decision.paymentResponse !== undefined) {\n res.setHeader(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return next();\n }\n\n // Denied — crawlers always get the SEO teaser; human-unverified only\n // gets the teaser for real browser navigations (Sec-Fetch-Mode or\n // Accept:text/html). Machine clients / x402 agents must get the 402.\n const isHU = decision.reason === \"human-unverified\";\n const previewable =\n decision.reason === \"crawler\" ||\n (isHU && isBrowserNavigation(webReq));\n if (previewable && opts?.seoPreview !== undefined) {\n const seo = opts.seoPreview({ slug });\n if (isHU && opts?.humanUnlock !== undefined) {\n const authBase = opts.humanUnlock.authBase ?? cfg.apiBase;\n const html = buildUnlockHtml({ slug, url: webReq.url, authBase, domain: cfg.domain, seo });\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, new Response(html, { status: 200, headers: { \"content-type\": \"text/html; charset=utf-8\" } }));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));\n return;\n }\n attachAdvertiseHeaders(res, opts?.advertise);\n await sendWebResponse(res, decision.response());\n } catch (err) {\n next(err as Error);\n }\n };\n },\n };\n}\n\n/**\n * Convenience export: create a Verivyx Express app-level middleware in one call.\n *\n * ```ts\n * import { verivyxMiddleware } from \"@verivyx/paywall-express\";\n * app.use(verivyxMiddleware({ domain: \"example.com\", token: process.env.VX_TOKEN }));\n * ```\n */\nexport function verivyxMiddleware(opts?: ExpressAdapterOptions): RequestHandler {\n return verivyxExpress(opts).middleware();\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verivyx/paywall-express",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "description": "Express adapter for the Verivyx paywall SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "typecheck:test": "tsc -p tsconfig.test.json"
35
35
  },
36
36
  "dependencies": {
37
- "@verivyx/paywall": "^0.1.0"
37
+ "@verivyx/paywall": "^0.2.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "express": ">=4.18 <6 || ^5"