@tanakayuto/intmax402-cli 0.2.0 → 0.3.1
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/dist/index.js +121 -70
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -1,106 +1,157 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
const
|
|
7
|
+
const minimist_1 = __importDefault(require("minimist"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ethers_1 = require("ethers");
|
|
5
10
|
const intmax402_core_1 = require("@tanakayuto/intmax402-core");
|
|
6
|
-
const
|
|
7
|
-
|
|
11
|
+
const argv = (0, minimist_1.default)(process.argv.slice(2), {
|
|
12
|
+
string: ["mode"],
|
|
13
|
+
boolean: ["help"],
|
|
14
|
+
alias: { h: "help" },
|
|
15
|
+
default: { mode: "identity" },
|
|
16
|
+
});
|
|
17
|
+
const command = argv._[0];
|
|
8
18
|
async function main() {
|
|
19
|
+
if (argv.help || !command) {
|
|
20
|
+
printHelp();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
9
23
|
switch (command) {
|
|
10
24
|
case "test":
|
|
11
|
-
await testCommand(
|
|
25
|
+
await testCommand(argv._[1], argv.mode);
|
|
12
26
|
break;
|
|
13
27
|
case "keygen":
|
|
14
28
|
keygenCommand();
|
|
15
29
|
break;
|
|
16
|
-
case "verify":
|
|
17
|
-
verifyCommand(args[0]);
|
|
18
|
-
break;
|
|
19
30
|
default:
|
|
20
|
-
|
|
31
|
+
console.error(chalk_1.default.red(`Unknown command: ${command}`));
|
|
32
|
+
printHelp();
|
|
33
|
+
process.exit(1);
|
|
21
34
|
}
|
|
22
35
|
}
|
|
23
|
-
async function testCommand(url) {
|
|
36
|
+
async function testCommand(url, mode = "identity") {
|
|
24
37
|
if (!url) {
|
|
25
|
-
console.error("Usage: intmax402 test <url>");
|
|
38
|
+
console.error(chalk_1.default.red("Usage: intmax402 test <url> [--mode identity|payment]"));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// Validate URL: only http:// and https:// are allowed (SSRF prevention)
|
|
42
|
+
try {
|
|
43
|
+
const parsed = new URL(url);
|
|
44
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
45
|
+
console.error(chalk_1.default.red("Error: Only http:// and https:// URLs are supported"));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
console.error(chalk_1.default.red(`Error: Invalid URL: ${url}`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
console.log(`Testing: ${chalk_1.default.cyan(url)}\n`);
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
// Generate a random wallet for testing
|
|
56
|
+
const wallet = ethers_1.ethers.Wallet.createRandom();
|
|
57
|
+
const privateKey = wallet.privateKey;
|
|
58
|
+
const address = wallet.address;
|
|
59
|
+
// Step 1: Initial GET → expect 401
|
|
60
|
+
process.stdout.write(` ${chalk_1.default.yellow("①")} GET ${new URL(url).pathname} → `);
|
|
61
|
+
let res1;
|
|
62
|
+
try {
|
|
63
|
+
res1 = await fetch(url);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.log(chalk_1.default.red(`error: ${err.message}`));
|
|
26
67
|
process.exit(1);
|
|
27
68
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.log(` Status: ${response.status}`);
|
|
33
|
-
if (response.status !== 401 && response.status !== 402) {
|
|
34
|
-
console.log(" No 402 challenge received. Endpoint may not be protected.");
|
|
69
|
+
const statusColor = res1.status === 401 || res1.status === 402 ? chalk_1.default.yellow : chalk_1.default.green;
|
|
70
|
+
console.log(statusColor(`${res1.status}`));
|
|
71
|
+
if (res1.status !== 401 && res1.status !== 402) {
|
|
72
|
+
console.log(chalk_1.default.yellow(" (No 402/401 challenge received. Endpoint may not be protected.)"));
|
|
35
73
|
return;
|
|
36
74
|
}
|
|
37
|
-
|
|
75
|
+
// Step 2: Parse nonce
|
|
76
|
+
const wwwAuth = res1.headers.get("www-authenticate");
|
|
38
77
|
if (!wwwAuth) {
|
|
39
|
-
console.error(" No WWW-Authenticate header found.");
|
|
40
|
-
|
|
78
|
+
console.error(chalk_1.default.red(" No WWW-Authenticate header found."));
|
|
79
|
+
process.exit(1);
|
|
41
80
|
}
|
|
42
|
-
console.log(` WWW-Authenticate: ${wwwAuth}`);
|
|
43
81
|
const challenge = (0, intmax402_core_1.parseWWWAuthenticate)(wwwAuth);
|
|
44
82
|
if (!challenge) {
|
|
45
|
-
console.error(" Failed to parse challenge.");
|
|
46
|
-
|
|
83
|
+
console.error(chalk_1.default.red(" Failed to parse WWW-Authenticate challenge."));
|
|
84
|
+
process.exit(1);
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
console.log(`
|
|
50
|
-
// Step
|
|
51
|
-
console.log("
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
console.log("\nStep 3: Retrying with credentials...");
|
|
60
|
-
const retryResponse = await client.fetch(url);
|
|
61
|
-
console.log(` Status: ${retryResponse.status}`);
|
|
62
|
-
if (retryResponse.ok) {
|
|
63
|
-
const body = await retryResponse.text();
|
|
64
|
-
console.log(` Response: ${body.slice(0, 200)}`);
|
|
86
|
+
const nonceShort = challenge.nonce.slice(0, 16) + "...";
|
|
87
|
+
console.log(` ${chalk_1.default.yellow("②")} nonce: ${chalk_1.default.dim(nonceShort)} (${challenge.realm})`);
|
|
88
|
+
// Step 3: Sign
|
|
89
|
+
console.log(` ${chalk_1.default.yellow("③")} Signing with wallet: ${chalk_1.default.dim(address.slice(0, 8) + "...")}`);
|
|
90
|
+
let authHeader;
|
|
91
|
+
if (mode === "payment") {
|
|
92
|
+
// Payment mode: include a mock txHash
|
|
93
|
+
const mockTxHash = "0x" + Buffer.alloc(32).fill(0xab).toString("hex");
|
|
94
|
+
// Sign only the nonce (same format as server expects)
|
|
95
|
+
const signature = await wallet.signMessage(challenge.nonce);
|
|
96
|
+
authHeader = `INTMAX402 address="${address}",nonce="${challenge.nonce}",signature="${signature}",txHash="${mockTxHash}"`;
|
|
65
97
|
}
|
|
66
98
|
else {
|
|
67
|
-
|
|
99
|
+
// Identity mode: sign just the nonce
|
|
100
|
+
const signature = await wallet.signMessage(challenge.nonce);
|
|
101
|
+
authHeader = `INTMAX402 address="${address}",nonce="${challenge.nonce}",signature="${signature}"`;
|
|
68
102
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
function verifyCommand(header) {
|
|
78
|
-
if (!header) {
|
|
79
|
-
console.error("Usage: intmax402 verify <authorization-header>");
|
|
80
|
-
process.exit(1);
|
|
103
|
+
// Step 4: Retry with Authorization
|
|
104
|
+
process.stdout.write(` ${chalk_1.default.yellow("④")} GET ${new URL(url).pathname} + Authorization → `);
|
|
105
|
+
let res2;
|
|
106
|
+
try {
|
|
107
|
+
res2 = await fetch(url, {
|
|
108
|
+
headers: { Authorization: authHeader },
|
|
109
|
+
});
|
|
81
110
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.error("Failed to parse Authorization header.");
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.log(chalk_1.default.red(`error: ${err.message}`));
|
|
85
113
|
process.exit(1);
|
|
86
114
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.log(` Signature: ${credential.signature.slice(0, 20)}...`);
|
|
91
|
-
if (credential.txHash) {
|
|
92
|
-
console.log(` TX Hash: ${credential.txHash}`);
|
|
115
|
+
const elapsed = Date.now() - startTime;
|
|
116
|
+
if (res2.ok) {
|
|
117
|
+
console.log(`${chalk_1.default.green(String(res2.status))} ${chalk_1.default.green("✅")}`);
|
|
93
118
|
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(`${chalk_1.default.red(String(res2.status))} ${chalk_1.default.red("❌")}`);
|
|
121
|
+
const body = await res2.text().catch(() => "");
|
|
122
|
+
if (body)
|
|
123
|
+
console.log(chalk_1.default.dim(` ${body.slice(0, 200)}`));
|
|
124
|
+
}
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(`${chalk_1.default.bold("Address:")} ${address}`);
|
|
127
|
+
console.log(`${chalk_1.default.bold("Time:")} ${elapsed}ms`);
|
|
128
|
+
if (!res2.ok) {
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function keygenCommand() {
|
|
133
|
+
const wallet = ethers_1.ethers.Wallet.createRandom();
|
|
134
|
+
console.log("\nGenerated wallet:");
|
|
135
|
+
console.log(` ${chalk_1.default.bold("Address:")} ${chalk_1.default.green(wallet.address)}`);
|
|
136
|
+
console.log(` ${chalk_1.default.bold("Private Key:")} ${chalk_1.default.yellow(wallet.privateKey)}`);
|
|
137
|
+
console.log();
|
|
138
|
+
console.log(chalk_1.default.red("⚠ Use for testing only. Never use in production."));
|
|
94
139
|
}
|
|
95
|
-
function
|
|
96
|
-
console.log("intmax402
|
|
97
|
-
console.log(
|
|
98
|
-
console.log("Usage:");
|
|
99
|
-
console.log(
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(
|
|
140
|
+
function printHelp() {
|
|
141
|
+
console.log(`${chalk_1.default.bold("intmax402")} - HTTP 402 Payment Gate CLI Tool`);
|
|
142
|
+
console.log();
|
|
143
|
+
console.log(chalk_1.default.bold("Usage:"));
|
|
144
|
+
console.log(` intmax402 ${chalk_1.default.cyan("test")} <url> [--mode identity|payment]`);
|
|
145
|
+
console.log(` Test 402 flow against a URL`);
|
|
146
|
+
console.log(` intmax402 ${chalk_1.default.cyan("keygen")} Generate a test Ethereum wallet`);
|
|
147
|
+
console.log(` intmax402 ${chalk_1.default.cyan("--help")} Show this help message`);
|
|
148
|
+
console.log();
|
|
149
|
+
console.log(chalk_1.default.bold("Examples:"));
|
|
150
|
+
console.log(` intmax402 test http://localhost:3760/identity`);
|
|
151
|
+
console.log(` intmax402 test http://localhost:3760/paid --mode payment`);
|
|
152
|
+
console.log(` intmax402 keygen`);
|
|
102
153
|
}
|
|
103
154
|
main().catch((err) => {
|
|
104
|
-
console.error("Error:", err.message);
|
|
155
|
+
console.error(chalk_1.default.red("Error:"), err.message);
|
|
105
156
|
process.exit(1);
|
|
106
157
|
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanakayuto/intmax402-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"intmax402": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@tanakayuto/intmax402-core": "0.2.0",
|
|
10
|
-
"
|
|
10
|
+
"chalk": "^4.1.2",
|
|
11
|
+
"ethers": "^6.16.0",
|
|
12
|
+
"minimist": "^1.2.8"
|
|
11
13
|
},
|
|
12
14
|
"devDependencies": {
|
|
13
|
-
"
|
|
14
|
-
"@types/
|
|
15
|
+
"@types/chalk": "^2.2.4",
|
|
16
|
+
"@types/minimist": "^1.2.5",
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"typescript": "^5.4.0"
|
|
15
19
|
},
|
|
16
20
|
"files": [
|
|
17
21
|
"dist",
|