@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,57 @@
1
+ import type { PayfetchFs } from "./fs.js";
2
+ import type { SafetyGuardConfig, TrustGuardConfig } from "../guards/types.js";
3
+ export type { TrustGuardConfig, SafetyGuardConfig };
4
+ export type HostPattern = string;
5
+ export type Policy = {
6
+ schema: "p3f.policy.v1";
7
+ mode: "open" | "allowlist";
8
+ allow: HostPattern[];
9
+ deny: HostPattern[];
10
+ caps: {
11
+ perCallUsd: number;
12
+ dailyUsd: number;
13
+ perHostDailyUsd: number;
14
+ totalUsd: number | null;
15
+ };
16
+ approval: {
17
+ thresholdUsd: number;
18
+ mode: "elicit" | "queue" | "deny";
19
+ elicitFallback: "queue" | "deny";
20
+ preApprovedUpToUsd: number | null;
21
+ preApprovedHosts: HostPattern[];
22
+ };
23
+ guards: {
24
+ trust: TrustGuardConfig;
25
+ safety: SafetyGuardConfig;
26
+ };
27
+ allowPrivateTargets: boolean;
28
+ autoDeny: {
29
+ enabled: boolean;
30
+ };
31
+ };
32
+ export declare function defaultPolicy(): Policy;
33
+ export declare function matchHostPattern(host: string, pattern: string): boolean;
34
+ export declare function matchesAnyHost(host: string, patterns: readonly HostPattern[]): boolean;
35
+ export declare function mergePolicy(base: Policy, over?: DeepPartial<Policy>): Policy;
36
+ type DeepPartial<T> = {
37
+ [K in keyof T]?: T[K] extends readonly (infer U)[] ? U[] : T[K] extends object ? DeepPartial<T[K]> : T[K];
38
+ };
39
+ export declare class PolicyLoadError extends Error {
40
+ readonly reason: string;
41
+ constructor(reason: string);
42
+ }
43
+ export type PolicyLoad = {
44
+ ok: true;
45
+ policy: Policy;
46
+ mtimeMs: number | null;
47
+ } | {
48
+ ok: false;
49
+ error: string;
50
+ };
51
+ export declare function validateConfig(parsed: unknown, base: Policy, warn?: (msg: string) => void): Policy;
52
+ export declare function configPath(dataDir: string): string;
53
+ export type PolicyIo = {
54
+ fs: PayfetchFs;
55
+ log?: (msg: string, fields?: Record<string, unknown>) => void;
56
+ };
57
+ export declare function loadPolicy(dataDir: string, io: PolicyIo, base: Policy): PolicyLoad;
@@ -0,0 +1,224 @@
1
+ import { join } from "node:path";
2
+ import { APPROVAL_PREAPPROVED_UP_TO_DEFAULT_USD, APPROVAL_THRESHOLD_DEFAULT_USD, DAILY_CAP_DEFAULT_USD, PER_CALL_CAP_DEFAULT_USD, PER_HOST_DAILY_CAP_DEFAULT_USD, TOTAL_CAP_DEFAULT_USD, } from "./constants.js";
3
+ import { DEFAULT_SAFETY_GUARD_CONFIG, DEFAULT_TRUST_GUARD_CONFIG, } from "../guards/types.js";
4
+ export function defaultPolicy() {
5
+ return Object.freeze({
6
+ schema: "p3f.policy.v1",
7
+ mode: "open",
8
+ allow: [],
9
+ deny: [],
10
+ caps: Object.freeze({
11
+ perCallUsd: PER_CALL_CAP_DEFAULT_USD,
12
+ dailyUsd: DAILY_CAP_DEFAULT_USD,
13
+ perHostDailyUsd: PER_HOST_DAILY_CAP_DEFAULT_USD,
14
+ totalUsd: TOTAL_CAP_DEFAULT_USD,
15
+ }),
16
+ approval: Object.freeze({
17
+ thresholdUsd: APPROVAL_THRESHOLD_DEFAULT_USD,
18
+ mode: "elicit",
19
+ elicitFallback: "deny",
20
+ preApprovedUpToUsd: APPROVAL_PREAPPROVED_UP_TO_DEFAULT_USD,
21
+ preApprovedHosts: [],
22
+ }),
23
+ guards: Object.freeze({
24
+ trust: {
25
+ ...DEFAULT_TRUST_GUARD_CONFIG,
26
+ blockVerdicts: [...DEFAULT_TRUST_GUARD_CONFIG.blockVerdicts],
27
+ },
28
+ safety: {
29
+ ...DEFAULT_SAFETY_GUARD_CONFIG,
30
+ blockVerdicts: [...DEFAULT_SAFETY_GUARD_CONFIG.blockVerdicts],
31
+ blockDeployerVerdicts: [...DEFAULT_SAFETY_GUARD_CONFIG.blockDeployerVerdicts],
32
+ },
33
+ }),
34
+ allowPrivateTargets: false,
35
+ autoDeny: { enabled: true },
36
+ });
37
+ }
38
+ export function matchHostPattern(host, pattern) {
39
+ const h = host.toLowerCase();
40
+ const p = pattern.toLowerCase();
41
+ if (p.startsWith("*.")) {
42
+ const suffix = p.slice(1);
43
+ return h.endsWith(suffix) && h.length > suffix.length;
44
+ }
45
+ return h === p;
46
+ }
47
+ export function matchesAnyHost(host, patterns) {
48
+ return patterns.some((p) => matchHostPattern(host, p));
49
+ }
50
+ export function mergePolicy(base, over) {
51
+ if (!over)
52
+ return base;
53
+ return {
54
+ schema: "p3f.policy.v1",
55
+ mode: over.mode ?? base.mode,
56
+ allow: over.allow ? [...over.allow] : [...base.allow],
57
+ deny: over.deny ? [...over.deny] : [...base.deny],
58
+ caps: { ...base.caps, ...(over.caps ?? {}) },
59
+ approval: {
60
+ ...base.approval,
61
+ ...(over.approval ?? {}),
62
+ preApprovedHosts: over.approval?.preApprovedHosts
63
+ ? [...over.approval.preApprovedHosts]
64
+ : [...base.approval.preApprovedHosts],
65
+ },
66
+ guards: {
67
+ trust: { ...base.guards.trust, ...(over.guards?.trust ?? {}) },
68
+ safety: { ...base.guards.safety, ...(over.guards?.safety ?? {}) },
69
+ },
70
+ allowPrivateTargets: over.allowPrivateTargets ?? base.allowPrivateTargets,
71
+ autoDeny: { ...base.autoDeny, ...(over.autoDeny ?? {}) },
72
+ };
73
+ }
74
+ export class PolicyLoadError extends Error {
75
+ reason;
76
+ constructor(reason) {
77
+ super(`payfetch: invalid ${"config.json"} — ${reason} (SPEC §4.1: fail closed).`);
78
+ this.reason = reason;
79
+ this.name = "PolicyLoadError";
80
+ }
81
+ }
82
+ function isPlainObject(v) {
83
+ return v != null && typeof v === "object" && !Array.isArray(v);
84
+ }
85
+ function fail(reason) {
86
+ throw new PolicyLoadError(reason);
87
+ }
88
+ function reqEnum(v, allowed, base, field) {
89
+ if (v === undefined)
90
+ return base;
91
+ if (typeof v === "string" && allowed.includes(v))
92
+ return v;
93
+ fail(`${field} must be one of ${allowed.join("|")}`);
94
+ }
95
+ function reqNumber(v, base, field) {
96
+ if (v === undefined)
97
+ return base;
98
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0)
99
+ return v;
100
+ fail(`${field} must be a non-negative number`);
101
+ }
102
+ function reqNumberOrNull(v, base, field) {
103
+ if (v === undefined)
104
+ return base;
105
+ if (v === null)
106
+ return null;
107
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0)
108
+ return v;
109
+ fail(`${field} must be a non-negative number or null`);
110
+ }
111
+ function reqBool(v, base, field) {
112
+ if (v === undefined)
113
+ return base;
114
+ if (typeof v === "boolean")
115
+ return v;
116
+ fail(`${field} must be a boolean`);
117
+ }
118
+ function reqStringArray(v, base, field) {
119
+ if (v === undefined)
120
+ return [...base];
121
+ if (Array.isArray(v) && v.every((x) => typeof x === "string"))
122
+ return [...v];
123
+ fail(`${field} must be an array of strings`);
124
+ }
125
+ export function validateConfig(parsed, base, warn) {
126
+ if (!isPlainObject(parsed))
127
+ fail("root must be a JSON object");
128
+ const root = parsed;
129
+ if (root.schema !== undefined && root.schema !== "p3f.policy.v1") {
130
+ fail(`schema must be "p3f.policy.v1"`);
131
+ }
132
+ const caps = root.caps === undefined ? {} : isPlainObject(root.caps) ? root.caps : fail("caps must be an object");
133
+ const approval = root.approval === undefined ? {} : isPlainObject(root.approval) ? root.approval : fail("approval must be an object");
134
+ const guards = root.guards === undefined ? {} : isPlainObject(root.guards) ? root.guards : fail("guards must be an object");
135
+ const trust = guards.trust === undefined
136
+ ? {}
137
+ : isPlainObject(guards.trust)
138
+ ? guards.trust
139
+ : fail("guards.trust must be an object");
140
+ const safety = guards.safety === undefined
141
+ ? {}
142
+ : isPlainObject(guards.safety)
143
+ ? guards.safety
144
+ : fail("guards.safety must be an object");
145
+ const autoDeny = root.autoDeny === undefined ? {} : isPlainObject(root.autoDeny) ? root.autoDeny : fail("autoDeny must be an object");
146
+ const policy = {
147
+ schema: "p3f.policy.v1",
148
+ mode: reqEnum(root.mode, ["open", "allowlist"], base.mode, "mode"),
149
+ allow: reqStringArray(root.allow, base.allow, "allow"),
150
+ deny: reqStringArray(root.deny, base.deny, "deny"),
151
+ caps: {
152
+ perCallUsd: reqNumber(caps.perCallUsd, base.caps.perCallUsd, "caps.perCallUsd"),
153
+ dailyUsd: reqNumber(caps.dailyUsd, base.caps.dailyUsd, "caps.dailyUsd"),
154
+ perHostDailyUsd: reqNumber(caps.perHostDailyUsd, base.caps.perHostDailyUsd, "caps.perHostDailyUsd"),
155
+ totalUsd: reqNumberOrNull(caps.totalUsd, base.caps.totalUsd, "caps.totalUsd"),
156
+ },
157
+ approval: {
158
+ thresholdUsd: reqNumber(approval.thresholdUsd, base.approval.thresholdUsd, "approval.thresholdUsd"),
159
+ mode: reqEnum(approval.mode, ["elicit", "queue", "deny"], base.approval.mode, "approval.mode"),
160
+ elicitFallback: reqEnum(approval.elicitFallback, ["queue", "deny"], base.approval.elicitFallback, "approval.elicitFallback"),
161
+ preApprovedUpToUsd: reqNumberOrNull(approval.preApprovedUpToUsd, base.approval.preApprovedUpToUsd, "approval.preApprovedUpToUsd"),
162
+ preApprovedHosts: reqStringArray(approval.preApprovedHosts, base.approval.preApprovedHosts, "approval.preApprovedHosts"),
163
+ },
164
+ guards: {
165
+ trust: {
166
+ enabled: reqBool(trust.enabled, base.guards.trust.enabled, "guards.trust.enabled"),
167
+ mode: reqEnum(trust.mode, ["advisory", "enforce"], base.guards.trust.mode, "guards.trust.mode"),
168
+ minScore: reqNumberOrNull(trust.minScore, base.guards.trust.minScore, "guards.trust.minScore"),
169
+ blockVerdicts: reqStringArray(trust.blockVerdicts, base.guards.trust.blockVerdicts, "guards.trust.blockVerdicts"),
170
+ blockUnrated: reqBool(trust.blockUnrated, base.guards.trust.blockUnrated, "guards.trust.blockUnrated"),
171
+ onUnavailable: reqEnum(trust.onUnavailable, ["proceed", "block"], base.guards.trust.onUnavailable, "guards.trust.onUnavailable"),
172
+ dailyBudgetUsd: reqNumber(trust.dailyBudgetUsd, base.guards.trust.dailyBudgetUsd, "guards.trust.dailyBudgetUsd"),
173
+ },
174
+ safety: {
175
+ enabled: reqBool(safety.enabled, base.guards.safety.enabled, "guards.safety.enabled"),
176
+ mode: reqEnum(safety.mode, ["advisory", "enforce"], base.guards.safety.mode, "guards.safety.mode"),
177
+ depth: reqEnum(safety.depth, ["basic", "deep"], base.guards.safety.depth, "guards.safety.depth"),
178
+ blockVerdicts: reqStringArray(safety.blockVerdicts, base.guards.safety.blockVerdicts, "guards.safety.blockVerdicts"),
179
+ blockDeployerVerdicts: reqStringArray(safety.blockDeployerVerdicts, base.guards.safety.blockDeployerVerdicts, "guards.safety.blockDeployerVerdicts"),
180
+ onUnavailable: reqEnum(safety.onUnavailable, ["proceed", "block"], base.guards.safety.onUnavailable, "guards.safety.onUnavailable"),
181
+ onDegraded: reqEnum(safety.onDegraded, ["block", "warn", "proceed"], base.guards.safety.onDegraded, "guards.safety.onDegraded"),
182
+ dailyBudgetUsd: reqNumber(safety.dailyBudgetUsd, base.guards.safety.dailyBudgetUsd, "guards.safety.dailyBudgetUsd"),
183
+ },
184
+ },
185
+ allowPrivateTargets: reqBool(root.allowPrivateTargets, base.allowPrivateTargets, "allowPrivateTargets"),
186
+ autoDeny: {
187
+ enabled: reqBool(autoDeny.enabled, base.autoDeny.enabled, "autoDeny.enabled"),
188
+ },
189
+ };
190
+ if (policy.approval.preApprovedUpToUsd !== null &&
191
+ policy.approval.preApprovedUpToUsd > policy.caps.perCallUsd) {
192
+ warn?.(`approval.preApprovedUpToUsd (${policy.approval.preApprovedUpToUsd}) exceeds ` +
193
+ `caps.perCallUsd (${policy.caps.perCallUsd}); the per-call cap clips every payment ` +
194
+ `first, so the excess pre-approval ceiling has no effect.`);
195
+ }
196
+ return policy;
197
+ }
198
+ export function configPath(dataDir) {
199
+ return join(dataDir, "config.json");
200
+ }
201
+ export function loadPolicy(dataDir, io, base) {
202
+ const path = configPath(dataDir);
203
+ const raw = io.fs.readText(path);
204
+ if (raw === null) {
205
+ io.fs.writeText(path, JSON.stringify(base, null, 2));
206
+ return { ok: true, policy: base, mtimeMs: io.fs.statMtimeMs(path) };
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = JSON.parse(raw);
211
+ }
212
+ catch (err) {
213
+ return { ok: false, error: `config.json is not valid JSON: ${err.message}` };
214
+ }
215
+ try {
216
+ const policy = validateConfig(parsed, base, (msg) => io.log?.("policy.config_warning", { message: msg }));
217
+ return { ok: true, policy, mtimeMs: io.fs.statMtimeMs(path) };
218
+ }
219
+ catch (err) {
220
+ if (err instanceof PolicyLoadError)
221
+ return { ok: false, error: err.reason };
222
+ return { ok: false, error: err.message };
223
+ }
224
+ }
@@ -0,0 +1,93 @@
1
+ import type { PayfetchFs } from "./fs.js";
2
+ import type { Policy } from "./policy.js";
3
+ export declare function parseIpv4(s: string): number[] | null;
4
+ export declare function parseIpv6(input: string): number[] | null;
5
+ export declare function isBlockedIp(ip: string): boolean;
6
+ export type TargetReason = "scheme" | "private_target" | "invalid_url" | "unresolved";
7
+ export type TargetVerdict = {
8
+ ok: true;
9
+ host: string;
10
+ } | {
11
+ ok: false;
12
+ reason: TargetReason;
13
+ };
14
+ export declare function evaluateTarget(url: string, resolvedIps: readonly string[], policy: Pick<Policy, "allowPrivateTargets">): TargetVerdict;
15
+ export type LookupAddress = {
16
+ address: string;
17
+ family: number;
18
+ };
19
+ export type NetLookupOptions = {
20
+ all?: boolean;
21
+ family?: number;
22
+ };
23
+ type NetLookupCb = (err: Error | null, address: string | LookupAddress[], family?: number) => void;
24
+ type NetLookup = (hostname: string, options: NetLookupOptions | undefined, callback: NetLookupCb) => void;
25
+ export declare function createPinnedLookup(resolve: (host: string) => Promise<string[]>, policy: Pick<Policy, "allowPrivateTargets">): NetLookup;
26
+ export type HttpResponse = {
27
+ status: number;
28
+ headers: Headers;
29
+ body: ReadableStream<Uint8Array> | null;
30
+ };
31
+ export type HttpRequestInit = {
32
+ method: string;
33
+ headers: Record<string, string>;
34
+ body: string | null;
35
+ signal?: AbortSignal;
36
+ };
37
+ export type HttpClient = (url: string, init: HttpRequestInit) => Promise<HttpResponse>;
38
+ export declare function adaptFetch(fetchFn: typeof fetch): HttpClient;
39
+ export type CappedBody = {
40
+ bytes: Uint8Array;
41
+ hardCapped: boolean;
42
+ };
43
+ export declare function readCapped(body: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<CappedBody>;
44
+ export type TransportIo = {
45
+ request: HttpClient;
46
+ resolve: (host: string) => Promise<string[]>;
47
+ fs: PayfetchFs;
48
+ now: () => number;
49
+ log: (msg: string, fields?: Record<string, unknown>) => void;
50
+ setTimer?: (fn: () => void, ms: number) => {
51
+ clear: () => void;
52
+ };
53
+ };
54
+ export type TransportError = "private_target_blocked" | "insecure_redirect" | "fetch_error";
55
+ export type TransportOpts = {
56
+ followRedirects?: boolean;
57
+ };
58
+ export type TransportResult = {
59
+ ok: boolean;
60
+ error: TransportError | null;
61
+ finalUrl: string;
62
+ finalHost: string;
63
+ status: number | null;
64
+ headers: Headers | null;
65
+ contentType: string | null;
66
+ rawBody: Uint8Array | null;
67
+ bodyBytes: number | null;
68
+ bodySha256: string | null;
69
+ hardCapped: boolean;
70
+ hopChain: string[];
71
+ redirectCount: number;
72
+ notes: string[];
73
+ totalMs: number;
74
+ };
75
+ export declare function transportFetch(url: string, init: {
76
+ method: string;
77
+ headers: Record<string, string>;
78
+ body: string | null;
79
+ }, policy: Pick<Policy, "allowPrivateTargets">, io: TransportIo, opts?: TransportOpts): Promise<TransportResult>;
80
+ export type DeliveredBody = {
81
+ mode: "inline";
82
+ text: string;
83
+ truncated: boolean;
84
+ } | {
85
+ mode: "file";
86
+ path: string;
87
+ truncated: false;
88
+ };
89
+ export declare function deliverBody(bytes: Uint8Array, responseMode: "inline" | "file", ctx: {
90
+ fs: PayfetchFs;
91
+ downloadPath: string;
92
+ }): DeliveredBody;
93
+ export {};