@verivyx/paywall-hono 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-hono
2
+
3
+ Hono adapter for the Verivyx paywall SDK — gate content from AI bots and charge agents on-chain via x402. Edge-portable: runs on Cloudflare Workers, Vercel Edge Functions, and any Web Platform runtime (no `node:*` imports).
4
+
5
+ Requires `@verivyx/paywall` (installed automatically as a dependency) and `hono` (peer dependency).
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i @verivyx/paywall-hono
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```ts
16
+ import { Hono } from "hono";
17
+ import { verivyxHono } from "@verivyx/paywall-hono";
18
+
19
+ const app = new Hono();
20
+
21
+ // Create an adapter (reads VERIVYX_TOKEN + VERIVYX_DOMAIN from env)
22
+ const vx = verivyxHono();
23
+
24
+ // Gate a route — verified/paid requests pass through; bots get a 402
25
+ app.get("/articles/:slug", vx.protect(async (c) => {
26
+ return c.json({ content: "..." });
27
+ }));
28
+
29
+ export default app;
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 `verivyxHono(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`, prefers `CF-Connecting-IP`), `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,97 @@
1
+ 'use strict';
2
+
3
+ var paywall = require('@verivyx/paywall');
4
+
5
+ // src/index.ts
6
+ function firstHop(xff) {
7
+ if (xff === null || xff === void 0 || xff === "") {
8
+ return void 0;
9
+ }
10
+ const first = xff.split(",")[0];
11
+ return first !== void 0 ? first.trim() || void 0 : void 0;
12
+ }
13
+ function lastPathSegment(pathname) {
14
+ const segments = pathname.split("/").filter((s) => s.length > 0);
15
+ const last = segments[segments.length - 1];
16
+ if (last === void 0) {
17
+ return "";
18
+ }
19
+ try {
20
+ return decodeURIComponent(last);
21
+ } catch {
22
+ return last;
23
+ }
24
+ }
25
+ function resolveIp(c, trustProxy) {
26
+ if (!trustProxy) {
27
+ return void 0;
28
+ }
29
+ const cfIp = c.req.header("cf-connecting-ip");
30
+ if (cfIp !== void 0 && cfIp !== "") {
31
+ return cfIp;
32
+ }
33
+ const xff = c.req.header("x-forwarded-for");
34
+ const hop = firstHop(xff);
35
+ if (hop !== void 0) {
36
+ return hop;
37
+ }
38
+ const xri = c.req.header("x-real-ip");
39
+ return xri !== void 0 && xri !== "" ? xri : void 0;
40
+ }
41
+ function withAdvertiseHeaders(res, advertise) {
42
+ if (advertise === void 0) {
43
+ return res;
44
+ }
45
+ const headers = new Headers(res.headers);
46
+ headers.append("Link", paywall.rslLinkHeader(advertise));
47
+ headers.set("Content-Usage", paywall.contentUsageHeader(advertise));
48
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
49
+ }
50
+ function verivyxHono(opts) {
51
+ const vx = opts?._core ?? paywall.verivyx(opts, {
52
+ verifyCrawlerDns: opts?.verifyCrawlerDns ?? paywall.createSearchCrawlerVerifier(),
53
+ ...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
54
+ });
55
+ const trustProxy = opts?.trustProxy !== false;
56
+ return {
57
+ protect(handler, o) {
58
+ return async function verivyxHonoGuard(c) {
59
+ const raw = c.req.raw;
60
+ const ip = resolveIp(c, trustProxy);
61
+ let coreReq;
62
+ if (ip !== void 0) {
63
+ const headers = new Headers(raw.headers);
64
+ headers.set("x-real-ip", ip);
65
+ coreReq = new Request(raw, { headers });
66
+ } else {
67
+ const headers = new Headers(raw.headers);
68
+ headers.delete("x-real-ip");
69
+ headers.delete("x-forwarded-for");
70
+ coreReq = new Request(raw, { headers });
71
+ }
72
+ const paramSlug = c.req.param("slug");
73
+ const slug = paramSlug !== void 0 && paramSlug !== "" ? paramSlug : lastPathSegment(new URL(raw.url).pathname);
74
+ const decision = await vx.protect(coreReq, { slug });
75
+ if (!decision.allowed) {
76
+ const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
77
+ if (isPreviewCandidate && o?.seoPreview !== void 0) {
78
+ return withAdvertiseHeaders(
79
+ paywall.buildSeoPreviewResponse(slug, raw.url, o.seoPreview),
80
+ opts?.advertise
81
+ );
82
+ }
83
+ return withAdvertiseHeaders(decision.response(), opts?.advertise);
84
+ }
85
+ const res = await handler(c);
86
+ return withAdvertiseHeaders(
87
+ paywall.attachPaymentResponse(res, decision.paymentResponse),
88
+ opts?.advertise
89
+ );
90
+ };
91
+ }
92
+ };
93
+ }
94
+
95
+ exports.verivyxHono = verivyxHono;
96
+ //# sourceMappingURL=index.cjs.map
97
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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// ---------------------------------------------------------------------------\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 * 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 a single `protect(handler)` method that wraps a\n * Hono route handler behind the Verivyx paywall gate.\n *\n * ```ts\n * const vx = verivyxHono({ domain: \"example.com\", token: \"...\" });\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\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} {\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 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. Get the raw Web Request from Hono context.\n const raw = c.req.raw;\n\n // 2. Resolve trusted client IP and inject into a cloned request so the\n // core classifier reads a reliable address regardless of edge hop.\n //\n // Security invariant:\n // trustProxy !== false → resolve IP from CF/proxy headers and set\n // x-real-ip on the cloned request (overrides any client value).\n // trustProxy === false → no socket IP is available in edge runtimes;\n // strip both x-real-ip and x-forwarded-for so a client cannot\n // spoof an IP into the core classifier (core sees no IP → safe).\n const ip = resolveIp(c, trustProxy);\n let coreReq: Request;\n if (ip !== undefined) {\n const headers = new Headers(raw.headers);\n headers.set(\"x-real-ip\", ip);\n // Clone the Request with updated headers. For GET/HEAD this is safe;\n // the core classify path reads headers only — body stays with raw.\n coreReq = new Request(raw, { 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 coreReq = new Request(raw, { headers });\n }\n\n // 3. 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 // 4. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug });\n\n // 5. 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, raw.url, o.seoPreview),\n opts?.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 6. Allowed — call the original Hono handler.\n const res = await handler(c);\n\n // 7. 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}\n"]}
@@ -0,0 +1,84 @@
1
+ import { VerivyxOptions, Verivyx, DiscoveryOptions } from '@verivyx/paywall';
2
+ import { Context, MiddlewareHandler } from 'hono';
3
+
4
+ /**
5
+ * @verivyx/paywall-hono
6
+ *
7
+ * Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge).
8
+ *
9
+ * Thin layer — all gate logic lives in @verivyx/paywall core.
10
+ * This module handles:
11
+ * 1. IP resolution from Cloudflare / proxy headers (CF-Connecting-IP first).
12
+ * 2. Cloning the Web `Request` with the resolved IP in `x-real-ip`.
13
+ * 3. Calling core `protect()` (decision overload).
14
+ * 4. Returning `decision.response()` when denied (handler NOT called).
15
+ * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.
16
+ *
17
+ * Edge-portable: NO `node:*` imports. Uses only Web Platform APIs and Hono types.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { verivyxHono } from "@verivyx/paywall-hono";
22
+ * const vx = verivyxHono({ domain: "example.com", token: process.env.VX_TOKEN });
23
+ * app.get("/articles/:slug", vx.protect(async (c) => c.json({ content: "..." })));
24
+ * ```
25
+ */
26
+
27
+ /**
28
+ * Options for the Hono adapter.
29
+ * Extends core VerivyxOptions with edge-specific controls.
30
+ */
31
+ interface HonoAdapterOptions extends VerivyxOptions {
32
+ /**
33
+ * When true (default), read the client IP from Cloudflare / proxy headers:
34
+ * CF-Connecting-IP → X-Forwarded-For first hop → X-Real-IP.
35
+ * Set to false if running without a trusted proxy.
36
+ */
37
+ trustProxy?: boolean;
38
+ /**
39
+ * Override the reverse-DNS search-crawler verifier injected into the core.
40
+ * When omitted, `createSearchCrawlerVerifier()` is used.
41
+ */
42
+ verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;
43
+ /**
44
+ * Override the Web Bot Auth verifier injected into the core.
45
+ * When omitted, the core's bundled RFC 9421 verifier is used.
46
+ */
47
+ verifyWebBotAuth?: (req: Request) => Promise<boolean>;
48
+ /**
49
+ * @internal
50
+ * Inject a pre-built `Verivyx` core instance. Used in tests via
51
+ * `verivyx.mock({...})` to avoid any network access. Production code
52
+ * should never set this; omit it and the adapter constructs the real core.
53
+ */
54
+ _core?: Verivyx;
55
+ /**
56
+ * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both
57
+ * the denied (402) and allowed handler responses.
58
+ * Default undefined = OFF (no headers added; existing behavior unchanged).
59
+ */
60
+ advertise?: DiscoveryOptions;
61
+ }
62
+ /**
63
+ * Create a Verivyx Hono adapter.
64
+ *
65
+ * Returns an object with a single `protect(handler)` method that wraps a
66
+ * Hono route handler behind the Verivyx paywall gate.
67
+ *
68
+ * ```ts
69
+ * const vx = verivyxHono({ domain: "example.com", token: "..." });
70
+ * app.get("/articles/:slug", vx.protect(async (c) => c.json({ content: "..." })));
71
+ * ```
72
+ */
73
+ declare function verivyxHono(opts?: HonoAdapterOptions): {
74
+ protect(handler: (c: Context) => Response | Promise<Response>, o?: {
75
+ seoPreview?: (c: {
76
+ slug: string;
77
+ }) => {
78
+ title: string;
79
+ excerpt: string;
80
+ };
81
+ }): MiddlewareHandler;
82
+ };
83
+
84
+ export { type HonoAdapterOptions, verivyxHono };
@@ -0,0 +1,84 @@
1
+ import { VerivyxOptions, Verivyx, DiscoveryOptions } from '@verivyx/paywall';
2
+ import { Context, MiddlewareHandler } from 'hono';
3
+
4
+ /**
5
+ * @verivyx/paywall-hono
6
+ *
7
+ * Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge).
8
+ *
9
+ * Thin layer — all gate logic lives in @verivyx/paywall core.
10
+ * This module handles:
11
+ * 1. IP resolution from Cloudflare / proxy headers (CF-Connecting-IP first).
12
+ * 2. Cloning the Web `Request` with the resolved IP in `x-real-ip`.
13
+ * 3. Calling core `protect()` (decision overload).
14
+ * 4. Returning `decision.response()` when denied (handler NOT called).
15
+ * 5. Attaching `PAYMENT-RESPONSE` header when a settlement receipt exists.
16
+ *
17
+ * Edge-portable: NO `node:*` imports. Uses only Web Platform APIs and Hono types.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { verivyxHono } from "@verivyx/paywall-hono";
22
+ * const vx = verivyxHono({ domain: "example.com", token: process.env.VX_TOKEN });
23
+ * app.get("/articles/:slug", vx.protect(async (c) => c.json({ content: "..." })));
24
+ * ```
25
+ */
26
+
27
+ /**
28
+ * Options for the Hono adapter.
29
+ * Extends core VerivyxOptions with edge-specific controls.
30
+ */
31
+ interface HonoAdapterOptions extends VerivyxOptions {
32
+ /**
33
+ * When true (default), read the client IP from Cloudflare / proxy headers:
34
+ * CF-Connecting-IP → X-Forwarded-For first hop → X-Real-IP.
35
+ * Set to false if running without a trusted proxy.
36
+ */
37
+ trustProxy?: boolean;
38
+ /**
39
+ * Override the reverse-DNS search-crawler verifier injected into the core.
40
+ * When omitted, `createSearchCrawlerVerifier()` is used.
41
+ */
42
+ verifyCrawlerDns?: (ip: string, ua: string) => Promise<boolean>;
43
+ /**
44
+ * Override the Web Bot Auth verifier injected into the core.
45
+ * When omitted, the core's bundled RFC 9421 verifier is used.
46
+ */
47
+ verifyWebBotAuth?: (req: Request) => Promise<boolean>;
48
+ /**
49
+ * @internal
50
+ * Inject a pre-built `Verivyx` core instance. Used in tests via
51
+ * `verivyx.mock({...})` to avoid any network access. Production code
52
+ * should never set this; omit it and the adapter constructs the real core.
53
+ */
54
+ _core?: Verivyx;
55
+ /**
56
+ * When set, attach RSL `Link` and AIPREF `Content-Usage` headers to both
57
+ * the denied (402) and allowed handler responses.
58
+ * Default undefined = OFF (no headers added; existing behavior unchanged).
59
+ */
60
+ advertise?: DiscoveryOptions;
61
+ }
62
+ /**
63
+ * Create a Verivyx Hono adapter.
64
+ *
65
+ * Returns an object with a single `protect(handler)` method that wraps a
66
+ * Hono route handler behind the Verivyx paywall gate.
67
+ *
68
+ * ```ts
69
+ * const vx = verivyxHono({ domain: "example.com", token: "..." });
70
+ * app.get("/articles/:slug", vx.protect(async (c) => c.json({ content: "..." })));
71
+ * ```
72
+ */
73
+ declare function verivyxHono(opts?: HonoAdapterOptions): {
74
+ protect(handler: (c: Context) => Response | Promise<Response>, o?: {
75
+ seoPreview?: (c: {
76
+ slug: string;
77
+ }) => {
78
+ title: string;
79
+ excerpt: string;
80
+ };
81
+ }): MiddlewareHandler;
82
+ };
83
+
84
+ export { type HonoAdapterOptions, verivyxHono };
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ import { verivyx, createSearchCrawlerVerifier, buildSeoPreviewResponse, attachPaymentResponse, rslLinkHeader, contentUsageHeader } from '@verivyx/paywall';
2
+
3
+ // src/index.ts
4
+ function firstHop(xff) {
5
+ if (xff === null || xff === void 0 || xff === "") {
6
+ return void 0;
7
+ }
8
+ const first = xff.split(",")[0];
9
+ return first !== void 0 ? first.trim() || void 0 : void 0;
10
+ }
11
+ function lastPathSegment(pathname) {
12
+ const segments = pathname.split("/").filter((s) => s.length > 0);
13
+ const last = segments[segments.length - 1];
14
+ if (last === void 0) {
15
+ return "";
16
+ }
17
+ try {
18
+ return decodeURIComponent(last);
19
+ } catch {
20
+ return last;
21
+ }
22
+ }
23
+ function resolveIp(c, trustProxy) {
24
+ if (!trustProxy) {
25
+ return void 0;
26
+ }
27
+ const cfIp = c.req.header("cf-connecting-ip");
28
+ if (cfIp !== void 0 && cfIp !== "") {
29
+ return cfIp;
30
+ }
31
+ const xff = c.req.header("x-forwarded-for");
32
+ const hop = firstHop(xff);
33
+ if (hop !== void 0) {
34
+ return hop;
35
+ }
36
+ const xri = c.req.header("x-real-ip");
37
+ return xri !== void 0 && xri !== "" ? xri : void 0;
38
+ }
39
+ function withAdvertiseHeaders(res, advertise) {
40
+ if (advertise === void 0) {
41
+ return res;
42
+ }
43
+ const headers = new Headers(res.headers);
44
+ headers.append("Link", rslLinkHeader(advertise));
45
+ headers.set("Content-Usage", contentUsageHeader(advertise));
46
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
47
+ }
48
+ function verivyxHono(opts) {
49
+ const vx = opts?._core ?? verivyx(opts, {
50
+ verifyCrawlerDns: opts?.verifyCrawlerDns ?? createSearchCrawlerVerifier(),
51
+ ...opts?.verifyWebBotAuth !== void 0 ? { verifyWebBotAuth: opts.verifyWebBotAuth } : {}
52
+ });
53
+ const trustProxy = opts?.trustProxy !== false;
54
+ return {
55
+ protect(handler, o) {
56
+ return async function verivyxHonoGuard(c) {
57
+ const raw = c.req.raw;
58
+ const ip = resolveIp(c, trustProxy);
59
+ let coreReq;
60
+ if (ip !== void 0) {
61
+ const headers = new Headers(raw.headers);
62
+ headers.set("x-real-ip", ip);
63
+ coreReq = new Request(raw, { headers });
64
+ } else {
65
+ const headers = new Headers(raw.headers);
66
+ headers.delete("x-real-ip");
67
+ headers.delete("x-forwarded-for");
68
+ coreReq = new Request(raw, { headers });
69
+ }
70
+ const paramSlug = c.req.param("slug");
71
+ const slug = paramSlug !== void 0 && paramSlug !== "" ? paramSlug : lastPathSegment(new URL(raw.url).pathname);
72
+ const decision = await vx.protect(coreReq, { slug });
73
+ if (!decision.allowed) {
74
+ const isPreviewCandidate = decision.reason === "crawler" || decision.reason === "human-unverified";
75
+ if (isPreviewCandidate && o?.seoPreview !== void 0) {
76
+ return withAdvertiseHeaders(
77
+ buildSeoPreviewResponse(slug, raw.url, o.seoPreview),
78
+ opts?.advertise
79
+ );
80
+ }
81
+ return withAdvertiseHeaders(decision.response(), opts?.advertise);
82
+ }
83
+ const res = await handler(c);
84
+ return withAdvertiseHeaders(
85
+ attachPaymentResponse(res, decision.paymentResponse),
86
+ opts?.advertise
87
+ );
88
+ };
89
+ }
90
+ };
91
+ }
92
+
93
+ export { verivyxHono };
94
+ //# sourceMappingURL=index.js.map
95
+ //# 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-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// ---------------------------------------------------------------------------\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 * 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 a single `protect(handler)` method that wraps a\n * Hono route handler behind the Verivyx paywall gate.\n *\n * ```ts\n * const vx = verivyxHono({ domain: \"example.com\", token: \"...\" });\n * app.get(\"/articles/:slug\", vx.protect(async (c) => c.json({ content: \"...\" })));\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} {\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 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. Get the raw Web Request from Hono context.\n const raw = c.req.raw;\n\n // 2. Resolve trusted client IP and inject into a cloned request so the\n // core classifier reads a reliable address regardless of edge hop.\n //\n // Security invariant:\n // trustProxy !== false → resolve IP from CF/proxy headers and set\n // x-real-ip on the cloned request (overrides any client value).\n // trustProxy === false → no socket IP is available in edge runtimes;\n // strip both x-real-ip and x-forwarded-for so a client cannot\n // spoof an IP into the core classifier (core sees no IP → safe).\n const ip = resolveIp(c, trustProxy);\n let coreReq: Request;\n if (ip !== undefined) {\n const headers = new Headers(raw.headers);\n headers.set(\"x-real-ip\", ip);\n // Clone the Request with updated headers. For GET/HEAD this is safe;\n // the core classify path reads headers only — body stays with raw.\n coreReq = new Request(raw, { 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 coreReq = new Request(raw, { headers });\n }\n\n // 3. 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 // 4. Ask the core to evaluate the request (decision overload).\n const decision = await vx.protect(coreReq, { slug });\n\n // 5. 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, raw.url, o.seoPreview),\n opts?.advertise,\n );\n }\n return withAdvertiseHeaders(decision.response(), opts?.advertise);\n }\n\n // 6. Allowed — call the original Hono handler.\n const res = await handler(c);\n\n // 7. 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}\n"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@verivyx/paywall-hono",
3
+ "version": "0.1.0",
4
+ "description": "Hono adapter for the Verivyx paywall SDK (Cloudflare Workers / Vercel Edge)",
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/hono"
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
+ "hono": ">=4"
41
+ },
42
+ "devDependencies": {
43
+ "hono": "^4.0.0",
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.5.0",
46
+ "vitest": "^2.0.0"
47
+ }
48
+ }