@foldset/cloudflare 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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # Typescript library for foldset?
@@ -0,0 +1,71 @@
1
+ import { HTTPAdapter } from "@x402/core/server";
2
+ import { Context } from "hono";
3
+ /**
4
+ * Hono adapter implementation
5
+ */
6
+ export declare class HonoAdapter implements HTTPAdapter {
7
+ private c;
8
+ /**
9
+ * Creates a new HonoAdapter instance.
10
+ *
11
+ * @param c - The Hono context object
12
+ */
13
+ constructor(c: Context);
14
+ /**
15
+ * Gets a header value from the request.
16
+ *
17
+ * @param name - The header name
18
+ * @returns The header value or undefined
19
+ */
20
+ getHeader(name: string): string | undefined;
21
+ /**
22
+ * Gets the HTTP method of the request.
23
+ *
24
+ * @returns The HTTP method
25
+ */
26
+ getMethod(): string;
27
+ /**
28
+ * Gets the path of the request.
29
+ *
30
+ * @returns The request path
31
+ */
32
+ getPath(): string;
33
+ /**
34
+ * Gets the full URL of the request.
35
+ *
36
+ * @returns The full request URL
37
+ */
38
+ getUrl(): string;
39
+ /**
40
+ * Gets the Accept header from the request.
41
+ *
42
+ * @returns The Accept header value or empty string
43
+ */
44
+ getAcceptHeader(): string;
45
+ /**
46
+ * Gets the User-Agent header from the request.
47
+ *
48
+ * @returns The User-Agent header value or empty string
49
+ */
50
+ getUserAgent(): string;
51
+ /**
52
+ * Gets all query parameters from the request URL.
53
+ *
54
+ * @returns Record of query parameter key-value pairs
55
+ */
56
+ getQueryParams(): Record<string, string | string[]>;
57
+ /**
58
+ * Gets a specific query parameter by name.
59
+ *
60
+ * @param name - The query parameter name
61
+ * @returns The query parameter value(s) or undefined
62
+ */
63
+ getQueryParam(name: string): string | string[] | undefined;
64
+ /**
65
+ * Gets the parsed request body.
66
+ * Requires appropriate body parsing middleware.
67
+ *
68
+ * @returns The parsed request body
69
+ */
70
+ getBody(): Promise<unknown>;
71
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Hono adapter implementation
3
+ */
4
+ export class HonoAdapter {
5
+ /**
6
+ * Creates a new HonoAdapter instance.
7
+ *
8
+ * @param c - The Hono context object
9
+ */
10
+ constructor(c) {
11
+ this.c = c;
12
+ }
13
+ /**
14
+ * Gets a header value from the request.
15
+ *
16
+ * @param name - The header name
17
+ * @returns The header value or undefined
18
+ */
19
+ getHeader(name) {
20
+ return this.c.req.header(name);
21
+ }
22
+ /**
23
+ * Gets the HTTP method of the request.
24
+ *
25
+ * @returns The HTTP method
26
+ */
27
+ getMethod() {
28
+ return this.c.req.method;
29
+ }
30
+ /**
31
+ * Gets the path of the request.
32
+ *
33
+ * @returns The request path
34
+ */
35
+ getPath() {
36
+ return this.c.req.path;
37
+ }
38
+ /**
39
+ * Gets the full URL of the request.
40
+ *
41
+ * @returns The full request URL
42
+ */
43
+ getUrl() {
44
+ return this.c.req.url;
45
+ }
46
+ /**
47
+ * Gets the Accept header from the request.
48
+ *
49
+ * @returns The Accept header value or empty string
50
+ */
51
+ getAcceptHeader() {
52
+ return this.c.req.header("Accept") || "";
53
+ }
54
+ /**
55
+ * Gets the User-Agent header from the request.
56
+ *
57
+ * @returns The User-Agent header value or empty string
58
+ */
59
+ getUserAgent() {
60
+ return this.c.req.header("User-Agent") || "";
61
+ }
62
+ /**
63
+ * Gets all query parameters from the request URL.
64
+ *
65
+ * @returns Record of query parameter key-value pairs
66
+ */
67
+ getQueryParams() {
68
+ const query = this.c.req.query();
69
+ // Convert single values to match the interface
70
+ const result = {};
71
+ for (const [key, value] of Object.entries(query)) {
72
+ result[key] = value;
73
+ }
74
+ return result;
75
+ }
76
+ /**
77
+ * Gets a specific query parameter by name.
78
+ *
79
+ * @param name - The query parameter name
80
+ * @returns The query parameter value(s) or undefined
81
+ */
82
+ getQueryParam(name) {
83
+ return this.c.req.query(name);
84
+ }
85
+ /**
86
+ * Gets the parsed request body.
87
+ * Requires appropriate body parsing middleware.
88
+ *
89
+ * @returns The parsed request body
90
+ */
91
+ async getBody() {
92
+ try {
93
+ return await this.c.req.json();
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,37 @@
1
+ import { Hono, type MiddlewareHandler } from 'hono';
2
+ import { PaywallConfig, PaywallProvider, x402ResourceServer } from '@x402/hono';
3
+ import type { RoutesConfig } from '@x402/core/http';
4
+ type Env = {
5
+ FOLDSET_API_KEY: string;
6
+ };
7
+ /**
8
+ * Hono payment middleware for x402 protocol (direct server instance).
9
+ *
10
+ * Use this when you want to pass a pre-configured x402ResourceServer instance.
11
+ * This provides more flexibility for testing, custom configuration, and reusing
12
+ * server instances across multiple middlewares.
13
+ *
14
+ * @param routes - Route configurations for protected endpoints
15
+ * @param server - Pre-configured x402ResourceServer instance
16
+ * @param paywallConfig - Optional configuration for the built-in paywall UI
17
+ * @param paywall - Optional custom paywall provider (overrides default)
18
+ * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true)
19
+ * @returns Hono middleware handler
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { paymentMiddleware } from "@x402/hono";
24
+ * import { x402ResourceServer } from "@x402/core/server";
25
+ * import { registerExactEvmScheme } from "@x402/evm/exact/server";
26
+ *
27
+ * const server = new x402ResourceServer(myFacilitatorClient);
28
+ * registerExactEvmScheme(server, {});
29
+ *
30
+ * app.use(paymentMiddleware(routes, server, paywallConfig));
31
+ * ```
32
+ */
33
+ export declare function paymentMiddleware(routes: RoutesConfig, server: x402ResourceServer, paywallConfig?: PaywallConfig, paywall?: PaywallProvider, syncFacilitatorOnStart?: boolean): MiddlewareHandler;
34
+ declare const app: Hono<{
35
+ Bindings: Env;
36
+ }, import("hono/types").BlankSchema, "/">;
37
+ export default app;
package/dist/index.js ADDED
@@ -0,0 +1,237 @@
1
+ // TODO rfradkin: Make a lot of these types not just functions
2
+ import { Hono } from 'hono';
3
+ import { x402ResourceServer, x402HTTPResourceServer } from '@x402/hono';
4
+ import { registerExactEvmScheme } from '@x402/evm/exact/server';
5
+ import { HTTPFacilitatorClient } from '@x402/core/server';
6
+ import pMemoize from 'p-memoize';
7
+ import { HonoAdapter } from './adapter';
8
+ async function _fetchFoldsetAPI(endpoint, env) {
9
+ const res = await fetch(`https://api.foldset.com/v1/${endpoint}`, {
10
+ method: 'GET',
11
+ headers: {
12
+ Authorization: `Bearer ${env.FOLDSET_API_KEY}`,
13
+ Accept: 'application/json',
14
+ },
15
+ });
16
+ if (!res.ok) {
17
+ throw new Error(`Foldset API (${endpoint}) failed: ${res.status}`);
18
+ }
19
+ return (await res.json());
20
+ }
21
+ const cacheTTL = 60000; // 60 seconds
22
+ // This is an in memory cache. Cloudflare also has a cache but it is not
23
+ // in memory, so it would survive cold starts (this won't). I think in-memory
24
+ // makes more sense here for now.
25
+ // TODO rfradkin: Also make sure this caching works, didn't test it
26
+ const fetchFoldsetAPI = pMemoize(_fetchFoldsetAPI, {
27
+ maxAge: cacheTTL,
28
+ cacheKey: ([endpoint]) => endpoint,
29
+ });
30
+ async function getRestrictions(env) {
31
+ const restrictionList = await fetchFoldsetAPI('restrictions', env);
32
+ const restrictions = {};
33
+ for (const restriction of restrictionList) {
34
+ restrictions[restriction.pathname] = restriction.price;
35
+ }
36
+ return restrictions;
37
+ }
38
+ async function getEVMAddress(env) {
39
+ const data = await fetchFoldsetAPI('get-address', env);
40
+ return data.address;
41
+ }
42
+ // TODO rfradkin: Add metadata for bazaar finding
43
+ // https://x402.gitbook.io/x402/getting-started/quickstart-for-sellers
44
+ function buildRoutesConfig(restrictions, payTo) {
45
+ const routesConfig = {};
46
+ for (const [pathname, price] of Object.entries(restrictions)) {
47
+ routesConfig[pathname] = {
48
+ accepts: [
49
+ {
50
+ scheme: 'exact',
51
+ price: `$${price}`,
52
+ network: 'eip155:84532',
53
+ payTo,
54
+ },
55
+ ],
56
+ description: 'Access to premium content',
57
+ mimeType: 'application/json',
58
+ };
59
+ }
60
+ return routesConfig;
61
+ }
62
+ let server = null;
63
+ // Right now, load times are like 800ms, which is pretty slow since
64
+ // static site is 200ms. Look into later, this call probably takes most of the time.
65
+ // This is like this cause can'd do async calls at module level in cloudflare
66
+ function getServer() {
67
+ if (!server) {
68
+ // TODO rfradkin: Hard coded for now but consider making it configurable
69
+ const facilitatorClient = new HTTPFacilitatorClient({
70
+ url: 'https://x402.org/facilitator',
71
+ });
72
+ server = new x402ResourceServer(facilitatorClient);
73
+ registerExactEvmScheme(server);
74
+ }
75
+ return server;
76
+ }
77
+ /**
78
+ * Hono payment middleware for x402 protocol (direct server instance).
79
+ *
80
+ * Use this when you want to pass a pre-configured x402ResourceServer instance.
81
+ * This provides more flexibility for testing, custom configuration, and reusing
82
+ * server instances across multiple middlewares.
83
+ *
84
+ * @param routes - Route configurations for protected endpoints
85
+ * @param server - Pre-configured x402ResourceServer instance
86
+ * @param paywallConfig - Optional configuration for the built-in paywall UI
87
+ * @param paywall - Optional custom paywall provider (overrides default)
88
+ * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true)
89
+ * @returns Hono middleware handler
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { paymentMiddleware } from "@x402/hono";
94
+ * import { x402ResourceServer } from "@x402/core/server";
95
+ * import { registerExactEvmScheme } from "@x402/evm/exact/server";
96
+ *
97
+ * const server = new x402ResourceServer(myFacilitatorClient);
98
+ * registerExactEvmScheme(server, {});
99
+ *
100
+ * app.use(paymentMiddleware(routes, server, paywallConfig));
101
+ * ```
102
+ */
103
+ export function paymentMiddleware(routes, server, paywallConfig, paywall, syncFacilitatorOnStart = true) {
104
+ // Create the x402 HTTP server instance with the resource server
105
+ const httpServer = new x402HTTPResourceServer(server, routes);
106
+ // Register custom paywall provider if provided
107
+ if (paywall) {
108
+ httpServer.registerPaywallProvider(paywall);
109
+ }
110
+ // Store initialization promise (not the result)
111
+ // httpServer.initialize() fetches facilitator support and validates routes
112
+ let initPromise = syncFacilitatorOnStart ? httpServer.initialize() : null;
113
+ // TODO rfradkin: Add bazaar extension
114
+ // // Dynamically register bazaar extension if routes declare it
115
+ // let bazaarPromise: Promise<void> | null = null;
116
+ // if (checkIfBazaarNeeded(routes)) {
117
+ // bazaarPromise = import("@x402/extensions/bazaar")
118
+ // .then(({ bazaarResourceServerExtension }) => {
119
+ // server.registerExtension(bazaarResourceServerExtension);
120
+ // })
121
+ // .catch(err => {
122
+ // console.error("Failed to load bazaar extension:", err);
123
+ // });
124
+ // }
125
+ return async (c, next) => {
126
+ // Create adapter and context
127
+ const adapter = new HonoAdapter(c);
128
+ const context = {
129
+ adapter,
130
+ path: c.req.path,
131
+ method: c.req.method,
132
+ paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("x-payment"),
133
+ };
134
+ // Check if route requires payment before initializing facilitator
135
+ if (!httpServer.requiresPayment(context)) {
136
+ return next();
137
+ }
138
+ // Only initialize when processing a protected route
139
+ if (initPromise) {
140
+ await initPromise;
141
+ initPromise = null; // Clear after first await
142
+ }
143
+ // // Await bazaar extension loading if needed
144
+ // if (bazaarPromise) {
145
+ // await bazaarPromise;
146
+ // bazaarPromise = null;
147
+ // }
148
+ // Process payment requirement check
149
+ const result = await httpServer.processHTTPRequest(context, paywallConfig);
150
+ // Handle the different result types
151
+ switch (result.type) {
152
+ case "no-payment-required":
153
+ // No payment needed, proceed directly to the route handler
154
+ return next();
155
+ case "payment-error":
156
+ // Payment required but not provided or invalid
157
+ const { response } = result;
158
+ Object.entries(response.headers).forEach(([key, value]) => {
159
+ c.header(key, value);
160
+ });
161
+ if (response.isHtml) {
162
+ return c.html(response.body, response.status);
163
+ }
164
+ else {
165
+ return c.json(response.body || {}, response.status);
166
+ }
167
+ case "payment-verified":
168
+ // Payment is valid, need to wrap response for settlement
169
+ const { paymentPayload, paymentRequirements } = result;
170
+ // Proceed to the next middleware or route handler
171
+ await next();
172
+ // Get the current response
173
+ let res = c.res;
174
+ // If the response from the protected route is >= 400, do not settle payment
175
+ if (res.status >= 400) {
176
+ return;
177
+ }
178
+ // Clear the response so we can modify headers
179
+ c.res = undefined;
180
+ try {
181
+ const settleResult = await httpServer.processSettlement(paymentPayload, paymentRequirements);
182
+ if (!settleResult.success) {
183
+ // Settlement failed - do not return the protected resource
184
+ res = c.json({
185
+ error: "Settlement failed",
186
+ details: settleResult.errorReason,
187
+ }, 402);
188
+ }
189
+ else {
190
+ // Settlement succeeded - add headers to response
191
+ Object.entries(settleResult.headers).forEach(([key, value]) => {
192
+ res.headers.set(key, value);
193
+ });
194
+ }
195
+ }
196
+ catch (error) {
197
+ console.error(error);
198
+ // If settlement fails, return an error response
199
+ res = c.json({
200
+ error: "Settlement failed",
201
+ details: error instanceof Error ? error.message : "Unknown error",
202
+ }, 402);
203
+ }
204
+ // Restore the response (potentially modified with settlement headers)
205
+ c.res = res;
206
+ return;
207
+ }
208
+ };
209
+ }
210
+ const app = new Hono();
211
+ // TODO rfradkin: This is still potentially quite slow since it is getting
212
+ // each of these on each request regardless of whether the request is in
213
+ // the restrictions or not. Should find a better solution down the road
214
+ app.use('*', async (c, next) => {
215
+ const [restrictions, payTo, server] = await Promise.all([
216
+ getRestrictions(c.env),
217
+ getEVMAddress(c.env),
218
+ getServer(),
219
+ ]);
220
+ const config = buildRoutesConfig(restrictions, payTo);
221
+ return paymentMiddleware(config, server)(c, next);
222
+ });
223
+ app.all('*', async (c) => {
224
+ const req = c.req.raw;
225
+ const resp = await fetch(req);
226
+ // This is cause the payment middleware needs to modify the headers to add the payment status
227
+ // (since it tags the data response after the request with settlement related headers)
228
+ // (ie success or failure)
229
+ // and the fetch headers returned by cloudlflare is immutable, so causes an issue
230
+ // TODO rfradkin: This could be cleaned up and made nicer, but works for now
231
+ return new Response(resp.body, {
232
+ status: resp.status,
233
+ statusText: resp.statusText,
234
+ headers: new Headers(resp.headers),
235
+ });
236
+ });
237
+ export default app;
@@ -0,0 +1,10 @@
1
+ export type Price = number;
2
+ export type Pathname = string;
3
+ export type Restriction = {
4
+ price: Price;
5
+ pathname: Pathname;
6
+ };
7
+ export type Restrictions = Record<Pathname, Price>;
8
+ export type AddressResponse = {
9
+ address: string;
10
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@foldset/cloudflare",
3
+ "version": "0.0.1",
4
+ "description": "SDK for creating Foldset-enabled Cloudflare Workers",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "foldset",
19
+ "cloudflare",
20
+ "workers",
21
+ "x402"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@x402/core": "^2.1.0",
26
+ "@x402/evm": "^2.1.0",
27
+ "@x402/hono": "^2.1.0",
28
+ "hono": "^4.11.3",
29
+ "p-memoize": "^8.0.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc"
33
+ }
34
+ }