@tanakayuto/intmax402-fetch 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 +121 -0
- package/dist/handler.d.ts +18 -0
- package/dist/handler.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @tanakayuto/intmax402-fetch
|
|
2
|
+
|
|
3
|
+
Web標準の `Request`/`Response` で動く INTMAX402 認証アダプター。
|
|
4
|
+
|
|
5
|
+
Hono、Next.js、Cloudflare Workers など、すべての fetch ベースフレームワークで使える共通基盤です。
|
|
6
|
+
|
|
7
|
+
## インストール
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @tanakayuto/intmax402-fetch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 使い方
|
|
14
|
+
|
|
15
|
+
### Cloudflare Workers
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { handleIntmax402 } from "@tanakayuto/intmax402-fetch"
|
|
19
|
+
|
|
20
|
+
const config = {
|
|
21
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
22
|
+
mode: "signature" as const,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
async fetch(request: Request): Promise<Response> {
|
|
27
|
+
const result = await handleIntmax402(request, config)
|
|
28
|
+
if (result.response) {
|
|
29
|
+
// 認証失敗 → エラーレスポンスを返す
|
|
30
|
+
return result.response
|
|
31
|
+
}
|
|
32
|
+
// 認証成功 → result.context にアドレス等が入る
|
|
33
|
+
const { address } = result.context
|
|
34
|
+
return new Response(`Hello, ${address}!`)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Hono
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { Hono } from "hono"
|
|
43
|
+
import { handleIntmax402 } from "@tanakayuto/intmax402-fetch"
|
|
44
|
+
|
|
45
|
+
const app = new Hono()
|
|
46
|
+
|
|
47
|
+
const config = {
|
|
48
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
49
|
+
mode: "signature" as const,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
app.use("/protected/*", async (c, next) => {
|
|
53
|
+
const result = await handleIntmax402(c.req.raw, config)
|
|
54
|
+
if (result.response) {
|
|
55
|
+
return result.response
|
|
56
|
+
}
|
|
57
|
+
c.set("intmax402", result.context)
|
|
58
|
+
await next()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
app.get("/protected/resource", (c) => {
|
|
62
|
+
const ctx = c.get("intmax402")
|
|
63
|
+
return c.json({ message: "Access granted", address: ctx.address })
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Next.js App Router (Middleware)
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// middleware.ts
|
|
71
|
+
import { NextRequest, NextResponse } from "next/server"
|
|
72
|
+
import { handleIntmax402 } from "@tanakayuto/intmax402-fetch"
|
|
73
|
+
|
|
74
|
+
const config = {
|
|
75
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
76
|
+
mode: "payment" as const,
|
|
77
|
+
serverAddress: process.env.SERVER_ADDRESS!,
|
|
78
|
+
amount: process.env.PAYMENT_AMOUNT!,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function middleware(request: NextRequest) {
|
|
82
|
+
const result = await handleIntmax402(request, config)
|
|
83
|
+
if (result.response) {
|
|
84
|
+
return result.response
|
|
85
|
+
}
|
|
86
|
+
// 認証成功 → ヘッダーにアドレスを追加して続行
|
|
87
|
+
const requestHeaders = new Headers(request.headers)
|
|
88
|
+
requestHeaders.set("x-intmax402-address", result.context.address)
|
|
89
|
+
return NextResponse.next({ request: { headers: requestHeaders } })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const config = {
|
|
93
|
+
matcher: "/api/protected/:path*",
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### `handleIntmax402(request, config)`
|
|
100
|
+
|
|
101
|
+
**パラメータ:**
|
|
102
|
+
- `request: Request` — Web標準の Request オブジェクト
|
|
103
|
+
- `config: INTMAX402Config` — 認証設定
|
|
104
|
+
|
|
105
|
+
**戻り値:**
|
|
106
|
+
- 認証失敗時: `{ response: Response, context: null }` — このレスポンスをクライアントに返す
|
|
107
|
+
- 認証成功時: `{ response: null, context: Intmax402Context }` — 次のハンドラに進む
|
|
108
|
+
|
|
109
|
+
### `Intmax402Context`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface Intmax402Context {
|
|
113
|
+
address: string // 認証済みウォレットアドレス
|
|
114
|
+
verified: boolean // 常に true
|
|
115
|
+
txHash?: string // payment モード時のトランザクションハッシュ
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## ライセンス
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { INTMAX402Config } from "@tanakayuto/intmax402-core";
|
|
2
|
+
export interface Intmax402Context {
|
|
3
|
+
address: string;
|
|
4
|
+
verified: boolean;
|
|
5
|
+
txHash?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Handle INTMAX402 authentication for any Web Standard fetch-based framework.
|
|
9
|
+
* Returns null if auth passes (proceed to next handler).
|
|
10
|
+
* Returns Response if auth fails (return this response to client).
|
|
11
|
+
*/
|
|
12
|
+
export declare function handleIntmax402(request: Request, config: INTMAX402Config): Promise<{
|
|
13
|
+
response: Response;
|
|
14
|
+
context: null;
|
|
15
|
+
} | {
|
|
16
|
+
response: null;
|
|
17
|
+
context: Intmax402Context;
|
|
18
|
+
}>;
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleIntmax402 = handleIntmax402;
|
|
4
|
+
const intmax402_core_1 = require("@tanakayuto/intmax402-core");
|
|
5
|
+
const crypto_1 = require("@tanakayuto/intmax402-express/dist/crypto");
|
|
6
|
+
const verify_payment_1 = require("@tanakayuto/intmax402-express/dist/verify-payment");
|
|
7
|
+
/**
|
|
8
|
+
* Handle INTMAX402 authentication for any Web Standard fetch-based framework.
|
|
9
|
+
* Returns null if auth passes (proceed to next handler).
|
|
10
|
+
* Returns Response if auth fails (return this response to client).
|
|
11
|
+
*/
|
|
12
|
+
async function handleIntmax402(request, config) {
|
|
13
|
+
const url = new URL(request.url);
|
|
14
|
+
const authHeader = request.headers.get("authorization");
|
|
15
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
|
|
16
|
+
?? request.headers.get("x-real-ip")
|
|
17
|
+
?? "unknown";
|
|
18
|
+
if (!authHeader) {
|
|
19
|
+
const nonce = (0, intmax402_core_1.generateNonce)(config.secret, ip, url.pathname, config.bindIp ?? false);
|
|
20
|
+
const statusCode = config.mode === "payment" ? 402 : 401;
|
|
21
|
+
return {
|
|
22
|
+
response: new Response(JSON.stringify({
|
|
23
|
+
error: config.mode === "payment" ? "Payment Required" : "Unauthorized",
|
|
24
|
+
protocol: "INTMAX402",
|
|
25
|
+
mode: config.mode,
|
|
26
|
+
}), {
|
|
27
|
+
status: statusCode,
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"WWW-Authenticate": buildWWWAuthenticate(nonce, config),
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
context: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const credential = (0, intmax402_core_1.parseAuthorization)(authHeader);
|
|
37
|
+
if (!credential) {
|
|
38
|
+
return { response: errorResponse(401, "Invalid authorization header"), context: null };
|
|
39
|
+
}
|
|
40
|
+
if (!(0, intmax402_core_1.verifyNonce)(credential.nonce, config.secret, ip, url.pathname, config.bindIp ?? false)) {
|
|
41
|
+
return { response: errorResponse(401, "Invalid or expired nonce"), context: null };
|
|
42
|
+
}
|
|
43
|
+
if (config.allowList && config.allowList.length > 0) {
|
|
44
|
+
if (!config.allowList.includes(credential.address.toLowerCase())) {
|
|
45
|
+
return { response: errorResponse(403, "Address not in allow list"), context: null };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const isValidSig = (0, crypto_1.verifySignature)(credential.signature, credential.nonce, credential.address);
|
|
49
|
+
if (!isValidSig) {
|
|
50
|
+
return { response: errorResponse(401, "Invalid signature"), context: null };
|
|
51
|
+
}
|
|
52
|
+
if (config.mode === "payment") {
|
|
53
|
+
if (!credential.txHash) {
|
|
54
|
+
return { response: errorResponse(402, "Payment transaction hash required"), context: null };
|
|
55
|
+
}
|
|
56
|
+
if (!config.serverAddress || !config.amount) {
|
|
57
|
+
return { response: errorResponse(500, "Server misconfigured"), context: null };
|
|
58
|
+
}
|
|
59
|
+
const paymentResult = await (0, verify_payment_1.verifyPayment)(credential.txHash, config.amount, config.serverAddress);
|
|
60
|
+
if (!paymentResult.valid) {
|
|
61
|
+
return { response: errorResponse(402, paymentResult.error ?? "Payment verification failed"), context: null };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
response: null,
|
|
66
|
+
context: { address: credential.address, verified: true, txHash: credential.txHash },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function buildWWWAuthenticate(nonce, config) {
|
|
70
|
+
let header = `INTMAX402 realm="intmax402", nonce="${nonce}", mode="${config.mode}"`;
|
|
71
|
+
if (config.serverAddress)
|
|
72
|
+
header += `, serverAddress="${config.serverAddress}"`;
|
|
73
|
+
if (config.amount)
|
|
74
|
+
header += `, amount="${config.amount}"`;
|
|
75
|
+
if (config.tokenAddress)
|
|
76
|
+
header += `, tokenAddress="${config.tokenAddress}"`;
|
|
77
|
+
if (config.chainId)
|
|
78
|
+
header += `, chainId="${config.chainId}"`;
|
|
79
|
+
return header;
|
|
80
|
+
}
|
|
81
|
+
function errorResponse(status, message) {
|
|
82
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
83
|
+
status,
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleIntmax402 = void 0;
|
|
4
|
+
var handler_1 = require("./handler");
|
|
5
|
+
Object.defineProperty(exports, "handleIntmax402", { enumerable: true, get: function () { return handler_1.handleIntmax402; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanakayuto/intmax402-fetch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/zaq2989/intmax402"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"intmax",
|
|
19
|
+
"http-402",
|
|
20
|
+
"payment",
|
|
21
|
+
"ai-agent",
|
|
22
|
+
"fetch",
|
|
23
|
+
"web-standard",
|
|
24
|
+
"cloudflare-workers"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@tanakayuto/intmax402-core": "0.2.2",
|
|
29
|
+
"@tanakayuto/intmax402-express": "0.2.2",
|
|
30
|
+
"ethers": "^6.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.4.0",
|
|
34
|
+
"@types/node": "^20.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"clean": "rm -rf dist"
|
|
39
|
+
}
|
|
40
|
+
}
|