@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 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)
@@ -0,0 +1,5 @@
1
+ import type { TestResult } from "./tester.js";
2
+ export interface DisplayOptions {
3
+ verbose?: boolean;
4
+ }
5
+ export declare function displayResults(result: TestResult, options?: DisplayOptions): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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();
@@ -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
+ }