@verivyx/paywall-hono 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +9 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -70,6 +70,11 @@ function resolveIp(c, trustProxy) {
|
|
|
70
70
|
const xri = c.req.header("x-real-ip");
|
|
71
71
|
return xri !== void 0 && xri !== "" ? xri : void 0;
|
|
72
72
|
}
|
|
73
|
+
function isBrowserNavigation(req) {
|
|
74
|
+
const secFetchMode = req.headers.get("sec-fetch-mode") ?? "";
|
|
75
|
+
const accept = req.headers.get("accept") ?? "";
|
|
76
|
+
return secFetchMode === "navigate" || accept.includes("text/html");
|
|
77
|
+
}
|
|
73
78
|
function withAdvertiseHeaders(res, advertise) {
|
|
74
79
|
if (advertise === void 0) {
|
|
75
80
|
return res;
|
|
@@ -109,8 +114,8 @@ function verivyxHono(opts) {
|
|
|
109
114
|
const slug = paramSlug !== void 0 && paramSlug !== "" ? paramSlug : lastPathSegment(new URL(raw.url).pathname);
|
|
110
115
|
const decision = await vx.protect(coreReq, { slug });
|
|
111
116
|
if (!decision.allowed) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
117
|
+
const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(c.req.raw);
|
|
118
|
+
if (previewable && o?.seoPreview !== void 0) {
|
|
114
119
|
return withAdvertiseHeaders(
|
|
115
120
|
paywall.buildSeoPreviewResponse(slug, publicUrl(raw, trustProxy), o.seoPreview),
|
|
116
121
|
opts?.advertise
|
|
@@ -142,8 +147,8 @@ function verivyxHono(opts) {
|
|
|
142
147
|
}
|
|
143
148
|
return;
|
|
144
149
|
}
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
150
|
+
const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(c.req.raw);
|
|
151
|
+
if (previewable && opts?.seoPreview !== void 0) {
|
|
147
152
|
return withAdvertiseHeaders(
|
|
148
153
|
paywall.buildSeoPreviewResponse(slug, publicUrl(c.req.raw, trustProxy), opts.seoPreview),
|
|
149
154
|
opts.advertise
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","buildSeoPreviewResponse","attachPaymentResponse"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @verivyx/paywall-hono\n *\n * Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge).\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module handles:\n * 1. IP resolution from Cloudflare / proxy headers (CF-Connecting-IP first).\n * 2. Cloning the Web `Request` with the resolved IP in `x-real-ip`.\n * 3. Calling core `protect()` (decision overload).\n * 4. Returning `decision.response()` when denied (handler NOT called).\n * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.\n *\n * Edge-portable: NO `node:*` imports. Uses only Web Platform APIs and Hono types.\n *\n * @example\n * ```ts\n * import { verivyxHono } from \"@verivyx/paywall-hono\";\n * const vx = verivyxHono({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * ```\n */\n\nimport {\n verivyx,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { Context, MiddlewareHandler } from \"hono\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Hono adapter.\n * Extends core VerivyxOptions with edge-specific controls.\n */\nexport interface HonoAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from Cloudflare / proxy headers:\n * CF-Connecting-IP → X-Forwarded-For first hop → X-Real-IP.\n * Set to false if running without a trusted proxy.\n */\n trustProxy?: boolean;\n\n /**\n * Override the reverse-DNS search-crawler verifier injected into the core.\n * When omitted, `createSearchCrawlerVerifier()` is used.\n */\n verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;\n\n /**\n * Override the Web Bot Auth verifier injected into the core.\n * When omitted, the core's bundled RFC 9421 verifier is used.\n */\n verifyWebBotAuth?: (req: Request) => Promise<boolean>;\n\n /**\n * @internal\n * Inject a pre-built `Verivyx` core instance. Used in tests via\n * `verivyx.mock({...})` to avoid any network access. Production code\n * should never set this; omit it and the adapter constructs the real core.\n */\n _core?: Verivyx;\n\n /**\n * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both\n * the denied (402) and allowed handler responses.\n * Default undefined = OFF (no headers added; existing behavior unchanged).\n */\n advertise?: DiscoveryOptions;\n\n /**\n * When set, unverified humans and search crawlers (reason: \"human-unverified\"\n * or \"crawler\") receive a 200 HTML teaser page instead of a bare 402.\n * Bots / agents (reason: \"bot-unpaid\") still get the 402 x402 response.\n *\n * Used by both `protect()` (when set on the factory opts) and `middleware()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header value.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | null | undefined): string | undefined {\n if (xff === null || xff === undefined || xff === \"\") {\n return undefined;\n }\n const first = xff.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Return true when `pathname` matches any of the `patterns`.\n * Supports `*` (single-segment wildcard) and `**` (multi-segment wildcard).\n * Performs an anchored full-path match.\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => {\n // Escape regex metacharacters except * which we handle specially.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Replace ** before * so the two-step substitution is order-safe.\n const regexStr = escaped\n .replace(/\\*\\*/g, \"\u0001\") // placeholder for **\n .replace(/\\*/g, \"[^/]*\") // single-segment wildcard\n .replace(/\u0001/g, \".*\"); // multi-segment wildcard\n return new RegExp(\"^\" + regexStr + \"$\").test(pathname);\n });\n}\n\n/**\n * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw request URL is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\n\n/**\n * Extract the last non-empty path segment from a URL pathname.\n * Used as a fallback slug when `c.req.param(\"slug\")` is unavailable.\n */\nfunction lastPathSegment(pathname: string): string {\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n if (last === undefined) {\n return \"\";\n }\n try {\n return decodeURIComponent(last);\n } catch {\n return last;\n }\n}\n\n/**\n * Resolve the trusted client IP from Hono context headers.\n *\n * Precedence (Cloudflare Workers best-practice order):\n * 1. CF-Connecting-IP — set by Cloudflare edge (single trusted value).\n * 2. X-Forwarded-For first hop — set by other proxies / Vercel.\n * 3. X-Real-IP — generic proxy header.\n * Returns undefined when trustProxy === false or no header is present.\n */\nfunction resolveIp(c: Context, trustProxy: boolean): string | undefined {\n if (!trustProxy) {\n return undefined;\n }\n const cfIp = c.req.header(\"cf-connecting-ip\");\n if (cfIp !== undefined && cfIp !== \"\") {\n return cfIp;\n }\n const xff = c.req.header(\"x-forwarded-for\");\n const hop = firstHop(xff);\n if (hop !== undefined) {\n return hop;\n }\n const xri = c.req.header(\"x-real-ip\");\n return xri !== undefined && xri !== \"\" ? xri : undefined;\n}\n\n/**\n * Attach RSL + AIPREF discovery headers to a Web Response by cloning it.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * Returns the same Response unchanged when `advertise` is undefined.\n */\nfunction withAdvertiseHeaders(res: Response, advertise: DiscoveryOptions | undefined): Response {\n if (advertise === undefined) {\n return res;\n }\n const headers = new Headers(res.headers);\n headers.append(\"Link\", rslLinkHeader(advertise));\n headers.set(\"Content-Usage\", contentUsageHeader(advertise));\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Hono adapter.\n *\n * Returns an object with `protect(handler)` (per-route gate) and\n * `middleware()` (whole-app settling gate).\n *\n * ```ts\n * const vx = verivyxHono({ domain: \"example.com\", token: \"...\" });\n * // Per-route:\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * // Whole-app:\n * app.use(\"*\", vx.middleware());\n * ```\n */\nexport function verivyxHono(opts?: HonoAdapterOptions): {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler;\n middleware(): MiddlewareHandler;\n} {\n // Resolve the core: use the injected `_core` (tests) or build the real one.\n // Only pass `verifyWebBotAuth` to the core deps when the caller overrode it;\n // the core bundled default is correct otherwise.\n const vx: Verivyx =\n opts?._core ??\n verivyx(opts, {\n verifyCrawlerDns:\n opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),\n ...(opts?.verifyWebBotAuth !== undefined\n ? { verifyWebBotAuth: opts.verifyWebBotAuth }\n : {}),\n });\n\n const trustProxy = opts?.trustProxy !== false; // default true\n\n /**\n * Build a core-compatible `Request` from a Hono context.\n * Resolves the trusted client IP (CF/proxy headers) and sets `x-real-ip`\n * on the cloned request, or strips forwarding headers when trustProxy:false.\n *\n * Security invariant: a client can never inject a fake IP into the core\n * classifier — either CF/proxy value wins, or no IP is seen at all.\n */\n function buildCoreRequest(c: Context): Request {\n const raw = c.req.raw;\n const ip = resolveIp(c, trustProxy);\n const url = publicUrl(raw, trustProxy);\n if (ip !== undefined) {\n const headers = new Headers(raw.headers);\n headers.set(\"x-real-ip\", ip);\n return new Request(url, { method: raw.method, headers });\n } else {\n // trustProxy === false: strip forwarding headers so the client cannot\n // inject a spoofed IP into the core.\n const headers = new Headers(raw.headers);\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n return new Request(url, { method: raw.method, headers });\n }\n }\n\n return {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler {\n return async function verivyxHonoGuard(c): Promise<Response> {\n // 1. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n const raw = c.req.raw;\n\n // 2. Resolve slug.\n // Priority: Hono named param \"slug\" > last URL path segment.\n const paramSlug: string | undefined = c.req.param(\"slug\");\n const slug: string =\n (paramSlug !== undefined && paramSlug !== \"\")\n ? paramSlug\n : lastPathSegment(new URL(raw.url).pathname);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug });\n\n // 4. Denied — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402. Handler NOT called.\n if (!decision.allowed) {\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(raw, trustProxy), o.seoPreview),\n opts?.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 5. Allowed — call the original Hono handler.\n const res = await handler(c);\n\n // 6. Attach the settlement receipt header when a payment was processed,\n // then attach discovery headers (single clone when both apply).\n return withAdvertiseHeaders(\n attachPaymentResponse(res, decision.paymentResponse),\n opts?.advertise,\n );\n };\n },\n\n middleware(): MiddlewareHandler {\n return async (c, next) => {\n // 1. Path-match filter: when match is set, skip non-matching paths.\n const pathname = c.req.path;\n if (opts?.match !== undefined && opts.match.length > 0 && !pathMatchesAny(pathname, opts.match)) {\n return next();\n }\n\n // 2. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n\n // 3. Slug = last path segment (middleware has no named :slug param).\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n\n // 4. Gate decision.\n const decision = await vx.protect(coreReq, { slug });\n\n if (decision.allowed) {\n // 5a. Allowed — run downstream handlers first.\n await next();\n // 5b. Attach settlement receipt on the outbound response.\n // After next(), c.res holds the downstream Response.\n // Reassign to a mutable clone so we can set the header.\n if (decision.paymentResponse !== undefined) {\n c.res = new Response(c.res.body, c.res);\n c.res.headers.set(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return;\n }\n\n // 5c. Blocked — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402. next() is NOT called.\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(c.req.raw, trustProxy), opts.seoPreview),\n opts.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Top-level convenience export\n// ---------------------------------------------------------------------------\n\n/**\n * Whole-app Hono middleware that gates every matched route behind the\n * Verivyx settling paywall.\n *\n * ```ts\n * import { verivyxHonoMiddleware } from \"@verivyx/paywall-hono\";\n * app.use(\"*\", verivyxHonoMiddleware({ domain: \"example.com\", token: \"...\" }));\n * ```\n */\nexport function verivyxHonoMiddleware(opts?: HonoAdapterOptions): MiddlewareHandler {\n return verivyxHono(opts).middleware();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["rslLinkHeader","contentUsageHeader","verivyx","createSearchCrawlerVerifier","buildSeoPreviewResponse","attachPaymentResponse"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @verivyx/paywall-hono\n *\n * Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge).\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module handles:\n * 1. IP resolution from Cloudflare / proxy headers (CF-Connecting-IP first).\n * 2. Cloning the Web `Request` with the resolved IP in `x-real-ip`.\n * 3. Calling core `protect()` (decision overload).\n * 4. Returning `decision.response()` when denied (handler NOT called).\n * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.\n *\n * Edge-portable: NO `node:*` imports. Uses only Web Platform APIs and Hono types.\n *\n * @example\n * ```ts\n * import { verivyxHono } from \"@verivyx/paywall-hono\";\n * const vx = verivyxHono({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * ```\n */\n\nimport {\n verivyx,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { Context, MiddlewareHandler } from \"hono\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Hono adapter.\n * Extends core VerivyxOptions with edge-specific controls.\n */\nexport interface HonoAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from Cloudflare / proxy headers:\n * CF-Connecting-IP → X-Forwarded-For first hop → X-Real-IP.\n * Set to false if running without a trusted proxy.\n */\n trustProxy?: boolean;\n\n /**\n * Override the reverse-DNS search-crawler verifier injected into the core.\n * When omitted, `createSearchCrawlerVerifier()` is used.\n */\n verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;\n\n /**\n * Override the Web Bot Auth verifier injected into the core.\n * When omitted, the core's bundled RFC 9421 verifier is used.\n */\n verifyWebBotAuth?: (req: Request) => Promise<boolean>;\n\n /**\n * @internal\n * Inject a pre-built `Verivyx` core instance. Used in tests via\n * `verivyx.mock({...})` to avoid any network access. Production code\n * should never set this; omit it and the adapter constructs the real core.\n */\n _core?: Verivyx;\n\n /**\n * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both\n * the denied (402) and allowed handler responses.\n * Default undefined = OFF (no headers added; existing behavior unchanged).\n */\n advertise?: DiscoveryOptions;\n\n /**\n * When set, search crawlers (reason: \"crawler\") always receive a 200 HTML\n * teaser page. Unverified humans (reason: \"human-unverified\") also receive\n * the teaser — but ONLY when the request is a real browser top-level\n * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).\n * Machine clients and x402 payment agents that lack those browser headers\n * receive the 402 x402 response so they can pay.\n *\n * Used by both `protect()` (when set on the factory opts) and `middleware()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header value.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | null | undefined): string | undefined {\n if (xff === null || xff === undefined || xff === \"\") {\n return undefined;\n }\n const first = xff.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Return true when `pathname` matches any of the `patterns`.\n * Supports `*` (single-segment wildcard) and `**` (multi-segment wildcard).\n * Performs an anchored full-path match.\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => {\n // Escape regex metacharacters except * which we handle specially.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Replace ** before * so the two-step substitution is order-safe.\n const regexStr = escaped\n .replace(/\\*\\*/g, \"\u0001\") // placeholder for **\n .replace(/\\*/g, \"[^/]*\") // single-segment wildcard\n .replace(/\u0001/g, \".*\"); // multi-segment wildcard\n return new RegExp(\"^\" + regexStr + \"$\").test(pathname);\n });\n}\n\n/**\n * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw request URL is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\n\n/**\n * Extract the last non-empty path segment from a URL pathname.\n * Used as a fallback slug when `c.req.param(\"slug\")` is unavailable.\n */\nfunction lastPathSegment(pathname: string): string {\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n if (last === undefined) {\n return \"\";\n }\n try {\n return decodeURIComponent(last);\n } catch {\n return last;\n }\n}\n\n/**\n * Resolve the trusted client IP from Hono context headers.\n *\n * Precedence (Cloudflare Workers best-practice order):\n * 1. CF-Connecting-IP — set by Cloudflare edge (single trusted value).\n * 2. X-Forwarded-For first hop — set by other proxies / Vercel.\n * 3. X-Real-IP — generic proxy header.\n * Returns undefined when trustProxy === false or no header is present.\n */\nfunction resolveIp(c: Context, trustProxy: boolean): string | undefined {\n if (!trustProxy) {\n return undefined;\n }\n const cfIp = c.req.header(\"cf-connecting-ip\");\n if (cfIp !== undefined && cfIp !== \"\") {\n return cfIp;\n }\n const xff = c.req.header(\"x-forwarded-for\");\n const hop = firstHop(xff);\n if (hop !== undefined) {\n return hop;\n }\n const xri = c.req.header(\"x-real-ip\");\n return xri !== undefined && xri !== \"\" ? xri : undefined;\n}\n\n/**\n * Return true when the request looks like a real top-level browser navigation.\n *\n * Real browsers send `Sec-Fetch-Mode: navigate` on top-level page loads AND/OR\n * an `Accept` header that includes `text/html`. Machine clients (undici, fetch,\n * x402 payment agents) send neither — they must receive the 402 so they can pay.\n *\n * Crawlers (search bots) are handled separately: they always get the SEO teaser\n * regardless of this check, so this function is only consulted for\n * `reason === \"human-unverified\"`.\n */\nfunction isBrowserNavigation(req: Request): boolean {\n const secFetchMode = req.headers.get(\"sec-fetch-mode\") ?? \"\";\n const accept = req.headers.get(\"accept\") ?? \"\";\n return secFetchMode === \"navigate\" || accept.includes(\"text/html\");\n}\n\n/**\n * Attach RSL + AIPREF discovery headers to a Web Response by cloning it.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * Returns the same Response unchanged when `advertise` is undefined.\n */\nfunction withAdvertiseHeaders(res: Response, advertise: DiscoveryOptions | undefined): Response {\n if (advertise === undefined) {\n return res;\n }\n const headers = new Headers(res.headers);\n headers.append(\"Link\", rslLinkHeader(advertise));\n headers.set(\"Content-Usage\", contentUsageHeader(advertise));\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Hono adapter.\n *\n * Returns an object with `protect(handler)` (per-route gate) and\n * `middleware()` (whole-app settling gate).\n *\n * ```ts\n * const vx = verivyxHono({ domain: \"example.com\", token: \"...\" });\n * // Per-route:\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * // Whole-app:\n * app.use(\"*\", vx.middleware());\n * ```\n */\nexport function verivyxHono(opts?: HonoAdapterOptions): {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler;\n middleware(): MiddlewareHandler;\n} {\n // Resolve the core: use the injected `_core` (tests) or build the real one.\n // Only pass `verifyWebBotAuth` to the core deps when the caller overrode it;\n // the core bundled default is correct otherwise.\n const vx: Verivyx =\n opts?._core ??\n verivyx(opts, {\n verifyCrawlerDns:\n opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),\n ...(opts?.verifyWebBotAuth !== undefined\n ? { verifyWebBotAuth: opts.verifyWebBotAuth }\n : {}),\n });\n\n const trustProxy = opts?.trustProxy !== false; // default true\n\n /**\n * Build a core-compatible `Request` from a Hono context.\n * Resolves the trusted client IP (CF/proxy headers) and sets `x-real-ip`\n * on the cloned request, or strips forwarding headers when trustProxy:false.\n *\n * Security invariant: a client can never inject a fake IP into the core\n * classifier — either CF/proxy value wins, or no IP is seen at all.\n */\n function buildCoreRequest(c: Context): Request {\n const raw = c.req.raw;\n const ip = resolveIp(c, trustProxy);\n const url = publicUrl(raw, trustProxy);\n if (ip !== undefined) {\n const headers = new Headers(raw.headers);\n headers.set(\"x-real-ip\", ip);\n return new Request(url, { method: raw.method, headers });\n } else {\n // trustProxy === false: strip forwarding headers so the client cannot\n // inject a spoofed IP into the core.\n const headers = new Headers(raw.headers);\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n return new Request(url, { method: raw.method, headers });\n }\n }\n\n return {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler {\n return async function verivyxHonoGuard(c): Promise<Response> {\n // 1. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n const raw = c.req.raw;\n\n // 2. Resolve slug.\n // Priority: Hono named param \"slug\" > last URL path segment.\n const paramSlug: string | undefined = c.req.param(\"slug\");\n const slug: string =\n (paramSlug !== undefined && paramSlug !== \"\")\n ? paramSlug\n : lastPathSegment(new URL(raw.url).pathname);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug });\n\n // 4. Denied — crawlers always get the SEO teaser (verified search bots\n // need the preview + JSON-LD). human-unverified gets the teaser ONLY\n // for real browser navigations (Sec-Fetch-Mode:navigate or Accept\n // includes text/html). Machine clients / x402 agents must get the 402.\n // Handler NOT called.\n if (!decision.allowed) {\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(c.req.raw));\n if (previewable && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(raw, trustProxy), o.seoPreview),\n opts?.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 5. Allowed — call the original Hono handler.\n const res = await handler(c);\n\n // 6. Attach the settlement receipt header when a payment was processed,\n // then attach discovery headers (single clone when both apply).\n return withAdvertiseHeaders(\n attachPaymentResponse(res, decision.paymentResponse),\n opts?.advertise,\n );\n };\n },\n\n middleware(): MiddlewareHandler {\n return async (c, next) => {\n // 1. Path-match filter: when match is set, skip non-matching paths.\n const pathname = c.req.path;\n if (opts?.match !== undefined && opts.match.length > 0 && !pathMatchesAny(pathname, opts.match)) {\n return next();\n }\n\n // 2. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n\n // 3. Slug = last path segment (middleware has no named :slug param).\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n\n // 4. Gate decision.\n const decision = await vx.protect(coreReq, { slug });\n\n if (decision.allowed) {\n // 5a. Allowed — run downstream handlers first.\n await next();\n // 5b. Attach settlement receipt on the outbound response.\n // After next(), c.res holds the downstream Response.\n // Reassign to a mutable clone so we can set the header.\n if (decision.paymentResponse !== undefined) {\n c.res = new Response(c.res.body, c.res);\n c.res.headers.set(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return;\n }\n\n // 5c. Blocked — crawlers always get the SEO teaser; human-unverified\n // only gets the teaser for real browser navigations (Sec-Fetch-Mode\n // or Accept:text/html). Machine clients / x402 agents get the 402.\n // next() is NOT called.\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(c.req.raw));\n if (previewable && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(c.req.raw, trustProxy), opts.seoPreview),\n opts.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Top-level convenience export\n// ---------------------------------------------------------------------------\n\n/**\n * Whole-app Hono middleware that gates every matched route behind the\n * Verivyx settling paywall.\n *\n * ```ts\n * import { verivyxHonoMiddleware } from \"@verivyx/paywall-hono\";\n * app.use(\"*\", verivyxHonoMiddleware({ domain: \"example.com\", token: \"...\" }));\n * ```\n */\nexport function verivyxHonoMiddleware(opts?: HonoAdapterOptions): MiddlewareHandler {\n return verivyxHono(opts).middleware();\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -59,9 +59,12 @@ interface HonoAdapterOptions extends VerivyxOptions {
|
|
|
59
59
|
*/
|
|
60
60
|
advertise?: DiscoveryOptions;
|
|
61
61
|
/**
|
|
62
|
-
* When set,
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* When set, search crawlers (reason: "crawler") always receive a 200 HTML
|
|
63
|
+
* teaser page. Unverified humans (reason: "human-unverified") also receive
|
|
64
|
+
* the teaser — but ONLY when the request is a real browser top-level
|
|
65
|
+
* navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).
|
|
66
|
+
* Machine clients and x402 payment agents that lack those browser headers
|
|
67
|
+
* receive the 402 x402 response so they can pay.
|
|
65
68
|
*
|
|
66
69
|
* Used by both `protect()` (when set on the factory opts) and `middleware()`.
|
|
67
70
|
* `protect()` also accepts `seoPreview` in its per-call options `o`; if both
|
package/dist/index.d.ts
CHANGED
|
@@ -59,9 +59,12 @@ interface HonoAdapterOptions extends VerivyxOptions {
|
|
|
59
59
|
*/
|
|
60
60
|
advertise?: DiscoveryOptions;
|
|
61
61
|
/**
|
|
62
|
-
* When set,
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* When set, search crawlers (reason: "crawler") always receive a 200 HTML
|
|
63
|
+
* teaser page. Unverified humans (reason: "human-unverified") also receive
|
|
64
|
+
* the teaser — but ONLY when the request is a real browser top-level
|
|
65
|
+
* navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).
|
|
66
|
+
* Machine clients and x402 payment agents that lack those browser headers
|
|
67
|
+
* receive the 402 x402 response so they can pay.
|
|
65
68
|
*
|
|
66
69
|
* Used by both `protect()` (when set on the factory opts) and `middleware()`.
|
|
67
70
|
* `protect()` also accepts `seoPreview` in its per-call options `o`; if both
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,11 @@ function resolveIp(c, trustProxy) {
|
|
|
68
68
|
const xri = c.req.header("x-real-ip");
|
|
69
69
|
return xri !== void 0 && xri !== "" ? xri : void 0;
|
|
70
70
|
}
|
|
71
|
+
function isBrowserNavigation(req) {
|
|
72
|
+
const secFetchMode = req.headers.get("sec-fetch-mode") ?? "";
|
|
73
|
+
const accept = req.headers.get("accept") ?? "";
|
|
74
|
+
return secFetchMode === "navigate" || accept.includes("text/html");
|
|
75
|
+
}
|
|
71
76
|
function withAdvertiseHeaders(res, advertise) {
|
|
72
77
|
if (advertise === void 0) {
|
|
73
78
|
return res;
|
|
@@ -107,8 +112,8 @@ function verivyxHono(opts) {
|
|
|
107
112
|
const slug = paramSlug !== void 0 && paramSlug !== "" ? paramSlug : lastPathSegment(new URL(raw.url).pathname);
|
|
108
113
|
const decision = await vx.protect(coreReq, { slug });
|
|
109
114
|
if (!decision.allowed) {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
115
|
+
const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(c.req.raw);
|
|
116
|
+
if (previewable && o?.seoPreview !== void 0) {
|
|
112
117
|
return withAdvertiseHeaders(
|
|
113
118
|
buildSeoPreviewResponse(slug, publicUrl(raw, trustProxy), o.seoPreview),
|
|
114
119
|
opts?.advertise
|
|
@@ -140,8 +145,8 @@ function verivyxHono(opts) {
|
|
|
140
145
|
}
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
148
|
+
const previewable = decision.reason === "crawler" || decision.reason === "human-unverified" && isBrowserNavigation(c.req.raw);
|
|
149
|
+
if (previewable && opts?.seoPreview !== void 0) {
|
|
145
150
|
return withAdvertiseHeaders(
|
|
146
151
|
buildSeoPreviewResponse(slug, publicUrl(c.req.raw, trustProxy), opts.seoPreview),
|
|
147
152
|
opts.advertise
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @verivyx/paywall-hono\n *\n * Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge).\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module handles:\n * 1. IP resolution from Cloudflare / proxy headers (CF-Connecting-IP first).\n * 2. Cloning the Web `Request` with the resolved IP in `x-real-ip`.\n * 3. Calling core `protect()` (decision overload).\n * 4. Returning `decision.response()` when denied (handler NOT called).\n * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.\n *\n * Edge-portable: NO `node:*` imports. Uses only Web Platform APIs and Hono types.\n *\n * @example\n * ```ts\n * import { verivyxHono } from \"@verivyx/paywall-hono\";\n * const vx = verivyxHono({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * ```\n */\n\nimport {\n verivyx,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { Context, MiddlewareHandler } from \"hono\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Hono adapter.\n * Extends core VerivyxOptions with edge-specific controls.\n */\nexport interface HonoAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from Cloudflare / proxy headers:\n * CF-Connecting-IP → X-Forwarded-For first hop → X-Real-IP.\n * Set to false if running without a trusted proxy.\n */\n trustProxy?: boolean;\n\n /**\n * Override the reverse-DNS search-crawler verifier injected into the core.\n * When omitted, `createSearchCrawlerVerifier()` is used.\n */\n verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;\n\n /**\n * Override the Web Bot Auth verifier injected into the core.\n * When omitted, the core's bundled RFC 9421 verifier is used.\n */\n verifyWebBotAuth?: (req: Request) => Promise<boolean>;\n\n /**\n * @internal\n * Inject a pre-built `Verivyx` core instance. Used in tests via\n * `verivyx.mock({...})` to avoid any network access. Production code\n * should never set this; omit it and the adapter constructs the real core.\n */\n _core?: Verivyx;\n\n /**\n * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both\n * the denied (402) and allowed handler responses.\n * Default undefined = OFF (no headers added; existing behavior unchanged).\n */\n advertise?: DiscoveryOptions;\n\n /**\n * When set, unverified humans and search crawlers (reason: \"human-unverified\"\n * or \"crawler\") receive a 200 HTML teaser page instead of a bare 402.\n * Bots / agents (reason: \"bot-unpaid\") still get the 402 x402 response.\n *\n * Used by both `protect()` (when set on the factory opts) and `middleware()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header value.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | null | undefined): string | undefined {\n if (xff === null || xff === undefined || xff === \"\") {\n return undefined;\n }\n const first = xff.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Return true when `pathname` matches any of the `patterns`.\n * Supports `*` (single-segment wildcard) and `**` (multi-segment wildcard).\n * Performs an anchored full-path match.\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => {\n // Escape regex metacharacters except * which we handle specially.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Replace ** before * so the two-step substitution is order-safe.\n const regexStr = escaped\n .replace(/\\*\\*/g, \"\u0001\") // placeholder for **\n .replace(/\\*/g, \"[^/]*\") // single-segment wildcard\n .replace(/\u0001/g, \".*\"); // multi-segment wildcard\n return new RegExp(\"^\" + regexStr + \"$\").test(pathname);\n });\n}\n\n/**\n * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw request URL is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\n\n/**\n * Extract the last non-empty path segment from a URL pathname.\n * Used as a fallback slug when `c.req.param(\"slug\")` is unavailable.\n */\nfunction lastPathSegment(pathname: string): string {\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n if (last === undefined) {\n return \"\";\n }\n try {\n return decodeURIComponent(last);\n } catch {\n return last;\n }\n}\n\n/**\n * Resolve the trusted client IP from Hono context headers.\n *\n * Precedence (Cloudflare Workers best-practice order):\n * 1. CF-Connecting-IP — set by Cloudflare edge (single trusted value).\n * 2. X-Forwarded-For first hop — set by other proxies / Vercel.\n * 3. X-Real-IP — generic proxy header.\n * Returns undefined when trustProxy === false or no header is present.\n */\nfunction resolveIp(c: Context, trustProxy: boolean): string | undefined {\n if (!trustProxy) {\n return undefined;\n }\n const cfIp = c.req.header(\"cf-connecting-ip\");\n if (cfIp !== undefined && cfIp !== \"\") {\n return cfIp;\n }\n const xff = c.req.header(\"x-forwarded-for\");\n const hop = firstHop(xff);\n if (hop !== undefined) {\n return hop;\n }\n const xri = c.req.header(\"x-real-ip\");\n return xri !== undefined && xri !== \"\" ? xri : undefined;\n}\n\n/**\n * Attach RSL + AIPREF discovery headers to a Web Response by cloning it.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * Returns the same Response unchanged when `advertise` is undefined.\n */\nfunction withAdvertiseHeaders(res: Response, advertise: DiscoveryOptions | undefined): Response {\n if (advertise === undefined) {\n return res;\n }\n const headers = new Headers(res.headers);\n headers.append(\"Link\", rslLinkHeader(advertise));\n headers.set(\"Content-Usage\", contentUsageHeader(advertise));\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Hono adapter.\n *\n * Returns an object with `protect(handler)` (per-route gate) and\n * `middleware()` (whole-app settling gate).\n *\n * ```ts\n * const vx = verivyxHono({ domain: \"example.com\", token: \"...\" });\n * // Per-route:\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * // Whole-app:\n * app.use(\"*\", vx.middleware());\n * ```\n */\nexport function verivyxHono(opts?: HonoAdapterOptions): {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler;\n middleware(): MiddlewareHandler;\n} {\n // Resolve the core: use the injected `_core` (tests) or build the real one.\n // Only pass `verifyWebBotAuth` to the core deps when the caller overrode it;\n // the core bundled default is correct otherwise.\n const vx: Verivyx =\n opts?._core ??\n verivyx(opts, {\n verifyCrawlerDns:\n opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),\n ...(opts?.verifyWebBotAuth !== undefined\n ? { verifyWebBotAuth: opts.verifyWebBotAuth }\n : {}),\n });\n\n const trustProxy = opts?.trustProxy !== false; // default true\n\n /**\n * Build a core-compatible `Request` from a Hono context.\n * Resolves the trusted client IP (CF/proxy headers) and sets `x-real-ip`\n * on the cloned request, or strips forwarding headers when trustProxy:false.\n *\n * Security invariant: a client can never inject a fake IP into the core\n * classifier — either CF/proxy value wins, or no IP is seen at all.\n */\n function buildCoreRequest(c: Context): Request {\n const raw = c.req.raw;\n const ip = resolveIp(c, trustProxy);\n const url = publicUrl(raw, trustProxy);\n if (ip !== undefined) {\n const headers = new Headers(raw.headers);\n headers.set(\"x-real-ip\", ip);\n return new Request(url, { method: raw.method, headers });\n } else {\n // trustProxy === false: strip forwarding headers so the client cannot\n // inject a spoofed IP into the core.\n const headers = new Headers(raw.headers);\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n return new Request(url, { method: raw.method, headers });\n }\n }\n\n return {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler {\n return async function verivyxHonoGuard(c): Promise<Response> {\n // 1. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n const raw = c.req.raw;\n\n // 2. Resolve slug.\n // Priority: Hono named param \"slug\" > last URL path segment.\n const paramSlug: string | undefined = c.req.param(\"slug\");\n const slug: string =\n (paramSlug !== undefined && paramSlug !== \"\")\n ? paramSlug\n : lastPathSegment(new URL(raw.url).pathname);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug });\n\n // 4. Denied — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402. Handler NOT called.\n if (!decision.allowed) {\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(raw, trustProxy), o.seoPreview),\n opts?.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 5. Allowed — call the original Hono handler.\n const res = await handler(c);\n\n // 6. Attach the settlement receipt header when a payment was processed,\n // then attach discovery headers (single clone when both apply).\n return withAdvertiseHeaders(\n attachPaymentResponse(res, decision.paymentResponse),\n opts?.advertise,\n );\n };\n },\n\n middleware(): MiddlewareHandler {\n return async (c, next) => {\n // 1. Path-match filter: when match is set, skip non-matching paths.\n const pathname = c.req.path;\n if (opts?.match !== undefined && opts.match.length > 0 && !pathMatchesAny(pathname, opts.match)) {\n return next();\n }\n\n // 2. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n\n // 3. Slug = last path segment (middleware has no named :slug param).\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n\n // 4. Gate decision.\n const decision = await vx.protect(coreReq, { slug });\n\n if (decision.allowed) {\n // 5a. Allowed — run downstream handlers first.\n await next();\n // 5b. Attach settlement receipt on the outbound response.\n // After next(), c.res holds the downstream Response.\n // Reassign to a mutable clone so we can set the header.\n if (decision.paymentResponse !== undefined) {\n c.res = new Response(c.res.body, c.res);\n c.res.headers.set(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return;\n }\n\n // 5c. Blocked — check if this is a crawler/human-unverified that we can\n // serve an SEO preview to instead of a bare 402. next() is NOT called.\n const isPreviewCandidate =\n decision.reason === \"crawler\" || decision.reason === \"human-unverified\";\n if (isPreviewCandidate && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(c.req.raw, trustProxy), opts.seoPreview),\n opts.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Top-level convenience export\n// ---------------------------------------------------------------------------\n\n/**\n * Whole-app Hono middleware that gates every matched route behind the\n * Verivyx settling paywall.\n *\n * ```ts\n * import { verivyxHonoMiddleware } from \"@verivyx/paywall-hono\";\n * app.use(\"*\", verivyxHonoMiddleware({ domain: \"example.com\", token: \"...\" }));\n * ```\n */\nexport function verivyxHonoMiddleware(opts?: HonoAdapterOptions): MiddlewareHandler {\n return verivyxHono(opts).middleware();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @verivyx/paywall-hono\n *\n * Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge).\n *\n * Thin layer — all gate logic lives in @verivyx/paywall core.\n * This module handles:\n * 1. IP resolution from Cloudflare / proxy headers (CF-Connecting-IP first).\n * 2. Cloning the Web `Request` with the resolved IP in `x-real-ip`.\n * 3. Calling core `protect()` (decision overload).\n * 4. Returning `decision.response()` when denied (handler NOT called).\n * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.\n *\n * Edge-portable: NO `node:*` imports. Uses only Web Platform APIs and Hono types.\n *\n * @example\n * ```ts\n * import { verivyxHono } from \"@verivyx/paywall-hono\";\n * const vx = verivyxHono({ domain: \"example.com\", token: process.env.VX_TOKEN });\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * ```\n */\n\nimport {\n verivyx,\n createSearchCrawlerVerifier,\n buildSeoPreviewResponse,\n attachPaymentResponse,\n rslLinkHeader,\n contentUsageHeader,\n} from \"@verivyx/paywall\";\nimport type { VerivyxOptions, Verivyx, DiscoveryOptions } from \"@verivyx/paywall\";\nimport type { Context, MiddlewareHandler } from \"hono\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Hono adapter.\n * Extends core VerivyxOptions with edge-specific controls.\n */\nexport interface HonoAdapterOptions extends VerivyxOptions {\n /**\n * When true (default), read the client IP from Cloudflare / proxy headers:\n * CF-Connecting-IP → X-Forwarded-For first hop → X-Real-IP.\n * Set to false if running without a trusted proxy.\n */\n trustProxy?: boolean;\n\n /**\n * Override the reverse-DNS search-crawler verifier injected into the core.\n * When omitted, `createSearchCrawlerVerifier()` is used.\n */\n verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;\n\n /**\n * Override the Web Bot Auth verifier injected into the core.\n * When omitted, the core's bundled RFC 9421 verifier is used.\n */\n verifyWebBotAuth?: (req: Request) => Promise<boolean>;\n\n /**\n * @internal\n * Inject a pre-built `Verivyx` core instance. Used in tests via\n * `verivyx.mock({...})` to avoid any network access. Production code\n * should never set this; omit it and the adapter constructs the real core.\n */\n _core?: Verivyx;\n\n /**\n * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both\n * the denied (402) and allowed handler responses.\n * Default undefined = OFF (no headers added; existing behavior unchanged).\n */\n advertise?: DiscoveryOptions;\n\n /**\n * When set, search crawlers (reason: \"crawler\") always receive a 200 HTML\n * teaser page. Unverified humans (reason: \"human-unverified\") also receive\n * the teaser — but ONLY when the request is a real browser top-level\n * navigation (Sec-Fetch-Mode: navigate OR Accept includes text/html).\n * Machine clients and x402 payment agents that lack those browser headers\n * receive the 402 x402 response so they can pay.\n *\n * Used by both `protect()` (when set on the factory opts) and `middleware()`.\n * `protect()` also accepts `seoPreview` in its per-call options `o`; if both\n * are set, the per-call value takes precedence.\n */\n seoPreview?: (ctx: { slug: string }) => { title: string; excerpt: string };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the first (client-most) hop from an `X-Forwarded-For` header value.\n * The header may contain a comma-separated list; the leftmost is the client.\n */\nfunction firstHop(xff: string | null | undefined): string | undefined {\n if (xff === null || xff === undefined || xff === \"\") {\n return undefined;\n }\n const first = xff.split(\",\")[0];\n return first !== undefined ? first.trim() || undefined : undefined;\n}\n\n/**\n * Return true when `pathname` matches any of the `patterns`.\n * Supports `*` (single-segment wildcard) and `**` (multi-segment wildcard).\n * Performs an anchored full-path match.\n */\nfunction pathMatchesAny(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => {\n // Escape regex metacharacters except * which we handle specially.\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Replace ** before * so the two-step substitution is order-safe.\n const regexStr = escaped\n .replace(/\\*\\*/g, \"\u0001\") // placeholder for **\n .replace(/\\*/g, \"[^/]*\") // single-segment wildcard\n .replace(/\u0001/g, \".*\"); // multi-segment wildcard\n return new RegExp(\"^\" + regexStr + \"$\").test(pathname);\n });\n}\n\n/**\n * Rebuild the absolute request URL using `X-Forwarded-Host` / `X-Forwarded-Proto`\n * when `trustProxy` is enabled, so the x402 resource URL reflects the public host\n * rather than the internal address assigned by a reverse proxy.\n *\n * When `trustProxy` is false the raw request URL is returned unchanged.\n */\nfunction publicUrl(req: Request, trustProxy: boolean): string {\n if (!trustProxy) return req.url;\n const u = new URL(req.url);\n const fwdProto = req.headers.get(\"x-forwarded-proto\");\n if (fwdProto) {\n const p = fwdProto.split(\",\")[0];\n if (p !== undefined) u.protocol = p.trim() + \":\";\n }\n const fwdHost = req.headers.get(\"x-forwarded-host\") ?? req.headers.get(\"host\");\n if (fwdHost) {\n const h = fwdHost.split(\",\")[0];\n if (h !== undefined) {\n const trimmed = h.trim();\n // If the forwarded host includes a port (\"host:port\"), split it.\n // Otherwise clear any internal port so we only expose the public host.\n const colonIdx = trimmed.lastIndexOf(\":\");\n if (colonIdx !== -1) {\n u.hostname = trimmed.slice(0, colonIdx);\n u.port = trimmed.slice(colonIdx + 1);\n } else {\n u.hostname = trimmed;\n u.port = \"\";\n }\n }\n }\n return u.toString();\n}\n\n/**\n * Extract the last non-empty path segment from a URL pathname.\n * Used as a fallback slug when `c.req.param(\"slug\")` is unavailable.\n */\nfunction lastPathSegment(pathname: string): string {\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const last = segments[segments.length - 1];\n if (last === undefined) {\n return \"\";\n }\n try {\n return decodeURIComponent(last);\n } catch {\n return last;\n }\n}\n\n/**\n * Resolve the trusted client IP from Hono context headers.\n *\n * Precedence (Cloudflare Workers best-practice order):\n * 1. CF-Connecting-IP — set by Cloudflare edge (single trusted value).\n * 2. X-Forwarded-For first hop — set by other proxies / Vercel.\n * 3. X-Real-IP — generic proxy header.\n * Returns undefined when trustProxy === false or no header is present.\n */\nfunction resolveIp(c: Context, trustProxy: boolean): string | undefined {\n if (!trustProxy) {\n return undefined;\n }\n const cfIp = c.req.header(\"cf-connecting-ip\");\n if (cfIp !== undefined && cfIp !== \"\") {\n return cfIp;\n }\n const xff = c.req.header(\"x-forwarded-for\");\n const hop = firstHop(xff);\n if (hop !== undefined) {\n return hop;\n }\n const xri = c.req.header(\"x-real-ip\");\n return xri !== undefined && xri !== \"\" ? xri : undefined;\n}\n\n/**\n * Return true when the request looks like a real top-level browser navigation.\n *\n * Real browsers send `Sec-Fetch-Mode: navigate` on top-level page loads AND/OR\n * an `Accept` header that includes `text/html`. Machine clients (undici, fetch,\n * x402 payment agents) send neither — they must receive the 402 so they can pay.\n *\n * Crawlers (search bots) are handled separately: they always get the SEO teaser\n * regardless of this check, so this function is only consulted for\n * `reason === \"human-unverified\"`.\n */\nfunction isBrowserNavigation(req: Request): boolean {\n const secFetchMode = req.headers.get(\"sec-fetch-mode\") ?? \"\";\n const accept = req.headers.get(\"accept\") ?? \"\";\n return secFetchMode === \"navigate\" || accept.includes(\"text/html\");\n}\n\n/**\n * Attach RSL + AIPREF discovery headers to a Web Response by cloning it.\n * Appends to any existing `Link` (preserves prior values); sets `Content-Usage`.\n * Returns the same Response unchanged when `advertise` is undefined.\n */\nfunction withAdvertiseHeaders(res: Response, advertise: DiscoveryOptions | undefined): Response {\n if (advertise === undefined) {\n return res;\n }\n const headers = new Headers(res.headers);\n headers.append(\"Link\", rslLinkHeader(advertise));\n headers.set(\"Content-Usage\", contentUsageHeader(advertise));\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n}\n\n// ---------------------------------------------------------------------------\n// Adapter factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Verivyx Hono adapter.\n *\n * Returns an object with `protect(handler)` (per-route gate) and\n * `middleware()` (whole-app settling gate).\n *\n * ```ts\n * const vx = verivyxHono({ domain: \"example.com\", token: \"...\" });\n * // Per-route:\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\n * // Whole-app:\n * app.use(\"*\", vx.middleware());\n * ```\n */\nexport function verivyxHono(opts?: HonoAdapterOptions): {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler;\n middleware(): MiddlewareHandler;\n} {\n // Resolve the core: use the injected `_core` (tests) or build the real one.\n // Only pass `verifyWebBotAuth` to the core deps when the caller overrode it;\n // the core bundled default is correct otherwise.\n const vx: Verivyx =\n opts?._core ??\n verivyx(opts, {\n verifyCrawlerDns:\n opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),\n ...(opts?.verifyWebBotAuth !== undefined\n ? { verifyWebBotAuth: opts.verifyWebBotAuth }\n : {}),\n });\n\n const trustProxy = opts?.trustProxy !== false; // default true\n\n /**\n * Build a core-compatible `Request` from a Hono context.\n * Resolves the trusted client IP (CF/proxy headers) and sets `x-real-ip`\n * on the cloned request, or strips forwarding headers when trustProxy:false.\n *\n * Security invariant: a client can never inject a fake IP into the core\n * classifier — either CF/proxy value wins, or no IP is seen at all.\n */\n function buildCoreRequest(c: Context): Request {\n const raw = c.req.raw;\n const ip = resolveIp(c, trustProxy);\n const url = publicUrl(raw, trustProxy);\n if (ip !== undefined) {\n const headers = new Headers(raw.headers);\n headers.set(\"x-real-ip\", ip);\n return new Request(url, { method: raw.method, headers });\n } else {\n // trustProxy === false: strip forwarding headers so the client cannot\n // inject a spoofed IP into the core.\n const headers = new Headers(raw.headers);\n headers.delete(\"x-real-ip\");\n headers.delete(\"x-forwarded-for\");\n return new Request(url, { method: raw.method, headers });\n }\n }\n\n return {\n protect(\n handler: (c: Context) => Response | Promise<Response>,\n o?: { seoPreview?: (c: { slug: string }) => { title: string; excerpt: string } },\n ): MiddlewareHandler {\n return async function verivyxHonoGuard(c): Promise<Response> {\n // 1. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n const raw = c.req.raw;\n\n // 2. Resolve slug.\n // Priority: Hono named param \"slug\" > last URL path segment.\n const paramSlug: string | undefined = c.req.param(\"slug\");\n const slug: string =\n (paramSlug !== undefined && paramSlug !== \"\")\n ? paramSlug\n : lastPathSegment(new URL(raw.url).pathname);\n\n // 3. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug });\n\n // 4. Denied — crawlers always get the SEO teaser (verified search bots\n // need the preview + JSON-LD). human-unverified gets the teaser ONLY\n // for real browser navigations (Sec-Fetch-Mode:navigate or Accept\n // includes text/html). Machine clients / x402 agents must get the 402.\n // Handler NOT called.\n if (!decision.allowed) {\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(c.req.raw));\n if (previewable && o?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(raw, trustProxy), o.seoPreview),\n opts?.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 5. Allowed — call the original Hono handler.\n const res = await handler(c);\n\n // 6. Attach the settlement receipt header when a payment was processed,\n // then attach discovery headers (single clone when both apply).\n return withAdvertiseHeaders(\n attachPaymentResponse(res, decision.paymentResponse),\n opts?.advertise,\n );\n };\n },\n\n middleware(): MiddlewareHandler {\n return async (c, next) => {\n // 1. Path-match filter: when match is set, skip non-matching paths.\n const pathname = c.req.path;\n if (opts?.match !== undefined && opts.match.length > 0 && !pathMatchesAny(pathname, opts.match)) {\n return next();\n }\n\n // 2. Build a core-compatible request (IP resolution + header hygiene).\n const coreReq = buildCoreRequest(c);\n\n // 3. Slug = last path segment (middleware has no named :slug param).\n const slug = pathname.split(\"/\").filter(Boolean).pop() ?? \"\";\n\n // 4. Gate decision.\n const decision = await vx.protect(coreReq, { slug });\n\n if (decision.allowed) {\n // 5a. Allowed — run downstream handlers first.\n await next();\n // 5b. Attach settlement receipt on the outbound response.\n // After next(), c.res holds the downstream Response.\n // Reassign to a mutable clone so we can set the header.\n if (decision.paymentResponse !== undefined) {\n c.res = new Response(c.res.body, c.res);\n c.res.headers.set(\"PAYMENT-RESPONSE\", decision.paymentResponse);\n }\n return;\n }\n\n // 5c. Blocked — crawlers always get the SEO teaser; human-unverified\n // only gets the teaser for real browser navigations (Sec-Fetch-Mode\n // or Accept:text/html). Machine clients / x402 agents get the 402.\n // next() is NOT called.\n const previewable =\n decision.reason === \"crawler\" ||\n (decision.reason === \"human-unverified\" && isBrowserNavigation(c.req.raw));\n if (previewable && opts?.seoPreview !== undefined) {\n return withAdvertiseHeaders(\n buildSeoPreviewResponse(slug, publicUrl(c.req.raw, trustProxy), opts.seoPreview),\n opts.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Top-level convenience export\n// ---------------------------------------------------------------------------\n\n/**\n * Whole-app Hono middleware that gates every matched route behind the\n * Verivyx settling paywall.\n *\n * ```ts\n * import { verivyxHonoMiddleware } from \"@verivyx/paywall-hono\";\n * app.use(\"*\", verivyxHonoMiddleware({ domain: \"example.com\", token: \"...\" }));\n * ```\n */\nexport function verivyxHonoMiddleware(opts?: HonoAdapterOptions): MiddlewareHandler {\n return verivyxHono(opts).middleware();\n}\n"]}
|