@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,104 @@
1
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ function isEnoent(err) {
4
+ return err?.code === "ENOENT";
5
+ }
6
+ function isEexist(err) {
7
+ return err?.code === "EEXIST";
8
+ }
9
+ export const realFs = {
10
+ ensureDir(dir) {
11
+ mkdirSync(dir, { recursive: true });
12
+ },
13
+ readText(path) {
14
+ try {
15
+ return readFileSync(path, "utf8");
16
+ }
17
+ catch (err) {
18
+ if (isEnoent(err))
19
+ return null;
20
+ throw err;
21
+ }
22
+ },
23
+ writeText(path, data) {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ const tmp = `${path}.tmp.${process.pid}`;
26
+ const fd = openSync(tmp, "w");
27
+ try {
28
+ writeSync(fd, data);
29
+ fsyncSync(fd);
30
+ }
31
+ finally {
32
+ closeSync(fd);
33
+ }
34
+ renameSync(tmp, path);
35
+ },
36
+ writeBytes(path, data) {
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ const tmp = `${path}.tmp.${process.pid}`;
39
+ const fd = openSync(tmp, "w");
40
+ try {
41
+ writeSync(fd, data);
42
+ fsyncSync(fd);
43
+ }
44
+ finally {
45
+ closeSync(fd);
46
+ }
47
+ renameSync(tmp, path);
48
+ },
49
+ appendLine(path, line, opts) {
50
+ mkdirSync(dirname(path), { recursive: true });
51
+ const fd = openSync(path, "a");
52
+ try {
53
+ writeSync(fd, line.endsWith("\n") ? line : `${line}\n`);
54
+ if (opts.fsync)
55
+ fsyncSync(fd);
56
+ }
57
+ finally {
58
+ closeSync(fd);
59
+ }
60
+ },
61
+ statMtimeMs(path) {
62
+ try {
63
+ return statSync(path).mtimeMs;
64
+ }
65
+ catch (err) {
66
+ if (isEnoent(err))
67
+ return null;
68
+ throw err;
69
+ }
70
+ },
71
+ listDir(dir) {
72
+ try {
73
+ return readdirSync(dir);
74
+ }
75
+ catch (err) {
76
+ if (isEnoent(err))
77
+ return [];
78
+ throw err;
79
+ }
80
+ },
81
+ tryCreateExclusive(path, contents, mode) {
82
+ mkdirSync(dirname(path), { recursive: true });
83
+ let fd;
84
+ try {
85
+ fd = openSync(path, "wx", mode ?? 0o666);
86
+ }
87
+ catch (err) {
88
+ if (isEexist(err))
89
+ return false;
90
+ throw err;
91
+ }
92
+ try {
93
+ writeSync(fd, contents);
94
+ fsyncSync(fd);
95
+ }
96
+ finally {
97
+ closeSync(fd);
98
+ }
99
+ return true;
100
+ },
101
+ remove(path) {
102
+ rmSync(path, { force: true });
103
+ },
104
+ };
@@ -0,0 +1,23 @@
1
+ import type { PayfetchFs } from "./fs.js";
2
+ export declare const INTEGRITY_KEY_BYTES = 32;
3
+ export declare const INTEGRITY_KEY_MODE = 384;
4
+ export type SidecarRecord = {
5
+ seq: number;
6
+ receiptId: string;
7
+ sha256: string;
8
+ mac: string;
9
+ };
10
+ export type MonthIntegrity = {
11
+ ok: boolean;
12
+ checked: number;
13
+ issues: string[];
14
+ };
15
+ export declare function integrityKeyPath(dataDir: string): string;
16
+ export declare function sidecarPath(monthFile: string): string;
17
+ export declare function lineHash(lineString: string): string;
18
+ export declare function genesisMac(key: Uint8Array, monthKey: string): string;
19
+ export declare function nextMac(key: Uint8Array, prevMacHex: string, lineSha256Hex: string): string;
20
+ export declare function loadOrCreateKey(fs: PayfetchFs, dataDir: string, random: () => Uint8Array): Uint8Array;
21
+ export declare function defaultRandom(): Uint8Array;
22
+ export declare function readSidecarRecords(fs: PayfetchFs, sidecar: string): SidecarRecord[];
23
+ export declare function verifyMonth(fs: PayfetchFs, _dataDir: string, key: Uint8Array, monthFile: string): MonthIntegrity;
@@ -0,0 +1,150 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { basename, join } from "node:path";
3
+ const GENESIS_PREFIX = "p3f-integrity-genesis:";
4
+ export const INTEGRITY_KEY_BYTES = 32;
5
+ export const INTEGRITY_KEY_MODE = 0o600;
6
+ const JSONL_SUFFIX = ".jsonl";
7
+ const SIDECAR_SUFFIX = ".integrity";
8
+ export function integrityKeyPath(dataDir) {
9
+ return join(dataDir, "integrity.key");
10
+ }
11
+ export function sidecarPath(monthFile) {
12
+ return `${monthFile}${SIDECAR_SUFFIX}`;
13
+ }
14
+ function monthKeyOfFile(monthFile) {
15
+ const base = basename(monthFile);
16
+ return base.endsWith(JSONL_SUFFIX) ? base.slice(0, -JSONL_SUFFIX.length) : base;
17
+ }
18
+ function bytesToHex(bytes) {
19
+ let s = "";
20
+ for (let i = 0; i < bytes.length; i++)
21
+ s += bytes[i].toString(16).padStart(2, "0");
22
+ return s;
23
+ }
24
+ function hexToBytes(hex) {
25
+ const h = hex.trim();
26
+ if (h.length === 0 || h.length % 2 !== 0)
27
+ return null;
28
+ const out = new Uint8Array(h.length / 2);
29
+ for (let i = 0; i < out.length; i++) {
30
+ const byte = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
31
+ if (Number.isNaN(byte))
32
+ return null;
33
+ out[i] = byte;
34
+ }
35
+ return out;
36
+ }
37
+ export function lineHash(lineString) {
38
+ return createHash("sha256").update(lineString).digest("hex");
39
+ }
40
+ export function genesisMac(key, monthKey) {
41
+ return createHmac("sha256", key).update(GENESIS_PREFIX + monthKey).digest("hex");
42
+ }
43
+ export function nextMac(key, prevMacHex, lineSha256Hex) {
44
+ return createHmac("sha256", key)
45
+ .update(`${prevMacHex}:${lineSha256Hex}`)
46
+ .digest("hex");
47
+ }
48
+ export function loadOrCreateKey(fs, dataDir, random) {
49
+ const path = integrityKeyPath(dataDir);
50
+ const existing = fs.readText(path);
51
+ if (existing !== null) {
52
+ const bytes = hexToBytes(existing);
53
+ if (bytes !== null)
54
+ return bytes;
55
+ }
56
+ const fresh = random();
57
+ const hex = bytesToHex(fresh);
58
+ if (fs.tryCreateExclusive(path, hex, INTEGRITY_KEY_MODE))
59
+ return fresh;
60
+ const raced = fs.readText(path);
61
+ const bytes = raced !== null ? hexToBytes(raced) : null;
62
+ return bytes ?? fresh;
63
+ }
64
+ export function defaultRandom() {
65
+ return new Uint8Array(randomBytes(INTEGRITY_KEY_BYTES));
66
+ }
67
+ function isSidecarRecord(o) {
68
+ if (o == null || typeof o !== "object")
69
+ return false;
70
+ const r = o;
71
+ return (typeof r.seq === "number" &&
72
+ typeof r.receiptId === "string" &&
73
+ typeof r.sha256 === "string" &&
74
+ typeof r.mac === "string");
75
+ }
76
+ export function readSidecarRecords(fs, sidecar) {
77
+ const raw = fs.readText(sidecar);
78
+ if (raw === null)
79
+ return [];
80
+ const out = [];
81
+ for (const seg of raw.split("\n")) {
82
+ if (seg.trim().length === 0)
83
+ continue;
84
+ try {
85
+ const o = JSON.parse(seg);
86
+ if (isSidecarRecord(o))
87
+ out.push(o);
88
+ }
89
+ catch {
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+ function readLedgerLines(fs, monthFile) {
95
+ const raw = fs.readText(monthFile);
96
+ if (raw === null)
97
+ return [];
98
+ const out = [];
99
+ for (const seg of raw.split("\n")) {
100
+ if (seg.trim().length === 0)
101
+ continue;
102
+ out.push(seg);
103
+ }
104
+ return out;
105
+ }
106
+ function lineReceiptId(line) {
107
+ try {
108
+ const o = JSON.parse(line);
109
+ return typeof o.receiptId === "string" ? o.receiptId : null;
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ export function verifyMonth(fs, _dataDir, key, monthFile) {
116
+ const issues = [];
117
+ const monthKey = monthKeyOfFile(monthFile);
118
+ const lines = readLedgerLines(fs, monthFile);
119
+ const records = readSidecarRecords(fs, sidecarPath(monthFile));
120
+ let prev = genesisMac(key, monthKey);
121
+ const n = Math.max(lines.length, records.length);
122
+ for (let i = 0; i < n; i++) {
123
+ const line = i < lines.length ? lines[i] : null;
124
+ const rec = i < records.length ? records[i] : null;
125
+ if (line === null && rec !== null) {
126
+ issues.push(`seq ${i} (receiptId ${rec.receiptId}): sidecar record has no matching ledger line (extra sidecar record / deleted ledger line)`);
127
+ prev = rec.mac;
128
+ continue;
129
+ }
130
+ if (line !== null && rec === null) {
131
+ issues.push(`seq ${i} (receiptId ${lineReceiptId(line) ?? "?"}): ledger line has no sidecar record (missing sidecar record)`);
132
+ prev = nextMac(key, prev, lineHash(line));
133
+ continue;
134
+ }
135
+ const sha = lineHash(line);
136
+ const expectedMac = nextMac(key, prev, sha);
137
+ const rid = lineReceiptId(line) ?? rec.receiptId;
138
+ if (rec.sha256 !== sha) {
139
+ issues.push(`seq ${i} (receiptId ${rid}): ledger line hash mismatch — line was edited or corrupted`);
140
+ }
141
+ if (rec.mac !== expectedMac) {
142
+ issues.push(`seq ${i} (receiptId ${rid}): MAC mismatch — broken hash-chain (tampered, reordered, or deleted line)`);
143
+ }
144
+ if (rec.seq !== i) {
145
+ issues.push(`seq ${i} (receiptId ${rid}): sidecar record has out-of-order seq ${rec.seq}`);
146
+ }
147
+ prev = rec.mac;
148
+ }
149
+ return { ok: issues.length === 0, checked: lines.length, issues };
150
+ }
@@ -0,0 +1,149 @@
1
+ import type { PayfetchFs } from "./fs.js";
2
+ import { type MonthIntegrity } from "./integrity.js";
3
+ import type { PaymentQuote } from "../payer/types.js";
4
+ import type { GuardResult } from "../guards/types.js";
5
+ export type GuardBlockReason = "danger" | "degraded" | "timeout" | "unavailable";
6
+ export type Outcome = "free" | "dry_run" | "policy_denied" | "guard_blocked" | "approval_denied" | "approval_queued" | "approval_timeout" | "paid_delivered" | "paid_not_delivered" | "payment_rejected" | "unknown_settlement" | "fetch_error";
7
+ export type Receipt = {
8
+ schema: "p3f.receipt.v1";
9
+ receiptId: string;
10
+ ts: number;
11
+ clientVersion: string;
12
+ policyVersion: "p3f-policy-1.5.0";
13
+ test: boolean;
14
+ url: string;
15
+ method: string;
16
+ host: string;
17
+ outcome: Outcome;
18
+ denyCode: string | null;
19
+ guardBlockReason?: GuardBlockReason | null;
20
+ verdictPath: string[];
21
+ quote: PaymentQuote | null;
22
+ rejectedQuotes: Record<string, number> | null;
23
+ guards: GuardResult[];
24
+ approval: {
25
+ mode: string;
26
+ approvedBy: "elicit" | "queue" | "config" | null;
27
+ } | null;
28
+ payment: {
29
+ payerAddress: string;
30
+ nonce: string;
31
+ validBeforeTs: number;
32
+ settledAmountUsd: number | null;
33
+ txRef: string | null;
34
+ settlementConfirmed: boolean;
35
+ } | null;
36
+ budgets: {
37
+ dayRemainingUsd: number;
38
+ hostRemainingUsd: number;
39
+ totalRemainingUsd: number | null;
40
+ };
41
+ http: {
42
+ status: number | null;
43
+ contentType: string | null;
44
+ bodyBytes: number | null;
45
+ bodySha256: string | null;
46
+ truncated: boolean;
47
+ totalMs: number;
48
+ } | null;
49
+ notes: string[];
50
+ };
51
+ export type AdjustKind = "hold_released_expiry" | "hold_settled_expiry" | "autodeny_set" | "autodeny_cleared" | "manual_note";
52
+ export type Adjust = {
53
+ schema: "p3f.adjust.v1";
54
+ receiptId: string;
55
+ ts: number;
56
+ kind: AdjustKind;
57
+ detail: Record<string, unknown>;
58
+ };
59
+ export type LedgerEntry = Receipt | Adjust;
60
+ export declare function isReceipt(e: LedgerEntry): e is Receipt;
61
+ export declare function isAdjust(e: LedgerEntry): e is Adjust;
62
+ export type StrikeClass = "confirmed" | "soft";
63
+ export type StrikeRecord = {
64
+ ts: number;
65
+ class: StrikeClass;
66
+ };
67
+ export type HostAutoDeny = {
68
+ strikes: StrikeRecord[];
69
+ deniedUntilTs: number | null;
70
+ };
71
+ export type Hold = {
72
+ holdId: string;
73
+ amountUsd: number;
74
+ host: string;
75
+ validBeforeTs: number;
76
+ createdTs: number;
77
+ counterKeys: string[];
78
+ signed?: boolean;
79
+ };
80
+ export type ApprovalGrant = {
81
+ approvalId: string;
82
+ host: string;
83
+ amountUsd: number;
84
+ createdTs: number;
85
+ resource: string | null;
86
+ status: "pending" | "granted";
87
+ };
88
+ export type State = {
89
+ schema: "p3f.state.v1";
90
+ installId: string;
91
+ counters: Record<string, number>;
92
+ holds: Hold[];
93
+ autoDeny: Record<string, HostAutoDeny>;
94
+ approvals: ApprovalGrant[];
95
+ };
96
+ export declare function utcDate(ms: number): string;
97
+ export declare function monthKey(ms: number): string;
98
+ export declare const TOTAL_KEY = "total";
99
+ export declare function dayKey(date: string): string;
100
+ export declare function hostDayKey(host: string, date: string): string;
101
+ export declare function guardDayKey(guardId: string, date: string): string;
102
+ export declare function mainCounterKeys(host: string, ms: number): string[];
103
+ export declare function isSettledOutcome(o: Outcome): boolean;
104
+ export declare function isHoldKeptOutcome(o: Outcome): boolean;
105
+ export declare function strikeClassForOutcome(o: Outcome): StrikeClass | null;
106
+ export type LedgerOptions = {
107
+ isPidAlive?: (pid: number) => boolean;
108
+ random?: () => Uint8Array;
109
+ };
110
+ export declare class LedgerLockedError extends Error {
111
+ constructor(pid: number);
112
+ }
113
+ export declare class LedgerAppendConflictError extends Error {
114
+ constructor(receiptId: string);
115
+ }
116
+ export declare class Ledger {
117
+ #private;
118
+ constructor(fs: PayfetchFs, dataDir: string, now: () => number, opts?: LedgerOptions);
119
+ private ledgerDir;
120
+ private monthFile;
121
+ private statePath;
122
+ private lockPath;
123
+ downloadPath(receiptId: string): string;
124
+ acquireLock(pid?: number): void;
125
+ private parseLock;
126
+ private refreshLock;
127
+ releaseLock(): void;
128
+ append(receipt: Receipt): void;
129
+ appendAdjust(adjust: Adjust): void;
130
+ private recordIntegrity;
131
+ private integrityKey;
132
+ verifyIntegrity(): {
133
+ ok: boolean;
134
+ months: Array<{
135
+ month: string;
136
+ } & MonthIntegrity>;
137
+ };
138
+ private readMonth;
139
+ readEntries(): LedgerEntry[];
140
+ readAllReceipts(): Receipt[];
141
+ loadStateRaw(): State | null;
142
+ saveState(state: State): void;
143
+ rebuildState(installId: string): State;
144
+ }
145
+ export declare function applyStrike(autoDeny: Record<string, HostAutoDeny>, host: string, ts: number, cls: StrikeClass): {
146
+ strikeCount: number;
147
+ engaged: boolean;
148
+ };
149
+ export declare function isHostAutoDenied(autoDeny: Record<string, HostAutoDeny>, host: string, now: number): boolean;