@emdash-cms/x402 0.0.1

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.
@@ -0,0 +1,144 @@
1
+ import { AstroIntegration } from "astro";
2
+ import { SettleResponse } from "@x402/core/types";
3
+
4
+ //#region src/types.d.ts
5
+ /** CAIP-2 network identifier (e.g., "eip155:8453" for Base mainnet) */
6
+ type Network = `${string}:${string}`;
7
+ /** Human-readable price: "$0.10", "0.50", or atomic units { amount, asset } */
8
+ type Price = string | number | {
9
+ amount: string;
10
+ asset: string;
11
+ extra?: Record<string, unknown>;
12
+ };
13
+ /**
14
+ * Configuration for the x402 Astro integration.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { x402 } from "@emdashcms/x402";
19
+ *
20
+ * export default defineConfig({
21
+ * integrations: [
22
+ * x402({
23
+ * payTo: "0xYourWallet",
24
+ * network: "eip155:8453",
25
+ * defaultPrice: "$0.01",
26
+ * botOnly: true,
27
+ * }),
28
+ * ],
29
+ * });
30
+ * ```
31
+ */
32
+ interface X402Config {
33
+ /** Destination wallet address for payments */
34
+ payTo: string;
35
+ /** CAIP-2 network identifier */
36
+ network: Network;
37
+ /** Default price for content (can be overridden per-page) */
38
+ defaultPrice?: Price;
39
+ /** Facilitator URL (defaults to x402.org testnet facilitator) */
40
+ facilitatorUrl?: string;
41
+ /** Payment scheme (defaults to "exact") */
42
+ scheme?: string;
43
+ /** Maximum timeout for payment signatures in seconds (defaults to 60) */
44
+ maxTimeoutSeconds?: number;
45
+ /** Enable EVM chain support (defaults to true) */
46
+ evm?: boolean;
47
+ /** Enable Solana chain support (defaults to false) */
48
+ svm?: boolean;
49
+ /**
50
+ * Only enforce payment for bots/agents, not humans.
51
+ * Uses Cloudflare Bot Management score from request.cf.botManagement.score.
52
+ * Requires Cloudflare deployment with Bot Management enabled.
53
+ * When true, requests with a bot score >= botScoreThreshold are treated as
54
+ * human and enforcement is skipped.
55
+ */
56
+ botOnly?: boolean;
57
+ /**
58
+ * Bot score threshold. Requests with a score below this are treated as bots.
59
+ * Only used when botOnly is true. Defaults to 30.
60
+ * Score range: 1 (almost certainly bot) to 99 (almost certainly human).
61
+ */
62
+ botScoreThreshold?: number;
63
+ }
64
+ /**
65
+ * Options passed to enforce() to override defaults for a specific page.
66
+ */
67
+ interface EnforceOptions {
68
+ /** Override the price for this specific request */
69
+ price?: Price;
70
+ /** Override the destination wallet */
71
+ payTo?: string;
72
+ /** Override the network */
73
+ network?: Network;
74
+ /** Override the payment scheme */
75
+ scheme?: string;
76
+ /** Resource description for the payment prompt */
77
+ description?: string;
78
+ /** MIME type hint for the resource */
79
+ mimeType?: string;
80
+ }
81
+ /**
82
+ * Result of a successful payment enforcement check.
83
+ * Returned when the request should proceed (either paid or skipped).
84
+ */
85
+ interface EnforceResult {
86
+ /** Whether payment was required and verified */
87
+ paid: boolean;
88
+ /** Whether enforcement was skipped (e.g., human in botOnly mode) */
89
+ skipped: boolean;
90
+ /** The payer's wallet address (if paid) */
91
+ payer?: string;
92
+ /** Settlement response (if payment was settled) */
93
+ settlement?: SettleResponse;
94
+ /** Headers to add to the response (e.g., PAYMENT-RESPONSE) */
95
+ responseHeaders: Record<string, string>;
96
+ }
97
+ /**
98
+ * The x402 enforcement interface available on Astro.locals.x402.
99
+ */
100
+ interface X402Enforcer {
101
+ /**
102
+ * Check if the current request includes valid payment.
103
+ * If not paid, returns a 402 Response that should be returned directly.
104
+ * If paid (or skipped in botOnly mode), returns an EnforceResult.
105
+ *
106
+ * @param request - The incoming Request object
107
+ * @param options - Optional overrides for this specific enforcement
108
+ * @returns A 402 Response (return it) or an EnforceResult (proceed with page render)
109
+ *
110
+ * @example
111
+ * ```astro
112
+ * ---
113
+ * const { x402 } = Astro.locals;
114
+ *
115
+ * const result = await x402.enforce(Astro.request, { price: "$0.01" });
116
+ * if (result instanceof Response) return result;
117
+ *
118
+ * x402.applyHeaders(result, Astro.response);
119
+ * ---
120
+ * ```
121
+ */
122
+ enforce(request: Request, options?: EnforceOptions): Promise<Response | EnforceResult>;
123
+ /**
124
+ * Apply x402 response headers (e.g., PAYMENT-RESPONSE) to the Astro response.
125
+ * Call this after a successful enforce() to include settlement proof in the response.
126
+ */
127
+ applyHeaders(result: EnforceResult, response: {
128
+ headers: Headers;
129
+ }): void;
130
+ /**
131
+ * Check if a request has a payment signature without verifying it.
132
+ * Useful for conditional rendering without enforcement.
133
+ */
134
+ hasPayment(request: Request): boolean;
135
+ }
136
+ //#endregion
137
+ //#region src/index.d.ts
138
+ /**
139
+ * Create the x402 Astro integration.
140
+ */
141
+ declare function x402(config: X402Config): AstroIntegration;
142
+ //#endregion
143
+ export { type EnforceOptions, type EnforceResult, type Network, type Price, type X402Config, type X402Enforcer, x402 };
144
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;;AAOA;AAAA,KAAY,OAAA;;KAGA,KAAA;EAGP,MAAA;EAAgB,KAAA;EAAe,KAAA,GAAQ,MAAA;AAAA;;;;;;;;AAqB5C;;;;;;;;;;;;UAAiB,UAAA;EAgBhB;EAdA,KAAA;EA4BA;EA1BA,OAAA,EAAS,OAAA;EA0BQ;EAxBjB,YAAA,GAAe,KAAA;EA8Be;EA5B9B,cAAA;EAkCiB;EAhCjB,MAAA;EA4BQ;EA1BR,iBAAA;EA8BA;EA5BA,GAAA;EA8BA;EA5BA,GAAA;EAgCA;;;AAOD;;;;EA/BC,OAAA;EAmCA;;;;;EA7BA,iBAAA;AAAA;;AAyCD;;UAnCiB,cAAA;EAyDC;EAvDjB,KAAA,GAAQ,KAAA;EAuDqD;EArD7D,KAAA;EAqDqD;EAnDrD,OAAA,GAAU,OAAA;EAyD+C;EAvDzD,MAAA;EA6D2B;EA3D3B,WAAA;EA+CA;EA7CA,QAAA;AAAA;;;;;UAOgB,aAAA;EA4ChB;EA1CA,IAAA;EA0Ca;EAxCb,OAAA;EAwCyD;EAtCzD,KAAA;EA4CA;EA1CA,UAAA,GAAa,cAAA;EA0CF;EAxCX,eAAA,EAAiB,MAAA;AAAA;;;;UAMD,YAAA;EC3DG;;;;;;;;;;;;;;;;;;;;;EDiFnB,OAAA,CAAQ,OAAA,EAAS,OAAA,EAAS,OAAA,GAAU,cAAA,GAAiB,OAAA,CAAQ,QAAA,GAAW,aAAA;;;;;EAMxE,YAAA,CAAa,MAAA,EAAQ,aAAA,EAAe,QAAA;IAAY,OAAA,EAAS,OAAA;EAAA;;;;;EAMzD,UAAA,CAAW,OAAA,EAAS,OAAA;AAAA;;;AArErB;;;AAAA,iBCxBgB,IAAA,CAAK,MAAA,EAAQ,UAAA,GAAa,gBAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,30 @@
1
+ //#region src/index.ts
2
+ const VIRTUAL_MODULE_ID = "virtual:x402/config";
3
+ const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
4
+ /**
5
+ * Create the x402 Astro integration.
6
+ */
7
+ function x402(config) {
8
+ return {
9
+ name: "@emdashcms/x402",
10
+ hooks: { "astro:config:setup": ({ addMiddleware, updateConfig }) => {
11
+ updateConfig({ vite: { plugins: [{
12
+ name: "x402-virtual-config",
13
+ resolveId(id) {
14
+ if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
15
+ },
16
+ load(id) {
17
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) return `export default ${JSON.stringify(config)}`;
18
+ }
19
+ }] } });
20
+ addMiddleware({
21
+ entrypoint: "@emdashcms/x402/middleware",
22
+ order: "pre"
23
+ });
24
+ } }
25
+ };
26
+ }
27
+
28
+ //#endregion
29
+ export { x402 };
30
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @emdashcms/x402 -- x402 Payment Integration for Astro\n *\n * An Astro integration that provides x402 payment enforcement via\n * Astro.locals.x402. Supports bot-only mode using Cloudflare Bot Management.\n *\n * @example\n * ```ts\n * // astro.config.mjs\n * import { x402 } from \"@emdashcms/x402\";\n *\n * export default defineConfig({\n * integrations: [\n * x402({\n * payTo: \"0xYourWallet\",\n * network: \"eip155:8453\",\n * defaultPrice: \"$0.01\",\n * botOnly: true,\n * }),\n * ],\n * });\n * ```\n *\n * ```astro\n * ---\n * const { x402 } = Astro.locals;\n *\n * const result = await x402.enforce(Astro.request, { price: \"$0.05\" });\n * if (result instanceof Response) return result;\n *\n * x402.applyHeaders(result, Astro.response);\n * ---\n * <article>Premium content here</article>\n * ```\n */\n\nimport type { AstroIntegration } from \"astro\";\n\nimport type { X402Config } from \"./types.js\";\n\nconst VIRTUAL_MODULE_ID = \"virtual:x402/config\";\nconst RESOLVED_VIRTUAL_MODULE_ID = \"\\0\" + VIRTUAL_MODULE_ID;\n\n/**\n * Create the x402 Astro integration.\n */\nexport function x402(config: X402Config): AstroIntegration {\n\treturn {\n\t\tname: \"@emdashcms/x402\",\n\t\thooks: {\n\t\t\t\"astro:config:setup\": ({ addMiddleware, updateConfig }) => {\n\t\t\t\t// Inject the virtual module that provides config to the middleware\n\t\t\t\tupdateConfig({\n\t\t\t\t\tvite: {\n\t\t\t\t\t\tplugins: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tname: \"x402-virtual-config\",\n\t\t\t\t\t\t\t\tresolveId(id: string) {\n\t\t\t\t\t\t\t\t\tif (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tload(id: string) {\n\t\t\t\t\t\t\t\t\tif (id === RESOLVED_VIRTUAL_MODULE_ID) {\n\t\t\t\t\t\t\t\t\t\treturn `export default ${JSON.stringify(config)}`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t});\n\n\t\t\t\t// Register the middleware that puts the enforcer on locals\n\t\t\t\taddMiddleware({\n\t\t\t\t\tentrypoint: \"@emdashcms/x402/middleware\",\n\t\t\t\t\torder: \"pre\",\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t};\n}\n\n// Re-export types for convenience\nexport type {\n\tEnforceOptions,\n\tEnforceResult,\n\tNetwork,\n\tPrice,\n\tX402Config,\n\tX402Enforcer,\n} from \"./types.js\";\n"],"mappings":";AAwCA,MAAM,oBAAoB;AAC1B,MAAM,6BAA6B,OAAO;;;;AAK1C,SAAgB,KAAK,QAAsC;AAC1D,QAAO;EACN,MAAM;EACN,OAAO,EACN,uBAAuB,EAAE,eAAe,mBAAmB;AAE1D,gBAAa,EACZ,MAAM,EACL,SAAS,CACR;IACC,MAAM;IACN,UAAU,IAAY;AACrB,SAAI,OAAO,kBAAmB,QAAO;;IAEtC,KAAK,IAAY;AAChB,SAAI,OAAO,2BACV,QAAO,kBAAkB,KAAK,UAAU,OAAO;;IAGjD,CACD,EACD,EACD,CAAC;AAGF,iBAAc;IACb,YAAY;IACZ,OAAO;IACP,CAAC;KAEH;EACD"}
package/locals.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { X402Enforcer } from "./src/types.js";
2
+
3
+ declare global {
4
+ namespace App {
5
+ interface Locals {
6
+ x402: X402Enforcer;
7
+ }
8
+ }
9
+ }
10
+
11
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@emdash-cms/x402",
3
+ "version": "0.0.1",
4
+ "description": "x402 payment protocol integration for Astro sites",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/cloudflare/emdash.git",
9
+ "directory": "packages/x402"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "locals.d.ts"
15
+ ],
16
+ "type": "module",
17
+ "main": "dist/index.mjs",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.mts",
21
+ "default": "./dist/index.mjs"
22
+ },
23
+ "./middleware": {
24
+ "types": "./dist/middleware.d.mts",
25
+ "default": "./dist/middleware.mjs"
26
+ },
27
+ "./locals": {
28
+ "types": "./locals.d.ts"
29
+ }
30
+ },
31
+ "dependencies": {
32
+ "@x402/core": "^2.8.0",
33
+ "@x402/evm": "^2.8.0"
34
+ },
35
+ "devDependencies": {
36
+ "@arethetypeswrong/cli": "^0.18.2",
37
+ "astro": "^6.0.1",
38
+ "publint": "0.3.17",
39
+ "tsdown": "0.20.3",
40
+ "typescript": "^5.9.3",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "peerDependencies": {
44
+ "astro": ">=6.0.0-beta.0"
45
+ },
46
+ "optionalDependencies": {
47
+ "@x402/svm": "^2.8.0"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "dev": "tsdown --watch",
52
+ "check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm --ignore-rules=no-resolution",
53
+ "test": "vitest",
54
+ "typecheck": "tsgo --noEmit"
55
+ }
56
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * x402 Payment Enforcer
3
+ *
4
+ * Creates the x402 enforcement interface. Uses the @x402/core SDK
5
+ * to handle the payment protocol negotiation.
6
+ */
7
+
8
+ import {
9
+ decodePaymentSignatureHeader,
10
+ encodePaymentRequiredHeader,
11
+ encodePaymentResponseHeader,
12
+ } from "@x402/core/http";
13
+ import { HTTPFacilitatorClient, x402ResourceServer, type ResourceConfig } from "@x402/core/server";
14
+
15
+ import type { EnforceOptions, EnforceResult, X402Config, X402Enforcer } from "./types.js";
16
+
17
+ const PAYMENT_SIGNATURE_HEADER = "payment-signature";
18
+ const PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
19
+ const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
20
+
21
+ const DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
22
+ const DEFAULT_SCHEME = "exact";
23
+ const DEFAULT_MAX_TIMEOUT_SECONDS = 60;
24
+ const DEFAULT_BOT_SCORE_THRESHOLD = 30;
25
+
26
+ /**
27
+ * Cached resource server instance.
28
+ * Initialized once per process, reused across requests.
29
+ */
30
+ let _resourceServer: x402ResourceServer | null = null;
31
+ let _initPromise: Promise<void> | null = null;
32
+
33
+ /**
34
+ * Get or create the x402ResourceServer singleton.
35
+ */
36
+ async function getResourceServer(config: X402Config): Promise<x402ResourceServer> {
37
+ if (!_resourceServer) {
38
+ const facilitatorUrl = config.facilitatorUrl ?? DEFAULT_FACILITATOR_URL;
39
+ const facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
40
+ const server = new x402ResourceServer(facilitator);
41
+
42
+ // Register EVM scheme (default)
43
+ if (config.evm !== false) {
44
+ try {
45
+ const evmMod = await import("@x402/evm/exact/server");
46
+ const evmScheme = new evmMod.ExactEvmScheme();
47
+ server.register("eip155:*" as `${string}:${string}`, evmScheme);
48
+ } catch {
49
+ // @x402/evm not installed -- skip EVM support
50
+ }
51
+ }
52
+
53
+ // Register SVM scheme (opt-in)
54
+ if (config.svm) {
55
+ try {
56
+ const svmMod = await import("@x402/svm/exact/server");
57
+ const svmScheme = new svmMod.ExactSvmScheme();
58
+ server.register("solana:*" as `${string}:${string}`, svmScheme);
59
+ } catch {
60
+ // @x402/svm not installed -- skip Solana support
61
+ }
62
+ }
63
+
64
+ _resourceServer = server;
65
+ _initPromise = server.initialize();
66
+ }
67
+
68
+ if (_initPromise) {
69
+ await _initPromise;
70
+ _initPromise = null;
71
+ }
72
+
73
+ return _resourceServer;
74
+ }
75
+
76
+ /**
77
+ * Check if a request is from a bot using Cloudflare Bot Management.
78
+ * Returns true if the request is likely from a bot, false otherwise.
79
+ * When bot management data is unavailable (local dev, non-CF deployment),
80
+ * returns false (treat as human).
81
+ */
82
+ function isBot(request: Request, threshold: number): boolean {
83
+ // Cloudflare Workers expose cf properties on the request
84
+ const cf = (request as unknown as { cf?: { botManagement?: { score?: number } } }).cf;
85
+ const score = cf?.botManagement?.score;
86
+ if (score == null) return false;
87
+ return score < threshold;
88
+ }
89
+
90
+ /**
91
+ * Create an X402Enforcer for the given configuration.
92
+ * Called once by the middleware, reused across requests.
93
+ */
94
+ export function createEnforcer(config: X402Config): X402Enforcer {
95
+ const botScoreThreshold = config.botScoreThreshold ?? DEFAULT_BOT_SCORE_THRESHOLD;
96
+
97
+ return {
98
+ async enforce(request: Request, options?: EnforceOptions): Promise<Response | EnforceResult> {
99
+ // In botOnly mode, skip enforcement for humans
100
+ if (config.botOnly && !isBot(request, botScoreThreshold)) {
101
+ return { paid: false, skipped: true, responseHeaders: {} };
102
+ }
103
+
104
+ const server = await getResourceServer(config);
105
+
106
+ const price = options?.price ?? config.defaultPrice;
107
+ if (price == null) {
108
+ throw new Error(
109
+ "x402: No price specified. Pass a price in enforce() options or set defaultPrice in the config.",
110
+ );
111
+ }
112
+
113
+ const payTo = options?.payTo ?? config.payTo;
114
+ const network = options?.network ?? config.network;
115
+ const scheme = options?.scheme ?? config.scheme ?? DEFAULT_SCHEME;
116
+ const maxTimeoutSeconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
117
+
118
+ const resourceConfig: ResourceConfig = {
119
+ scheme,
120
+ payTo,
121
+ price: normalizePrice(price),
122
+ network,
123
+ maxTimeoutSeconds,
124
+ };
125
+
126
+ const url = new URL(request.url);
127
+ const resourceInfo = {
128
+ url: url.pathname,
129
+ description: options?.description,
130
+ mimeType: options?.mimeType,
131
+ };
132
+
133
+ // Check for payment signature header
134
+ const paymentHeader =
135
+ request.headers.get(PAYMENT_SIGNATURE_HEADER) || request.headers.get("PAYMENT-SIGNATURE");
136
+
137
+ if (!paymentHeader) {
138
+ return make402(server, resourceConfig, resourceInfo, "Payment required");
139
+ }
140
+
141
+ // Payment present -- decode and verify
142
+ const paymentPayload = decodePaymentSignatureHeader(paymentHeader);
143
+ const requirements = await server.buildPaymentRequirements(resourceConfig);
144
+ const matchingReqs = server.findMatchingRequirements(requirements, paymentPayload);
145
+
146
+ if (!matchingReqs) {
147
+ return make402(
148
+ server,
149
+ resourceConfig,
150
+ resourceInfo,
151
+ "Payment does not match accepted requirements",
152
+ );
153
+ }
154
+
155
+ // Verify with facilitator
156
+ const verifyResult = await server.verifyPayment(paymentPayload, matchingReqs);
157
+
158
+ if (!verifyResult.isValid) {
159
+ return make402(
160
+ server,
161
+ resourceConfig,
162
+ resourceInfo,
163
+ verifyResult.invalidReason ?? "Payment verification failed",
164
+ );
165
+ }
166
+
167
+ // Settle
168
+ const settleResult = await server.settlePayment(paymentPayload, matchingReqs);
169
+
170
+ const responseHeaders: Record<string, string> = {};
171
+ if (settleResult) {
172
+ responseHeaders[PAYMENT_RESPONSE_HEADER] = encodePaymentResponseHeader(settleResult);
173
+ }
174
+
175
+ return {
176
+ paid: true,
177
+ skipped: false,
178
+ payer: verifyResult.payer,
179
+ settlement: settleResult,
180
+ responseHeaders,
181
+ };
182
+ },
183
+
184
+ applyHeaders(result: EnforceResult, response: { headers: Headers }): void {
185
+ for (const [key, value] of Object.entries(result.responseHeaders)) {
186
+ response.headers.set(key, value);
187
+ }
188
+ },
189
+
190
+ hasPayment(request: Request): boolean {
191
+ return !!(
192
+ request.headers.get(PAYMENT_SIGNATURE_HEADER) || request.headers.get("PAYMENT-SIGNATURE")
193
+ );
194
+ },
195
+ };
196
+ }
197
+
198
+ /** Build and return a 402 Response */
199
+ async function make402(
200
+ server: x402ResourceServer,
201
+ resourceConfig: ResourceConfig,
202
+ resourceInfo: { url: string; description?: string; mimeType?: string },
203
+ error: string,
204
+ ): Promise<Response> {
205
+ const requirements = await server.buildPaymentRequirements(resourceConfig);
206
+ const paymentRequired = await server.createPaymentRequiredResponse(
207
+ requirements,
208
+ resourceInfo,
209
+ error,
210
+ );
211
+
212
+ return new Response(JSON.stringify(paymentRequired), {
213
+ status: 402,
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ [PAYMENT_REQUIRED_HEADER]: encodePaymentRequiredHeader(paymentRequired),
217
+ },
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Normalize a user-friendly price into the format expected by x402 SDK.
223
+ */
224
+ function normalizePrice(
225
+ price: string | number | { amount: string; asset: string; extra?: Record<string, unknown> },
226
+ ): string | number | { amount: string; asset: string; extra?: Record<string, unknown> } {
227
+ if (typeof price === "string" && price.startsWith("$")) {
228
+ return price.slice(1);
229
+ }
230
+ return price;
231
+ }
package/src/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @emdash-cms/x402 -- x402 Payment Integration for Astro
3
+ *
4
+ * An Astro integration that provides x402 payment enforcement via
5
+ * Astro.locals.x402. Supports bot-only mode using Cloudflare Bot Management.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // astro.config.mjs
10
+ * import { x402 } from "@emdash-cms/x402";
11
+ *
12
+ * export default defineConfig({
13
+ * integrations: [
14
+ * x402({
15
+ * payTo: "0xYourWallet",
16
+ * network: "eip155:8453",
17
+ * defaultPrice: "$0.01",
18
+ * botOnly: true,
19
+ * }),
20
+ * ],
21
+ * });
22
+ * ```
23
+ *
24
+ * ```astro
25
+ * ---
26
+ * const { x402 } = Astro.locals;
27
+ *
28
+ * const result = await x402.enforce(Astro.request, { price: "$0.05" });
29
+ * if (result instanceof Response) return result;
30
+ *
31
+ * x402.applyHeaders(result, Astro.response);
32
+ * ---
33
+ * <article>Premium content here</article>
34
+ * ```
35
+ */
36
+
37
+ import type { AstroIntegration } from "astro";
38
+
39
+ import type { X402Config } from "./types.js";
40
+
41
+ const VIRTUAL_MODULE_ID = "virtual:x402/config";
42
+ const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
43
+
44
+ /**
45
+ * Create the x402 Astro integration.
46
+ */
47
+ export function x402(config: X402Config): AstroIntegration {
48
+ return {
49
+ name: "@emdash-cms/x402",
50
+ hooks: {
51
+ "astro:config:setup": ({ addMiddleware, updateConfig }) => {
52
+ // Inject the virtual module that provides config to the middleware
53
+ updateConfig({
54
+ vite: {
55
+ plugins: [
56
+ {
57
+ name: "x402-virtual-config",
58
+ resolveId(id: string) {
59
+ if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
60
+ },
61
+ load(id: string) {
62
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) {
63
+ return `export default ${JSON.stringify(config)}`;
64
+ }
65
+ },
66
+ },
67
+ ],
68
+ },
69
+ });
70
+
71
+ // Register the middleware that puts the enforcer on locals
72
+ addMiddleware({
73
+ entrypoint: "@emdash-cms/x402/middleware",
74
+ order: "pre",
75
+ });
76
+ },
77
+ },
78
+ };
79
+ }
80
+
81
+ // Re-export types for convenience
82
+ export type {
83
+ EnforceOptions,
84
+ EnforceResult,
85
+ Network,
86
+ Price,
87
+ X402Config,
88
+ X402Enforcer,
89
+ } from "./types.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * x402 Astro Middleware
3
+ *
4
+ * Injected by the x402 integration. Creates the enforcer and
5
+ * places it on Astro.locals.x402 for use in page frontmatter.
6
+ *
7
+ * The config is passed via the virtual module resolved by the integration.
8
+ */
9
+
10
+ import { defineMiddleware } from "astro:middleware";
11
+ // The integration injects config via a virtual module.
12
+ // @ts-ignore -- virtual module, resolved at build time
13
+ import x402Config from "virtual:x402/config";
14
+
15
+ import { createEnforcer } from "./enforcer.js";
16
+ import type { X402Config } from "./types.js";
17
+
18
+ const config = x402Config as X402Config;
19
+ const enforcer = createEnforcer(config);
20
+
21
+ export const onRequest = defineMiddleware(async (context, next) => {
22
+ context.locals.x402 = enforcer;
23
+ return next();
24
+ });
package/src/types.ts ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * x402 Payment Integration Types
3
+ */
4
+
5
+ import type { SettleResponse } from "@x402/core/types";
6
+
7
+ /** CAIP-2 network identifier (e.g., "eip155:8453" for Base mainnet) */
8
+ export type Network = `${string}:${string}`;
9
+
10
+ /** Human-readable price: "$0.10", "0.50", or atomic units { amount, asset } */
11
+ export type Price =
12
+ | string
13
+ | number
14
+ | { amount: string; asset: string; extra?: Record<string, unknown> };
15
+
16
+ /**
17
+ * Configuration for the x402 Astro integration.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { x402 } from "@emdash-cms/x402";
22
+ *
23
+ * export default defineConfig({
24
+ * integrations: [
25
+ * x402({
26
+ * payTo: "0xYourWallet",
27
+ * network: "eip155:8453",
28
+ * defaultPrice: "$0.01",
29
+ * botOnly: true,
30
+ * }),
31
+ * ],
32
+ * });
33
+ * ```
34
+ */
35
+ export interface X402Config {
36
+ /** Destination wallet address for payments */
37
+ payTo: string;
38
+ /** CAIP-2 network identifier */
39
+ network: Network;
40
+ /** Default price for content (can be overridden per-page) */
41
+ defaultPrice?: Price;
42
+ /** Facilitator URL (defaults to x402.org testnet facilitator) */
43
+ facilitatorUrl?: string;
44
+ /** Payment scheme (defaults to "exact") */
45
+ scheme?: string;
46
+ /** Maximum timeout for payment signatures in seconds (defaults to 60) */
47
+ maxTimeoutSeconds?: number;
48
+ /** Enable EVM chain support (defaults to true) */
49
+ evm?: boolean;
50
+ /** Enable Solana chain support (defaults to false) */
51
+ svm?: boolean;
52
+ /**
53
+ * Only enforce payment for bots/agents, not humans.
54
+ * Uses Cloudflare Bot Management score from request.cf.botManagement.score.
55
+ * Requires Cloudflare deployment with Bot Management enabled.
56
+ * When true, requests with a bot score >= botScoreThreshold are treated as
57
+ * human and enforcement is skipped.
58
+ */
59
+ botOnly?: boolean;
60
+ /**
61
+ * Bot score threshold. Requests with a score below this are treated as bots.
62
+ * Only used when botOnly is true. Defaults to 30.
63
+ * Score range: 1 (almost certainly bot) to 99 (almost certainly human).
64
+ */
65
+ botScoreThreshold?: number;
66
+ }
67
+
68
+ /**
69
+ * Options passed to enforce() to override defaults for a specific page.
70
+ */
71
+ export interface EnforceOptions {
72
+ /** Override the price for this specific request */
73
+ price?: Price;
74
+ /** Override the destination wallet */
75
+ payTo?: string;
76
+ /** Override the network */
77
+ network?: Network;
78
+ /** Override the payment scheme */
79
+ scheme?: string;
80
+ /** Resource description for the payment prompt */
81
+ description?: string;
82
+ /** MIME type hint for the resource */
83
+ mimeType?: string;
84
+ }
85
+
86
+ /**
87
+ * Result of a successful payment enforcement check.
88
+ * Returned when the request should proceed (either paid or skipped).
89
+ */
90
+ export interface EnforceResult {
91
+ /** Whether payment was required and verified */
92
+ paid: boolean;
93
+ /** Whether enforcement was skipped (e.g., human in botOnly mode) */
94
+ skipped: boolean;
95
+ /** The payer's wallet address (if paid) */
96
+ payer?: string;
97
+ /** Settlement response (if payment was settled) */
98
+ settlement?: SettleResponse;
99
+ /** Headers to add to the response (e.g., PAYMENT-RESPONSE) */
100
+ responseHeaders: Record<string, string>;
101
+ }
102
+
103
+ /**
104
+ * The x402 enforcement interface available on Astro.locals.x402.
105
+ */
106
+ export interface X402Enforcer {
107
+ /**
108
+ * Check if the current request includes valid payment.
109
+ * If not paid, returns a 402 Response that should be returned directly.
110
+ * If paid (or skipped in botOnly mode), returns an EnforceResult.
111
+ *
112
+ * @param request - The incoming Request object
113
+ * @param options - Optional overrides for this specific enforcement
114
+ * @returns A 402 Response (return it) or an EnforceResult (proceed with page render)
115
+ *
116
+ * @example
117
+ * ```astro
118
+ * ---
119
+ * const { x402 } = Astro.locals;
120
+ *
121
+ * const result = await x402.enforce(Astro.request, { price: "$0.01" });
122
+ * if (result instanceof Response) return result;
123
+ *
124
+ * x402.applyHeaders(result, Astro.response);
125
+ * ---
126
+ * ```
127
+ */
128
+ enforce(request: Request, options?: EnforceOptions): Promise<Response | EnforceResult>;
129
+
130
+ /**
131
+ * Apply x402 response headers (e.g., PAYMENT-RESPONSE) to the Astro response.
132
+ * Call this after a successful enforce() to include settlement proof in the response.
133
+ */
134
+ applyHeaders(result: EnforceResult, response: { headers: Headers }): void;
135
+
136
+ /**
137
+ * Check if a request has a payment signature without verifying it.
138
+ * Useful for conditional rendering without enforcement.
139
+ */
140
+ hasPayment(request: Request): boolean;
141
+ }