@foldset/cloudflare 0.0.1 → 0.0.3
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/ai-crawlers/index.d.ts +15 -0
- package/dist/ai-crawlers/index.d.ts.map +1 -0
- package/dist/ai-crawlers/index.js +26 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/facilitators/index.d.ts +16 -0
- package/dist/facilitators/index.d.ts.map +1 -0
- package/dist/facilitators/index.js +35 -0
- package/dist/hono.d.ts +2 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +1 -0
- package/dist/index.d.ts +8 -37
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -237
- package/dist/payment/adapter.d.ts +16 -0
- package/dist/payment/adapter.d.ts.map +1 -0
- package/dist/payment/adapter.js +43 -0
- package/dist/payment/handler.d.ts +6 -0
- package/dist/payment/handler.d.ts.map +1 -0
- package/dist/payment/handler.js +86 -0
- package/dist/payment/paywall.d.ts +4 -0
- package/dist/payment/paywall.d.ts.map +1 -0
- package/dist/payment/paywall.js +99 -0
- package/dist/payment/routes.d.ts +5 -0
- package/dist/payment/routes.d.ts.map +1 -0
- package/dist/payment/routes.js +31 -0
- package/dist/payment/settlement.d.ts +8 -0
- package/dist/payment/settlement.d.ts.map +1 -0
- package/dist/payment/settlement.js +27 -0
- package/dist/payment/setup.d.ts +7 -0
- package/dist/payment/setup.d.ts.map +1 -0
- package/dist/payment/setup.js +85 -0
- package/dist/payment-methods/index.d.ts +18 -0
- package/dist/payment-methods/index.d.ts.map +1 -0
- package/dist/payment-methods/index.js +18 -0
- package/dist/restrictions/index.d.ts +16 -0
- package/dist/restrictions/index.d.ts.map +1 -0
- package/dist/restrictions/index.js +18 -0
- package/dist/telemetry/logging.d.ts +18 -0
- package/dist/telemetry/logging.d.ts.map +1 -0
- package/dist/telemetry/logging.js +47 -0
- package/dist/telemetry/sentry.d.ts +8 -0
- package/dist/telemetry/sentry.d.ts.map +1 -0
- package/dist/telemetry/sentry.js +19 -0
- package/dist/types.d.ts +5 -9
- package/dist/types.d.ts.map +1 -0
- package/dist/webhooks/index.d.ts +23 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +50 -0
- package/package.json +21 -18
- package/README.md +0 -1
- package/dist/adapter.d.ts +0 -71
- package/dist/adapter.js +0 -99
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export function generateHtml(paymentRequired, config) {
|
|
2
|
+
const resource = paymentRequired.resource;
|
|
3
|
+
const description = resource?.description ?? "This resource requires payment";
|
|
4
|
+
const url = resource?.url ?? config?.currentUrl ?? "";
|
|
5
|
+
const accepts = paymentRequired.accepts ?? [];
|
|
6
|
+
// Group payment options by blockchain network (e.g., Base Mainnet, Solana Mainnet)
|
|
7
|
+
const optionsByNetwork = new Map();
|
|
8
|
+
for (const accept of accepts) {
|
|
9
|
+
const existing = optionsByNetwork.get(accept.network) ?? [];
|
|
10
|
+
existing.push(accept);
|
|
11
|
+
optionsByNetwork.set(accept.network, existing);
|
|
12
|
+
}
|
|
13
|
+
const paymentOptionsHtml = Array.from(optionsByNetwork.entries()).map(([_network, networkOptions]) => {
|
|
14
|
+
// Chain display name (e.g., "Base Mainnet", "Solana Mainnet")
|
|
15
|
+
const chainDisplayName = networkOptions[0].extra?.["chainDisplayName"] ?? "Unknown Network";
|
|
16
|
+
const chainCaip2Id = networkOptions[0].network;
|
|
17
|
+
// Recipient wallet address (where payments are sent)
|
|
18
|
+
const recipientAddresses = Array.from(new Set(networkOptions.map((opt) => opt.payTo)));
|
|
19
|
+
const recipientAddress = recipientAddresses[0] ?? "";
|
|
20
|
+
// Build list of accepted tokens for this network
|
|
21
|
+
const acceptedTokensHtml = networkOptions.map((accept) => {
|
|
22
|
+
// Token display name (e.g., "USD Coin", "USDC")
|
|
23
|
+
const tokenDisplayName = accept.extra?.["assetDisplayName"] ?? "Unknown Token";
|
|
24
|
+
// Token contract address (e.g., "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" for USDC on Base)
|
|
25
|
+
const tokenContractAddress = accept.asset;
|
|
26
|
+
// Amount in token's smallest unit (e.g., for USDC with 6 decimals: 1000000 = $1.00)
|
|
27
|
+
const rawTokenAmount = accept.amount;
|
|
28
|
+
const rawPrice = accept.extra?.["price"] ?? "Unknown Price";
|
|
29
|
+
return `
|
|
30
|
+
<tr>
|
|
31
|
+
<td>${tokenDisplayName}</td>
|
|
32
|
+
<td>${rawTokenAmount}</td>
|
|
33
|
+
<td>${rawPrice}</td>
|
|
34
|
+
<td><code>${tokenContractAddress}</code></td>
|
|
35
|
+
</tr>`;
|
|
36
|
+
}).join("");
|
|
37
|
+
return `
|
|
38
|
+
<section>
|
|
39
|
+
<h3>${chainDisplayName} (${chainCaip2Id})</h3>
|
|
40
|
+
|
|
41
|
+
<p><strong>Send payment to:</strong> <code>${recipientAddress}</code></p>
|
|
42
|
+
|
|
43
|
+
<table border="1" cellpadding="8" cellspacing="0">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>Token</th>
|
|
47
|
+
<th>Amount (on-chain units)</th>
|
|
48
|
+
<th>Price (USD)</th>
|
|
49
|
+
<th>Contract Address</th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>${acceptedTokensHtml}
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</section>`;
|
|
56
|
+
}).join("\n");
|
|
57
|
+
return `<!DOCTYPE html>
|
|
58
|
+
<html>
|
|
59
|
+
<head>
|
|
60
|
+
<title>HTTP 402 - Payment Required</title>
|
|
61
|
+
<style>
|
|
62
|
+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
|
63
|
+
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
64
|
+
table { border-collapse: collapse; margin: 10px 0; }
|
|
65
|
+
th { background: #f5f5f5; text-align: left; }
|
|
66
|
+
section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
|
67
|
+
h3 { margin-top: 0; }
|
|
68
|
+
.notice { background: #f7f7f7; border: 1px solid #e2e2e2; border-radius: 6px; padding: 10px 12px; margin: 16px 0; color: #555; font-size: 0.92em; }
|
|
69
|
+
.notice strong { display: block; margin-bottom: 4px; font-weight: 600; }
|
|
70
|
+
.notice p { margin: 0; }
|
|
71
|
+
footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #eee; font-size: 0.9em; color: #666; }
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<h1>HTTP 402 - Payment Required</h1>
|
|
76
|
+
|
|
77
|
+
<p>This content requires a payment via the <a href="https://github.com/coinbase/x402">x402 protocol</a>.</p>
|
|
78
|
+
|
|
79
|
+
<h2>Protected Resource</h2>
|
|
80
|
+
<ul>
|
|
81
|
+
<li><strong>URL:</strong> <code>${url}</code></li>
|
|
82
|
+
<li><strong>Description:</strong> ${description}</li>
|
|
83
|
+
</ul>
|
|
84
|
+
|
|
85
|
+
<h2>Accepted Payment Options</h2>
|
|
86
|
+
<p>Pay using any of the following blockchain networks and tokens:</p>
|
|
87
|
+
${paymentOptionsHtml}
|
|
88
|
+
|
|
89
|
+
<div class="notice">
|
|
90
|
+
<strong>Important for payment libraries</strong>
|
|
91
|
+
<p>An HTTP 200 response was returned to encourage agents to view this page. Please treat it as you would a 402 error. All required x402 headers are present. Some unmodified x402 libraries require a 402 response to initiate payment; instead of checking only the response code, check for the presence of the x402 headers.</p>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<footer>
|
|
95
|
+
<p><strong>Powered by:</strong> <a href="https://www.foldset.com">Foldset</a></p>
|
|
96
|
+
</footer>
|
|
97
|
+
</body>
|
|
98
|
+
</html>`;
|
|
99
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RoutesConfig } from "@x402/core/http";
|
|
2
|
+
import type { Restriction } from "../restrictions";
|
|
3
|
+
import type { PaymentMethod } from "../payment-methods";
|
|
4
|
+
export declare function buildRoutesConfig(restrictions: Restriction[], paymentMethods: PaymentMethod[]): RoutesConfig;
|
|
5
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/payment/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAOxD,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,WAAW,EAAE,EAC3B,cAAc,EAAE,aAAa,EAAE,GAC9B,YAAY,CA4Bd"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function priceToAmount(priceUsd, decimals) {
|
|
2
|
+
const amount = priceUsd * Math.pow(10, decimals);
|
|
3
|
+
return Math.round(amount).toString();
|
|
4
|
+
}
|
|
5
|
+
export function buildRoutesConfig(restrictions, paymentMethods) {
|
|
6
|
+
const routesConfig = {};
|
|
7
|
+
for (const restriction of restrictions) {
|
|
8
|
+
routesConfig[restriction.path] = {
|
|
9
|
+
accepts: paymentMethods.map((paymentMethod) => ({
|
|
10
|
+
scheme: restriction.scheme,
|
|
11
|
+
price: {
|
|
12
|
+
amount: priceToAmount(restriction.price, paymentMethod.decimals),
|
|
13
|
+
asset: paymentMethod.contract_address,
|
|
14
|
+
// eip712 required extra
|
|
15
|
+
extra: {
|
|
16
|
+
...paymentMethod.extra,
|
|
17
|
+
decimals: paymentMethod.decimals,
|
|
18
|
+
chainDisplayName: paymentMethod.chain_display_name,
|
|
19
|
+
assetDisplayName: paymentMethod.asset_display_name,
|
|
20
|
+
price: restriction.price,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
network: paymentMethod.caip2_id,
|
|
24
|
+
payTo: paymentMethod.circle_wallet_address,
|
|
25
|
+
})),
|
|
26
|
+
description: restriction.description,
|
|
27
|
+
mimeType: "application/json",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return routesConfig;
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PaymentPayload, PaymentRequirements } from "@x402/core/types";
|
|
2
|
+
import type { x402HTTPResourceServer } from "@x402/hono";
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
import type { Env } from "../types";
|
|
5
|
+
export declare function handleSettlement(c: Context<{
|
|
6
|
+
Bindings: Env;
|
|
7
|
+
}>, httpServer: x402HTTPResourceServer, paymentPayload: PaymentPayload, paymentRequirements: PaymentRequirements, res: Response): Promise<Response>;
|
|
8
|
+
//# sourceMappingURL=settlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settlement.d.ts","sourceRoot":"","sources":["../../src/payment/settlement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAsB,gBAAgB,CACpC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,EAC7B,UAAU,EAAE,sBAAsB,EAClC,cAAc,EAAE,cAAc,EAC9B,mBAAmB,EAAE,mBAAmB,EACxC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,QAAQ,CAAC,CAmCnB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/cloudflare";
|
|
2
|
+
export async function handleSettlement(c, httpServer, paymentPayload, paymentRequirements, res) {
|
|
3
|
+
if (res.status >= 400) {
|
|
4
|
+
return res;
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
const settleResult = await httpServer.processSettlement(paymentPayload, paymentRequirements);
|
|
8
|
+
if (!settleResult.success) {
|
|
9
|
+
Sentry.captureException(new Error(`Settlement failed: ${settleResult.errorReason}`));
|
|
10
|
+
return c.json({
|
|
11
|
+
error: "Settlement failed",
|
|
12
|
+
details: settleResult.errorReason,
|
|
13
|
+
}, 402);
|
|
14
|
+
}
|
|
15
|
+
Object.entries(settleResult.headers).forEach(([key, value]) => {
|
|
16
|
+
res.headers.set(key, value);
|
|
17
|
+
});
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
Sentry.captureException(error);
|
|
22
|
+
return c.json({
|
|
23
|
+
error: "Settlement failed",
|
|
24
|
+
details: error instanceof Error ? error.message : "Unknown error",
|
|
25
|
+
}, 402);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { x402HTTPResourceServer } from "@x402/hono";
|
|
3
|
+
import type { Env } from "../types";
|
|
4
|
+
export declare function getHttpServer(c: Context<{
|
|
5
|
+
Bindings: Env;
|
|
6
|
+
}>): Promise<x402HTTPResourceServer | null>;
|
|
7
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/payment/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAEL,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAMpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AA+DpC,wBAAsB,aAAa,CACjC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,GAC5B,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CA0CxC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { x402ResourceServer, x402HTTPResourceServer, } from "@x402/hono";
|
|
2
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/server";
|
|
3
|
+
import { registerExactSvmScheme } from "@x402/svm/exact/server";
|
|
4
|
+
import { getRestrictions } from "../restrictions";
|
|
5
|
+
import { getPaymentMethods } from "../payment-methods";
|
|
6
|
+
import { buildRoutesConfig } from "./routes";
|
|
7
|
+
import { getFacilitator } from "../facilitators";
|
|
8
|
+
import { generateHtml } from "./paywall";
|
|
9
|
+
const paywallProvider = {
|
|
10
|
+
generateHtml
|
|
11
|
+
};
|
|
12
|
+
let cachedHttpServer = null;
|
|
13
|
+
let cachedRestrictions = null;
|
|
14
|
+
let cachedPaymentMethods = null;
|
|
15
|
+
/**
|
|
16
|
+
* Custom createHTTPResponse implementation for Foldset
|
|
17
|
+
* Monkey-patched onto x402HTTPResourceServer instances
|
|
18
|
+
*/
|
|
19
|
+
function createFoldsetHTTPResponse(paymentRequired, isWebBrowser, paywallConfig, customHtml, unpaidResponse) {
|
|
20
|
+
// @ts-expect-error - accessing private method
|
|
21
|
+
const html = this.generatePaywallHTML(paymentRequired, paywallConfig, customHtml);
|
|
22
|
+
// Always generate and provide
|
|
23
|
+
// if (isWebBrowser) {
|
|
24
|
+
// // @ts-expect-error - accessing private method
|
|
25
|
+
// const html = this.generatePaywallHTML(paymentRequired, paywallConfig, customHtml);
|
|
26
|
+
// return {
|
|
27
|
+
// status: 402,
|
|
28
|
+
// headers: { "Content-Type": "text/html" },
|
|
29
|
+
// body: html,
|
|
30
|
+
// isHtml: true,
|
|
31
|
+
// };
|
|
32
|
+
// }
|
|
33
|
+
// @ts-expect-error - accessing private method
|
|
34
|
+
const response = this.createHTTPPaymentRequiredResponse(paymentRequired);
|
|
35
|
+
// We don't provide a callback
|
|
36
|
+
// Use callback result if provided, otherwise default to JSON with empty object
|
|
37
|
+
// const contentType = unpaidResponse ? unpaidResponse.contentType : "application/json";
|
|
38
|
+
// const body = unpaidResponse ? unpaidResponse.body : {};
|
|
39
|
+
// Status should be 402 but we return 200 so AI crawlers view the payment instructions
|
|
40
|
+
return {
|
|
41
|
+
status: 200,
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "text/html",
|
|
44
|
+
...response.headers,
|
|
45
|
+
},
|
|
46
|
+
body: html,
|
|
47
|
+
isHtml: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Maybe this function is never needed and can just updated cachedHttpServer on changes to upstream configs
|
|
51
|
+
// ie consider changing to a chained pattern
|
|
52
|
+
export async function getHttpServer(c) {
|
|
53
|
+
const restrictions = await getRestrictions(c);
|
|
54
|
+
const paymentMethods = await getPaymentMethods(c);
|
|
55
|
+
if (restrictions === null || paymentMethods === null) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (cachedHttpServer &&
|
|
59
|
+
restrictions === cachedRestrictions &&
|
|
60
|
+
paymentMethods === cachedPaymentMethods) {
|
|
61
|
+
return cachedHttpServer;
|
|
62
|
+
}
|
|
63
|
+
const facilitator = await getFacilitator(c);
|
|
64
|
+
if (facilitator === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// This could be pulled out
|
|
68
|
+
const server = new x402ResourceServer(facilitator);
|
|
69
|
+
registerExactEvmScheme(server);
|
|
70
|
+
registerExactSvmScheme(server);
|
|
71
|
+
const routesConfig = buildRoutesConfig(restrictions, paymentMethods);
|
|
72
|
+
const httpServer = new x402HTTPResourceServer(server, routesConfig);
|
|
73
|
+
// Monkey-patch createHTTPResponse with our custom implementation
|
|
74
|
+
// @ts-expect-error - overriding private method
|
|
75
|
+
httpServer.createHTTPResponse = createFoldsetHTTPResponse;
|
|
76
|
+
// TODO rfradkin: This is probably the slowest part of the cold start. Figure out how to speed this up.
|
|
77
|
+
// Also this is slow on every request where its updated.
|
|
78
|
+
// Consider looking into creating a serverless facilitator so don't have to request outside the serverless environment
|
|
79
|
+
await httpServer.initialize();
|
|
80
|
+
httpServer.registerPaywallProvider(paywallProvider);
|
|
81
|
+
cachedHttpServer = httpServer;
|
|
82
|
+
cachedRestrictions = restrictions;
|
|
83
|
+
cachedPaymentMethods = paymentMethods;
|
|
84
|
+
return httpServer;
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Env } from "../types";
|
|
3
|
+
export interface PaymentMethod {
|
|
4
|
+
caip2_id: string;
|
|
5
|
+
decimals: number;
|
|
6
|
+
contract_address: string;
|
|
7
|
+
circle_wallet_address: string;
|
|
8
|
+
extra?: Record<string, string>;
|
|
9
|
+
chain_display_name: string;
|
|
10
|
+
asset_display_name: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function getPaymentMethods(c: Context<{
|
|
13
|
+
Bindings: Env;
|
|
14
|
+
}>): Promise<PaymentMethod[] | null>;
|
|
15
|
+
export declare function storePaymentMethods(c: Context<{
|
|
16
|
+
Bindings: Env;
|
|
17
|
+
}>, paymentMethods: PaymentMethod[]): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/payment-methods/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAOD,wBAAsB,iBAAiB,CACrC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,GAC5B,OAAO,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,CAUjC;AAED,wBAAsB,mBAAmB,CACvC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,EAC7B,cAAc,EAAE,aAAa,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const CACHE_TTL_MS = 30_000;
|
|
2
|
+
let cachedPaymentMethods = null;
|
|
3
|
+
let cacheTimestamp = 0;
|
|
4
|
+
export async function getPaymentMethods(c) {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
if (cachedPaymentMethods !== null && now - cacheTimestamp < CACHE_TTL_MS) {
|
|
7
|
+
return cachedPaymentMethods;
|
|
8
|
+
}
|
|
9
|
+
const response = await c.env.FOLDSET_CONFIG.get("payment-methods");
|
|
10
|
+
cachedPaymentMethods = response ? JSON.parse(response) : null;
|
|
11
|
+
cacheTimestamp = now;
|
|
12
|
+
return cachedPaymentMethods;
|
|
13
|
+
}
|
|
14
|
+
export async function storePaymentMethods(c, paymentMethods) {
|
|
15
|
+
await c.env.FOLDSET_CONFIG.put("payment-methods", JSON.stringify(paymentMethods), {
|
|
16
|
+
expirationTtl: 60 * 60 * 3 + 60 * 30, // 3 hours + 30 minutes
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Env } from "../types";
|
|
3
|
+
export interface Restriction {
|
|
4
|
+
host: string;
|
|
5
|
+
path: string;
|
|
6
|
+
description: string;
|
|
7
|
+
price: number;
|
|
8
|
+
scheme: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getRestrictions(c: Context<{
|
|
11
|
+
Bindings: Env;
|
|
12
|
+
}>): Promise<Restriction[] | null>;
|
|
13
|
+
export declare function storeRestrictions(c: Context<{
|
|
14
|
+
Bindings: Env;
|
|
15
|
+
}>, restrictions: Restriction[]): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/restrictions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAsB,eAAe,CACnC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,GAC5B,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAU/B;AAED,wBAAsB,iBAAiB,CACrC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,EAC7B,YAAY,EAAE,WAAW,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const CACHE_TTL_MS = 30_000;
|
|
2
|
+
let cachedRestrictions = null;
|
|
3
|
+
let cacheTimestamp = 0;
|
|
4
|
+
export async function getRestrictions(c) {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
if (cachedRestrictions !== null && now - cacheTimestamp < CACHE_TTL_MS) {
|
|
7
|
+
return cachedRestrictions;
|
|
8
|
+
}
|
|
9
|
+
const response = await c.env.FOLDSET_CONFIG.get("restrictions");
|
|
10
|
+
cachedRestrictions = response ? JSON.parse(response) : null;
|
|
11
|
+
cacheTimestamp = now;
|
|
12
|
+
return cachedRestrictions;
|
|
13
|
+
}
|
|
14
|
+
export async function storeRestrictions(c, restrictions) {
|
|
15
|
+
await c.env.FOLDSET_CONFIG.put("restrictions", JSON.stringify(restrictions), {
|
|
16
|
+
expirationTtl: 60 * 60 * 3 + 60 * 30, // 3 hours + 30 minutes
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Env } from "../types";
|
|
3
|
+
export type EventPayload = {
|
|
4
|
+
method: string;
|
|
5
|
+
status_code: number;
|
|
6
|
+
user_agent: string | null;
|
|
7
|
+
referer?: string | null;
|
|
8
|
+
href: string;
|
|
9
|
+
hostname: string;
|
|
10
|
+
pathname: string;
|
|
11
|
+
search: string;
|
|
12
|
+
ip_address?: string | null;
|
|
13
|
+
payment_response?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function logVisitEvent(c: Context<{
|
|
16
|
+
Bindings: Env;
|
|
17
|
+
}>, response: Response): void;
|
|
18
|
+
//# sourceMappingURL=logging.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["../../src/telemetry/logging.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAIpC,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,QA8C9E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/cloudflare";
|
|
2
|
+
import { API_BASE_URL } from "../config";
|
|
3
|
+
const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
|
|
4
|
+
export function logVisitEvent(c, response) {
|
|
5
|
+
const url = new URL(c.req.url);
|
|
6
|
+
const ipAddressHeader = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for");
|
|
7
|
+
const ipAddress = ipAddressHeader?.split(",")[0]?.trim() || null;
|
|
8
|
+
const paymentResponse = response.headers.get(PAYMENT_RESPONSE_HEADER) || undefined;
|
|
9
|
+
const payload = {
|
|
10
|
+
method: c.req.method,
|
|
11
|
+
status_code: response.status,
|
|
12
|
+
user_agent: c.req.header("user-agent") || null,
|
|
13
|
+
referer: c.req.header("referer") || null,
|
|
14
|
+
href: url.href,
|
|
15
|
+
hostname: url.hostname,
|
|
16
|
+
pathname: url.pathname,
|
|
17
|
+
search: url.search,
|
|
18
|
+
ip_address: ipAddress,
|
|
19
|
+
...(paymentResponse ? { payment_response: paymentResponse } : {}),
|
|
20
|
+
};
|
|
21
|
+
const requestPromise = fetch(`${API_BASE_URL}/events`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${c.env.FOLDSET_API_KEY}`,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
Accept: "application/json",
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(payload),
|
|
29
|
+
}).catch((error) => {
|
|
30
|
+
Sentry.captureException(error, {
|
|
31
|
+
extra: {
|
|
32
|
+
method: "POST",
|
|
33
|
+
url: `${API_BASE_URL}/events`,
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: `Bearer ${c.env.FOLDSET_API_KEY}`,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(payload),
|
|
40
|
+
},
|
|
41
|
+
tags: {
|
|
42
|
+
endpoint: "/events",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
c.executionCtx.waitUntil(requestPromise);
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
2
|
+
type SentryWrapperArgs = {
|
|
3
|
+
request: Request;
|
|
4
|
+
context: ExecutionContext;
|
|
5
|
+
};
|
|
6
|
+
export declare const wrappedPaymentHandler: (wrapperArgs: SentryWrapperArgs, handler: () => Promise<Response>) => Promise<Response>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=sentry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../../src/telemetry/sentry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAIlE,KAAK,iBAAiB,GAAG;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAuBF,eAAO,MAAM,qBAAqB,gBAnBX,iBAAiB,WAAW,MAAM,OAAO,CAAC,QAAQ,CAAC,sBAmBE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/cloudflare";
|
|
2
|
+
const SENTRY_DSN = "https://f68380669974dab9bbcf5aec9414bcb8@o4510648631296000.ingest.us.sentry.io/4510718170038272";
|
|
3
|
+
// TODO rfradkin: Add webhook support for updating this
|
|
4
|
+
function createWrappedPaymentHandler(dsn) {
|
|
5
|
+
return (wrapperArgs, handler) => {
|
|
6
|
+
if (!dsn) {
|
|
7
|
+
return handler();
|
|
8
|
+
}
|
|
9
|
+
return Sentry.wrapRequestHandler({
|
|
10
|
+
options: {
|
|
11
|
+
dsn,
|
|
12
|
+
sendDefaultPii: true,
|
|
13
|
+
},
|
|
14
|
+
request: wrapperArgs.request,
|
|
15
|
+
context: wrapperArgs.context,
|
|
16
|
+
}, handler);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export const wrappedPaymentHandler = createWrappedPaymentHandler(SENTRY_DSN);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
pathname: Pathname;
|
|
6
|
-
};
|
|
7
|
-
export type Restrictions = Record<Pathname, Price>;
|
|
8
|
-
export type AddressResponse = {
|
|
9
|
-
address: string;
|
|
1
|
+
import type { KVNamespace } from "@cloudflare/workers-types";
|
|
2
|
+
export type Env = {
|
|
3
|
+
FOLDSET_API_KEY: string;
|
|
4
|
+
FOLDSET_CONFIG: KVNamespace;
|
|
10
5
|
};
|
|
6
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,MAAM,GAAG,GAAG;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,WAAW,CAAC;CAC7B,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Env } from "../types";
|
|
3
|
+
import { type Restriction } from "../restrictions";
|
|
4
|
+
import { type PaymentMethod } from "../payment-methods";
|
|
5
|
+
import { type AiCrawler } from "../ai-crawlers";
|
|
6
|
+
import { type FacilitatorConfig } from "../facilitators";
|
|
7
|
+
export type FoldsetWebhook = {
|
|
8
|
+
event_type: "restrictions";
|
|
9
|
+
event_object: Restriction[];
|
|
10
|
+
} | {
|
|
11
|
+
event_type: "payment-methods";
|
|
12
|
+
event_object: PaymentMethod[];
|
|
13
|
+
} | {
|
|
14
|
+
event_type: "ai-crawlers";
|
|
15
|
+
event_object: AiCrawler[];
|
|
16
|
+
} | {
|
|
17
|
+
event_type: "facilitator";
|
|
18
|
+
event_object: FacilitatorConfig;
|
|
19
|
+
};
|
|
20
|
+
export declare function handleWebhook(c: Context<{
|
|
21
|
+
Bindings: Env;
|
|
22
|
+
}>): Promise<Response>;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webhooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,KAAK,SAAS,EAAmB,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,iBAAiB,CAAC;AAE3E,MAAM,MAAM,cAAc,GACtB;IAAE,UAAU,EAAE,cAAc,CAAC;IAAC,YAAY,EAAE,WAAW,EAAE,CAAA;CAAE,GAC3D;IAAE,UAAU,EAAE,iBAAiB,CAAC;IAAC,YAAY,EAAE,aAAa,EAAE,CAAA;CAAE,GAChE;IAAE,UAAU,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,SAAS,EAAE,CAAA;CAAE,GACxD;IAAE,UAAU,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAgCnE,wBAAsB,aAAa,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0BpF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { storeRestrictions } from "../restrictions";
|
|
2
|
+
import { storePaymentMethods } from "../payment-methods";
|
|
3
|
+
import { storeAiCrawlers } from "../ai-crawlers";
|
|
4
|
+
import { storeFacilitator } from "../facilitators";
|
|
5
|
+
async function verifySignature(body, signature, apiKey) {
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const keyHashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(apiKey));
|
|
8
|
+
const hashedKeyHex = Array.from(new Uint8Array(keyHashBuffer))
|
|
9
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
10
|
+
.join("");
|
|
11
|
+
const hmacKey = await crypto.subtle.importKey("raw", encoder.encode(hashedKeyHex), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
12
|
+
const expectedSig = await crypto.subtle.sign("HMAC", hmacKey, encoder.encode(body));
|
|
13
|
+
const expectedHex = Array.from(new Uint8Array(expectedSig))
|
|
14
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
15
|
+
.join("");
|
|
16
|
+
// Timing-safe comparison
|
|
17
|
+
if (signature.length !== expectedHex.length)
|
|
18
|
+
return false;
|
|
19
|
+
let result = 0;
|
|
20
|
+
for (let i = 0; i < signature.length; i++) {
|
|
21
|
+
result |= signature.charCodeAt(i) ^ expectedHex.charCodeAt(i);
|
|
22
|
+
}
|
|
23
|
+
return result === 0;
|
|
24
|
+
}
|
|
25
|
+
export async function handleWebhook(c) {
|
|
26
|
+
const signature = c.req.header("X-Foldset-Signature");
|
|
27
|
+
if (!signature) {
|
|
28
|
+
return new Response("Missing signature", { status: 401 });
|
|
29
|
+
}
|
|
30
|
+
const body = await c.req.text();
|
|
31
|
+
const isValid = await verifySignature(body, signature, c.env.FOLDSET_API_KEY);
|
|
32
|
+
if (!isValid) {
|
|
33
|
+
return new Response("Invalid signature", { status: 401 });
|
|
34
|
+
}
|
|
35
|
+
const webhook = JSON.parse(body);
|
|
36
|
+
// Fails the entire webhook if the put fails. Not standard but think it makes sense for now
|
|
37
|
+
if (webhook.event_type === "restrictions") {
|
|
38
|
+
await storeRestrictions(c, webhook.event_object);
|
|
39
|
+
}
|
|
40
|
+
else if (webhook.event_type === "payment-methods") {
|
|
41
|
+
await storePaymentMethods(c, webhook.event_object);
|
|
42
|
+
}
|
|
43
|
+
else if (webhook.event_type === "ai-crawlers") {
|
|
44
|
+
await storeAiCrawlers(c, webhook.event_object);
|
|
45
|
+
}
|
|
46
|
+
else if (webhook.event_type === "facilitator") {
|
|
47
|
+
await storeFacilitator(c, webhook.event_object);
|
|
48
|
+
}
|
|
49
|
+
return new Response("Ok", { status: 200 });
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldset/cloudflare",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"private": false,
|
|
6
5
|
"main": "./dist/index.js",
|
|
7
6
|
"types": "./dist/index.d.ts",
|
|
8
7
|
"exports": {
|
|
9
8
|
".": {
|
|
10
9
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./hono": {
|
|
13
|
+
"types": "./dist/hono.d.ts",
|
|
14
|
+
"default": "./dist/hono.js"
|
|
12
15
|
}
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
15
18
|
"dist"
|
|
16
19
|
],
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"x402"
|
|
22
|
-
],
|
|
23
|
-
"license": "MIT",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typegen": "wrangler types --path types/worker-configuration.d.ts",
|
|
22
|
+
"build": "tsc -p tsconfig.json --noEmit false --outDir dist --rootDir src"
|
|
23
|
+
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@
|
|
26
|
-
"@
|
|
27
|
-
"@x402/
|
|
28
|
-
"
|
|
29
|
-
"
|
|
25
|
+
"@cloudflare/workers-types": "^4.20260117.0",
|
|
26
|
+
"@sentry/cloudflare": "^10.34.0",
|
|
27
|
+
"@x402/core": "^2.2.0",
|
|
28
|
+
"@x402/evm": "^2.2.0",
|
|
29
|
+
"@x402/hono": "^2.2.0",
|
|
30
|
+
"@x402/svm": "^2.2.0",
|
|
31
|
+
"hono": "^4.11.4"
|
|
30
32
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"wrangler": "^4.59.1"
|
|
33
36
|
}
|
|
34
37
|
}
|
package/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Typescript library for foldset?
|