@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,364 @@
1
+ import { createHash } from "node:crypto";
2
+ import { FETCH_TIMEOUT_MS, INTEGRATION_HEADER, MAX_REDIRECTS, RESPONSE_INLINE_MAX_BYTES, RESPONSE_MAX_BYTES, X_PAYMENT_HEADER, X_PAYMENT_RESPONSE_HEADER, } from "./constants.js";
3
+ export function parseIpv4(s) {
4
+ const m = s.split(".");
5
+ if (m.length !== 4)
6
+ return null;
7
+ const out = [];
8
+ for (const part of m) {
9
+ if (!/^\d{1,3}$/.test(part))
10
+ return null;
11
+ const n = Number(part);
12
+ if (n > 255)
13
+ return null;
14
+ out.push(n);
15
+ }
16
+ return out;
17
+ }
18
+ export function parseIpv6(input) {
19
+ let s = input;
20
+ if (s.startsWith("[") && s.endsWith("]"))
21
+ s = s.slice(1, -1);
22
+ const pct = s.indexOf("%");
23
+ if (pct >= 0)
24
+ s = s.slice(0, pct);
25
+ if (!s.includes(":"))
26
+ return null;
27
+ let tailHextets = [];
28
+ const lastColon = s.lastIndexOf(":");
29
+ const tail = s.slice(lastColon + 1);
30
+ if (tail.includes(".")) {
31
+ const v4 = parseIpv4(tail);
32
+ if (!v4)
33
+ return null;
34
+ tailHextets = [(v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]];
35
+ s = s.slice(0, lastColon + 1);
36
+ }
37
+ const halves = s.split("::");
38
+ if (halves.length > 2)
39
+ return null;
40
+ const toHextets = (part) => {
41
+ if (part === "")
42
+ return [];
43
+ const groups = part.split(":").filter((g) => g.length > 0);
44
+ const out = [];
45
+ for (const g of groups) {
46
+ if (!/^[0-9a-fA-F]{1,4}$/.test(g))
47
+ return null;
48
+ out.push(parseInt(g, 16));
49
+ }
50
+ return out;
51
+ };
52
+ if (halves.length === 2) {
53
+ const head = toHextets(halves[0]);
54
+ const rest = toHextets(halves[1]);
55
+ if (head === null || rest === null)
56
+ return null;
57
+ const full = [...head, ...rest, ...tailHextets];
58
+ if (full.length > 8)
59
+ return null;
60
+ const zeros = new Array(8 - full.length).fill(0);
61
+ return [...head, ...zeros, ...rest, ...tailHextets];
62
+ }
63
+ const head = toHextets(halves[0]);
64
+ if (head === null)
65
+ return null;
66
+ const full = [...head, ...tailHextets];
67
+ return full.length === 8 ? full : null;
68
+ }
69
+ function isBlockedIpv4(o) {
70
+ const [a, b] = o;
71
+ if (a === 0)
72
+ return true;
73
+ if (a === 10)
74
+ return true;
75
+ if (a === 127)
76
+ return true;
77
+ if (a === 169 && b === 254)
78
+ return true;
79
+ if (a === 172 && b >= 16 && b <= 31)
80
+ return true;
81
+ if (a === 192 && b === 168)
82
+ return true;
83
+ if (a === 100 && b >= 64 && b <= 127)
84
+ return true;
85
+ return false;
86
+ }
87
+ function isBlockedIpv6(h) {
88
+ if (h.every((x) => x === 0))
89
+ return true;
90
+ if (h.slice(0, 7).every((x) => x === 0) && h[7] === 1)
91
+ return true;
92
+ if ((h[0] & 0xffc0) === 0xfe80)
93
+ return true;
94
+ if ((h[0] & 0xfe00) === 0xfc00)
95
+ return true;
96
+ if (h.slice(0, 5).every((x) => x === 0) && h[5] === 0xffff) {
97
+ const v4 = [(h[6] >> 8) & 0xff, h[6] & 0xff, (h[7] >> 8) & 0xff, h[7] & 0xff];
98
+ return isBlockedIpv4(v4);
99
+ }
100
+ return false;
101
+ }
102
+ export function isBlockedIp(ip) {
103
+ const v4 = parseIpv4(ip);
104
+ if (v4)
105
+ return isBlockedIpv4(v4);
106
+ const v6 = parseIpv6(ip);
107
+ if (v6)
108
+ return isBlockedIpv6(v6);
109
+ return true;
110
+ }
111
+ export function evaluateTarget(url, resolvedIps, policy) {
112
+ let u;
113
+ try {
114
+ u = new URL(url);
115
+ }
116
+ catch {
117
+ return { ok: false, reason: "invalid_url" };
118
+ }
119
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
120
+ return { ok: false, reason: "scheme" };
121
+ }
122
+ if (policy.allowPrivateTargets)
123
+ return { ok: true, host: u.hostname };
124
+ if (resolvedIps.length === 0)
125
+ return { ok: false, reason: "unresolved" };
126
+ for (const ip of resolvedIps) {
127
+ if (isBlockedIp(ip))
128
+ return { ok: false, reason: "private_target" };
129
+ }
130
+ return { ok: true, host: u.hostname };
131
+ }
132
+ export function createPinnedLookup(resolve, policy) {
133
+ return (hostname, options, callback) => {
134
+ const fail = (err) => {
135
+ if (options?.all)
136
+ callback(err, []);
137
+ else
138
+ callback(err, "", 0);
139
+ };
140
+ const deliver = (ips) => {
141
+ if (options?.all) {
142
+ callback(null, ips.map((ip) => ({ address: ip, family: parseIpv4(ip) ? 4 : 6 })));
143
+ return;
144
+ }
145
+ const ip = ips[0];
146
+ callback(null, ip, parseIpv4(ip) ? 4 : 6);
147
+ };
148
+ resolve(hostname)
149
+ .then((ips) => {
150
+ if (ips.length === 0) {
151
+ fail(new Error(`payfetch: DNS resolution failed for ${hostname}`));
152
+ return;
153
+ }
154
+ if (policy.allowPrivateTargets) {
155
+ deliver(ips);
156
+ return;
157
+ }
158
+ if (ips.some((ip) => isBlockedIp(ip))) {
159
+ fail(new Error(`payfetch: refusing to dial ${hostname} — resolves to a blocked address`));
160
+ return;
161
+ }
162
+ deliver(ips);
163
+ })
164
+ .catch((err) => fail(err));
165
+ };
166
+ }
167
+ export function adaptFetch(fetchFn) {
168
+ return async (url, init) => {
169
+ const resp = await fetchFn(url, {
170
+ method: init.method,
171
+ headers: init.headers,
172
+ body: init.body ?? undefined,
173
+ redirect: "manual",
174
+ signal: init.signal,
175
+ });
176
+ return { status: resp.status, headers: resp.headers, body: resp.body };
177
+ };
178
+ }
179
+ export async function readCapped(body, maxBytes) {
180
+ if (body === null)
181
+ return { bytes: new Uint8Array(0), hardCapped: false };
182
+ const reader = body.getReader();
183
+ const chunks = [];
184
+ let total = 0;
185
+ let hardCapped = false;
186
+ for (;;) {
187
+ const { done, value } = await reader.read();
188
+ if (done)
189
+ break;
190
+ if (!value)
191
+ continue;
192
+ if (total + value.length > maxBytes) {
193
+ chunks.push(value.subarray(0, maxBytes - total));
194
+ total = maxBytes;
195
+ hardCapped = true;
196
+ await reader.cancel().catch(() => { });
197
+ break;
198
+ }
199
+ chunks.push(value);
200
+ total += value.length;
201
+ }
202
+ const bytes = new Uint8Array(total);
203
+ let off = 0;
204
+ for (const c of chunks) {
205
+ bytes.set(c, off);
206
+ off += c.length;
207
+ }
208
+ return { bytes, hardCapped };
209
+ }
210
+ function sha256(bytes) {
211
+ return createHash("sha256").update(bytes).digest("hex");
212
+ }
213
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
214
+ const SENSITIVE_REDIRECT_HEADERS = new Set([
215
+ "authorization",
216
+ "cookie",
217
+ "proxy-authorization",
218
+ X_PAYMENT_HEADER.toLowerCase(),
219
+ X_PAYMENT_RESPONSE_HEADER.toLowerCase(),
220
+ INTEGRATION_HEADER.toLowerCase(),
221
+ ]);
222
+ function stripSensitiveHeaders(headers) {
223
+ const out = {};
224
+ for (const [k, v] of Object.entries(headers)) {
225
+ if (!SENSITIVE_REDIRECT_HEADERS.has(k.toLowerCase()))
226
+ out[k] = v;
227
+ }
228
+ return out;
229
+ }
230
+ function defaultTimer(fn, ms) {
231
+ const t = setTimeout(fn, ms);
232
+ return { clear: () => clearTimeout(t) };
233
+ }
234
+ export async function transportFetch(url, init, policy, io, opts = {}) {
235
+ const startMs = io.now();
236
+ const timer = io.setTimer ?? defaultTimer;
237
+ const followRedirects = opts.followRedirects ?? true;
238
+ const hopChain = [];
239
+ const notes = [];
240
+ let current = url;
241
+ let method = init.method;
242
+ let body = init.body;
243
+ let headers = init.headers;
244
+ let redirectCount = 0;
245
+ const base = (finalUrl) => ({
246
+ ok: false,
247
+ error: null,
248
+ finalUrl,
249
+ finalHost: safeHost(finalUrl),
250
+ status: null,
251
+ headers: null,
252
+ contentType: null,
253
+ rawBody: null,
254
+ bodyBytes: null,
255
+ bodySha256: null,
256
+ hardCapped: false,
257
+ hopChain,
258
+ redirectCount,
259
+ notes,
260
+ totalMs: io.now() - startMs,
261
+ });
262
+ for (;;) {
263
+ let host;
264
+ let ips;
265
+ try {
266
+ host = new URL(current).hostname;
267
+ }
268
+ catch {
269
+ return { ...base(current), error: "fetch_error" };
270
+ }
271
+ try {
272
+ ips = await io.resolve(host);
273
+ }
274
+ catch {
275
+ ips = [];
276
+ }
277
+ const verdict = evaluateTarget(current, ips, policy);
278
+ if (!verdict.ok) {
279
+ if (verdict.reason === "scheme" || verdict.reason === "private_target") {
280
+ notes.push("private_target_blocked");
281
+ return { ...base(current), error: "private_target_blocked" };
282
+ }
283
+ return { ...base(current), error: "fetch_error" };
284
+ }
285
+ hopChain.push(current);
286
+ const controller = new AbortController();
287
+ const t = timer(() => controller.abort(), FETCH_TIMEOUT_MS);
288
+ let resp;
289
+ try {
290
+ resp = await io.request(current, { method, headers, body, signal: controller.signal });
291
+ }
292
+ catch (err) {
293
+ io.log("transport.fetch_error", { host, message: err.message });
294
+ return { ...base(current), error: "fetch_error" };
295
+ }
296
+ finally {
297
+ t.clear();
298
+ }
299
+ if (followRedirects && REDIRECT_STATUSES.has(resp.status) && redirectCount < MAX_REDIRECTS) {
300
+ const location = resp.headers.get("location");
301
+ if (location) {
302
+ await resp.body?.cancel().catch(() => { });
303
+ let next;
304
+ try {
305
+ next = new URL(location, current);
306
+ }
307
+ catch {
308
+ return { ...base(current), error: "fetch_error" };
309
+ }
310
+ if (new URL(current).protocol === "https:" && next.protocol === "http:") {
311
+ notes.push("insecure_redirect");
312
+ return { ...base(current), error: "insecure_redirect" };
313
+ }
314
+ redirectCount += 1;
315
+ if (new URL(current).origin !== next.origin) {
316
+ headers = stripSensitiveHeaders(headers);
317
+ }
318
+ if (resp.status === 303 || (method !== "GET" && method !== "HEAD" && resp.status !== 307 && resp.status !== 308)) {
319
+ method = "GET";
320
+ body = null;
321
+ }
322
+ current = next.toString();
323
+ continue;
324
+ }
325
+ }
326
+ const { bytes, hardCapped } = await readCapped(resp.body, RESPONSE_MAX_BYTES);
327
+ if (redirectCount > 0)
328
+ notes.push(`redirected:${redirectCount}`);
329
+ return {
330
+ ok: true,
331
+ error: null,
332
+ finalUrl: current,
333
+ finalHost: safeHost(current),
334
+ status: resp.status,
335
+ headers: resp.headers,
336
+ contentType: resp.headers.get("content-type"),
337
+ rawBody: bytes,
338
+ bodyBytes: bytes.length,
339
+ bodySha256: sha256(bytes),
340
+ hardCapped,
341
+ hopChain,
342
+ redirectCount,
343
+ notes,
344
+ totalMs: io.now() - startMs,
345
+ };
346
+ }
347
+ }
348
+ function safeHost(url) {
349
+ try {
350
+ return new URL(url).hostname;
351
+ }
352
+ catch {
353
+ return "";
354
+ }
355
+ }
356
+ export function deliverBody(bytes, responseMode, ctx) {
357
+ if (responseMode === "file") {
358
+ ctx.fs.writeBytes(ctx.downloadPath, bytes);
359
+ return { mode: "file", path: ctx.downloadPath, truncated: false };
360
+ }
361
+ const truncated = bytes.length > RESPONSE_INLINE_MAX_BYTES;
362
+ const slice = truncated ? bytes.subarray(0, RESPONSE_INLINE_MAX_BYTES) : bytes;
363
+ return { mode: "inline", text: new TextDecoder("utf-8", { fatal: false }).decode(slice), truncated };
364
+ }
@@ -0,0 +1,4 @@
1
+ export { createTrustGuard } from "./trust.js";
2
+ export { createSafetyGuard } from "./safety.js";
3
+ export type { PrePayGuard, GuardId, GuardInput, GuardResult, TrustGuardConfig, SafetyGuardConfig, GuardRuntime, } from "./types.js";
4
+ export { DEFAULT_TRUST_GUARD_CONFIG, DEFAULT_SAFETY_GUARD_CONFIG } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { createTrustGuard } from "./trust.js";
2
+ export { createSafetyGuard } from "./safety.js";
3
+ export { DEFAULT_TRUST_GUARD_CONFIG, DEFAULT_SAFETY_GUARD_CONFIG } from "./types.js";
@@ -0,0 +1,17 @@
1
+ import type { NoteCode } from "../core/notes.js";
2
+ import type { GuardId, GuardResult, GuardRuntime } from "./types.js";
3
+ export declare const GUARD_UNRATED_NOTE: NoteCode;
4
+ export declare function integrationHeaderValue(installId8: string, via: string | null): string;
5
+ export declare function guardHeaders(rt: GuardRuntime): Record<string, string>;
6
+ export declare function stripTarget(url: string): string;
7
+ export declare function trustScoreUrl(base: string, target: string): string;
8
+ export declare function safetyScreenUrl(base: string, mint: string, chain: string, deep: boolean): string;
9
+ export declare function guardFetchWithTimeout(rt: GuardRuntime, url: string, init: {
10
+ method: string;
11
+ headers: Record<string, string>;
12
+ body?: string;
13
+ }, budgetMs: number, dryRun?: boolean): Promise<Response>;
14
+ export declare function unwrapEnvelope(body: unknown): Record<string, unknown> | null;
15
+ export declare function blockOrWarn(mode: "advisory" | "enforce"): "block" | "warn";
16
+ export declare function guardResult(id: GuardId, verdict: GuardResult["verdict"], detail: Record<string, unknown>, startMs: number, rt: GuardRuntime): GuardResult;
17
+ export declare function unavailable(id: GuardId, reason: string, startMs: number, rt: GuardRuntime): GuardResult;
@@ -0,0 +1,74 @@
1
+ import { GUARD_SEND_QUERY, INTEGRATION_HEADER, } from "../core/constants.js";
2
+ export const GUARD_UNRATED_NOTE = "guard_unrated";
3
+ export function integrationHeaderValue(installId8, via) {
4
+ const value = `payfetch/1;i=${installId8}`;
5
+ return via != null && via !== "" ? `${value};via=${via}` : value;
6
+ }
7
+ export function guardHeaders(rt) {
8
+ return {
9
+ [INTEGRATION_HEADER]: integrationHeaderValue(rt.installId8, rt.via),
10
+ Accept: "application/json",
11
+ };
12
+ }
13
+ export function stripTarget(url) {
14
+ try {
15
+ const u = new URL(url);
16
+ u.hash = "";
17
+ if (!GUARD_SEND_QUERY)
18
+ u.search = "";
19
+ return u.toString();
20
+ }
21
+ catch {
22
+ return url;
23
+ }
24
+ }
25
+ function trimSlash(base) {
26
+ return base.replace(/\/+$/, "");
27
+ }
28
+ export function trustScoreUrl(base, target) {
29
+ const u = new URL(`${trimSlash(base)}/v1/trust/score`);
30
+ u.searchParams.set("url", target);
31
+ return u.toString();
32
+ }
33
+ export function safetyScreenUrl(base, mint, chain, deep) {
34
+ const path = deep ? "/v1/safety/screen/deep" : "/v1/safety/screen";
35
+ const u = new URL(`${trimSlash(base)}${path}`);
36
+ u.searchParams.set("mint", mint);
37
+ u.searchParams.set("chain", chain);
38
+ return u.toString();
39
+ }
40
+ export async function guardFetchWithTimeout(rt, url, init, budgetMs, dryRun) {
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), budgetMs);
43
+ try {
44
+ return await rt.guardFetch(url, { ...init, signal: controller.signal }, { dryRun });
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ export function unwrapEnvelope(body) {
51
+ if (typeof body !== "object" || body === null)
52
+ return null;
53
+ const b = body;
54
+ const data = b.data;
55
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) {
56
+ return data;
57
+ }
58
+ return b;
59
+ }
60
+ export function blockOrWarn(mode) {
61
+ return mode === "enforce" ? "block" : "warn";
62
+ }
63
+ export function guardResult(id, verdict, detail, startMs, rt) {
64
+ return {
65
+ id,
66
+ verdict,
67
+ detail,
68
+ latencyMs: Math.max(0, rt.now() - startMs),
69
+ costUsd: 0,
70
+ };
71
+ }
72
+ export function unavailable(id, reason, startMs, rt) {
73
+ return guardResult(id, "unavailable", { reason }, startMs, rt);
74
+ }
@@ -0,0 +1,2 @@
1
+ import type { PrePayGuard, GuardRuntime, SafetyGuardConfig } from "./types.js";
2
+ export declare function createSafetyGuard(cfg: SafetyGuardConfig, rt: GuardRuntime): PrePayGuard;
@@ -0,0 +1,94 @@
1
+ import { guardBudgetMs } from "../core/constants.js";
2
+ import { GUARD_UNRATED_NOTE, blockOrWarn, guardFetchWithTimeout, guardHeaders, guardResult, safetyScreenUrl, unavailable, unwrapEnvelope, } from "./internal.js";
3
+ const DANGER_INCONCLUSIVE_VERDICTS = new Set(["unknown", "caution"]);
4
+ function parseScreen(body) {
5
+ const b = unwrapEnvelope(body);
6
+ if (b === null)
7
+ return null;
8
+ if (typeof b.verdict !== "string")
9
+ return null;
10
+ const score = typeof b.score === "number" ? b.score : null;
11
+ const degraded = b.degraded === true;
12
+ let deployer = null;
13
+ let deployerVerdict = null;
14
+ if (typeof b.deployer === "object" && b.deployer !== null) {
15
+ deployer = b.deployer;
16
+ const dv = deployer.verdict;
17
+ deployerVerdict = typeof dv === "string" ? dv : null;
18
+ }
19
+ return { verdict: b.verdict, score, deployer, deployerVerdict, degraded };
20
+ }
21
+ function mapSafety(cfg, parsed, startMs, rt) {
22
+ const { verdict, score, deployer, deployerVerdict, degraded } = parsed;
23
+ const detail = { verdict, score, deployer, degraded };
24
+ if (cfg.blockVerdicts.includes(verdict)) {
25
+ return guardResult("safety", blockOrWarn(cfg.mode), detail, startMs, rt);
26
+ }
27
+ if (cfg.depth === "deep" &&
28
+ deployerVerdict !== null &&
29
+ cfg.blockDeployerVerdicts.includes(deployerVerdict)) {
30
+ return guardResult("safety", blockOrWarn(cfg.mode), detail, startMs, rt);
31
+ }
32
+ if (cfg.mode === "enforce" && degraded && DANGER_INCONCLUSIVE_VERDICTS.has(verdict)) {
33
+ return unavailable("safety", "degraded_screen", startMs, rt);
34
+ }
35
+ const inconclusive = verdict === "unknown" ||
36
+ (cfg.depth === "deep" && deployerVerdict === "insufficient_history");
37
+ if (inconclusive) {
38
+ detail.notes = [GUARD_UNRATED_NOTE];
39
+ }
40
+ return guardResult("safety", "pass", detail, startMs, rt);
41
+ }
42
+ export function createSafetyGuard(cfg, rt) {
43
+ return {
44
+ id: "safety",
45
+ applies(req) {
46
+ const token = req.context.tokenAddress;
47
+ return typeof token === "string" && token.length > 0;
48
+ },
49
+ async check(req) {
50
+ const start = rt.now();
51
+ const activeCfg = req.config ?? cfg;
52
+ try {
53
+ const base = rt.baseUrls.safety;
54
+ if (base === null) {
55
+ return unavailable("safety", "base_url_unset", start, rt);
56
+ }
57
+ const token = req.context.tokenAddress;
58
+ if (token == null || token === "") {
59
+ return unavailable("safety", "no_token", start, rt);
60
+ }
61
+ const chain = req.context.chain ?? "solana";
62
+ const url = safetyScreenUrl(base, token, chain, activeCfg.depth === "deep");
63
+ let res;
64
+ try {
65
+ res = await guardFetchWithTimeout(rt, url, {
66
+ method: "GET",
67
+ headers: guardHeaders(rt),
68
+ }, guardBudgetMs(activeCfg.mode), req.dryRun);
69
+ }
70
+ catch {
71
+ return unavailable("safety", "fetch_failed", start, rt);
72
+ }
73
+ if (!res.ok) {
74
+ return unavailable("safety", `http_${res.status}`, start, rt);
75
+ }
76
+ let body;
77
+ try {
78
+ body = await res.json();
79
+ }
80
+ catch {
81
+ return unavailable("safety", "malformed_json", start, rt);
82
+ }
83
+ const parsed = parseScreen(body);
84
+ if (parsed === null) {
85
+ return unavailable("safety", "malformed_body", start, rt);
86
+ }
87
+ return mapSafety(activeCfg, parsed, start, rt);
88
+ }
89
+ catch {
90
+ return unavailable("safety", "crash", start, rt);
91
+ }
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,2 @@
1
+ import type { PrePayGuard, GuardRuntime, TrustGuardConfig } from "./types.js";
2
+ export declare function createTrustGuard(cfg: TrustGuardConfig, rt: GuardRuntime): PrePayGuard;
@@ -0,0 +1,79 @@
1
+ import { guardBudgetMs } from "../core/constants.js";
2
+ import { GUARD_UNRATED_NOTE, blockOrWarn, guardFetchWithTimeout, guardHeaders, guardResult, stripTarget, trustScoreUrl, unavailable, unwrapEnvelope, } from "./internal.js";
3
+ function parseTrustScore(body) {
4
+ const b = unwrapEnvelope(body);
5
+ if (b === null)
6
+ return null;
7
+ if (typeof b.verdict !== "string")
8
+ return null;
9
+ const score = b.score;
10
+ if (typeof score !== "number" && score !== null)
11
+ return null;
12
+ return { score, verdict: b.verdict, counts: b.counts ?? null };
13
+ }
14
+ function mapTrust(cfg, parsed, startMs, rt) {
15
+ const { score, verdict, counts } = parsed;
16
+ const detail = { score, verdict, counts };
17
+ if (cfg.blockVerdicts.includes(verdict)) {
18
+ return guardResult("trust", blockOrWarn(cfg.mode), detail, startMs, rt);
19
+ }
20
+ if (score !== null && cfg.minScore !== null && score < cfg.minScore) {
21
+ return guardResult("trust", blockOrWarn(cfg.mode), detail, startMs, rt);
22
+ }
23
+ if (verdict === "unrated") {
24
+ if (cfg.blockUnrated) {
25
+ return guardResult("trust", blockOrWarn(cfg.mode), detail, startMs, rt);
26
+ }
27
+ detail.notes = [GUARD_UNRATED_NOTE];
28
+ return guardResult("trust", "pass", detail, startMs, rt);
29
+ }
30
+ return guardResult("trust", "pass", detail, startMs, rt);
31
+ }
32
+ export function createTrustGuard(cfg, rt) {
33
+ return {
34
+ id: "trust",
35
+ applies() {
36
+ return true;
37
+ },
38
+ async check(req) {
39
+ const start = rt.now();
40
+ const activeCfg = req.config ?? cfg;
41
+ try {
42
+ const base = rt.baseUrls.trust;
43
+ if (base === null) {
44
+ return unavailable("trust", "base_url_unset", start, rt);
45
+ }
46
+ const target = stripTarget(req.url);
47
+ const url = trustScoreUrl(base, target);
48
+ let res;
49
+ try {
50
+ res = await guardFetchWithTimeout(rt, url, {
51
+ method: "GET",
52
+ headers: guardHeaders(rt),
53
+ }, guardBudgetMs(activeCfg.mode), req.dryRun);
54
+ }
55
+ catch {
56
+ return unavailable("trust", "fetch_failed", start, rt);
57
+ }
58
+ if (!res.ok) {
59
+ return unavailable("trust", `http_${res.status}`, start, rt);
60
+ }
61
+ let body;
62
+ try {
63
+ body = await res.json();
64
+ }
65
+ catch {
66
+ return unavailable("trust", "malformed_json", start, rt);
67
+ }
68
+ const parsed = parseTrustScore(body);
69
+ if (parsed === null) {
70
+ return unavailable("trust", "malformed_body", start, rt);
71
+ }
72
+ return mapTrust(activeCfg, parsed, start, rt);
73
+ }
74
+ catch {
75
+ return unavailable("trust", "crash", start, rt);
76
+ }
77
+ },
78
+ };
79
+ }