@forum-labs/payfetch 1.0.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/dist/bin/payfetch-mcp.d.ts +2 -0
  4. package/dist/bin/payfetch-mcp.js +9 -0
  5. package/dist/bin/payfetch.d.ts +2 -0
  6. package/dist/bin/payfetch.js +126 -0
  7. package/dist/src/config.d.ts +35 -0
  8. package/dist/src/config.js +170 -0
  9. package/dist/src/core/budget.d.ts +62 -0
  10. package/dist/src/core/budget.js +236 -0
  11. package/dist/src/core/constants.d.ts +66 -0
  12. package/dist/src/core/constants.js +115 -0
  13. package/dist/src/core/fs.d.ts +14 -0
  14. package/dist/src/core/fs.js +104 -0
  15. package/dist/src/core/integrity.d.ts +23 -0
  16. package/dist/src/core/integrity.js +150 -0
  17. package/dist/src/core/ledger.d.ts +149 -0
  18. package/dist/src/core/ledger.js +357 -0
  19. package/dist/src/core/notes.d.ts +22 -0
  20. package/dist/src/core/notes.js +24 -0
  21. package/dist/src/core/pipeline.d.ts +143 -0
  22. package/dist/src/core/pipeline.js +795 -0
  23. package/dist/src/core/policy.d.ts +57 -0
  24. package/dist/src/core/policy.js +224 -0
  25. package/dist/src/core/transport.d.ts +93 -0
  26. package/dist/src/core/transport.js +364 -0
  27. package/dist/src/guards/index.d.ts +4 -0
  28. package/dist/src/guards/index.js +3 -0
  29. package/dist/src/guards/internal.d.ts +17 -0
  30. package/dist/src/guards/internal.js +74 -0
  31. package/dist/src/guards/safety.d.ts +2 -0
  32. package/dist/src/guards/safety.js +94 -0
  33. package/dist/src/guards/trust.d.ts +2 -0
  34. package/dist/src/guards/trust.js +79 -0
  35. package/dist/src/guards/types.d.ts +59 -0
  36. package/dist/src/guards/types.js +20 -0
  37. package/dist/src/index.d.ts +58 -0
  38. package/dist/src/index.js +151 -0
  39. package/dist/src/mcp/server.d.ts +6 -0
  40. package/dist/src/mcp/server.js +125 -0
  41. package/dist/src/mcp/tools.d.ts +46 -0
  42. package/dist/src/mcp/tools.js +321 -0
  43. package/dist/src/payer/mpp.d.ts +7 -0
  44. package/dist/src/payer/mpp.js +13 -0
  45. package/dist/src/payer/parse402.d.ts +6 -0
  46. package/dist/src/payer/parse402.js +169 -0
  47. package/dist/src/payer/signer_cdp.d.ts +14 -0
  48. package/dist/src/payer/signer_cdp.js +64 -0
  49. package/dist/src/payer/signer_local.d.ts +8 -0
  50. package/dist/src/payer/signer_local.js +14 -0
  51. package/dist/src/payer/types.d.ts +99 -0
  52. package/dist/src/payer/types.js +10 -0
  53. package/dist/src/payer/x402.d.ts +23 -0
  54. package/dist/src/payer/x402.js +165 -0
  55. package/dist/src/report/report.d.ts +162 -0
  56. package/dist/src/report/report.js +220 -0
  57. package/mcpb/manifest.json +104 -0
  58. package/package.json +72 -0
