@verivyx/paywall-express 0.2.0 → 0.3.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
@@ -169,6 +169,12 @@ function verivyxExpress(opts) {
169
169
  }
170
170
  return next();
171
171
  }
172
+ const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
173
+ if (isPreviewCandidate && opts?.seoPreview !== void 0) {
174
+ attachAdvertiseHeaders(res, opts?.advertise);
175
+ await sendWebResponse(res, paywall.buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));
176
+ return;
177
+ }
172
178
  attachAdvertiseHeaders(res, opts?.advertise);
173
179
  await sendWebResponse(res, decision.response());
174
180
  } catch (err) {
@@ -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// ---------------------------------------------------------------------------\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 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","buildSeoPreviewResponse"],"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,EAAQA,qBAAA,CAAc,SAAS,CAAC,CAAA;AAC3C,EAAA,GAAA,CAAI,SAAA,CAAU,eAAA,EAAiBC,0BAAA,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,IACNC,eAAA,CAAQ,IAAA,EAAM;AAAA,IACZ,gBAAA,EACE,IAAA,EAAM,gBAAA,IAAoBC,mCAAA,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,KAAKC,+BAAA,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,KAAKA,+BAAA,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.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"]}
package/dist/index.d.cts CHANGED
@@ -71,6 +71,21 @@ interface ExpressAdapterOptions extends VerivyxOptions {
71
71
  * Default undefined = OFF (no headers added; existing behavior unchanged).
72
72
  */
73
73
  advertise?: DiscoveryOptions;
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.
78
+ *
79
+ * Used by both `protect()` (when set on the factory opts) and `middleware()`.
80
+ * `protect()` also accepts `seoPreview` in its per-call options `o`; if both
81
+ * are set, the per-call value takes precedence.
82
+ */
83
+ seoPreview?: (ctx: {
84
+ slug: string;
85
+ }) => {
86
+ title: string;
87
+ excerpt: string;
88
+ };
74
89
  }
75
90
  /**
76
91
  * Convert a Web API `Response` to an Express response.
package/dist/index.d.ts CHANGED
@@ -71,6 +71,21 @@ interface ExpressAdapterOptions extends VerivyxOptions {
71
71
  * Default undefined = OFF (no headers added; existing behavior unchanged).
72
72
  */
73
73
  advertise?: DiscoveryOptions;
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.
78
+ *
79
+ * Used by both `protect()` (when set on the factory opts) and `middleware()`.
80
+ * `protect()` also accepts `seoPreview` in its per-call options `o`; if both
81
+ * are set, the per-call value takes precedence.
82
+ */
83
+ seoPreview?: (ctx: {
84
+ slug: string;
85
+ }) => {
86
+ title: string;
87
+ excerpt: string;
88
+ };
74
89
  }
75
90
  /**
76
91
  * Convert a Web API `Response` to an Express response.
package/dist/index.js CHANGED
@@ -167,6 +167,12 @@ function verivyxExpress(opts) {
167
167
  }
168
168
  return next();
169
169
  }
170
+ const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
171
+ if (isPreviewCandidate && opts?.seoPreview !== void 0) {
172
+ attachAdvertiseHeaders(res, opts?.advertise);
173
+ await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, opts.seoPreview));
174
+ return;
175
+ }
170
176
  attachAdvertiseHeaders(res, opts?.advertise);
171
177
  await sendWebResponse(res, decision.response());
172
178
  } catch (err) {
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-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// ---------------------------------------------------------------------------\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 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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verivyx/paywall-express",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Express adapter for the Verivyx paywall SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",