next-token-auth 1.0.9 → 1.0.10

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.
@@ -0,0 +1,282 @@
1
+ // src/utils/crypto.ts
2
+ var ALGO = "AES-GCM";
3
+ function getTextEncoder() {
4
+ return new TextEncoder();
5
+ }
6
+ function getTextDecoder() {
7
+ return new TextDecoder();
8
+ }
9
+ async function deriveKey(secret) {
10
+ const raw = getTextEncoder().encode(secret.padEnd(32, "0").slice(0, 32));
11
+ return crypto.subtle.importKey("raw", raw, { name: ALGO }, false, [
12
+ "encrypt",
13
+ "decrypt"
14
+ ]);
15
+ }
16
+ async function decrypt(data, secret) {
17
+ const [ivB64, cipherB64] = data.split(".");
18
+ if (!ivB64 || !cipherB64) {
19
+ throw new Error("decrypt: invalid ciphertext format");
20
+ }
21
+ const key = await deriveKey(secret);
22
+ const ivBytes = base64ToBuffer(ivB64);
23
+ const iv = ivBytes.buffer.slice(
24
+ ivBytes.byteOffset,
25
+ ivBytes.byteOffset + ivBytes.byteLength
26
+ );
27
+ const cipherBytes = base64ToBuffer(cipherB64);
28
+ const cipherBuffer = cipherBytes.buffer.slice(
29
+ cipherBytes.byteOffset,
30
+ cipherBytes.byteOffset + cipherBytes.byteLength
31
+ );
32
+ const plainBuffer = await crypto.subtle.decrypt({ name: ALGO, iv }, key, cipherBuffer);
33
+ return getTextDecoder().decode(plainBuffer);
34
+ }
35
+ function base64ToBuffer(b64) {
36
+ const padded = b64.replace(/-/g, "+").replace(/_/g, "/");
37
+ const binary = atob(padded);
38
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
39
+ }
40
+
41
+ // src/utils/expiry.ts
42
+ var UNIT_MAP = {
43
+ s: 1,
44
+ m: 60,
45
+ h: 3600,
46
+ d: 86400,
47
+ w: 604800
48
+ };
49
+ function parseExpiry(input) {
50
+ if (input === void 0 || input === null) {
51
+ throw new Error("parseExpiry: no expiry value provided");
52
+ }
53
+ if (typeof input === "number") {
54
+ if (input <= 0) throw new Error("parseExpiry: value must be positive");
55
+ return input;
56
+ }
57
+ const trimmed = input.trim();
58
+ if (/^\d+$/.test(trimmed)) {
59
+ return parseInt(trimmed, 10);
60
+ }
61
+ const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*([smhdw])$/i);
62
+ if (!match) {
63
+ throw new Error(
64
+ `parseExpiry: unrecognised format "${input}". Expected a number or a string like "15m", "2h", "2d", "7d", "1w".`
65
+ );
66
+ }
67
+ const value = parseFloat(match[1]);
68
+ const unit = match[2].toLowerCase();
69
+ return Math.floor(value * UNIT_MAP[unit]);
70
+ }
71
+ function safeParseExpiry(input, fallbackSeconds = 900) {
72
+ try {
73
+ return parseExpiry(input);
74
+ } catch {
75
+ return fallbackSeconds;
76
+ }
77
+ }
78
+ function resolveAccessTokenExpiry(response, configExpiry, strategy = "hybrid") {
79
+ const now = Date.now();
80
+ const fromBackend = response.accessTokenExpiresIn ?? response.expiresIn ?? void 0;
81
+ if (strategy === "backend") {
82
+ if (fromBackend === void 0) {
83
+ throw new Error(
84
+ 'resolveAccessTokenExpiry: strategy is "backend" but API returned no expiry'
85
+ );
86
+ }
87
+ return now + parseExpiry(fromBackend) * 1e3;
88
+ }
89
+ if (strategy === "config") {
90
+ if (configExpiry === void 0) {
91
+ throw new Error(
92
+ 'resolveAccessTokenExpiry: strategy is "config" but no expiry configured'
93
+ );
94
+ }
95
+ return now + parseExpiry(configExpiry) * 1e3;
96
+ }
97
+ if (fromBackend !== void 0) {
98
+ return now + safeParseExpiry(fromBackend) * 1e3;
99
+ }
100
+ if (configExpiry !== void 0) {
101
+ return now + safeParseExpiry(configExpiry) * 1e3;
102
+ }
103
+ return now + 900 * 1e3;
104
+ }
105
+ function resolveRefreshTokenExpiry(response, configExpiry, strategy = "hybrid") {
106
+ const now = Date.now();
107
+ const fromBackend = response.refreshTokenExpiresIn;
108
+ if (strategy === "backend") {
109
+ return fromBackend !== void 0 ? now + parseExpiry(fromBackend) * 1e3 : void 0;
110
+ }
111
+ if (strategy === "config") {
112
+ return configExpiry !== void 0 ? now + parseExpiry(configExpiry) * 1e3 : void 0;
113
+ }
114
+ if (fromBackend !== void 0) {
115
+ return now + safeParseExpiry(fromBackend) * 1e3;
116
+ }
117
+ if (configExpiry !== void 0) {
118
+ return now + safeParseExpiry(configExpiry) * 1e3;
119
+ }
120
+ return void 0;
121
+ }
122
+
123
+ // src/server/getServerSession.ts
124
+ async function getServerSession(req, config) {
125
+ const cookieName = config.token.cookieName ?? "next-token-auth.session";
126
+ const cookieValue = req.cookies.get(cookieName)?.value;
127
+ if (!cookieValue) {
128
+ return { user: null, tokens: null, isAuthenticated: false };
129
+ }
130
+ let tokens = null;
131
+ try {
132
+ const json = await decrypt(cookieValue, config.secret);
133
+ tokens = JSON.parse(json);
134
+ } catch {
135
+ return { user: null, tokens: null, isAuthenticated: false };
136
+ }
137
+ const now = Date.now();
138
+ const threshold = (config.refreshThreshold ?? 60) * 1e3;
139
+ const accessExpired = now >= tokens.accessTokenExpiresAt - threshold;
140
+ const refreshExpired = tokens.refreshTokenExpiresAt ? now >= tokens.refreshTokenExpiresAt : false;
141
+ if (accessExpired && refreshExpired) {
142
+ return { user: null, tokens: null, isAuthenticated: false };
143
+ }
144
+ if (accessExpired && !refreshExpired) {
145
+ const refreshed = await serverRefresh(tokens, config);
146
+ if (!refreshed) {
147
+ return { user: null, tokens: null, isAuthenticated: false };
148
+ }
149
+ tokens = refreshed;
150
+ }
151
+ const user = await fetchUser(tokens.accessToken, config);
152
+ return {
153
+ user,
154
+ tokens,
155
+ isAuthenticated: true
156
+ };
157
+ }
158
+ async function serverRefresh(tokens, config) {
159
+ try {
160
+ const fetchFn = config.fetchFn ?? fetch;
161
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
162
+ const refreshPath = config.endpoints.refresh.replace(/^\//, "");
163
+ const res = await fetchFn(`${baseUrl}/${refreshPath}`, {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ refreshToken: tokens.refreshToken })
167
+ });
168
+ if (!res.ok) return null;
169
+ const data = await res.json();
170
+ const strategy = config.expiry?.strategy ?? "hybrid";
171
+ return {
172
+ accessToken: data.accessToken,
173
+ refreshToken: data.refreshToken,
174
+ accessTokenExpiresAt: resolveAccessTokenExpiry(
175
+ data,
176
+ config.expiry?.accessTokenExpiresIn,
177
+ strategy
178
+ ),
179
+ refreshTokenExpiresAt: resolveRefreshTokenExpiry(
180
+ data,
181
+ config.expiry?.refreshTokenExpiresIn,
182
+ strategy
183
+ )
184
+ };
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ async function fetchUser(accessToken, config) {
190
+ if (!config.endpoints.me) return null;
191
+ try {
192
+ const fetchFn = config.fetchFn ?? fetch;
193
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
194
+ const mePath = config.endpoints.me.replace(/^\//, "");
195
+ const res = await fetchFn(`${baseUrl}/${mePath}`, {
196
+ headers: { Authorization: `Bearer ${accessToken}` }
197
+ });
198
+ if (!res.ok) return null;
199
+ return await res.json();
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ // src/server/withAuth.ts
206
+ function withAuth(config, handler, options = {}) {
207
+ return async (req) => {
208
+ const session = await getServerSession(req, config);
209
+ if (!session.isAuthenticated) {
210
+ const redirectTo = options.redirectTo ?? "/login";
211
+ const loginUrl = new URL(redirectTo, `https://${req.nextUrl.pathname}`);
212
+ return Response.redirect(loginUrl);
213
+ }
214
+ return handler(req, session);
215
+ };
216
+ }
217
+
218
+ // src/server/middleware.ts
219
+ function authMiddleware(authConfig) {
220
+ return async function middleware(request) {
221
+ const { NextResponse } = await import('next/server');
222
+ const pathname = request.nextUrl.pathname;
223
+ const cookieName = authConfig.token.cookieName ?? "next-token-auth.session";
224
+ const cookieValue = request.cookies.get(cookieName)?.value;
225
+ const isAuthenticated = await checkSession(cookieValue, authConfig.secret);
226
+ const guestOnlyRoutes = authConfig.routes?.guestOnly ?? [];
227
+ if (isGuestOnlyRoute(pathname, guestOnlyRoutes)) {
228
+ if (isAuthenticated) {
229
+ const redirectTo = authConfig.routes?.redirectAuthenticatedTo ?? "/dashboard";
230
+ return NextResponse.redirect(new URL(redirectTo, request.nextUrl.origin));
231
+ }
232
+ return NextResponse.next();
233
+ }
234
+ const publicRoutes = authConfig.routes?.public ?? [];
235
+ if (isPublicRoute(pathname, publicRoutes)) {
236
+ return NextResponse.next();
237
+ }
238
+ const protectedRoutes = authConfig.routes?.protected ?? [];
239
+ const requiresAuth = protectedRoutes.length === 0 || isProtectedRoute(pathname, protectedRoutes);
240
+ if (!requiresAuth) {
241
+ return NextResponse.next();
242
+ }
243
+ if (!isAuthenticated) {
244
+ return redirectToLogin(request, NextResponse);
245
+ }
246
+ return NextResponse.next();
247
+ };
248
+ }
249
+ async function checkSession(cookieValue, secret) {
250
+ if (!cookieValue) return false;
251
+ try {
252
+ const json = await decrypt(cookieValue, secret);
253
+ const tokens = JSON.parse(json);
254
+ const now = Date.now();
255
+ const refreshExpired = tokens.refreshTokenExpiresAt ? now >= tokens.refreshTokenExpiresAt : false;
256
+ return !refreshExpired;
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+ function isGuestOnlyRoute(pathname, routes) {
262
+ return routes.some((route) => matchRoute(pathname, route));
263
+ }
264
+ function isPublicRoute(pathname, publicRoutes) {
265
+ return publicRoutes.some((route) => matchRoute(pathname, route));
266
+ }
267
+ function isProtectedRoute(pathname, protectedRoutes) {
268
+ return protectedRoutes.some((route) => matchRoute(pathname, route));
269
+ }
270
+ function matchRoute(pathname, pattern) {
271
+ if (pattern.endsWith("*")) {
272
+ return pathname.startsWith(pattern.slice(0, -1));
273
+ }
274
+ return pathname === pattern;
275
+ }
276
+ function redirectToLogin(request, NextResponse) {
277
+ return NextResponse.redirect(new URL("/login", request.nextUrl.origin));
278
+ }
279
+
280
+ export { authMiddleware, getServerSession, withAuth };
281
+ //# sourceMappingURL=index.mjs.map
282
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/crypto.ts","../../src/utils/expiry.ts","../../src/server/getServerSession.ts","../../src/server/withAuth.ts","../../src/server/middleware.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Lightweight symmetric encryption using AES-GCM via the Web Crypto API.\n * Works in both browser and Node.js (>=18) / Edge runtimes.\n */\n\nconst ALGO = \"AES-GCM\";\nconst IV_LENGTH = 12; // bytes\n\nfunction getTextEncoder() {\n return new TextEncoder();\n}\n\nfunction getTextDecoder() {\n return new TextDecoder();\n}\n\nasync function deriveKey(secret: string): Promise<CryptoKey> {\n const raw = getTextEncoder().encode(secret.padEnd(32, \"0\").slice(0, 32));\n return crypto.subtle.importKey(\"raw\", raw, { name: ALGO }, false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\n/**\n * Encrypts a plaintext string using AES-GCM.\n * Returns a base64url-encoded string: `<iv>.<ciphertext>`\n */\nexport async function encrypt(data: string, secret: string): Promise<string> {\n const key = await deriveKey(secret);\n const ivArray = crypto.getRandomValues(new Uint8Array(IV_LENGTH));\n // Ensure we have a plain ArrayBuffer for SubtleCrypto\n const iv = ivArray.buffer.slice(0, IV_LENGTH) as ArrayBuffer;\n const encoded = getTextEncoder().encode(data);\n\n const cipherBuffer = await crypto.subtle.encrypt({ name: ALGO, iv }, key, encoded);\n\n const ivB64 = bufferToBase64(new Uint8Array(iv));\n const cipherB64 = bufferToBase64(new Uint8Array(cipherBuffer));\n return `${ivB64}.${cipherB64}`;\n}\n\n/**\n * Decrypts a string produced by `encrypt`.\n */\nexport async function decrypt(data: string, secret: string): Promise<string> {\n const [ivB64, cipherB64] = data.split(\".\");\n if (!ivB64 || !cipherB64) {\n throw new Error(\"decrypt: invalid ciphertext format\");\n }\n\n const key = await deriveKey(secret);\n const ivBytes = base64ToBuffer(ivB64);\n const iv = ivBytes.buffer.slice(\n ivBytes.byteOffset,\n ivBytes.byteOffset + ivBytes.byteLength\n ) as ArrayBuffer;\n\n const cipherBytes = base64ToBuffer(cipherB64);\n const cipherBuffer = cipherBytes.buffer.slice(\n cipherBytes.byteOffset,\n cipherBytes.byteOffset + cipherBytes.byteLength\n ) as ArrayBuffer;\n\n const plainBuffer = await crypto.subtle.decrypt({ name: ALGO, iv }, key, cipherBuffer);\n\n return getTextDecoder().decode(plainBuffer);\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction bufferToBase64(buffer: Uint8Array): string {\n return btoa(String.fromCharCode(...buffer))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction base64ToBuffer(b64: string): Uint8Array {\n const padded = b64.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const binary = atob(padded);\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\n}\n","import type { ExpiryInput, ExpiryStrategy, LoginResponse } from \"../types\";\n\nconst UNIT_MAP: Record<string, number> = {\n s: 1,\n m: 60,\n h: 3600,\n d: 86400,\n w: 604800,\n};\n\n/**\n * Parses an expiry value into seconds.\n * Accepts:\n * - number → treated as seconds\n * - string → e.g. \"15m\", \"2h\", \"2d\", \"7d\", \"1w\"\n *\n * @throws if the format is unrecognised\n */\nexport function parseExpiry(input?: ExpiryInput): number {\n if (input === undefined || input === null) {\n throw new Error(\"parseExpiry: no expiry value provided\");\n }\n\n if (typeof input === \"number\") {\n if (input <= 0) throw new Error(\"parseExpiry: value must be positive\");\n return input;\n }\n\n const trimmed = input.trim();\n\n // Pure numeric string\n if (/^\\d+$/.test(trimmed)) {\n return parseInt(trimmed, 10);\n }\n\n const match = trimmed.match(/^(\\d+(?:\\.\\d+)?)\\s*([smhdw])$/i);\n if (!match) {\n throw new Error(\n `parseExpiry: unrecognised format \"${input}\". ` +\n `Expected a number or a string like \"15m\", \"2h\", \"2d\", \"7d\", \"1w\".`\n );\n }\n\n const value = parseFloat(match[1]);\n const unit = match[2].toLowerCase();\n return Math.floor(value * UNIT_MAP[unit]);\n}\n\n/**\n * Safely parses an expiry value, returning a fallback on failure.\n */\nexport function safeParseExpiry(\n input?: ExpiryInput,\n fallbackSeconds = 900\n): number {\n try {\n return parseExpiry(input);\n } catch {\n return fallbackSeconds;\n }\n}\n\n/**\n * Resolves the access token expiry timestamp (ms) from a login response\n * using the configured strategy.\n */\nexport function resolveAccessTokenExpiry(\n response: LoginResponse,\n configExpiry?: ExpiryInput,\n strategy: ExpiryStrategy = \"hybrid\"\n): number {\n const now = Date.now();\n\n const fromBackend =\n response.accessTokenExpiresIn ?? response.expiresIn ?? undefined;\n\n if (strategy === \"backend\") {\n if (fromBackend === undefined) {\n throw new Error(\n 'resolveAccessTokenExpiry: strategy is \"backend\" but API returned no expiry'\n );\n }\n return now + parseExpiry(fromBackend) * 1000;\n }\n\n if (strategy === \"config\") {\n if (configExpiry === undefined) {\n throw new Error(\n 'resolveAccessTokenExpiry: strategy is \"config\" but no expiry configured'\n );\n }\n return now + parseExpiry(configExpiry) * 1000;\n }\n\n // hybrid: backend first, fallback to config\n if (fromBackend !== undefined) {\n return now + safeParseExpiry(fromBackend) * 1000;\n }\n if (configExpiry !== undefined) {\n return now + safeParseExpiry(configExpiry) * 1000;\n }\n\n // Last resort: 15 minutes\n return now + 900 * 1000;\n}\n\n/**\n * Resolves the refresh token expiry timestamp (ms).\n */\nexport function resolveRefreshTokenExpiry(\n response: LoginResponse,\n configExpiry?: ExpiryInput,\n strategy: ExpiryStrategy = \"hybrid\"\n): number | undefined {\n const now = Date.now();\n const fromBackend = response.refreshTokenExpiresIn;\n\n if (strategy === \"backend\") {\n return fromBackend !== undefined\n ? now + parseExpiry(fromBackend) * 1000\n : undefined;\n }\n\n if (strategy === \"config\") {\n return configExpiry !== undefined\n ? now + parseExpiry(configExpiry) * 1000\n : undefined;\n }\n\n // hybrid\n if (fromBackend !== undefined) {\n return now + safeParseExpiry(fromBackend) * 1000;\n }\n if (configExpiry !== undefined) {\n return now + safeParseExpiry(configExpiry) * 1000;\n }\n\n return undefined;\n}\n","import type { AuthConfig, AuthSession, AuthTokens, LoginResponse } from \"../types\";\nimport { decrypt } from \"../utils/crypto\";\nimport {\n resolveAccessTokenExpiry,\n resolveRefreshTokenExpiry,\n} from \"../utils/expiry\";\n\ntype NextRequest = {\n cookies: {\n get(name: string): { value: string } | undefined;\n };\n};\n\n/**\n * Retrieves and validates the auth session on the server side.\n * Reads the encrypted session cookie, validates token expiry,\n * and refreshes if needed.\n *\n * Compatible with Next.js App Router (NextRequest) and Pages Router (IncomingMessage).\n *\n * @example\n * // app/dashboard/page.tsx\n * import { getServerSession } from \"next-token-auth/server\";\n *\n * export default async function Page({ request }) {\n * const session = await getServerSession(request, config);\n * if (!session.isAuthenticated) redirect(\"/login\");\n * }\n */\nexport async function getServerSession<User = unknown>(\n req: NextRequest,\n config: AuthConfig<User>\n): Promise<AuthSession<User>> {\n const cookieName = config.token.cookieName ?? \"next-token-auth.session\";\n const cookieValue = req.cookies.get(cookieName)?.value;\n\n if (!cookieValue) {\n return { user: null, tokens: null, isAuthenticated: false };\n }\n\n let tokens: AuthTokens | null = null;\n\n try {\n const json = await decrypt(cookieValue, config.secret);\n tokens = JSON.parse(json) as AuthTokens;\n } catch {\n return { user: null, tokens: null, isAuthenticated: false };\n }\n\n const now = Date.now();\n const threshold = (config.refreshThreshold ?? 60) * 1000;\n const accessExpired = now >= tokens.accessTokenExpiresAt - threshold;\n const refreshExpired = tokens.refreshTokenExpiresAt\n ? now >= tokens.refreshTokenExpiresAt\n : false;\n\n // Both expired → clear session\n if (accessExpired && refreshExpired) {\n return { user: null, tokens: null, isAuthenticated: false };\n }\n\n // Access expired but refresh valid → attempt server-side refresh\n if (accessExpired && !refreshExpired) {\n const refreshed = await serverRefresh<User>(tokens, config);\n if (!refreshed) {\n return { user: null, tokens: null, isAuthenticated: false };\n }\n tokens = refreshed;\n }\n\n const user = await fetchUser<User>(tokens.accessToken, config);\n\n return {\n user,\n tokens,\n isAuthenticated: true,\n };\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nasync function serverRefresh<User>(\n tokens: AuthTokens,\n config: AuthConfig<User>\n): Promise<AuthTokens | null> {\n try {\n const fetchFn = config.fetchFn ?? fetch;\n const baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n const refreshPath = config.endpoints.refresh.replace(/^\\//, \"\");\n\n const res = await fetchFn(`${baseUrl}/${refreshPath}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refreshToken: tokens.refreshToken }),\n });\n\n if (!res.ok) return null;\n\n const data = (await res.json()) as LoginResponse<User>;\n const strategy = config.expiry?.strategy ?? \"hybrid\";\n\n return {\n accessToken: data.accessToken,\n refreshToken: data.refreshToken,\n accessTokenExpiresAt: resolveAccessTokenExpiry(\n data,\n config.expiry?.accessTokenExpiresIn,\n strategy\n ),\n refreshTokenExpiresAt: resolveRefreshTokenExpiry(\n data,\n config.expiry?.refreshTokenExpiresIn,\n strategy\n ),\n };\n } catch {\n return null;\n }\n}\n\nasync function fetchUser<User>(\n accessToken: string,\n config: AuthConfig<User>\n): Promise<User | null> {\n if (!config.endpoints.me) return null;\n\n try {\n const fetchFn = config.fetchFn ?? fetch;\n const baseUrl = config.baseUrl.replace(/\\/$/, \"\");\n const mePath = config.endpoints.me.replace(/^\\//, \"\");\n\n const res = await fetchFn(`${baseUrl}/${mePath}`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n\n if (!res.ok) return null;\n return (await res.json()) as User;\n } catch {\n return null;\n }\n}\n","import type { AuthConfig, AuthSession } from \"../types\";\nimport { getServerSession } from \"./getServerSession\";\n\ntype NextRequest = {\n cookies: { get(name: string): { value: string } | undefined };\n nextUrl: { pathname: string };\n};\n\ntype NextResponse = {\n redirect(url: URL): NextResponse;\n next(): NextResponse;\n};\n\ntype RouteHandler<User = unknown> = (\n req: NextRequest,\n session: AuthSession<User>\n) => Promise<Response> | Response;\n\n/**\n * Higher-order function that wraps a Next.js route handler with auth protection.\n * Redirects unauthenticated requests to the login page.\n *\n * @example\n * // app/api/protected/route.ts\n * export const GET = withAuth(config, async (req, session) => {\n * return Response.json({ user: session.user });\n * });\n */\nexport function withAuth<User = unknown>(\n config: AuthConfig<User>,\n handler: RouteHandler<User>,\n options: { redirectTo?: string } = {}\n) {\n return async (req: NextRequest): Promise<Response> => {\n const session = await getServerSession<User>(req, config);\n\n if (!session.isAuthenticated) {\n const redirectTo = options.redirectTo ?? \"/login\";\n const loginUrl = new URL(redirectTo, `https://${req.nextUrl.pathname}`);\n return Response.redirect(loginUrl);\n }\n\n return handler(req, session);\n };\n}\n","import type { AuthConfig } from \"../types\";\nimport { decrypt } from \"../utils/crypto\";\nimport type { AuthTokens } from \"../types\";\n\n/**\n * Next.js middleware factory for route protection.\n *\n * @example\n * // middleware.ts (project root)\n * import { authMiddleware } from \"next-token-auth/server\";\n * import { authConfig } from \"./lib/auth\";\n *\n * export const middleware = authMiddleware(authConfig);\n *\n * export const config = {\n * matcher: [\"/dashboard/:path*\", \"/login\", \"/register\"],\n * };\n */\nexport function authMiddleware<User = unknown>(authConfig: AuthConfig<User>) {\n return async function middleware(request: {\n cookies: { get(name: string): { value: string } | undefined };\n nextUrl: { pathname: string; origin: string };\n url: string;\n }): Promise<Response> {\n const { NextResponse } = await import(\"next/server\");\n\n const pathname = request.nextUrl.pathname;\n const cookieName = authConfig.token.cookieName ?? \"next-token-auth.session\";\n const cookieValue = request.cookies.get(cookieName)?.value;\n\n // Resolve whether the session is valid\n const isAuthenticated = await checkSession(cookieValue, authConfig.secret);\n\n // ── Guest-only routes (e.g. /login, /register) ──────────────────────────\n // Accessible only when NOT authenticated. Redirect authenticated users away.\n const guestOnlyRoutes = authConfig.routes?.guestOnly ?? [];\n if (isGuestOnlyRoute(pathname, guestOnlyRoutes)) {\n if (isAuthenticated) {\n const redirectTo = authConfig.routes?.redirectAuthenticatedTo ?? \"/dashboard\";\n return NextResponse.redirect(new URL(redirectTo, request.nextUrl.origin));\n }\n return NextResponse.next();\n }\n\n // ── Public routes ────────────────────────────────────────────────────────\n // Always accessible regardless of auth state.\n const publicRoutes = authConfig.routes?.public ?? [];\n if (isPublicRoute(pathname, publicRoutes)) {\n return NextResponse.next();\n }\n\n // ── Protected routes ─────────────────────────────────────────────────────\n const protectedRoutes = authConfig.routes?.protected ?? [];\n const requiresAuth =\n protectedRoutes.length === 0 || isProtectedRoute(pathname, protectedRoutes);\n\n if (!requiresAuth) {\n return NextResponse.next();\n }\n\n if (!isAuthenticated) {\n return redirectToLogin(request, NextResponse);\n }\n\n return NextResponse.next();\n };\n}\n\n// ─── Session check ────────────────────────────────────────────────────────────\n\nasync function checkSession(\n cookieValue: string | undefined,\n secret: string\n): Promise<boolean> {\n if (!cookieValue) return false;\n\n try {\n const json = await decrypt(cookieValue, secret);\n const tokens = JSON.parse(json) as AuthTokens;\n\n const now = Date.now();\n const refreshExpired = tokens.refreshTokenExpiresAt\n ? now >= tokens.refreshTokenExpiresAt\n : false;\n\n return !refreshExpired;\n } catch {\n return false;\n }\n}\n\n// ─── Route matchers ───────────────────────────────────────────────────────────\n\nfunction isGuestOnlyRoute(pathname: string, routes: string[]): boolean {\n return routes.some((route) => matchRoute(pathname, route));\n}\n\nfunction isPublicRoute(pathname: string, publicRoutes: string[]): boolean {\n return publicRoutes.some((route) => matchRoute(pathname, route));\n}\n\nfunction isProtectedRoute(pathname: string, protectedRoutes: string[]): boolean {\n return protectedRoutes.some((route) => matchRoute(pathname, route));\n}\n\nfunction matchRoute(pathname: string, pattern: string): boolean {\n if (pattern.endsWith(\"*\")) {\n return pathname.startsWith(pattern.slice(0, -1));\n }\n return pathname === pattern;\n}\n\nfunction redirectToLogin(\n request: { nextUrl: { origin: string } },\n NextResponse: { redirect(url: URL): Response }\n): Response {\n return NextResponse.redirect(new URL(\"/login\", request.nextUrl.origin));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-token-auth",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Production-grade authentication library for Next.js (App Router & Pages Router)",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -27,7 +27,7 @@
27
27
  }
28
28
  },
29
29
  "scripts": {
30
- "build": "tsup src/index.ts --dts",
30
+ "build": "tsup",
31
31
  "dev": "tsup --watch",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "lint": "eslint src --ext .ts,.tsx"