@tanakayuto/intmax402-express 0.1.1 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { intmax402 } from "./middleware";
2
2
  export { verifySignature } from "./crypto";
3
+ export { initPaymentVerifier, verifyPayment, getPaymentVerifierAddress } from "./verify-payment";
4
+ export type { PaymentVerificationConfig, VerifyPaymentResult } from "./verify-payment";
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifySignature = exports.intmax402 = void 0;
3
+ exports.getPaymentVerifierAddress = exports.verifyPayment = exports.initPaymentVerifier = exports.verifySignature = exports.intmax402 = void 0;
4
4
  var middleware_1 = require("./middleware");
5
5
  Object.defineProperty(exports, "intmax402", { enumerable: true, get: function () { return middleware_1.intmax402; } });
6
6
  var crypto_1 = require("./crypto");
7
7
  Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return crypto_1.verifySignature; } });
8
+ var verify_payment_1 = require("./verify-payment");
9
+ Object.defineProperty(exports, "initPaymentVerifier", { enumerable: true, get: function () { return verify_payment_1.initPaymentVerifier; } });
10
+ Object.defineProperty(exports, "verifyPayment", { enumerable: true, get: function () { return verify_payment_1.verifyPayment; } });
11
+ Object.defineProperty(exports, "getPaymentVerifierAddress", { enumerable: true, get: function () { return verify_payment_1.getPaymentVerifierAddress; } });
@@ -6,6 +6,7 @@ declare global {
6
6
  intmax402?: {
7
7
  address: string;
8
8
  verified: boolean;
9
+ txHash?: string;
9
10
  };
10
11
  }
11
12
  }
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.intmax402 = intmax402;
4
- const core_1 = require("@tanakayuto/intmax402-core");
4
+ const intmax402_core_1 = require("@tanakayuto/intmax402-core");
5
5
  const crypto_1 = require("./crypto");
