@verivyx/paywall-express 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -71,6 +71,35 @@ async function readRawBody(req) {
71
71
  req.on("error", reject);
72
72
  });
73
73
  }
74
+ function pathMatchesAny(pathname, patterns) {
75
+ for (const pattern of patterns) {
76
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
77
+ const regexStr = "^" + escaped.replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*") + "$";
78
+ if (new RegExp(regexStr).test(pathname)) {
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ async function buildWebRequest(req, opts) {
85
+ const ip = resolveIp(req, opts);
86
+ const host = (typeof req.get === "function" ? req.get("host") : void 0) ?? (typeof req.headers.host === "string" ? req.headers.host : void 0) ?? "localhost";
87
+ const protocol = req.protocol ?? "http";
88
+ const absoluteUrl = `${protocol}://${host}${req.originalUrl}`;
89
+ const webHeaders = toWebHeaders(req.headers, ip);
90
+ let rawBody;
91
+ try {
92
+ rawBody = await readRawBody(req);
93
+ } catch {
94
+ rawBody = void 0;
95
+ }
96
+ return new Request(absoluteUrl, {
97
+ method: req.method,
98
+ headers: webHeaders,
99
+ body: rawBody !== void 0 ? rawBody : void 0,
100
+ ...rawBody !== void 0 ? { duplex: "half" } : {}
101
+ });
102
+ }
74
103
  function attachAdvertiseHeaders(res, advertise) {
75
104
  if (advertise === void 0) {
76
105
  return;
@@ -99,29 +128,14 @@ function verivyxExpress(opts) {
99
128
  protect(handler, o) {
100
129
  return async function verivyxGuard(req, res, next) {
101
130
  try {
102
- const ip = resolveIp(req, opts);
103
- const absoluteUrl = `${req.protocol}://${req.get("host") ?? "localhost"}${req.originalUrl}`;
104
- const webHeaders = toWebHeaders(req.headers, ip);
105
- let rawBody;
106
- try {
107
- rawBody = await readRawBody(req);
108
- } catch {
109
- rawBody = void 0;
110
- }
111
- const webReq = new Request(absoluteUrl, {
112
- method: req.method,
113
- headers: webHeaders,
114
- body: rawBody !== void 0 ? rawBody : void 0,
115
- // duplex required for request bodies in some environments
116
- ...rawBody !== void 0 ? { duplex: "half" } : {}
117
- });
131
+ const webReq = await buildWebRequest(req, opts);
118
132
  const slug = req.params.slug ?? lastPathSegment(req.path);
119
133
  const decision = await vx.protect(webReq, { slug });
120
134
  if (!decision.allowed) {
121
135
  const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
122
136
  if (isPreviewCandidate && o?.seoPreview !== void 0) {
123
137
  attachAdvertiseHeaders(res, opts?.advertise);
124
- await sendWebResponse(res, paywall.buildSeoPreviewResponse(slug, absoluteUrl, o.seoPreview));
138
+ await sendWebResponse(res, paywall.buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));
125
139
  return;
126
140
  }
127
141
  attachAdvertiseHeaders(res, opts?.advertise);
@@ -137,11 +151,39 @@ function verivyxExpress(opts) {
137
151
  next(err);
138
152
  }
139
153
  };
154
+ },
155
+ middleware() {
156
+ return async (req, res, next) => {
157
+ try {
158
+ const rawPath = req.originalUrl ?? req.url ?? "/";
159
+ const pathname = rawPath.split("?")[0];
160
+ if (opts?.match !== void 0 && opts.match.length > 0 && !pathMatchesAny(pathname, opts.match)) {
161
+ return next();
162
+ }
163
+ const webReq = await buildWebRequest(req, opts);
164
+ const slug = pathname.split("/").filter(Boolean).pop() ?? "";
165
+ const decision = await vx.protect(webReq, { slug });
166
+ if (decision.allowed) {
167
+ if (decision.paymentResponse !== void 0) {
168
+ res.setHeader("PAYMENT-RESPONSE", decision.paymentResponse);
169
+ }
170
+ return next();
171
+ }
172
+ attachAdvertiseHeaders(res, opts?.advertise);
173
+ await sendWebResponse(res, decision.response());
174
+ } catch (err) {
175
+ next(err);
176
+ }
177
+ };
140
178
  }
141
179
  };
142
180
  }
181
+ function verivyxMiddleware(opts) {
182
+ return verivyxExpress(opts).middleware();
183
+ }
143
184
 
144
185
  exports.sendWebResponse = sendWebResponse;
145
186
  exports.verivyxExpress = verivyxExpress;
187
+ exports.verivyxMiddleware = verivyxMiddleware;
146
188
  //# sourceMappingURL=index.cjs.map
147
189
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","buildSeoPreviewResponse"],"mappings":";;;;;AAqGA,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;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,EAK7B;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,EAAA,GAAK,SAAA,CAAU,GAAA,EAAK,IAAI,CAAA;AAI9B,UAAA,MAAM,WAAA,GAAc,CAAA,EAAG,GAAA,CAAI,QAAQ,CAAA,GAAA,EAAM,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA,IAAK,WAAW,CAAA,EAAG,GAAA,CAAI,WAAW,CAAA,CAAA;AACzF,UAAA,MAAM,UAAA,GAAa,YAAA,CAAa,GAAA,CAAI,OAAA,EAAS,EAAE,CAAA;AAE/C,UAAA,IAAI,OAAA;AACJ,UAAA,IAAI;AACF,YAAA,OAAA,GAAU,MAAM,YAAY,GAAG,CAAA;AAAA,UACjC,CAAA,CAAA,MAAQ;AAEN,YAAA,OAAA,GAAU,KAAA,CAAA;AAAA,UACZ;AAEA,UAAA,MAAM,MAAA,GAAS,IAAI,OAAA,CAAQ,WAAA,EAAa;AAAA,YACtC,QAAQ,GAAA,CAAI,MAAA;AAAA,YACZ,OAAA,EAAS,UAAA;AAAA,YACT,IAAA,EAAM,OAAA,KAAY,KAAA,CAAA,GAAY,OAAA,GAAU,KAAA,CAAA;AAAA;AAAA,YAExC,GAAI,OAAA,KAAY,KAAA,CAAA,GAAY,EAAE,MAAA,EAAQ,MAAA,KAAW;AAAC,WACpC,CAAA;AAGhB,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,gBAAgB,GAAA,EAAKC,+BAAA,CAAwB,MAAM,WAAA,EAAa,CAAA,CAAE,UAAU,CAAC,CAAA;AACnF,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;AAAA,GACF;AACF","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 * 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} {\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. Resolve trusted client IP.\n const ip = resolveIp(req, opts);\n\n // 2. Build a Web Request from the Express request.\n // Absolute URL is required for the core's URL-based slug derivation.\n const absoluteUrl = `${req.protocol}://${req.get(\"host\") ?? \"localhost\"}${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 // Body read failure — proceed with no body (safe: auth/classify do not need it).\n rawBody = undefined;\n }\n\n const webReq = new Request(absoluteUrl, {\n method: req.method,\n headers: webHeaders,\n body: rawBody !== undefined ? rawBody : undefined,\n // duplex required for request bodies in some environments\n ...(rawBody !== undefined ? { duplex: \"half\" } : {}),\n } as RequestInit);\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, absoluteUrl, 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}\n"]}
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"]}
package/dist/index.d.cts CHANGED
@@ -105,6 +105,16 @@ declare function verivyxExpress(opts?: ExpressAdapterOptions): {
105
105
  excerpt: string;
106
106
  };
107
107
  }): RequestHandler;
108
+ middleware(): RequestHandler;
108
109
  };
110
+ /**
111
+ * Convenience export: create a Verivyx Express app-level middleware in one call.
112
+ *
113
+ * ```ts
114
+ * import { verivyxMiddleware } from "@verivyx/paywall-express";
115
+ * app.use(verivyxMiddleware({ domain: "example.com", token: process.env.VX_TOKEN }));
116
+ * ```
117
+ */
118
+ declare function verivyxMiddleware(opts?: ExpressAdapterOptions): RequestHandler;
109
119
 
110
- export { type ExpressAdapterOptions, sendWebResponse, verivyxExpress };
120
+ export { type ExpressAdapterOptions, sendWebResponse, verivyxExpress, verivyxMiddleware };
package/dist/index.d.ts CHANGED
@@ -105,6 +105,16 @@ declare function verivyxExpress(opts?: ExpressAdapterOptions): {
105
105
  excerpt: string;
106
106
  };
107
107
  }): RequestHandler;
