@x402sentinel/test 0.1.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.
- package/README.md +39 -0
- package/dist/display.d.ts +5 -0
- package/dist/display.js +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/tester.d.ts +27 -0
- package/dist/tester.js +329 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# sentinel-test
|
|
2
|
+
|
|
3
|
+
Test any x402 payment endpoint from your terminal. Like Postman for x402.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx sentinel-test https://api.example.com/endpoint
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What It Tests
|
|
12
|
+
|
|
13
|
+
1. **Reachability** — endpoint responds with HTTP 402
|
|
14
|
+
2. **Payment Schema** — valid amount, currency, network, receiver address
|
|
15
|
+
3. **Facilitator** — facilitator URL is reachable (if provided)
|
|
16
|
+
4. **Response Quality** — response time, CORS headers, HTTPS
|
|
17
|
+
5. **Sentinel Integration** — checks for Sentinel audit trail headers
|
|
18
|
+
|
|
19
|
+
## Options
|
|
20
|
+
|
|
21
|
+
| Flag | Description |
|
|
22
|
+
|------|-------------|
|
|
23
|
+
| `--network <name>` | Expected network (base, solana, etc) |
|
|
24
|
+
| `--verbose` | Show full response details |
|
|
25
|
+
| `--json` | Output results as JSON |
|
|
26
|
+
| `--timeout <ms>` | Request timeout (default: 10000) |
|
|
27
|
+
|
|
28
|
+
## Score
|
|
29
|
+
|
|
30
|
+
Each endpoint gets a score from 0-10:
|
|
31
|
+
- Failed critical checks: -3 each
|
|
32
|
+
- Failed warning checks: -1 each
|
|
33
|
+
- Exit code 0 if score >= 5, exit code 1 otherwise
|
|
34
|
+
|
|
35
|
+
## Links
|
|
36
|
+
|
|
37
|
+
- [Sentinel Dashboard](https://sentinel.valeocash.com)
|
|
38
|
+
- [Sentinel CLI](https://npmjs.com/package/create-sentinel)
|
|
39
|
+
- [x402 SDK](https://npmjs.com/package/@x402sentinel/x402)
|
package/dist/display.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export function displayResults(result, options = {}) {
|
|
3
|
+
console.log();
|
|
4
|
+
// Score header
|
|
5
|
+
const scoreColor = result.score >= 8 ? chalk.green : result.score >= 5 ? chalk.yellow : chalk.red;
|
|
6
|
+
console.log(chalk.bold(" URL: ") + chalk.cyan(result.url));
|
|
7
|
+
console.log(chalk.bold(" Score: ") + scoreColor.bold(`${result.score}/10`) +
|
|
8
|
+
chalk.dim(` (${result.totalTime}ms)`));
|
|
9
|
+
console.log();
|
|
10
|
+
// Checks
|
|
11
|
+
const passed = result.checks.filter((c) => c.passed);
|
|
12
|
+
const failed = result.checks.filter((c) => !c.passed);
|
|
13
|
+
if (failed.length > 0) {
|
|
14
|
+
console.log(chalk.bold.red(" ISSUES"));
|
|
15
|
+
console.log();
|
|
16
|
+
for (const check of failed) {
|
|
17
|
+
printCheck(check);
|
|
18
|
+
}
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
21
|
+
if (passed.length > 0) {
|
|
22
|
+
console.log(chalk.bold.green(" PASSED"));
|
|
23
|
+
console.log();
|
|
24
|
+
for (const check of passed) {
|
|
25
|
+
printCheck(check);
|
|
26
|
+
}
|
|
27
|
+
console.log();
|
|
28
|
+
}
|
|
29
|
+
// Payment details
|
|
30
|
+
if (result.paymentDetails) {
|
|
31
|
+
const pd = result.paymentDetails;
|
|
32
|
+
console.log(chalk.bold(" PAYMENT DETAILS"));
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(` Amount: ${chalk.white.bold(pd.amount)} ${chalk.dim(pd.currency)}`);
|
|
35
|
+
console.log(` Network: ${chalk.white(pd.network)}`);
|
|
36
|
+
console.log(` Receiver: ${chalk.dim(pd.receiver)}`);
|
|
37
|
+
if (pd.facilitator) {
|
|
38
|
+
console.log(` Facilitator: ${chalk.dim(pd.facilitator)}`);
|
|
39
|
+
}
|
|
40
|
+
console.log();
|
|
41
|
+
}
|
|
42
|
+
// Summary line
|
|
43
|
+
const critFail = result.checks.filter((c) => !c.passed && c.severity === "critical").length;
|
|
44
|
+
const warnFail = result.checks.filter((c) => !c.passed && c.severity === "warning").length;
|
|
45
|
+
const parts = [
|
|
46
|
+
chalk.green(`${passed.length} passed`),
|
|
47
|
+
];
|
|
48
|
+
if (critFail > 0)
|
|
49
|
+
parts.push(chalk.red(`${critFail} critical`));
|
|
50
|
+
if (warnFail > 0)
|
|
51
|
+
parts.push(chalk.yellow(`${warnFail} warnings`));
|
|
52
|
+
console.log(` ${parts.join(chalk.dim(" · "))}`);
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
function printCheck(check) {
|
|
56
|
+
const icon = check.passed ? chalk.green("✓") : check.severity === "critical" ? chalk.red("✗") : chalk.yellow("!");
|
|
57
|
+
const label = check.passed ? chalk.white(check.name) : severityColor(check)(check.name);
|
|
58
|
+
let line = ` ${icon} ${label}`;
|
|
59
|
+
if (check.value) {
|
|
60
|
+
line += chalk.dim(` → ${check.value}`);
|
|
61
|
+
}
|
|
62
|
+
if (!check.passed && check.expected) {
|
|
63
|
+
line += chalk.dim(` (expected: ${check.expected})`);
|
|
64
|
+
}
|
|
65
|
+
console.log(line);
|
|
66
|
+
}
|
|
67
|
+
function severityColor(check) {
|
|
68
|
+
if (check.severity === "critical")
|
|
69
|
+
return chalk.red;
|
|
70
|
+
if (check.severity === "warning")
|
|
71
|
+
return chalk.yellow;
|
|
72
|
+
return chalk.dim;
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { testEndpoint } from "./tester.js";
|
|
5
|
+
import { displayResults } from "./display.js";
|
|
6
|
+
const banner = `
|
|
7
|
+
${chalk.bold.cyan(" 🛡 Sentinel x402 Endpoint Tester")}
|
|
8
|
+
${chalk.dim(" Test any x402 payment endpoint")}
|
|
9
|
+
`;
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name("sentinel-test")
|
|
13
|
+
.description("Test any x402 payment endpoint")
|
|
14
|
+
.version("0.1.0")
|
|
15
|
+
.argument("<url>", "URL of the x402 endpoint to test")
|
|
16
|
+
.option("--network <network>", "Expected network (base, solana, etc)")
|
|
17
|
+
.option("--verbose", "Show full response details")
|
|
18
|
+
.option("--json", "Output results as JSON")
|
|
19
|
+
.option("--timeout <ms>", "Request timeout in ms", "10000")
|
|
20
|
+
.action(async (url, opts) => {
|
|
21
|
+
if (!opts.json) {
|
|
22
|
+
console.log(banner);
|
|
23
|
+
}
|
|
24
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
25
|
+
url = `https://${url}`;
|
|
26
|
+
}
|
|
27
|
+
const result = await testEndpoint(url, {
|
|
28
|
+
network: opts.network,
|
|
29
|
+
verbose: !!opts.verbose,
|
|
30
|
+
timeout: Number(opts.timeout) || 10000,
|
|
31
|
+
});
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify(result, null, 2));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
displayResults(result, { verbose: !!opts.verbose });
|
|
37
|
+
}
|
|
38
|
+
process.exit(result.score >= 5 ? 0 : 1);
|
|
39
|
+
});
|
|
40
|
+
program.parse();
|
package/dist/tester.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface TestCheck {
|
|
2
|
+
name: string;
|
|
3
|
+
passed: boolean;
|
|
4
|
+
value?: string;
|
|
5
|
+
expected?: string;
|
|
6
|
+
severity: "critical" | "warning" | "info";
|
|
7
|
+
}
|
|
8
|
+
export interface PaymentDetails {
|
|
9
|
+
amount: string;
|
|
10
|
+
currency: string;
|
|
11
|
+
network: string;
|
|
12
|
+
receiver: string;
|
|
13
|
+
facilitator?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface TestResult {
|
|
16
|
+
url: string;
|
|
17
|
+
checks: TestCheck[];
|
|
18
|
+
score: number;
|
|
19
|
+
totalTime: number;
|
|
20
|
+
paymentDetails?: PaymentDetails;
|
|
21
|
+
}
|
|
22
|
+
export interface TestOptions {
|
|
23
|
+
network?: string;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function testEndpoint(url: string, options?: TestOptions): Promise<TestResult>;
|
package/dist/tester.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const RECOGNIZED_CURRENCIES = ["USDC", "USDT", "DAI", "WETH", "ETH", "SOL"];
|
|
2
|
+
const RECOGNIZED_NETWORKS = [
|
|
3
|
+
"base", "base-sepolia",
|
|
4
|
+
"ethereum", "sepolia",
|
|
5
|
+
"polygon", "arbitrum", "optimism",
|
|
6
|
+
"solana", "solana-devnet",
|
|
7
|
+
];
|
|
8
|
+
function isEvmAddress(s) {
|
|
9
|
+
return /^0x[a-fA-F0-9]{40}$/.test(s);
|
|
10
|
+
}
|
|
11
|
+
function isSolanaAddress(s) {
|
|
12
|
+
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s);
|
|
13
|
+
}
|
|
14
|
+
export async function testEndpoint(url, options = {}) {
|
|
15
|
+
const checks = [];
|
|
16
|
+
let paymentDetails;
|
|
17
|
+
const timeout = options.timeout ?? 10000;
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
let response = null;
|
|
20
|
+
let body = null;
|
|
21
|
+
// --- Step 1: Reachability ---
|
|
22
|
+
try {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
25
|
+
response = await fetch(url, {
|
|
26
|
+
method: "GET",
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
redirect: "follow",
|
|
29
|
+
});
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
body = await response.text();
|
|
32
|
+
checks.push({
|
|
33
|
+
name: "Endpoint reachable",
|
|
34
|
+
passed: true,
|
|
35
|
+
value: `HTTP ${response.status}`,
|
|
36
|
+
severity: "critical",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
const isTimeout = message.includes("abort");
|
|
42
|
+
checks.push({
|
|
43
|
+
name: "Endpoint reachable",
|
|
44
|
+
passed: false,
|
|
45
|
+
value: isTimeout ? `Timed out after ${timeout}ms` : message,
|
|
46
|
+
severity: "critical",
|
|
47
|
+
});
|
|
48
|
+
const totalTime = Date.now() - start;
|
|
49
|
+
return { url, checks, score: calcScore(checks), totalTime };
|
|
50
|
+
}
|
|
51
|
+
const totalTime = Date.now() - start;
|
|
52
|
+
// Check for 402
|
|
53
|
+
if (response.status === 402) {
|
|
54
|
+
checks.push({
|
|
55
|
+
name: "Returns HTTP 402",
|
|
56
|
+
passed: true,
|
|
57
|
+
value: "Payment Required",
|
|
58
|
+
severity: "critical",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (response.status === 200) {
|
|
62
|
+
checks.push({
|
|
63
|
+
name: "Returns HTTP 402",
|
|
64
|
+
passed: false,
|
|
65
|
+
value: "Endpoint doesn't require payment (returned 200)",
|
|
66
|
+
expected: "402",
|
|
67
|
+
severity: "critical",
|
|
68
|
+
});
|
|
69
|
+
return { url, checks, score: calcScore(checks), totalTime };
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
checks.push({
|
|
73
|
+
name: "Returns HTTP 402",
|
|
74
|
+
passed: false,
|
|
75
|
+
value: `Endpoint returned ${response.status}`,
|
|
76
|
+
expected: "402",
|
|
77
|
+
severity: "critical",
|
|
78
|
+
});
|
|
79
|
+
return { url, checks, score: calcScore(checks), totalTime };
|
|
80
|
+
}
|
|
81
|
+
// --- Step 2: Parse 402 body ---
|
|
82
|
+
let parsed = null;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(body);
|
|
85
|
+
checks.push({
|
|
86
|
+
name: "Response body is valid JSON",
|
|
87
|
+
passed: true,
|
|
88
|
+
severity: "critical",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
checks.push({
|
|
93
|
+
name: "Response body is valid JSON",
|
|
94
|
+
passed: false,
|
|
95
|
+
value: "Body is not valid JSON",
|
|
96
|
+
severity: "critical",
|
|
97
|
+
});
|
|
98
|
+
return { url, checks, score: calcScore(checks), totalTime };
|
|
99
|
+
}
|
|
100
|
+
const payReq = extractPaymentRequirements(parsed);
|
|
101
|
+
// Amount
|
|
102
|
+
const amount = payReq.amount;
|
|
103
|
+
if (amount && !isNaN(Number(amount)) && Number(amount) > 0) {
|
|
104
|
+
checks.push({
|
|
105
|
+
name: "Valid payment amount",
|
|
106
|
+
passed: true,
|
|
107
|
+
value: amount,
|
|
108
|
+
severity: "critical",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
checks.push({
|
|
113
|
+
name: "Valid payment amount",
|
|
114
|
+
passed: false,
|
|
115
|
+
value: amount ?? "missing",
|
|
116
|
+
expected: "Number > 0",
|
|
117
|
+
severity: "critical",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Currency
|
|
121
|
+
const currency = payReq.currency?.toUpperCase();
|
|
122
|
+
if (currency && RECOGNIZED_CURRENCIES.includes(currency)) {
|
|
123
|
+
checks.push({
|
|
124
|
+
name: "Recognized currency",
|
|
125
|
+
passed: true,
|
|
126
|
+
value: currency,
|
|
127
|
+
severity: "warning",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
checks.push({
|
|
132
|
+
name: "Recognized currency",
|
|
133
|
+
passed: !currency,
|
|
134
|
+
value: currency ?? "not specified",
|
|
135
|
+
expected: RECOGNIZED_CURRENCIES.join(", "),
|
|
136
|
+
severity: "warning",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// Network
|
|
140
|
+
const network = payReq.network?.toLowerCase();
|
|
141
|
+
if (network && RECOGNIZED_NETWORKS.includes(network)) {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: "Recognized network",
|
|
144
|
+
passed: true,
|
|
145
|
+
value: network,
|
|
146
|
+
severity: "warning",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
checks.push({
|
|
151
|
+
name: "Recognized network",
|
|
152
|
+
passed: false,
|
|
153
|
+
value: network ?? "not specified",
|
|
154
|
+
expected: "base, ethereum, solana, ...",
|
|
155
|
+
severity: "warning",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (options.network && network && network !== options.network.toLowerCase()) {
|
|
159
|
+
checks.push({
|
|
160
|
+
name: "Network matches expected",
|
|
161
|
+
passed: false,
|
|
162
|
+
value: network,
|
|
163
|
+
expected: options.network,
|
|
164
|
+
severity: "warning",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Receiver address
|
|
168
|
+
const receiver = payReq.receiver;
|
|
169
|
+
if (receiver) {
|
|
170
|
+
const isSolana = network?.startsWith("solana");
|
|
171
|
+
const valid = isSolana ? isSolanaAddress(receiver) : isEvmAddress(receiver);
|
|
172
|
+
checks.push({
|
|
173
|
+
name: "Valid receiver address",
|
|
174
|
+
passed: valid,
|
|
175
|
+
value: receiver.length > 20 ? `${receiver.slice(0, 10)}...${receiver.slice(-6)}` : receiver,
|
|
176
|
+
expected: isSolana ? "Base58 (32-44 chars)" : "0x... (42 chars)",
|
|
177
|
+
severity: "critical",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
checks.push({
|
|
182
|
+
name: "Valid receiver address",
|
|
183
|
+
passed: false,
|
|
184
|
+
value: "missing",
|
|
185
|
+
severity: "critical",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Facilitator
|
|
189
|
+
const facilitator = payReq.facilitator;
|
|
190
|
+
paymentDetails = {
|
|
191
|
+
amount: amount ?? "unknown",
|
|
192
|
+
currency: currency ?? "unknown",
|
|
193
|
+
network: network ?? "unknown",
|
|
194
|
+
receiver: receiver ?? "unknown",
|
|
195
|
+
facilitator: facilitator ?? undefined,
|
|
196
|
+
};
|
|
197
|
+
// --- Step 3: Schema & facilitator ---
|
|
198
|
+
checks.push({
|
|
199
|
+
name: "Payment schema present",
|
|
200
|
+
passed: !!(amount && receiver),
|
|
201
|
+
value: amount && receiver ? "amount + receiver found" : "incomplete schema",
|
|
202
|
+
severity: "critical",
|
|
203
|
+
});
|
|
204
|
+
if (facilitator) {
|
|
205
|
+
try {
|
|
206
|
+
const fc = new AbortController();
|
|
207
|
+
const ft = setTimeout(() => fc.abort(), 5000);
|
|
208
|
+
const fRes = await fetch(facilitator, { method: "HEAD", signal: fc.signal });
|
|
209
|
+
clearTimeout(ft);
|
|
210
|
+
checks.push({
|
|
211
|
+
name: "Facilitator reachable",
|
|
212
|
+
passed: fRes.ok || fRes.status === 405,
|
|
213
|
+
value: `HTTP ${fRes.status}`,
|
|
214
|
+
severity: "warning",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
checks.push({
|
|
219
|
+
name: "Facilitator reachable",
|
|
220
|
+
passed: false,
|
|
221
|
+
value: "Connection failed",
|
|
222
|
+
severity: "warning",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// --- Step 4: Response quality ---
|
|
227
|
+
if (totalTime < 500) {
|
|
228
|
+
checks.push({
|
|
229
|
+
name: "Response time",
|
|
230
|
+
passed: true,
|
|
231
|
+
value: `${totalTime}ms (fast)`,
|
|
232
|
+
severity: "info",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else if (totalTime < 2000) {
|
|
236
|
+
checks.push({
|
|
237
|
+
name: "Response time",
|
|
238
|
+
passed: true,
|
|
239
|
+
value: `${totalTime}ms`,
|
|
240
|
+
severity: "info",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
checks.push({
|
|
245
|
+
name: "Response time",
|
|
246
|
+
passed: false,
|
|
247
|
+
value: `${totalTime}ms (slow)`,
|
|
248
|
+
expected: "< 2000ms",
|
|
249
|
+
severity: "warning",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const corsHeader = response.headers.get("access-control-allow-origin");
|
|
253
|
+
checks.push({
|
|
254
|
+
name: "CORS headers present",
|
|
255
|
+
passed: !!corsHeader,
|
|
256
|
+
value: corsHeader ?? "missing",
|
|
257
|
+
severity: "warning",
|
|
258
|
+
});
|
|
259
|
+
const isHttps = url.startsWith("https://");
|
|
260
|
+
checks.push({
|
|
261
|
+
name: "Uses HTTPS",
|
|
262
|
+
passed: isHttps,
|
|
263
|
+
value: isHttps ? "yes" : "no — insecure",
|
|
264
|
+
severity: "critical",
|
|
265
|
+
});
|
|
266
|
+
// --- Step 5: Sentinel integration ---
|
|
267
|
+
const sentinelHeader = response.headers.get("x-sentinel-receipt") ||
|
|
268
|
+
response.headers.get("x-sentinel-agent");
|
|
269
|
+
checks.push({
|
|
270
|
+
name: "Sentinel integration",
|
|
271
|
+
passed: !!sentinelHeader,
|
|
272
|
+
value: sentinelHeader
|
|
273
|
+
? "This endpoint uses Sentinel for audit tracking"
|
|
274
|
+
: "Tip: Add Sentinel for payment audit trails → npmjs.com/package/@x402sentinel/x402",
|
|
275
|
+
severity: "info",
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
url,
|
|
279
|
+
checks,
|
|
280
|
+
score: calcScore(checks),
|
|
281
|
+
totalTime,
|
|
282
|
+
paymentDetails,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function calcScore(checks) {
|
|
286
|
+
let score = 10;
|
|
287
|
+
for (const c of checks) {
|
|
288
|
+
if (c.passed)
|
|
289
|
+
continue;
|
|
290
|
+
if (c.severity === "critical")
|
|
291
|
+
score -= 3;
|
|
292
|
+
if (c.severity === "warning")
|
|
293
|
+
score -= 1;
|
|
294
|
+
}
|
|
295
|
+
return Math.max(0, score);
|
|
296
|
+
}
|
|
297
|
+
function extractPaymentRequirements(body) {
|
|
298
|
+
// x402 can nest payment info at top level or under "payment" / "paymentRequirements"
|
|
299
|
+
const sources = [
|
|
300
|
+
body,
|
|
301
|
+
body.payment,
|
|
302
|
+
body.paymentRequirements,
|
|
303
|
+
// Handle array format: paymentRequirements: [{ ... }]
|
|
304
|
+
...(Array.isArray(body.paymentRequirements) ? body.paymentRequirements : []),
|
|
305
|
+
].filter(Boolean);
|
|
306
|
+
let amount;
|
|
307
|
+
let currency;
|
|
308
|
+
let network;
|
|
309
|
+
let receiver;
|
|
310
|
+
let facilitator;
|
|
311
|
+
for (const src of sources) {
|
|
312
|
+
if (!amount)
|
|
313
|
+
amount = str(src.amount ?? src.maxAmountRequired);
|
|
314
|
+
if (!currency)
|
|
315
|
+
currency = str(src.currency ?? src.asset ?? src.token);
|
|
316
|
+
if (!network)
|
|
317
|
+
network = str(src.network ?? src.chain ?? src.chainId);
|
|
318
|
+
if (!receiver)
|
|
319
|
+
receiver = str(src.receiver ?? src.payTo ?? src.address ?? src.recipient);
|
|
320
|
+
if (!facilitator)
|
|
321
|
+
facilitator = str(src.facilitator ?? src.facilitatorUrl ?? src.facilitatorAddress);
|
|
322
|
+
}
|
|
323
|
+
return { amount, currency, network, receiver, facilitator };
|
|
324
|
+
}
|
|
325
|
+
function str(v) {
|
|
326
|
+
if (v === null || v === undefined)
|
|
327
|
+
return undefined;
|
|
328
|
+
return String(v);
|
|
329
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x402sentinel/test",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Test any x402 payment endpoint",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sentinel-test": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"commander": "^12.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"tsx": "^4.0.0",
|
|
21
|
+
"typescript": "^5.4.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["x402", "sentinel", "test", "payments", "402"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/valeo-cash/Sentinel.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://sentinel.valeocash.com"
|
|
30
|
+
}
|