@verivyx/paywall-express 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Verivyx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @verivyx/paywall-express
2
+
3
+ Express adapter for the Verivyx paywall SDK — gate content from AI bots and charge agents on-chain via x402, with zero-config reverse-DNS crawler verification for SEO safety.
4
+
5
+ Requires `@verivyx/paywall` (installed automatically as a dependency) and `express` (peer dependency).
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i @verivyx/paywall-express
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```ts
16
+ import express from "express";
17
+ import { verivyxExpress } from "@verivyx/paywall-express";
18
+
19
+ const app = express();
20
+
21
+ // Create an adapter (reads VERIVYX_TOKEN + VERIVYX_DOMAIN from env)
22
+ const vx = verivyxExpress();
23
+
24
+ // Gate a route — verified/paid requests pass through; bots get a 402
25
+ app.get("/articles/:slug", vx.protect(async (req, res) => {
26
+ res.json({ content: "..." });
27
+ }));
28
+
29
+ app.listen(3000);
30
+ ```
31
+
32
+ ### With SEO preview
33
+
34
+ ```ts
35
+ app.get("/articles/:slug", vx.protect(myHandler, {
36
+ seoPreview: ({ slug }) => ({
37
+ title: "Article title",
38
+ excerpt: "A short teaser visible to search crawlers.",
39
+ }),
40
+ }));
41
+ ```
42
+
43
+ ## Config
44
+
45
+ All options can be passed to `verivyxExpress(opts)` or set via environment variables.
46
+
47
+ | Env var | Required | Description |
48
+ |---|---|---|
49
+ | `VERIVYX_TOKEN` | yes (server-only) | Domain provisioning token from the Verivyx dashboard |
50
+ | `VERIVYX_DOMAIN` | yes | Your site domain, e.g. `example.com` |
51
+ | `VERIVYX_MATCH` | no | Comma-separated glob patterns to gate (e.g. `/articles/**`). Empty = gate all routes. Also accepts `string[]` in code. |
52
+ | `VERIVYX_FAIL_MODE` | no | Behaviour when the Verivyx backend is unreachable: `teaser` (default) \| `open` \| `closed` |
53
+ | `VERIVYX_TIMEOUT_MS` | no | Backend request timeout in milliseconds (default `800`) |
54
+
55
+ Additional code-only options: `trustProxy` (default `true`), `clientIp`, `advertise` (RSL/AIPREF discovery headers).
56
+
57
+ ## Docs
58
+
59
+ [https://docs.verivyx.com/docs/sdk](https://docs.verivyx.com/docs/sdk)
package/dist/index.cjs ADDED
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ var paywall = require('@verivyx/paywall');
4
+
5
+ // src/index.ts
6
+ function firstHop(xff) {
7
+ if (xff === void 0) {
8
+ return void 0;
9
+ }
10
+ const raw = Array.isArray(xff) ? xff[0] : xff;
11
+ if (!raw) {
12
+ return void 0;
13
+ }
14
+ const first = raw.split(",")[0];
15
+ return first !== void 0 ? first.trim() || void 0 : void 0;
16
+ }
17
+ function lastPathSegment(path) {
18
+ const segments = path.split("/").filter((s) => s.length > 0);
19
+ const last = segments[segments.length - 1];
20
+ return last !== void 0 ? decodeURIComponent(last) : "";
21
+ }
22
+ function toWebHeaders(nodeHeaders, ip) {
23
+ const headers = new Headers();
24
+ for (const [key, value] of Object.entries(nodeHeaders)) {
25
+ if (value === void 0) {
26
+ continue;
27
+ }
28
+ if (Array.isArray(value)) {
29
+ for (const v of value) {
30
+ headers.append(key, v);
31
+ }
32
+ } else {
33
+ headers.set(key, value);
34
+ }
35
+ }
36
+ if (ip !== void 0) {
37
+ headers.set("x-real-ip", ip);
38
+ }
39
+ return headers;
40
+ }
41
+ function resolveIp(req, opts) {
42
+ if (opts?.clientIp) {
43
+ return opts.clientIp(req);
44
+ }
45
+ if (opts?.trustProxy !== false) {
46
+ const xff = req.headers["x-forwarded-for"];
47
+ const hop = firstHop(xff);
48
+ if (hop) {
49
+ return hop;
50
+ }
51
+ const xri = req.headers["x-real-ip"];
52
+ if (typeof xri === "string" && xri) {
53
+ return xri;
54
+ }
55
+ }
56
+ return req.socket.remoteAddress;
57
+ }
58
+ async function readRawBody(req) {
59
+ const method = req.method.toUpperCase();
60
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
61
+ return void 0;
62
+ }
63
+ const body = req.body;
64
+ if (Buffer.isBuffer(body)) {
65
+ return new Uint8Array(body);
66
+ }
67
+ return new Promise((resolve, reject) => {
68
+ const chunks = [];
69
+ req.on("data", (chunk) => chunks.push(chunk));
70
+ req.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
71
+ req.on("error", reject);
72
+ });
73
+ }
74
+ function attachAdvertiseHeaders(res, advertise) {
75
+ if (advertise === void 0) {
76
+ return;
77
+ }
78
+ res.append("Link", paywall.rslLinkHeader(advertise));
79
+ res.setHeader("Content-Usage", paywall.contentUsageHeader(advertise));
80
+ }
81
+ async function sendWebResponse(res, webRes) {
82
+ res.status(webRes.status);
83
+ webRes.headers.forEach((value, key) => {
84
+ if (key.toLowerCase() === "set-cookie") {
85
+ res.append(key, value);
86
+ } else {
87
+ res.setHeader(key, value);
88
+ }
89
+ });
90
+ const buf = Buffer.from(await webRes.arrayBuffer());
91
+ res.send(buf);
92
+ }
93
+ function verivyxExpress(opts) {
94
+ const vx = opts?._core ?? paywall.verivyx(opts, {
95
+ verifyCrawlerDns: opts?.verifyCrawlerDns ?? paywall.createSearchCrawlerVerifier(),
96
+ ...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
97
+ });
98
+ return {
99
+ protect(handler, o) {
100
+ return async function verivyxGuard(req, res, next) {
101
+ 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
+ });
118
+ const slug = req.params.slug ?? lastPathSegment(req.path);
119
+ const decision = await vx.protect(webReq, { slug });
120
+ if (!decision.allowed) {
121
+ const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
122
+ if (isPreviewCandidate && o?.seoPreview !== void 0) {
123
+ attachAdvertiseHeaders(res, opts?.advertise);
124
+ await sendWebResponse(res, paywall.buildSeoPreviewResponse(slug, absoluteUrl, o.seoPreview));
125
+ return;
126
+ }
127
+ attachAdvertiseHeaders(res, opts?.advertise);
128
+ await sendWebResponse(res, decision.response());
129
+ return;
130
+ }
131
+ if (decision.paymentResponse !== void 0) {
132
+ res.setHeader("PAYMENT-RESPONSE", decision.paymentResponse);
133
+ }
134
+ attachAdvertiseHeaders(res, opts?.advertise);
135
+ handler(req, res, next);
136
+ } catch (err) {
137
+ next(err);
138
+ }
139
+ };
140
+ }
141
+ };
142
+ }
143
+
144
+ exports.sendWebResponse = sendWebResponse;
145
+ exports.verivyxExpress = verivyxExpress;
146
+ //# sourceMappingURL=index.cjs.map
147
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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 * 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"]}
@@ -0,0 +1,110 @@
1
+ import { Request as Request$1, Response as Response$1, RequestHandler } from 'express';
2
+ import { VerivyxOptions, Verivyx, DiscoveryOptions } from '@verivyx/paywall';
3
+
4
+ /**
5
+ * @verivyx/paywall-express
6
+ *
7
+ * Express adapter for the Verivyx paywall SDK.
8
+ *
9
+ * Thin layer — all gate logic lives in @verivyx/paywall core.
10
+ * This module only handles:
11
+ * 1. IP resolution from Express request.
12
+ * 2. Converting Express IncomingMessage → Web Request.
13
+ * 3. Calling core `protect()` (decision overload).
14
+ * 4. Converting the Web Response back to Express response, OR calling the
15
+ * original Express handler when the request is allowed through.
16
+ *
17
+ * Mock-injection seam (for Tasks 18/19 to reuse):
18
+ * Pass `_core: verivyx.mock({...})` in options to bypass network.
19
+ * In production: omit `_core`; the adapter instantiates `verivyx(opts, deps)`.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { verivyxExpress } from "@verivyx/paywall-express";
24
+ * const vx = verivyxExpress({ domain: "example.com", token: process.env.VX_TOKEN });
25
+ * app.get("/articles/:slug", vx.protect(myHandler));
26
+ * ```
27
+ */
28
+
29
+ /**
30
+ * Options for the Express adapter.
31
+ * Extends core VerivyxOptions with Express-specific IP resolution controls.
32
+ */
33
+ interface ExpressAdapterOptions extends VerivyxOptions {
34
+ /**
35
+ * When true (default), read the client IP from `X-Forwarded-For` /
36
+ * `X-Real-IP` headers (set by a trusted reverse proxy). Set to false if
37
+ * the Express app is not behind a proxy, to use the raw socket address.
38
+ */
39
+ trustProxy?: boolean;
40
+ /**
41
+ * Custom IP extractor. When provided, this overrides the built-in
42
+ * `trustProxy` / `X-Forwarded-For` logic entirely.
43
+ *
44
+ * @param req - The Express request object.
45
+ * @returns The resolved client IP, or undefined to fall back to socket.
46
+ */
47
+ clientIp?: (req: Request$1) => string | undefined;
48
+ /**
49
+ * Override the reverse-DNS search-crawler verifier injected into the core.
50
+ * When omitted, `createSearchCrawlerVerifier()` is used (the Node.js impl).
51
+ */
52
+ verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;
53
+ /**
54
+ * Override the Web Bot Auth verifier injected into the core.
55
+ * When omitted, the core's bundled RFC 9421 verifier is used.
56
+ */
57
+ verifyWebBotAuth?: (req: Request) => Promise<boolean>;
58
+ /**
59
+ * @internal
60
+ * Inject a pre-built `Verivyx` core instance. Used in tests via
61
+ * `verivyx.mock({...})` to avoid any network access. Production code
62
+ * should never set this; omit it and the adapter constructs the real core.
63
+ *
64
+ * Pattern for Next.js / Hono adapters to reuse:
65
+ * `const vx = opts?._core ?? verivyx(opts, { verifyCrawlerDns });`
66
+ */
67
+ _core?: Verivyx;
68
+ /**
69
+ * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both
70
+ * the denied (402) and allowed handler responses.
71
+ * Default undefined = OFF (no headers added; existing behavior unchanged).
72
+ */
73
+ advertise?: DiscoveryOptions;
74
+ }
75
+ /**
76
+ * Convert a Web API `Response` to an Express response.
77
+ * Copies status, all response headers, and the body.
78
+ *
79
+ * @internal Exported for unit-testing the Set-Cookie accumulation fix.
80
+ * Production callers should use the `protect()` middleware instead.
81
+ *
82
+ * Set-Cookie is special: `Headers.forEach` emits each cookie as a separate
83
+ * call with the same key, and `res.setHeader` REPLACES on repeated same-key
84
+ * calls — so all but the last cookie would be silently dropped.
85
+ * `res.append` accumulates values into an array, preserving every cookie.
86
+ */
87
+ declare function sendWebResponse(res: Response$1, webRes: Response): Promise<void>;
88
+ /**
89
+ * Create a Verivyx Express adapter.
90
+ *
91
+ * Returns an object with a single `protect(handler)` method that wraps an
92
+ * Express `RequestHandler` behind the Verivyx paywall gate.
93
+ *
94
+ * ```ts
95
+ * const vx = verivyxExpress({ domain: "example.com", token: "..." });
96
+ * app.get("/articles/:slug", vx.protect(myHandler));
97
+ * ```
98
+ */
99
+ declare function verivyxExpress(opts?: ExpressAdapterOptions): {
100
+ protect(handler: RequestHandler, o?: {
101
+ seoPreview?: (c: {
102
+ slug: string;
103
+ }) => {
104
+ title: string;
105
+ excerpt: string;
106
+ };
107
+ }): RequestHandler;
108
+ };
109
+
110
+ export { type ExpressAdapterOptions, sendWebResponse, verivyxExpress };
@@ -0,0 +1,110 @@
1
+ import { Request as Request$1, Response as Response$1, RequestHandler } from 'express';
2
+ import { VerivyxOptions, Verivyx, DiscoveryOptions } from '@verivyx/paywall';
3
+
4
+ /**
5
+ * @verivyx/paywall-express
6
+ *
7
+ * Express adapter for the Verivyx paywall SDK.
8
+ *
9
+ * Thin layer — all gate logic lives in @verivyx/paywall core.
10
+ * This module only handles:
11
+ * 1. IP resolution from Express request.
12
+ * 2. Converting Express IncomingMessage → Web Request.
13
+ * 3. Calling core `protect()` (decision overload).
14
+ * 4. Converting the Web Response back to Express response, OR calling the
15
+ * original Express handler when the request is allowed through.
16
+ *
17
+ * Mock-injection seam (for Tasks 18/19 to reuse):
18
+ * Pass `_core: verivyx.mock({...})` in options to bypass network.
19
+ * In production: omit `_core`; the adapter instantiates `verivyx(opts, deps)`.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { verivyxExpress } from "@verivyx/paywall-express";
24
+ * const vx = verivyxExpress({ domain: "example.com", token: process.env.VX_TOKEN });
25
+ * app.get("/articles/:slug", vx.protect(myHandler));
26
+ * ```
27
+ */
28
+
29
+ /**
30
+ * Options for the Express adapter.
31
+ * Extends core VerivyxOptions with Express-specific IP resolution controls.
32
+ */
33
+ interface ExpressAdapterOptions extends VerivyxOptions {
34
+ /**
35
+ * When true (default), read the client IP from `X-Forwarded-For` /
36
+ * `X-Real-IP` headers (set by a trusted reverse proxy). Set to false if
37
+ * the Express app is not behind a proxy, to use the raw socket address.
38
+ */
39
+ trustProxy?: boolean;
40
+ /**
41
+ * Custom IP extractor. When provided, this overrides the built-in
42
+ * `trustProxy` / `X-Forwarded-For` logic entirely.
43
+ *
44
+ * @param req - The Express request object.
45
+ * @returns The resolved client IP, or undefined to fall back to socket.
46
+ */
47
+ clientIp?: (req: Request$1) => string | undefined;
48
+ /**
49
+ * Override the reverse-DNS search-crawler verifier injected into the core.
50
+ * When omitted, `createSearchCrawlerVerifier()` is used (the Node.js impl).
51
+ */
52
+ verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;
53
+ /**
54
+ * Override the Web Bot Auth verifier injected into the core.
55
+ * When omitted, the core's bundled RFC 9421 verifier is used.
56
+ */
57
+ verifyWebBotAuth?: (req: Request) => Promise<boolean>;
58
+ /**
59
+ * @internal
60
+ * Inject a pre-built `Verivyx` core instance. Used in tests via
61
+ * `verivyx.mock({...})` to avoid any network access. Production code
62
+ * should never set this; omit it and the adapter constructs the real core.
63
+ *
64
+ * Pattern for Next.js / Hono adapters to reuse:
65
+ * `const vx = opts?._core ?? verivyx(opts, { verifyCrawlerDns });`
66
+ */
67
+ _core?: Verivyx;
68
+ /**
69
+ * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both
70
+ * the denied (402) and allowed handler responses.
71
+ * Default undefined = OFF (no headers added; existing behavior unchanged).
72
+ */
73
+ advertise?: DiscoveryOptions;
74
+ }
75
+ /**
76
+ * Convert a Web API `Response` to an Express response.
77
+ * Copies status, all response headers, and the body.
78
+ *
79
+ * @internal Exported for unit-testing the Set-Cookie accumulation fix.
80
+ * Production callers should use the `protect()` middleware instead.
81
+ *
82
+ * Set-Cookie is special: `Headers.forEach` emits each cookie as a separate
83
+ * call with the same key, and `res.setHeader` REPLACES on repeated same-key
84
+ * calls — so all but the last cookie would be silently dropped.
85
+ * `res.append` accumulates values into an array, preserving every cookie.
86
+ */
87
+ declare function sendWebResponse(res: Response$1, webRes: Response): Promise<void>;
88
+ /**
89
+ * Create a Verivyx Express adapter.
90
+ *
91
+ * Returns an object with a single `protect(handler)` method that wraps an
92
+ * Express `RequestHandler` behind the Verivyx paywall gate.
93
+ *
94
+ * ```ts
95
+ * const vx = verivyxExpress({ domain: "example.com", token: "..." });
96
+ * app.get("/articles/:slug", vx.protect(myHandler));
97
+ * ```
98
+ */
99
+ declare function verivyxExpress(opts?: ExpressAdapterOptions): {
100
+ protect(handler: RequestHandler, o?: {
101
+ seoPreview?: (c: {
102
+ slug: string;
103
+ }) => {
104
+ title: string;
105
+ excerpt: string;
106
+ };
107
+ }): RequestHandler;
108
+ };
109
+
110
+ export { type ExpressAdapterOptions, sendWebResponse, verivyxExpress };
package/dist/index.js ADDED
@@ -0,0 +1,144 @@
1
+ import { verivyx, createSearchCrawlerVerifier, buildSeoPreviewResponse, rslLinkHeader, contentUsageHeader } from '@verivyx/paywall';
2
+
3
+ // src/index.ts
4
+ function firstHop(xff) {
5
+ if (xff === void 0) {
6
+ return void 0;
7
+ }
8
+ const raw = Array.isArray(xff) ? xff[0] : xff;
9
+ if (!raw) {
10
+ return void 0;
11
+ }
12
+ const first = raw.split(",")[0];
13
+ return first !== void 0 ? first.trim() || void 0 : void 0;
14
+ }
15
+ function lastPathSegment(path) {
16
+ const segments = path.split("/").filter((s) => s.length > 0);
17
+ const last = segments[segments.length - 1];
18
+ return last !== void 0 ? decodeURIComponent(last) : "";
19
+ }
20
+ function toWebHeaders(nodeHeaders, ip) {
21
+ const headers = new Headers();
22
+ for (const [key, value] of Object.entries(nodeHeaders)) {
23
+ if (value === void 0) {
24
+ continue;
25
+ }
26
+ if (Array.isArray(value)) {
27
+ for (const v of value) {
28
+ headers.append(key, v);
29
+ }
30
+ } else {
31
+ headers.set(key, value);
32
+ }
33
+ }
34
+ if (ip !== void 0) {
35
+ headers.set("x-real-ip", ip);
36
+ }
37
+ return headers;
38
+ }
39
+ function resolveIp(req, opts) {
40
+ if (opts?.clientIp) {
41
+ return opts.clientIp(req);
42
+ }
43
+ if (opts?.trustProxy !== false) {
44
+ const xff = req.headers["x-forwarded-for"];
45
+ const hop = firstHop(xff);
46
+ if (hop) {
47
+ return hop;
48
+ }
49
+ const xri = req.headers["x-real-ip"];
50
+ if (typeof xri === "string" && xri) {
51
+ return xri;
52
+ }
53
+ }
54
+ return req.socket.remoteAddress;
55
+ }
56
+ async function readRawBody(req) {
57
+ const method = req.method.toUpperCase();
58
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
59
+ return void 0;
60
+ }
61
+ const body = req.body;
62
+ if (Buffer.isBuffer(body)) {
63
+ return new Uint8Array(body);
64
+ }
65
+ return new Promise((resolve, reject) => {
66
+ const chunks = [];
67
+ req.on("data", (chunk) => chunks.push(chunk));
68
+ req.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
69
+ req.on("error", reject);
70
+ });
71
+ }
72
+ function attachAdvertiseHeaders(res, advertise) {
73
+ if (advertise === void 0) {
74
+ return;
75
+ }
76
+ res.append("Link", rslLinkHeader(advertise));
77
+ res.setHeader("Content-Usage", contentUsageHeader(advertise));
78
+ }
79
+ async function sendWebResponse(res, webRes) {
80
+ res.status(webRes.status);
81
+ webRes.headers.forEach((value, key) => {
82
+ if (key.toLowerCase() === "set-cookie") {
83
+ res.append(key, value);
84
+ } else {
85
+ res.setHeader(key, value);
86
+ }
87
+ });
88
+ const buf = Buffer.from(await webRes.arrayBuffer());
89
+ res.send(buf);
90
+ }
91
+ function verivyxExpress(opts) {
92
+ const vx = opts?._core ?? verivyx(opts, {
93
+ verifyCrawlerDns: opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),
94
+ ...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
95
+ });
96
+ return {
97
+ protect(handler, o) {
98
+ return async function verivyxGuard(req, res, next) {
99
+ 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
+ });
116
+ const slug = req.params.slug ?? lastPathSegment(req.path);
117
+ const decision = await vx.protect(webReq, { slug });
118
+ if (!decision.allowed) {
119
+ const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
120
+ if (isPreviewCandidate && o?.seoPreview !== void 0) {
121
+ attachAdvertiseHeaders(res, opts?.advertise);
122
+ await sendWebResponse(res, buildSeoPreviewResponse(slug, absoluteUrl, o.seoPreview));
123
+ return;
124
+ }
125
+ attachAdvertiseHeaders(res, opts?.advertise);
126
+ await sendWebResponse(res, decision.response());
127
+ return;
128
+ }
129
+ if (decision.paymentResponse !== void 0) {
130
+ res.setHeader("PAYMENT-RESPONSE", decision.paymentResponse);
131
+ }
132
+ attachAdvertiseHeaders(res, opts?.advertise);
133
+ handler(req, res, next);
134
+ } catch (err) {
135
+ next(err);
136
+ }
137
+ };
138
+ }
139
+ };
140
+ }
141
+
142
+ export { sendWebResponse, verivyxExpress };
143
+ //# sourceMappingURL=index.js.map
144
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@verivyx/paywall-express",
3
+ "version": "0.1.0",
4
+ "description": "Express adapter for the Verivyx paywall SDK",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Verivyx",
8
+ "sideEffects": false,
9
+ "keywords": ["paywall", "x402", "ai", "bot", "stellar", "verivyx"],
10
+ "homepage": "https://docs.verivyx.com/docs/sdk",
11
+ "bugs": { "url": "https://github.com/VerivyX/verivyx-monorepo/issues" },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/VerivyX/verivyx-monorepo.git",
15
+ "directory": "services/publisher-sdk/packages/express"
16
+ },
17
+ "publishConfig": { "access": "public" },
18
+ "exports": {
19
+ ".": {
20
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
21
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
22
+ }
23
+ },
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.cts",
27
+ "files": ["dist", "README.md", "LICENSE"],
28
+ "engines": { "node": ">=18" },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "prepublishOnly": "npm run build",
32
+ "test": "vitest run",
33
+ "typecheck": "tsc --noEmit",
34
+ "typecheck:test": "tsc -p tsconfig.test.json"
35
+ },
36
+ "dependencies": {
37
+ "@verivyx/paywall": "^0.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "express": ">=4.18 <6 || ^5"
41
+ },
42
+ "devDependencies": {
43
+ "express": "^5.0.0",
44
+ "supertest": "^7.0.0",
45
+ "@types/express": "^5.0.0",
46
+ "@types/supertest": "^6.0.0",
47
+ "@types/node": "^20.0.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.5.0",
50
+ "vitest": "^2.0.0"
51
+ }
52
+ }