108
+ middleware(): RequestHandler;
108
109
  };
110
+ /**
111
+ * Convenience export: create a Verivyx Express app-level middleware in one call.
112
+ *
113
+ * ```ts
114
+ * import { verivyxMiddleware } from "@verivyx/paywall-express";
115
+ * app.use(verivyxMiddleware({ domain: "example.com", token: process.env.VX_TOKEN }));
116
+ * ```
117
+ */
118
+ declare function verivyxMiddleware(opts?: ExpressAdapterOptions): RequestHandler;
109
119
 
110
- export { type ExpressAdapterOptions, sendWebResponse, verivyxExpress };
120
+ export { type ExpressAdapterOptions, sendWebResponse, verivyxExpress, verivyxMiddleware };
package/dist/index.js CHANGED
@@ -69,6 +69,35 @@ async function readRawBody(req) {
69
69
  req.on("error", reject);
70
70
  });
71
71
  }
72
+ function pathMatchesAny(pathname, patterns) {
73
+ for (const pattern of patterns) {
74
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
75
+ const regexStr = "^" + escaped.replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*") + "$";
76
+ if (new RegExp(regexStr).test(pathname)) {
77
+ return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ async function buildWebRequest(req, opts) {
83
+ const ip = resolveIp(req, opts);
84
+ const host = (typeof req.get === "function" ? req.get("host") : void 0) ?? (typeof req.headers.host === "string" ? req.headers.host : void 0) ?? "localhost";
85
+ const protocol = req.protocol ?? "http";
86
+ const absoluteUrl = `${protocol}://${host}${req.originalUrl}`;
87
+ const webHeaders = toWebHeaders(req.headers, ip);
88
+ let rawBody;
89
+ try {
90
+ rawBody = await readRawBody(req);
91
+ } catch {
92
+ rawBody = void 0;
93
+ }
94
+ return new Request(absoluteUrl, {
95
+ method: req.method,
96
+ headers: webHeaders,
97
+ body: rawBody !== void 0 ? rawBody : void 0,
98
+ ...rawBody !== void 0 ? { duplex: "half" } : {}
99
+ });
100
+ }
72
101
  function attachAdvertiseHeaders(res, advertise) {
73
102
  if (advertise === void 0) {
74
103
  return;
@@ -97,29 +126,14 @@ function verivyxExpress(opts) {
97
126
  protect(handler, o) {
98
127
  return async function verivyxGuard(req, res, next) {
99
128
  try {
100
- const ip = resolveIp(req, opts);
101
- const absoluteUrl = `${req.protocol}://${req.get("host") ?? "localhost"}${req.originalUrl}`;
102
- const webHeaders = toWebHeaders(req.headers, ip);
103
- let rawBody;
104
- try {
105
- rawBody = await readRawBody(req);
106
- } catch {
107
- rawBody = void 0;
108
- }
109
- const webReq = new Request(absoluteUrl, {
110
- method: req.method,
111
- headers: webHeaders,
112
- body: rawBody !== void 0 ? rawBody : void 0,
113
- // duplex required for request bodies in some environments
114
- ...rawBody !== void 0 ? { duplex: "half" } : {}
115
- });
129
+ const webReq = await buildWebRequest(req, opts);
116
130
  const slug = req.params.slug ?? lastPathSegment(req.path);
117
131
  const decision = await vx.protect(webReq, { slug });
118
132
  if (!decision.allowed) {
119
133
  const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
120
134
  if (isPreviewCandidate && o?.seoPreview !== void 0) {
121
135
  attachAdvertiseHeaders(res, opts?.advertise);
122
- await sendWebResponse(res, buildSeoPreviewResponse(slug, absoluteUrl, o.seoPreview));
136
+ await sendWebResponse(res, buildSeoPreviewResponse(slug, webReq.url, o.seoPreview));
123
137
  return;
124
138
  }
125
139
  attachAdvertiseHeaders(res, opts?.advertise);
@@ -135,10 +149,37 @@ function verivyxExpress(opts) {
135
149
  next(err);
136
150
  }
137
151
  };
152
+ },
153
+ middleware() {
154
+ return async (req, res, next) => {
155
+ try {
156
+ const rawPath = req.originalUrl ?? req.url ?? "/";
157
+ const pathname = rawPath.split("?")[0];
158
+ if (opts?.match !== void 0 && opts.match.length > 0 && !pathMatchesAny(pathname, opts.match)) {
159
+ return next();
160
+ }
161
+ const webReq = await buildWebRequest(req, opts);
162
+ const slug = pathname.split("/").filter(Boolean).pop() ?? "";
163
+ const decision = await vx.protect(webReq, { slug });
164
+ if (decision.allowed) {
165
+ if (decision.paymentResponse !== void 0) {
166
+ res.setHeader("PAYMENT-RESPONSE", decision.paymentResponse);
167
+ }
168
+ return next();
169
+ }
170
+ attachAdvertiseHeaders(res, opts?.advertise);
171
+ await sendWebResponse(res, decision.response());
172
+ } catch (err) {
173
+ next(err);
174
+ }
175
+ };
138
176
  }
139
177
  };
140
178
  }
179
+ function verivyxMiddleware(opts) {
180
+ return verivyxExpress(opts).middleware();
181
+ }
141
182
 
142
- export { sendWebResponse, verivyxExpress };
183
+ export { sendWebResponse, verivyxExpress, verivyxMiddleware };
143
184
  //# sourceMappingURL=index.js.map
144
185
  //# sourceMappingURL=index.js.map
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 * 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} {\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. Resolve trusted client IP.\n const ip = resolveIp(req, opts);\n\n // 2. Build a Web Request from the Express request.\n // Absolute URL is required for the core's URL-based slug derivation.\n const absoluteUrl = `${req.protocol}://${req.get(\"host\") ?? \"localhost\"}${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 // Body read failure — proceed with no body (safe: auth/classify do not need it).\n rawBody = undefined;\n }\n\n const webReq = new Request(absoluteUrl, {\n method: req.method,\n headers: webHeaders,\n body: rawBody !== undefined ? rawBody : undefined,\n // duplex required for request bodies in some environments\n ...(rawBody !== undefined ? { duplex: \"half\" } : {}),\n } as RequestInit);\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, absoluteUrl, 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}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAqGA,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;AAEA,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// ---------------------------------------------------------------------------\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verivyx/paywall-express",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Express adapter for the Verivyx paywall SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",