@tanakayuto/intmax402-express 0.3.2 → 0.3.4
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 +135 -9
- package/dist/middleware.js +24 -3
- package/dist/verify-payment.js +6 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,23 +1,149 @@
|
|
|
1
1
|
# @tanakayuto/intmax402-express
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Express middleware for [intmax402](https://github.com/zaq2989/intmax402) — wallet-based authentication and INTMAX L2 micropayments for Express apps.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tanakayuto/intmax402-express
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Identity Mode
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
Require wallet ownership proof — no payment, no blockchain node needed.
|
|
10
16
|
|
|
11
17
|
```typescript
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
import express from 'express'
|
|
19
|
+
import { intmax402 } from '@tanakayuto/intmax402-express'
|
|
20
|
+
|
|
21
|
+
const app = express()
|
|
22
|
+
|
|
23
|
+
app.use('/protected', intmax402({
|
|
24
|
+
mode: 'identity',
|
|
25
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
app.get('/protected', (req, res) => {
|
|
29
|
+
res.json({ message: 'Access granted', address: req.intmax402?.address })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
app.listen(3000)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Payment Mode
|
|
36
|
+
|
|
37
|
+
Require an INTMAX L2 transfer before granting access.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import express from 'express'
|
|
41
|
+
import { intmax402 } from '@tanakayuto/intmax402-express'
|
|
42
|
+
|
|
43
|
+
const app = express()
|
|
44
|
+
|
|
45
|
+
app.use('/api/premium', intmax402({
|
|
46
|
+
mode: 'payment',
|
|
47
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
48
|
+
serverAddress: process.env.INTMAX_ADDRESS!,
|
|
49
|
+
amount: '1000000000000000', // 0.001 ETH in wei
|
|
15
50
|
environment: 'mainnet',
|
|
51
|
+
ethPrivateKey: process.env.ETH_PRIVATE_KEY!,
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
app.get('/api/premium', (req, res) => {
|
|
55
|
+
res.json({
|
|
56
|
+
message: 'Payment verified!',
|
|
57
|
+
paidBy: req.intmax402?.address,
|
|
58
|
+
txHash: req.intmax402?.txHash,
|
|
59
|
+
})
|
|
16
60
|
})
|
|
17
61
|
|
|
18
|
-
|
|
62
|
+
app.listen(3000)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### `intmax402(config)`
|
|
68
|
+
|
|
69
|
+
Returns an Express `RequestHandler` middleware.
|
|
70
|
+
|
|
71
|
+
On success, populates `req.intmax402`:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
req.intmax402 = {
|
|
75
|
+
address: string, // verified Ethereum address
|
|
76
|
+
verified: boolean, // always true
|
|
77
|
+
txHash?: string, // payment mode only — INTMAX transfer digest
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Config Options
|
|
82
|
+
|
|
83
|
+
| Option | Type | Required | Description |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `mode` | `'identity' \| 'payment'` | ✅ | Authentication mode |
|
|
86
|
+
| `secret` | `string` | ✅ | HMAC secret for nonce generation (keep private) |
|
|
87
|
+
| `serverAddress` | `string` | payment | Your INTMAX L2 address to receive payments |
|
|
88
|
+
| `amount` | `string` | payment | Required payment in token smallest unit (wei for ETH) |
|
|
89
|
+
| `ethPrivateKey` | `string` | payment† | Ethereum private key — auto-initializes the INTMAX payment verifier (v0.3.1+) |
|
|
90
|
+
| `environment` | `'mainnet' \| 'testnet'` | — | Network environment. Default: `'mainnet'` |
|
|
91
|
+
| `l1RpcUrl` | `string` | — | Custom L1 RPC URL override |
|
|
92
|
+
| `allowList` | `string[]` | — | Identity mode: restrict to specific addresses |
|
|
93
|
+
| `bindIp` | `boolean` | — | Bind nonce to client IP. Default `false` (recommended for AI agents) |
|
|
94
|
+
| `tokenAddress` | `string` | — | ERC-20 token for payment. Default: native ETH |
|
|
95
|
+
|
|
96
|
+
†`ethPrivateKey` auto-initializes the payment verifier on first use. If not provided, call `initPaymentVerifier()` manually before handling requests.
|
|
97
|
+
|
|
98
|
+
#### HTTP Responses
|
|
99
|
+
|
|
100
|
+
| Scenario | Status | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| No `Authorization` header | `401` (identity) / `402` (payment) | Returns `WWW-Authenticate` header with nonce |
|
|
103
|
+
| Invalid signature | `401` | Signature verification failed |
|
|
104
|
+
| Address not in `allowList` | `403` | Access denied by allowlist |
|
|
105
|
+
| Payment not verified | `402` | Transfer not found or amount/recipient mismatch |
|
|
106
|
+
| INTMAX network down | `503` | Payment verifier temporarily unavailable |
|
|
107
|
+
| Auth verified | calls `next()` | `req.intmax402` is populated |
|
|
108
|
+
|
|
109
|
+
### Additional Exports
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import {
|
|
113
|
+
intmax402,
|
|
114
|
+
verifySignature,
|
|
115
|
+
initPaymentVerifier,
|
|
116
|
+
verifyPayment,
|
|
117
|
+
getPaymentVerifierAddress,
|
|
118
|
+
} from '@tanakayuto/intmax402-express'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `initPaymentVerifier(config)`
|
|
122
|
+
|
|
123
|
+
Manually initialize the INTMAX payment verifier (call once at server startup).
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { initPaymentVerifier } from '@tanakayuto/intmax402-express'
|
|
127
|
+
|
|
19
128
|
await initPaymentVerifier({
|
|
20
129
|
eth_private_key: process.env.ETH_PRIVATE_KEY as `0x${string}`,
|
|
21
|
-
environment: 'testnet'
|
|
130
|
+
environment: 'mainnet', // or 'testnet'
|
|
22
131
|
})
|
|
23
132
|
```
|
|
133
|
+
|
|
134
|
+
#### `verifySignature(signature, nonce, address)`
|
|
135
|
+
|
|
136
|
+
Low-level utility to verify an Ethereum signature against a nonce.
|
|
137
|
+
|
|
138
|
+
Returns: `boolean`
|
|
139
|
+
|
|
140
|
+
## Network
|
|
141
|
+
|
|
142
|
+
| Environment | Network | Notes |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `mainnet` (default) | Ethereum mainnet + Scroll | Production use |
|
|
145
|
+
| `testnet` | Sepolia + Scroll Sepolia | Fund wallet at testnet.intmax.io |
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/dist/middleware.js
CHANGED
|
@@ -27,7 +27,7 @@ function intmax402(config) {
|
|
|
27
27
|
catch (e) {
|
|
28
28
|
res.status(503).json({
|
|
29
29
|
error: 'Payment verifier temporarily unavailable',
|
|
30
|
-
|
|
30
|
+
error_code: intmax402_core_1.INTMAX402_ERROR_CODES.INTMAX_NETWORK_UNAVAILABLE,
|
|
31
31
|
protocol: 'INTMAX402',
|
|
32
32
|
});
|
|
33
33
|
return;
|
|
@@ -40,6 +40,7 @@ function intmax402(config) {
|
|
|
40
40
|
res.setHeader("WWW-Authenticate", (0, intmax402_core_1.buildWWWAuthenticate)(nonce, config));
|
|
41
41
|
res.status(statusCode).json({
|
|
42
42
|
error: config.mode === "payment" ? "Payment Required" : "Unauthorized",
|
|
43
|
+
error_code: intmax402_core_1.INTMAX402_ERROR_CODES.MISSING_AUTH_HEADER,
|
|
43
44
|
protocol: "INTMAX402",
|
|
44
45
|
mode: config.mode,
|
|
45
46
|
});
|
|
@@ -76,9 +77,29 @@ function intmax402(config) {
|
|
|
76
77
|
res.status(500).json({ error: "Server misconfigured: serverAddress and amount required for payment mode" });
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
let paymentResult;
|
|
81
|
+
try {
|
|
82
|
+
paymentResult = await (0, verify_payment_1.verifyPayment)(credential.txHash, config.amount, config.serverAddress);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err instanceof intmax402_core_1.INTMAX402Error) {
|
|
86
|
+
const status = err.code === intmax402_core_1.INTMAX402_ERROR_CODES.INTMAX_NETWORK_UNAVAILABLE ? 503 : 402;
|
|
87
|
+
res.status(status).json({
|
|
88
|
+
error: err.message,
|
|
89
|
+
error_code: err.code,
|
|
90
|
+
protocol: 'INTMAX402',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
res.status(402).json({
|
|
95
|
+
error: 'Payment verification failed',
|
|
96
|
+
protocol: 'INTMAX402',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
80
101
|
if (!paymentResult.valid) {
|
|
81
|
-
res.status(402).json({ error: paymentResult.error || "Payment verification failed" });
|
|
102
|
+
res.status(402).json({ error: paymentResult.error || "Payment verification failed", protocol: "INTMAX402" });
|
|
82
103
|
return;
|
|
83
104
|
}
|
|
84
105
|
}
|
package/dist/verify-payment.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.getPaymentVerifierAddress = getPaymentVerifierAddress;
|
|
|
5
5
|
exports.verifyPayment = verifyPayment;
|
|
6
6
|
exports._resetPaymentVerifier = _resetPaymentVerifier;
|
|
7
7
|
const intmax2_server_sdk_1 = require("intmax2-server-sdk");
|
|
8
|
+
const intmax402_core_1 = require("@tanakayuto/intmax402-core");
|
|
8
9
|
// Singleton IntMaxNodeClient
|
|
9
10
|
let client = null;
|
|
10
11
|
let loginPromise = null;
|
|
@@ -55,15 +56,12 @@ async function initPaymentVerifier(config) {
|
|
|
55
56
|
}
|
|
56
57
|
function getPaymentVerifierAddress() {
|
|
57
58
|
if (!client)
|
|
58
|
-
throw new
|
|
59
|
+
throw new intmax402_core_1.INTMAX402Error(intmax402_core_1.INTMAX402_ERROR_CODES.INTMAX_NETWORK_UNAVAILABLE, "Payment verifier not initialized. Call initPaymentVerifier() first.");
|
|
59
60
|
return client.address;
|
|
60
61
|
}
|
|
61
62
|
async function verifyPayment(txHash, expectedAmount, serverAddress, tokenIndex) {
|
|
62
63
|
if (!client || !client.isLoggedIn) {
|
|
63
|
-
|
|
64
|
-
valid: false,
|
|
65
|
-
error: "Payment verifier temporarily unavailable. INTMAX network may be down.",
|
|
66
|
-
};
|
|
64
|
+
throw new intmax402_core_1.INTMAX402Error(intmax402_core_1.INTMAX402_ERROR_CODES.INTMAX_NETWORK_UNAVAILABLE, "Payment verifier temporarily unavailable. INTMAX network may be down.");
|
|
67
65
|
}
|
|
68
66
|
// Replay prevention: check if txHash was already used (or pending)
|
|
69
67
|
cleanupExpiredHashes();
|
|
@@ -104,19 +102,19 @@ async function verifyPayment(txHash, expectedAmount, serverAddress, tokenIndex)
|
|
|
104
102
|
if (!match) {
|
|
105
103
|
// Fix 1: Rollback on validation failure
|
|
106
104
|
usedTxHashes.delete(txHash);
|
|
107
|
-
|
|
105
|
+
throw new intmax402_core_1.INTMAX402Error(intmax402_core_1.INTMAX402_ERROR_CODES.PAYMENT_NOT_FOUND, "Transaction not found in recent transfers", { txHash });
|
|
108
106
|
}
|
|
109
107
|
// Verify recipient matches server address
|
|
110
108
|
if (match.to?.toLowerCase() !== serverAddress.toLowerCase()) {
|
|
111
109
|
// Fix 1: Rollback on validation failure
|
|
112
110
|
usedTxHashes.delete(txHash);
|
|
113
|
-
|
|
111
|
+
throw new intmax402_core_1.INTMAX402Error(intmax402_core_1.INTMAX402_ERROR_CODES.PAYMENT_RECIPIENT_MISMATCH, "Recipient does not match server address", { expected: serverAddress, got: match.to });
|
|
114
112
|
}
|
|
115
113
|
// Fix 2: Verify amount using BigInt comparison (allows >= expectedAmount)
|
|
116
114
|
if (BigInt(match.amount) < BigInt(expectedAmount)) {
|
|
117
115
|
// Fix 1: Rollback on validation failure
|
|
118
116
|
usedTxHashes.delete(txHash);
|
|
119
|
-
|
|
117
|
+
throw new intmax402_core_1.INTMAX402Error(intmax402_core_1.INTMAX402_ERROR_CODES.PAYMENT_AMOUNT_MISMATCH, `Amount mismatch: expected ${expectedAmount}, got ${match.amount}`, { expected: expectedAmount, got: match.amount });
|
|
120
118
|
}
|
|
121
119
|
// Verify token if specified
|
|
122
120
|
if (tokenIndex !== undefined && match.tokenIndex !== tokenIndex) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanakayuto/intmax402-express",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@tanakayuto/intmax402-core": "0.3.
|
|
29
|
+
"@tanakayuto/intmax402-core": "0.3.2",
|
|
30
30
|
"ethers": "^6.16.0",
|
|
31
31
|
"intmax2-server-sdk": "^1.5.2"
|
|
32
32
|
},
|