@@ -0,0 +1,321 @@
1
+ import { join } from "node:path";
2
+ import { USDC_DECIMALS } from "../core/constants.js";
3
+ export const TOOL_NAMES = {
4
+ PAID_FETCH: "paid_fetch",
5
+ PAYMENT_QUOTE: "payment_quote",
6
+ SPEND_STATUS: "spend_status",
7
+ LIST_RECEIPTS: "list_receipts",
8
+ APPROVE_PENDING: "approve_pending",
9
+ };
10
+ export const TOOL_DESCRIPTIONS = {
11
+ paid_fetch: "Fetch a URL, automatically paying if it requires payment (HTTP 402, x402 protocol) — within the operator's spending policy. Free URLs are fetched normally at no cost. Use payment_quote first if you only want to know the price. Payments above the operator's approval threshold will ask the human for confirmation.",
12
+ payment_quote: "Check what a paid URL costs and whether the current spending policy would allow paying it, WITHOUT paying. Returns the price, payment terms, trust-check results, and the policy decision.",
13
+ spend_status: "Show today's agent spending: totals, remaining budgets overall and per host, active holds, and recent payments.",
14
+ list_receipts: "Query the local payment receipt ledger (audit trail). Filter by time, host, or outcome.",
15
+ approve_pending: "List or resolve payments waiting for human approval (queue mode). Approving grants a one-time re-run permission for that exact payment.",
16
+ };
17
+ const LIST_RECEIPTS_DEFAULT_LIMIT = 50;
18
+ const LIST_RECEIPTS_MAX_LIMIT = 200;
19
+ const HTTP_METHOD_ENUM = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"];
20
+ const RESPONSE_MODE_ENUM = ["inline", "file"];
21
+ const CHAIN_ENUM = ["solana", "base", "ethereum"];
22
+ export const PAID_FETCH_INPUT_SCHEMA = {
23
+ type: "object",
24
+ properties: {
25
+ url: { type: "string", format: "uri" },
26
+ method: { enum: [...HTTP_METHOD_ENUM], default: "GET" },
27
+ headers: { type: "object", additionalProperties: { type: "string" } },
28
+ body: { type: "string" },
29
+ maxAmountUsd: { type: "number", minimum: 0 },
30
+ dryRun: { type: "boolean", default: false },
31
+ responseMode: { enum: [...RESPONSE_MODE_ENUM], default: "inline" },
32
+ tokenAddress: { type: "string" },
33
+ chain: { enum: [...CHAIN_ENUM] },
34
+ },
35
+ required: ["url"],
36
+ };
37
+ export const PAYMENT_QUOTE_INPUT_SCHEMA = {
38
+ type: "object",
39
+ properties: {
40
+ url: { type: "string", format: "uri" },
41
+ method: { enum: [...HTTP_METHOD_ENUM], default: "GET" },
42
+ headers: { type: "object", additionalProperties: { type: "string" } },
43
+ body: { type: "string" },
44
+ tokenAddress: { type: "string" },
45
+ chain: { enum: [...CHAIN_ENUM] },
46
+ },
47
+ required: ["url"],
48
+ };
49
+ export const SPEND_STATUS_INPUT_SCHEMA = {
50
+ type: "object",
51
+ properties: {},
52
+ additionalProperties: false,
53
+ };
54
+ export const LIST_RECEIPTS_INPUT_SCHEMA = {
55
+ type: "object",
56
+ properties: {
57
+ sinceTs: { type: "number" },
58
+ host: { type: "string" },
59
+ outcome: { type: "string" },
60
+ limit: {
61
+ type: "number",
62
+ default: LIST_RECEIPTS_DEFAULT_LIMIT,
63
+ minimum: 1,
64
+ maximum: LIST_RECEIPTS_MAX_LIMIT,
65
+ },
66
+ },
67
+ };
68
+ export const APPROVE_PENDING_INPUT_SCHEMA = {
69
+ type: "object",
70
+ properties: {
71
+ action: { enum: ["list", "approve", "deny"] },
72
+ approvalId: { type: "string" },
73
+ },
74
+ required: ["action"],
75
+ };
76
+ export const PAYFETCH_TOOLS = [
77
+ {
78
+ name: TOOL_NAMES.PAID_FETCH,
79
+ description: TOOL_DESCRIPTIONS.paid_fetch,
80
+ inputSchema: PAID_FETCH_INPUT_SCHEMA,
81
+ },
82
+ {
83
+ name: TOOL_NAMES.PAYMENT_QUOTE,
84
+ description: TOOL_DESCRIPTIONS.payment_quote,
85
+ inputSchema: PAYMENT_QUOTE_INPUT_SCHEMA,
86
+ },
87
+ {
88
+ name: TOOL_NAMES.SPEND_STATUS,
89
+ description: TOOL_DESCRIPTIONS.spend_status,
90
+ inputSchema: SPEND_STATUS_INPUT_SCHEMA,
91
+ },
92
+ {
93
+ name: TOOL_NAMES.LIST_RECEIPTS,
94
+ description: TOOL_DESCRIPTIONS.list_receipts,
95
+ inputSchema: LIST_RECEIPTS_INPUT_SCHEMA,
96
+ },
97
+ {
98
+ name: TOOL_NAMES.APPROVE_PENDING,
99
+ description: TOOL_DESCRIPTIONS.approve_pending,
100
+ inputSchema: APPROVE_PENDING_INPUT_SCHEMA,
101
+ },
102
+ ];
103
+ export function policyLockNotice(dataDir) {
104
+ return `Spending limits and lists are set by the operator in ${dataDir}/config.json — they cannot be changed from this session.`;
105
+ }
106
+ export class UnknownToolError extends Error {
107
+ toolName;
108
+ constructor(toolName) {
109
+ super(`unknown tool: ${toolName}`);
110
+ this.toolName = toolName;
111
+ this.name = "UnknownToolError";
112
+ }
113
+ }
114
+ export class ToolInputError extends Error {
115
+ constructor(message) {
116
+ super(message);
117
+ this.name = "ToolInputError";
118
+ }
119
+ }
120
+ export async function dispatchTool(ctx, name, args) {
121
+ switch (name) {
122
+ case TOOL_NAMES.PAID_FETCH:
123
+ return handlePaidFetch(ctx, args);
124
+ case TOOL_NAMES.PAYMENT_QUOTE:
125
+ return handlePaymentQuote(ctx, args);
126
+ case TOOL_NAMES.SPEND_STATUS:
127
+ return ctx.pf.status();
128
+ case TOOL_NAMES.LIST_RECEIPTS:
129
+ return handleListReceipts(ctx, args);
130
+ case TOOL_NAMES.APPROVE_PENDING:
131
+ return handleApprovePending(ctx, args);
132
+ default:
133
+ throw new UnknownToolError(name);
134
+ }
135
+ }
136
+ async function handlePaidFetch(ctx, args) {
137
+ const url = requireString(args, "url");
138
+ const init = buildRequestInit(args);
139
+ const opts = {};
140
+ if (typeof args.maxAmountUsd === "number")
141
+ opts.maxAmountUsd = args.maxAmountUsd;
142
+ if (typeof args.dryRun === "boolean")
143
+ opts.dryRun = args.dryRun;
144
+ const responseMode = args.responseMode === "file" ? "file" : "inline";
145
+ opts.responseMode = responseMode;
146
+ if (typeof args.tokenAddress === "string")
147
+ opts.tokenAddress = args.tokenAddress;
148
+ if (typeof args.chain === "string")
149
+ opts.chain = args.chain;
150
+ const { response, receipt } = await ctx.pf.fetch(url, init, opts);
151
+ return mapPaidFetchResult(ctx.dataDir, receipt, response, responseMode);
152
+ }
153
+ async function mapPaidFetchResult(dataDir, receipt, response, responseMode) {
154
+ const base = {
155
+ receiptId: receipt.receiptId,
156
+ outcome: receipt.outcome,
157
+ status: receipt.http?.status ?? null,
158
+ contentType: receipt.http?.contentType ?? null,
159
+ bodyBytes: receipt.http?.bodyBytes ?? null,
160
+ truncated: receipt.http?.truncated ?? false,
161
+ payment: receipt.payment
162
+ ? {
163
+ outcome: receipt.outcome,
164
+ amountUsd: receipt.payment.settledAmountUsd ?? receipt.quote?.amountUsd ?? null,
165
+ txRef: receipt.payment.txRef,
166
+ }
167
+ : null,
168
+ warnings: buildWarnings(receipt),
169
+ };
170
+ if (receipt.outcome === "guard_blocked" && receipt.guardBlockReason) {
171
+ base.guardBlockReason = receipt.guardBlockReason;
172
+ base.guardBlockGuidance = guardBlockGuidance(receipt.guardBlockReason);
173
+ }
174
+ if (response !== null) {
175
+ if (responseMode === "file") {
176
+ return { ...base, bodyPath: join(dataDir, "downloads", receipt.receiptId) };
177
+ }
178
+ return { ...base, body: await response.text() };
179
+ }
180
+ if (receipt.outcome === "dry_run") {
181
+ return {
182
+ ...base,
183
+ denyCode: receipt.denyCode,
184
+ quote: receipt.quote,
185
+ remainingBudgets: receipt.budgets,
186
+ };
187
+ }
188
+ const denied = {
189
+ ...base,
190
+ denyCode: receipt.denyCode,
191
+ quote: receipt.quote,
192
+ remainingBudgets: receipt.budgets,
193
+ policyNotice: policyLockNotice(dataDir),
194
+ };
195
+ if (receipt.outcome === "approval_queued") {
196
+ denied.approvalId = receipt.receiptId;
197
+ denied.approvalInstructions =
198
+ "A human must approve this via the approve_pending tool (queue mode) before it can be re-run.";
199
+ }
200
+ if (receipt.outcome === "approval_denied" &&
201
+ (receipt.notes.includes("elicit_unsupported") || receipt.notes.includes("elicit_cancelled"))) {
202
+ denied.approvalGuidance = elicitBlockedGuidance(dataDir);
203
+ }
204
+ return denied;
205
+ }
206
+ function elicitBlockedGuidance(dataDir) {
207
+ return ("This payment is above the approval threshold, and the connected MCP client " +
208
+ "cannot prompt a human to approve it (it does not support elicitation, or it " +
209
+ "dismissed the prompt). This is NOT a denial by a human — it is blocked because " +
210
+ "there is no in-session approval channel. To allow payments like this WITHOUT a " +
211
+ `human dialog, the operator can edit ${dataDir}/config.json to (a) raise ` +
212
+ "approval.thresholdUsd, (b) set approval.preApprovedUpToUsd to a ceiling, or " +
213
+ "(c) add this host to approval.preApprovedHosts. Spending caps still apply, and " +
214
+ "these limits cannot be changed from this session.");
215
+ }
216
+ function guardBlockGuidance(reason) {
217
+ switch (reason) {
218
+ case "danger":
219
+ return "A trust/safety guard flagged this host or token as dangerous. This is a verdict, not a transient error — do NOT retry; use a different resource.";
220
+ case "degraded":
221
+ return "The safety screen was DEGRADED (incomplete danger data) so the enforce guard failed closed. A warm re-screen may clear it — retrying shortly may succeed.";
222
+ case "timeout":
223
+ return "The guard timed out on a cold screen. A warm screen is faster — retrying shortly may succeed.";
224
+ case "unavailable":
225
+ return "The guard was unavailable (upstream down, unpaid, or malformed). Retrying later may succeed; an operator can also adjust guards.*.onUnavailable.";
226
+ }
227
+ }
228
+ async function handlePaymentQuote(ctx, args) {
229
+ const url = requireString(args, "url");
230
+ const init = buildRequestInit(args);
231
+ const opts = { dryRun: true };
232
+ if (typeof args.tokenAddress === "string")
233
+ opts.tokenAddress = args.tokenAddress;
234
+ if (typeof args.chain === "string")
235
+ opts.chain = args.chain;
236
+ const { receipt } = await ctx.pf.fetch(url, init, opts);
237
+ return {
238
+ terms: receipt.quote ? [receipt.quote] : [],
239
+ selectedQuote: receipt.quote,
240
+ rejectedQuotes: receipt.rejectedQuotes,
241
+ decision: {
242
+ outcome: receipt.outcome,
243
+ denyCode: receipt.denyCode,
244
+ decision: receipt.outcome === "dry_run"
245
+ ? "would_pay"
246
+ : receipt.outcome === "free"
247
+ ? "free"
248
+ : "would_deny",
249
+ ...(receipt.guardBlockReason ? { guardBlockReason: receipt.guardBlockReason } : {}),
250
+ },
251
+ guards: receipt.guards,
252
+ remainingBudgets: receipt.budgets,
253
+ receiptId: receipt.receiptId,
254
+ warnings: buildWarnings(receipt),
255
+ };
256
+ }
257
+ async function handleListReceipts(ctx, args) {
258
+ const q = {};
259
+ if (typeof args.sinceTs === "number")
260
+ q.sinceTs = args.sinceTs;
261
+ if (typeof args.host === "string")
262
+ q.host = args.host;
263
+ if (typeof args.outcome === "string")
264
+ q.outcome = args.outcome;
265
+ if (typeof args.limit === "number")
266
+ q.limit = args.limit;
267
+ const receipts = await ctx.pf.receipts(q);
268
+ return { receipts, count: receipts.length };
269
+ }
270
+ function handleApprovePending(ctx, args) {
271
+ const action = args.action;
272
+ if (action === "list") {
273
+ return { approvals: ctx.pf.engine.listApprovals() };
274
+ }
275
+ if (action === "approve" || action === "deny") {
276
+ const approvalId = args.approvalId;
277
+ if (typeof approvalId !== "string" || approvalId.length === 0) {
278
+ throw new ToolInputError(`approvalId is required for action "${action}"`);
279
+ }
280
+ const res = ctx.pf.engine.resolveApproval(approvalId, action === "approve");
281
+ if (!res.ok) {
282
+ return { ok: false, error: res.error, action, approvalId };
283
+ }
284
+ return { ok: true, action, approvalId };
285
+ }
286
+ throw new ToolInputError(`unknown action "${String(action)}" — expected "list", "approve", or "deny"`);
287
+ }
288
+ function requireString(args, key) {
289
+ const v = args[key];
290
+ if (typeof v !== "string" || v.length === 0) {
291
+ throw new ToolInputError(`"${key}" is required and must be a non-empty string`);
292
+ }
293
+ return v;
294
+ }
295
+ function buildRequestInit(args) {
296
+ const init = {};
297
+ if (typeof args.method === "string")
298
+ init.method = args.method;
299
+ if (isStringRecord(args.headers))
300
+ init.headers = args.headers;
301
+ if (typeof args.body === "string")
302
+ init.body = args.body;
303
+ return init;
304
+ }
305
+ function isStringRecord(v) {
306
+ if (v === null || typeof v !== "object" || Array.isArray(v))
307
+ return false;
308
+ return Object.values(v).every((x) => typeof x === "string");
309
+ }
310
+ function buildWarnings(receipt) {
311
+ const out = [];
312
+ for (const g of receipt.guards) {
313
+ if (g.verdict === "warn" || g.verdict === "block" || g.verdict === "unavailable") {
314
+ out.push(`guard ${g.id}: ${g.verdict}`);
315
+ }
316
+ }
317
+ for (const n of receipt.notes)
318
+ out.push(n);
319
+ return out;
320
+ }
321
+ export const USD_DISPLAY_DECIMALS = USDC_DECIMALS;
@@ -0,0 +1,7 @@
1
+ import type { ParsedChallenge, PayfetchDeps, PaymentPayer, PaymentProof, PaymentQuote, WalletSigner } from "./types.js";
2
+ export declare class MppPayer implements PaymentPayer {
3
+ readonly rail: "mpp";
4
+ detects(_challenge: ParsedChallenge): boolean;
5
+ quotes(_challenge: ParsedChallenge): PaymentQuote[];
6
+ buildPayment(_quote: PaymentQuote, _signer: WalletSigner, _deps: PayfetchDeps): Promise<PaymentProof>;
7
+ }
@@ -0,0 +1,13 @@
1
+ import { UnsupportedRailError } from "./types.js";
2
+ export class MppPayer {
3
+ rail = "mpp";
4
+ detects(_challenge) {
5
+ return false;
6
+ }
7
+ quotes(_challenge) {
8
+ throw new UnsupportedRailError("mpp", "quotes");
9
+ }
10
+ buildPayment(_quote, _signer, _deps) {
11
+ throw new UnsupportedRailError("mpp", "buildPayment");
12
+ }
13
+ }
@@ -0,0 +1,6 @@
1
+ import type { ParsedChallenge, X402Terms } from "./types.js";
2
+ export declare function parseAcceptsEntry(raw: unknown): X402Terms | null;
3
+ export declare function parseAccepts(rawAccepts: unknown): X402Terms[] | null;
4
+ export declare function hashTerms(terms: readonly X402Terms[]): string;
5
+ export declare function parseChallenge(rawBody: unknown): ParsedChallenge;
6
+ export declare function parse402Challenge(rawBody: unknown, paymentRequiredHeader: string | null): ParsedChallenge;
@@ -0,0 +1,169 @@
1
+ import { createHash } from "node:crypto";
2
+ import { safeBase64Decode } from "@x402/core/utils";
3
+ import { canonicalNetwork, deriveAmountUsd } from "../core/constants.js";
4
+ function asRecord(v) {
5
+ return v != null && typeof v === "object" && !Array.isArray(v)
6
+ ? v
7
+ : null;
8
+ }
9
+ function asString(v) {
10
+ return typeof v === "string" ? v : null;
11
+ }
12
+ function asAtomicString(v) {
13
+ if (typeof v === "string" && v.trim().length > 0)
14
+ return v;
15
+ if (typeof v === "number" && Number.isFinite(v))
16
+ return String(v);
17
+ return null;
18
+ }
19
+ function asFiniteNumber(v) {
20
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
21
+ }
22
+ function sha256hex(input) {
23
+ return createHash("sha256").update(input).digest("hex");
24
+ }
25
+ export function parseAcceptsEntry(raw) {
26
+ const r = asRecord(raw);
27
+ if (!r)
28
+ return null;
29
+ const scheme = asString(r.scheme);
30
+ const networkRaw = asString(r.network);
31
+ const payToRaw = asString(r.payTo);
32
+ const amountAtomic = asAtomicString(r.maxAmountRequired ?? r.amount);
33
+ if (!scheme || !networkRaw || !payToRaw || amountAtomic == null)
34
+ return null;
35
+ const network = canonicalNetwork(networkRaw) ?? networkRaw;
36
+ const asset = asString(r.asset);
37
+ const payTo = payToRaw.toLowerCase();
38
+ const maxTimeoutSeconds = asFiniteNumber(r.maxTimeoutSeconds);
39
+ const mimeType = asString(r.mimeType);
40
+ const resource = asString(r.resource);
41
+ const outputSchema = asRecord(r.outputSchema);
42
+ const hasOutputSchema = outputSchema != null;
43
+ const outputSchemaSha256 = hasOutputSchema ? sha256hex(stableStringify(outputSchema)) : null;
44
+ const amountUsd = deriveAmountUsd(amountAtomic, asset);
45
+ return {
46
+ scheme,
47
+ network,
48
+ asset,
49
+ amountAtomic,
50
+ amountUsd,
51
+ payTo,
52
+ maxTimeoutSeconds,
53
+ mimeType,
54
+ hasOutputSchema,
55
+ outputSchemaSha256,
56
+ networkAsDeclared: networkRaw,
57
+ resource,
58
+ rawAccepts: raw,
59
+ };
60
+ }
61
+ export function parseAccepts(rawAccepts) {
62
+ if (!Array.isArray(rawAccepts))
63
+ return null;
64
+ const out = [];
65
+ for (const entry of rawAccepts) {
66
+ const t = parseAcceptsEntry(entry);
67
+ if (t)
68
+ out.push(t);
69
+ }
70
+ return out.length > 0 ? out : null;
71
+ }
72
+ export function hashTerms(terms) {
73
+ const rows = terms.map((t) => JSON.stringify([
74
+ t.scheme,
75
+ t.network,
76
+ t.asset,
77
+ t.amountAtomic,
78
+ t.payTo,
79
+ t.maxTimeoutSeconds,
80
+ t.mimeType,
81
+ t.outputSchemaSha256,
82
+ ]));
83
+ rows.sort();
84
+ return sha256hex(rows.join("\n")).slice(0, 16);
85
+ }
86
+ const MALFORMED = Object.freeze({
87
+ rail: null,
88
+ x402Version: null,
89
+ terms: [],
90
+ termsHash: null,
91
+ malformed: true,
92
+ });
93
+ export function parseChallenge(rawBody) {
94
+ const root = asRecord(toJson(rawBody));
95
+ if (!root)
96
+ return { ...MALFORMED };
97
+ const x402Version = asFiniteNumber(root.x402Version);
98
+ const acceptsPresent = Array.isArray(root.accepts);
99
+ const rail = x402Version !== null && acceptsPresent ? "x402" : null;
100
+ const terms = parseAccepts(root.accepts);
101
+ if (terms === null) {
102
+ return { rail, x402Version, terms: [], termsHash: null, malformed: true };
103
+ }
104
+ const resourceUrl = asString(asRecord(root.resource)?.url);
105
+ if (resourceUrl !== null) {
106
+ for (const t of terms)
107
+ if (t.resource === null)
108
+ t.resource = resourceUrl;
109
+ }
110
+ return { rail, x402Version, terms, termsHash: hashTerms(terms), malformed: false };
111
+ }
112
+ function challengeFromHeader(headerValue) {
113
+ let json;
114
+ try {
115
+ json = safeBase64Decode(headerValue);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ return parseChallenge(json);
121
+ }
122
+ export function parse402Challenge(rawBody, paymentRequiredHeader) {
123
+ if (paymentRequiredHeader != null && paymentRequiredHeader.length > 0) {
124
+ const fromHeader = challengeFromHeader(paymentRequiredHeader);
125
+ if (fromHeader !== null && !fromHeader.malformed)
126
+ return fromHeader;
127
+ const fromBody = parseChallenge(rawBody);
128
+ if (!fromBody.malformed)
129
+ return fromBody;
130
+ return fromHeader ?? fromBody;
131
+ }
132
+ return parseChallenge(rawBody);
133
+ }
134
+ function toJson(rawBody) {
135
+ if (rawBody == null)
136
+ return null;
137
+ if (typeof rawBody === "string")
138
+ return tryParseJson(rawBody);
139
+ if (rawBody instanceof Uint8Array) {
140
+ let text;
141
+ try {
142
+ text = new TextDecoder("utf-8", { fatal: false }).decode(rawBody);
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ return tryParseJson(text);
148
+ }
149
+ if (typeof rawBody === "object")
150
+ return rawBody;
151
+ return null;
152
+ }
153
+ function tryParseJson(text) {
154
+ try {
155
+ return JSON.parse(text);
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ function stableStringify(value) {
162
+ if (value === null || typeof value !== "object")
163
+ return JSON.stringify(value) ?? "null";
164
+ if (Array.isArray(value))
165
+ return `[${value.map((v) => stableStringify(v)).join(",")}]`;
166
+ const rec = value;
167
+ const keys = Object.keys(rec).sort();
168
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
169
+ }
@@ -0,0 +1,14 @@
1
+ import type { Eip712TypedData, WalletSigner } from "./types.js";
2
+ export type CdpSignerConfig = {
3
+ apiKeyId: string;
4
+ apiKeySecret: string;
5
+ walletSecret: string;
6
+ accountName?: string;
7
+ };
8
+ export declare class CdpServerWalletSigner implements WalletSigner {
9
+ #private;
10
+ readonly kind: "cdp_server_wallet";
11
+ constructor(config: CdpSignerConfig);
12
+ address(): Promise<string>;
13
+ signTypedData(td: Eip712TypedData): Promise<`0x${string}`>;
14
+ }
@@ -0,0 +1,64 @@
1
+ import { CdpClient } from "@coinbase/cdp-sdk";
2
+ import { DEFAULT_CDP_ACCOUNT_NAME } from "../core/constants.js";
3
+ export class CdpServerWalletSigner {
4
+ kind = "cdp_server_wallet";
5
+ #apiKeyId;
6
+ #apiKeySecret;
7
+ #walletSecret;
8
+ #accountName;
9
+ #accountPromise = null;
10
+ constructor(config) {
11
+ this.#apiKeyId = config.apiKeyId;
12
+ this.#apiKeySecret = config.apiKeySecret;
13
+ this.#walletSecret = config.walletSecret;
14
+ this.#accountName =
15
+ config.accountName && config.accountName.length > 0
16
+ ? config.accountName
17
+ : DEFAULT_CDP_ACCOUNT_NAME;
18
+ }
19
+ async address() {
20
+ const account = await this.#account();
21
+ return account.address.toLowerCase();
22
+ }
23
+ async signTypedData(td) {
24
+ const account = await this.#account();
25
+ try {
26
+ return await account.signTypedData(td);
27
+ }
28
+ catch (err) {
29
+ throw this.#scrubbedError(err, "CDP signTypedData failed");
30
+ }
31
+ }
32
+ #account() {
33
+ if (this.#accountPromise === null) {
34
+ this.#accountPromise = this.#connect().catch((err) => {
35
+ this.#accountPromise = null;
36
+ throw this.#scrubbedError(err, "CDP account resolution failed");
37
+ });
38
+ }
39
+ return this.#accountPromise;
40
+ }
41
+ async #connect() {
42
+ const cdp = new CdpClient({
43
+ apiKeyId: this.#apiKeyId,
44
+ apiKeySecret: this.#apiKeySecret,
45
+ walletSecret: this.#walletSecret,
46
+ });
47
+ return cdp.evm.getOrCreateAccount({ name: this.#accountName });
48
+ }
49
+ #scrubbedError(err, context) {
50
+ const raw = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
51
+ return new Error(`${context}: ${this.#scrub(raw)}`);
52
+ }
53
+ #scrub(text) {
54
+ let out = text;
55
+ for (const secret of [this.#apiKeySecret, this.#walletSecret, this.#apiKeyId]) {
56
+ if (secret && secret.length > 0)
57
+ out = out.split(secret).join("[redacted]");
58
+ }
59
+ out = out.replace(/-----BEGIN[\s\S]*?PRIVATE KEY-----[\s\S]*?-----END[\s\S]*?PRIVATE KEY-----/g, "[redacted-pem]");
60
+ out = out.replace(/\b(?:0x)?[0-9a-fA-F]{32,}\b/g, "[redacted-hex]");
61
+ out = out.replace(/\b[A-Za-z0-9_-]{40,}\b/g, "[redacted-token]");
62
+ return out;
63
+ }
64
+ }
@@ -0,0 +1,8 @@
1
+ import type { Eip712TypedData, WalletSigner } from "./types.js";
2
+ export declare class LocalKeySigner implements WalletSigner {
3
+ #private;
4
+ readonly kind: "local_key";
5
+ constructor(privateKey: `0x${string}`);
6
+ address(): Promise<string>;
7
+ signTypedData(td: Eip712TypedData): Promise<`0x${string}`>;
8
+ }
@@ -0,0 +1,14 @@
1
+ import { privateKeyToAccount } from "viem/accounts";
2
+ export class LocalKeySigner {
3
+ kind = "local_key";
4
+ #account;
5
+ constructor(privateKey) {
6
+ this.#account = privateKeyToAccount(privateKey);
7
+ }
8
+ async address() {
9
+ return this.#account.address.toLowerCase();
10
+ }
11
+ async signTypedData(td) {
12
+ return this.#account.signTypedData(td);
13
+ }
14
+ }