6
+ const verify_payment_1 = require("./verify-payment");
6
7
  function buildWWWAuthenticate(nonce, config) {
7
8
  let header = `INTMAX402 realm="intmax402", nonce="${nonce}", mode="${config.mode}"`;
8
9
  if (config.serverAddress) {
@@ -24,7 +25,7 @@ function intmax402(config) {
24
25
  const authHeader = req.headers.authorization;
25
26
  if (!authHeader) {
26
27
  const ip = req.ip || req.socket.remoteAddress || "unknown";
27
- const nonce = (0, core_1.generateNonce)(config.secret, ip, req.path, config.bindIp ?? false);
28
+ const nonce = (0, intmax402_core_1.generateNonce)(config.secret, ip, req.path, config.bindIp ?? false);
28
29
  const statusCode = config.mode === "payment" ? 402 : 401;
29
30
  res.setHeader("WWW-Authenticate", buildWWWAuthenticate(nonce, config));
30
31
  res.status(statusCode).json({
@@ -34,13 +35,13 @@ function intmax402(config) {
34
35
  });
35
36
  return;
36
37
  }
37
- const credential = (0, core_1.parseAuthorization)(authHeader);
38
+ const credential = (0, intmax402_core_1.parseAuthorization)(authHeader);
38
39
  if (!credential) {
39
40
  res.status(401).json({ error: "Invalid authorization header" });
40
41
  return;
41
42
  }
42
43
  const ip = req.ip || req.socket.remoteAddress || "unknown";
43
- if (!(0, core_1.verifyNonce)(credential.nonce, config.secret, ip, req.path, config.bindIp ?? false)) {
44
+ if (!(0, intmax402_core_1.verifyNonce)(credential.nonce, config.secret, ip, req.path, config.bindIp ?? false)) {
44
45
  res.status(401).json({ error: "Invalid or expired nonce" });
45
46
  return;
46
47
  }
@@ -60,12 +61,20 @@ function intmax402(config) {
60
61
  res.status(402).json({ error: "Payment transaction hash required" });
61
62
  return;
62
63
  }
63
- // In production, verify payment via intmax2-server-sdk fetchTransfers()
64
- // For now, trust the txHash presence as proof-of-payment placeholder
64
+ if (!config.serverAddress || !config.amount) {
65
+ res.status(500).json({ error: "Server misconfigured: serverAddress and amount required for payment mode" });
66
+ return;
67
+ }
68
+ const paymentResult = await (0, verify_payment_1.verifyPayment)(credential.txHash, config.amount, config.serverAddress);
69
+ if (!paymentResult.valid) {
70
+ res.status(402).json({ error: paymentResult.error || "Payment verification failed" });
71
+ return;
72
+ }
65
73
  }
66
74
  req.intmax402 = {
67
75
  address: credential.address,
68
76
  verified: true,
77
+ txHash: credential.txHash,
69
78
  };
70
79
  next();
71
80
  };
@@ -0,0 +1,13 @@
1
+ export interface PaymentVerificationConfig {
2
+ eth_private_key: `0x${string}`;
3
+ environment: "testnet" | "mainnet";
4
+ l1_rpc_url?: string;
5
+ }
6
+ export interface VerifyPaymentResult {
7
+ valid: boolean;
8
+ error?: string;
9
+ }
10
+ export declare function initPaymentVerifier(config: PaymentVerificationConfig): Promise<void>;
11
+ export declare function getPaymentVerifierAddress(): string;
12
+ export declare function verifyPayment(txHash: string, expectedAmount: string, serverAddress: string, tokenIndex?: number): Promise<VerifyPaymentResult>;
13
+ export declare function _resetPaymentVerifier(): void;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initPaymentVerifier = initPaymentVerifier;
4
+ exports.getPaymentVerifierAddress = getPaymentVerifierAddress;
5
+ exports.verifyPayment = verifyPayment;
6
+ exports._resetPaymentVerifier = _resetPaymentVerifier;
7
+ const intmax2_server_sdk_1 = require("intmax2-server-sdk");
8
+ // Singleton IntMaxNodeClient
9
+ let client = null;
10
+ let loginPromise = null;
11
+ // Used txHash tracking with TTL for replay prevention
12
+ const usedTxHashes = new Map();
13
+ const TX_HASH_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
14
+ function cleanupExpiredHashes() {
15
+ const now = Date.now();
16
+ for (const [hash, expiry] of usedTxHashes) {
17
+ if (now > expiry) {
18
+ usedTxHashes.delete(hash);
19
+ }
20
+ }
21
+ }
22
+ async function initPaymentVerifier(config) {
23
+ if (client && client.isLoggedIn)
24
+ return;
25
+ if (loginPromise) {
26
+ await loginPromise;
27
+ return;
28
+ }
29
+ client = new intmax2_server_sdk_1.IntMaxNodeClient({
30
+ environment: config.environment,
31
+ eth_private_key: config.eth_private_key,
32
+ l1_rpc_url: config.l1_rpc_url,
33
+ loggerLevel: "warn",
34
+ });
35
+ loginPromise = client.login().then(() => {
36
+ loginPromise = null;
37
+ });
38
+ await loginPromise;
39
+ }
40
+ function getPaymentVerifierAddress() {
41
+ if (!client)
42
+ throw new Error("Payment verifier not initialized. Call initPaymentVerifier() first.");
43
+ return client.address;
44
+ }
45
+ async function verifyPayment(txHash, expectedAmount, serverAddress, tokenIndex) {
46
+ if (!client || !client.isLoggedIn) {
47
+ return { valid: false, error: "Payment verifier not initialized" };
48
+ }
49
+ // Replay prevention: check if txHash was already used
50
+ cleanupExpiredHashes();
51
+ if (usedTxHashes.has(txHash)) {
52
+ return { valid: false, error: "Transaction already used" };
53
+ }
54
+ try {
55
+ // Fetch recent transfers (incoming)
56
+ const response = await client.fetchTransfers({ cursor: null, limit: 50 });
57
+ const transfers = response.items;
58
+ // Find matching transaction by digest
59
+ const match = transfers.find((tx) => tx.digest === txHash);
60
+ if (!match) {
61
+ return { valid: false, error: "Transaction not found in recent transfers" };
62
+ }
63
+ // Verify recipient matches server address
64
+ if (match.to?.toLowerCase() !== serverAddress.toLowerCase()) {
65
+ return { valid: false, error: "Recipient does not match server address" };
66
+ }
67
+ // Verify amount (compare as BigInt-safe string)
68
+ if (match.amount !== expectedAmount) {
69
+ return { valid: false, error: `Amount mismatch: expected ${expectedAmount}, got ${match.amount}` };
70
+ }
71
+ // Verify token if specified
72
+ if (tokenIndex !== undefined && match.tokenIndex !== tokenIndex) {
73
+ return { valid: false, error: `Token mismatch: expected index ${tokenIndex}, got ${match.tokenIndex}` };
74
+ }
75
+ // Mark as used (replay prevention)
76
+ usedTxHashes.set(txHash, Date.now() + TX_HASH_TTL_MS);
77
+ return { valid: true };
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ return { valid: false, error: `Payment verification failed: ${message}` };
82
+ }
83
+ }
84
+ // For testing: reset state
85
+ function _resetPaymentVerifier() {
86
+ client = null;
87
+ loginPromise = null;
88
+ usedTxHashes.clear();
89
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@tanakayuto/intmax402-express",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
7
- "@tanakayuto/intmax402-core": "0.1.0",
8
- "ethers": "^6.16.0"
7
+ "@tanakayuto/intmax402-core": "0.2.0",
8
+ "ethers": "^6.16.0",
9
+ "intmax2-server-sdk": "^1.5.2"
9
10
  },
10
11
  "peerDependencies": {
11
12
  "express": "^4.18.0"