@tanakayuto/intmax402-nextjs 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 +92 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +242 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @tanakayuto/intmax402-nextjs
|
|
2
|
+
|
|
3
|
+
INTMAX402 authentication middleware for Next.js App Router.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tanakayuto/intmax402-nextjs
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @tanakayuto/intmax402-nextjs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### `withIntmax402` — Route Handler wrapper
|
|
16
|
+
|
|
17
|
+
Wrap individual route handlers to protect them with INTMAX402 authentication.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// app/api/premium/route.ts
|
|
21
|
+
import { withIntmax402 } from '@tanakayuto/intmax402-nextjs'
|
|
22
|
+
|
|
23
|
+
export const GET = withIntmax402(
|
|
24
|
+
async (req) => {
|
|
25
|
+
return Response.json({
|
|
26
|
+
data: 'premium content',
|
|
27
|
+
address: req.intmax402.address,
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
32
|
+
mode: 'identity',
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### Payment mode
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
export const GET = withIntmax402(
|
|
41
|
+
async (req) => {
|
|
42
|
+
return Response.json({ data: 'paid content', address: req.intmax402.address })
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
46
|
+
mode: 'payment',
|
|
47
|
+
serverAddress: process.env.INTMAX_SERVER_ADDRESS!,
|
|
48
|
+
amount: '0.001',
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `intmax402Middleware` — Next.js middleware.ts
|
|
54
|
+
|
|
55
|
+
Protect entire route groups at the middleware level (Edge Runtime compatible).
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// middleware.ts
|
|
59
|
+
import { intmax402Middleware } from '@tanakayuto/intmax402-nextjs'
|
|
60
|
+
|
|
61
|
+
export default intmax402Middleware({
|
|
62
|
+
secret: process.env.INTMAX402_SECRET!,
|
|
63
|
+
mode: 'identity',
|
|
64
|
+
matcher: ['/api/premium/:path*'],
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export const config = {
|
|
68
|
+
matcher: ['/api/premium/:path*'],
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> **Note:** The middleware runs on Edge Runtime and performs full signature verification.
|
|
73
|
+
> Authenticated address is forwarded to route handlers via the `x-intmax402-address` header.
|
|
74
|
+
|
|
75
|
+
## Environment Variables
|
|
76
|
+
|
|
77
|
+
| Variable | Description | Required |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `INTMAX402_SECRET` | HMAC secret for nonce generation | ✅ |
|
|
80
|
+
| `INTMAX_SERVER_ADDRESS` | Your INTMAX server address (payment mode) | Payment only |
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
1. Client makes a request without `Authorization` header
|
|
85
|
+
2. Server responds with `401`/`402` + `WWW-Authenticate: INTMAX402 nonce=<nonce>, ...`
|
|
86
|
+
3. Client signs the nonce with their Ethereum wallet
|
|
87
|
+
4. Client retries with `Authorization: INTMAX402 address=<addr>,nonce=<nonce>,signature=<sig>`
|
|
88
|
+
5. Server verifies signature and grants access
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { INTMAX402Config } from "@tanakayuto/intmax402-core";
|
|
2
|
+
export interface Intmax402Info {
|
|
3
|
+
address: string;
|
|
4
|
+
verified: boolean;
|
|
5
|
+
txHash?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Extended Request type that carries intmax402 context after successful auth */
|
|
8
|
+
export interface Intmax402Request extends Request {
|
|
9
|
+
intmax402: Intmax402Info;
|
|
10
|
+
}
|
|
11
|
+
export interface WithIntmax402Options extends INTMAX402Config {
|
|
12
|
+
verifyPayment?: (txHash: string, amount: string, serverAddress: string) => Promise<{
|
|
13
|
+
valid: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface MiddlewareOptions extends Omit<INTMAX402Config, "mode"> {
|
|
18
|
+
mode?: INTMAX402Config["mode"];
|
|
19
|
+
/** Path patterns to intercept. Matched against request.nextUrl.pathname */
|
|
20
|
+
matcher?: string[];
|
|
21
|
+
}
|
|
22
|
+
type NextRouteHandler = (req: Request, ctx?: unknown) => Promise<Response> | Response;
|
|
23
|
+
type AuthedRouteHandler = (req: Intmax402Request, ctx?: unknown) => Promise<Response> | Response;
|
|
24
|
+
/**
|
|
25
|
+
* Wraps a Next.js App Router Route Handler with INTMAX402 authentication.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // app/api/premium/route.ts
|
|
29
|
+
* import { withIntmax402 } from '@tanakayuto/intmax402-nextjs'
|
|
30
|
+
*
|
|
31
|
+
* export const GET = withIntmax402(
|
|
32
|
+
* async (req) => {
|
|
33
|
+
* return Response.json({ data: 'premium content', address: req.intmax402.address })
|
|
34
|
+
* },
|
|
35
|
+
* { secret: process.env.INTMAX402_SECRET!, mode: 'identity' }
|
|
36
|
+
* )
|
|
37
|
+
*/
|
|
38
|
+
export declare function withIntmax402(handler: AuthedRouteHandler, options: WithIntmax402Options): NextRouteHandler;
|
|
39
|
+
/**
|
|
40
|
+
* Returns a Next.js middleware function for INTMAX402 authentication.
|
|
41
|
+
* Compatible with Edge Runtime.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // middleware.ts
|
|
45
|
+
* import { intmax402Middleware } from '@tanakayuto/intmax402-nextjs'
|
|
46
|
+
*
|
|
47
|
+
* export default intmax402Middleware({
|
|
48
|
+
* secret: process.env.INTMAX402_SECRET!,
|
|
49
|
+
* mode: 'identity',
|
|
50
|
+
* matcher: ['/api/premium'],
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* export const config = { matcher: ['/api/premium/:path*'] }
|
|
54
|
+
*/
|
|
55
|
+
export declare function intmax402Middleware(options: MiddlewareOptions): (request: Request) => Promise<Response>;
|
|
56
|
+
export type { INTMAX402Config, INTMAX402Mode } from "@tanakayuto/intmax402-core";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.withIntmax402 = withIntmax402;
|
|
37
|
+
exports.intmax402Middleware = intmax402Middleware;
|
|
38
|
+
const intmax402_fetch_1 = require("@tanakayuto/intmax402-fetch");
|
|
39
|
+
const intmax402_core_1 = require("@tanakayuto/intmax402-core");
|
|
40
|
+
const ethers_1 = require("ethers");
|
|
41
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Edge-compatible utilities (Web Crypto API — no Node.js crypto)
|
|
43
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
const WINDOW_MS = 30_000;
|
|
45
|
+
async function hmacHex(secret, data) {
|
|
46
|
+
const enc = new TextEncoder();
|
|
47
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
48
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(data));
|
|
49
|
+
return Array.from(new Uint8Array(sig))
|
|
50
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
async function generateNonceEdge(secret, ip, path, bindIp = false) {
|
|
54
|
+
const window = Math.floor(Date.now() / WINDOW_MS);
|
|
55
|
+
const data = bindIp ? `${window}:${ip}:${path}` : `${window}:${path}`;
|
|
56
|
+
return hmacHex(secret, data);
|
|
57
|
+
}
|
|
58
|
+
async function verifyNonceEdge(nonce, secret, ip, path, bindIp = false) {
|
|
59
|
+
if (!/^[0-9a-f]+$/i.test(nonce))
|
|
60
|
+
return false;
|
|
61
|
+
const window = Math.floor(Date.now() / WINDOW_MS);
|
|
62
|
+
for (const w of [window, window - 1]) {
|
|
63
|
+
const data = bindIp ? `${w}:${ip}:${path}` : `${w}:${path}`;
|
|
64
|
+
const expected = await hmacHex(secret, data);
|
|
65
|
+
if (expected === nonce)
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function verifySignatureEdge(signature, message, claimedAddress) {
|
|
71
|
+
try {
|
|
72
|
+
const recovered = ethers_1.ethers.verifyMessage(message, signature);
|
|
73
|
+
return recovered.toLowerCase() === claimedAddress.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function parseAuthorizationEdge(header) {
|
|
80
|
+
// Format: INTMAX402 address=<addr>,nonce=<nonce>,signature=<sig>[,txHash=<hash>]
|
|
81
|
+
const match = header.match(/^INTMAX402\s+(.+)$/i);
|
|
82
|
+
if (!match)
|
|
83
|
+
return null;
|
|
84
|
+
const params = {};
|
|
85
|
+
for (const part of match[1].split(",")) {
|
|
86
|
+
const eq = part.indexOf("=");
|
|
87
|
+
if (eq === -1)
|
|
88
|
+
continue;
|
|
89
|
+
const k = part.slice(0, eq).trim();
|
|
90
|
+
const v = part.slice(eq + 1).trim();
|
|
91
|
+
params[k] = v;
|
|
92
|
+
}
|
|
93
|
+
if (!params.address || !params.nonce || !params.signature)
|
|
94
|
+
return null;
|
|
95
|
+
return {
|
|
96
|
+
address: params.address,
|
|
97
|
+
nonce: params.nonce,
|
|
98
|
+
signature: params.signature,
|
|
99
|
+
txHash: params.txHash,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Wraps a Next.js App Router Route Handler with INTMAX402 authentication.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // app/api/premium/route.ts
|
|
107
|
+
* import { withIntmax402 } from '@tanakayuto/intmax402-nextjs'
|
|
108
|
+
*
|
|
109
|
+
* export const GET = withIntmax402(
|
|
110
|
+
* async (req) => {
|
|
111
|
+
* return Response.json({ data: 'premium content', address: req.intmax402.address })
|
|
112
|
+
* },
|
|
113
|
+
* { secret: process.env.INTMAX402_SECRET!, mode: 'identity' }
|
|
114
|
+
* )
|
|
115
|
+
*/
|
|
116
|
+
function withIntmax402(handler, options) {
|
|
117
|
+
return async (req, ctx) => {
|
|
118
|
+
const config = {
|
|
119
|
+
mode: options.mode,
|
|
120
|
+
secret: options.secret,
|
|
121
|
+
serverAddress: options.serverAddress,
|
|
122
|
+
amount: options.amount,
|
|
123
|
+
tokenAddress: options.tokenAddress,
|
|
124
|
+
chainId: options.chainId,
|
|
125
|
+
environment: options.environment,
|
|
126
|
+
allowList: options.allowList,
|
|
127
|
+
bindIp: options.bindIp,
|
|
128
|
+
};
|
|
129
|
+
const result = await (0, intmax402_fetch_1.handleIntmax402)(req, config);
|
|
130
|
+
if (result.response !== null) {
|
|
131
|
+
return result.response;
|
|
132
|
+
}
|
|
133
|
+
// Attach auth context to request (cast to extended type)
|
|
134
|
+
const authedReq = req;
|
|
135
|
+
authedReq.intmax402 = result.context;
|
|
136
|
+
return handler(authedReq, ctx);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
// intmax402Middleware — Next.js middleware.ts (Edge Runtime compatible)
|
|
141
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
142
|
+
/**
|
|
143
|
+
* Returns a Next.js middleware function for INTMAX402 authentication.
|
|
144
|
+
* Compatible with Edge Runtime.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // middleware.ts
|
|
148
|
+
* import { intmax402Middleware } from '@tanakayuto/intmax402-nextjs'
|
|
149
|
+
*
|
|
150
|
+
* export default intmax402Middleware({
|
|
151
|
+
* secret: process.env.INTMAX402_SECRET!,
|
|
152
|
+
* mode: 'identity',
|
|
153
|
+
* matcher: ['/api/premium'],
|
|
154
|
+
* })
|
|
155
|
+
*
|
|
156
|
+
* export const config = { matcher: ['/api/premium/:path*'] }
|
|
157
|
+
*/
|
|
158
|
+
function intmax402Middleware(options) {
|
|
159
|
+
const mode = options.mode ?? "identity";
|
|
160
|
+
const config = { ...options, mode };
|
|
161
|
+
return async function middleware(request) {
|
|
162
|
+
// Dynamic import of NextResponse to avoid bundling issues in non-Next environments
|
|
163
|
+
const { NextResponse } = await Promise.resolve().then(() => __importStar(require("next/server")));
|
|
164
|
+
const url = new URL(request.url);
|
|
165
|
+
const pathname = url.pathname;
|
|
166
|
+
// Path matching
|
|
167
|
+
if (options.matcher && options.matcher.length > 0) {
|
|
168
|
+
const matched = options.matcher.some((pattern) => matchPath(pattern, pathname));
|
|
169
|
+
if (!matched) {
|
|
170
|
+
return NextResponse.next();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
|
174
|
+
request.headers.get("x-real-ip") ??
|
|
175
|
+
"unknown";
|
|
176
|
+
const authHeader = request.headers.get("authorization");
|
|
177
|
+
if (!authHeader) {
|
|
178
|
+
// Generate challenge nonce (Edge-compatible)
|
|
179
|
+
const nonce = await generateNonceEdge(config.secret, ip, pathname, config.bindIp ?? false);
|
|
180
|
+
const statusCode = mode === "payment" ? 402 : 401;
|
|
181
|
+
return new Response(JSON.stringify({
|
|
182
|
+
error: mode === "payment" ? "Payment Required" : "Unauthorized",
|
|
183
|
+
protocol: "INTMAX402",
|
|
184
|
+
mode,
|
|
185
|
+
}), {
|
|
186
|
+
status: statusCode,
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
"WWW-Authenticate": (0, intmax402_core_1.buildWWWAuthenticate)(nonce, config),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Verify the authorization header
|
|
194
|
+
const credential = parseAuthorizationEdge(authHeader);
|
|
195
|
+
if (!credential) {
|
|
196
|
+
return edgeError(401, "Invalid authorization header");
|
|
197
|
+
}
|
|
198
|
+
const nonceValid = await verifyNonceEdge(credential.nonce, config.secret, ip, pathname, config.bindIp ?? false);
|
|
199
|
+
if (!nonceValid) {
|
|
200
|
+
return edgeError(401, "Invalid or expired nonce");
|
|
201
|
+
}
|
|
202
|
+
if (config.allowList && config.allowList.length > 0) {
|
|
203
|
+
const normalized = config.allowList.map((a) => a.toLowerCase());
|
|
204
|
+
if (!normalized.includes(credential.address.toLowerCase())) {
|
|
205
|
+
return edgeError(403, "Address not in allow list");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const sigValid = verifySignatureEdge(credential.signature, credential.nonce, credential.address);
|
|
209
|
+
if (!sigValid) {
|
|
210
|
+
return edgeError(401, "Invalid signature");
|
|
211
|
+
}
|
|
212
|
+
// For payment mode in middleware, we skip on-chain verification (do it in route handler)
|
|
213
|
+
// Pass auth info downstream via headers
|
|
214
|
+
const response = NextResponse.next();
|
|
215
|
+
response.headers.set("x-intmax402-address", credential.address);
|
|
216
|
+
response.headers.set("x-intmax402-verified", "true");
|
|
217
|
+
if (credential.txHash) {
|
|
218
|
+
response.headers.set("x-intmax402-tx-hash", credential.txHash);
|
|
219
|
+
}
|
|
220
|
+
return response;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function edgeError(status, message) {
|
|
224
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
225
|
+
status,
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Simple path pattern matcher.
|
|
231
|
+
* Supports wildcards: `/api/premium/:path*` → matches `/api/premium/anything`
|
|
232
|
+
*/
|
|
233
|
+
function matchPath(pattern, pathname) {
|
|
234
|
+
// Convert pattern to regex
|
|
235
|
+
const regexStr = pattern
|
|
236
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&") // escape special chars
|
|
237
|
+
.replace(/:([^/]+)\*/g, ".*") // :param* → .*
|
|
238
|
+
.replace(/:([^/]+)/g, "[^/]+") // :param → [^/]+
|
|
239
|
+
.replace(/\*/g, ".*"); // * → .*
|
|
240
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
241
|
+
return regex.test(pathname);
|
|
242
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanakayuto/intmax402-nextjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "INTMAX402 middleware for Next.js App Router",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": ["dist", "README.md"],
|
|
14
|
+
"publishConfig": { "access": "public" },
|
|
15
|
+
"repository": { "type": "git", "url": "https://github.com/zaq2989/intmax402" },
|
|
16
|
+
"keywords": ["intmax", "http-402", "payment", "nextjs", "app-router", "middleware", "ai-agent"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"next": ">=14.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@tanakayuto/intmax402-core": "workspace:*",
|
|
23
|
+
"@tanakayuto/intmax402-fetch": "workspace:*",
|
|
24
|
+
"ethers": "^6.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.4.0",
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"next": "^14.0.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"clean": "rm -rf dist"
|
|
34
|
+
}
|
|
35
|
+
}
|