next-token-auth 1.0.11 → 1.0.12
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 +13 -8
- package/dist/{index-ChpgBFYz.d.mts → index-CejL5heu.d.mts} +6 -1
- package/dist/{index-ChpgBFYz.d.ts → index-CejL5heu.d.ts} +6 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/react/index.d.mts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +9 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +9 -13
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,8 +202,9 @@ interface AuthConfig<User = unknown> {
|
|
|
202
202
|
|
|
203
203
|
routes?: {
|
|
204
204
|
public: string[]; // always accessible, e.g. ["/", "/about"]
|
|
205
|
-
protected: string[]; // require auth, supports wildcard: "/dashboard
|
|
206
|
-
guestOnly?: string[]; // accessible only when NOT authenticated, e.g. ["/login", "/register"]
|
|
205
|
+
protected: string[]; // require auth, supports wildcard: "/dashboard*"
|
|
206
|
+
guestOnly?: string[]; // accessible only when NOT authenticated, e.g. ["/auth/login", "/auth/register"]
|
|
207
|
+
loginPath?: string; // where to redirect unauthenticated users (default: "/login")
|
|
207
208
|
redirectAuthenticatedTo?: string; // where to send authenticated users who hit a guestOnly route (default: "/dashboard")
|
|
208
209
|
};
|
|
209
210
|
|
|
@@ -366,9 +367,10 @@ export const authConfig: AuthConfig = {
|
|
|
366
367
|
// ...
|
|
367
368
|
routes: {
|
|
368
369
|
public: ["/", "/about"],
|
|
369
|
-
guestOnly: ["/login", "/register"],
|
|
370
|
-
protected: ["/dashboard
|
|
371
|
-
|
|
370
|
+
guestOnly: ["/auth/login", "/auth/register"], // authenticated users get redirected away
|
|
371
|
+
protected: ["/dashboard*", "/profile*"],
|
|
372
|
+
loginPath: "/auth/login", // where to redirect unauthenticated users
|
|
373
|
+
redirectAuthenticatedTo: "/dashboard", // where to redirect authenticated users on guestOnly routes
|
|
372
374
|
},
|
|
373
375
|
};
|
|
374
376
|
```
|
|
@@ -382,7 +384,7 @@ export const middleware = authMiddleware(authConfig);
|
|
|
382
384
|
|
|
383
385
|
export const config = {
|
|
384
386
|
// Include all routes you want the middleware to run on
|
|
385
|
-
matcher: ["/login", "/register", "/dashboard
|
|
387
|
+
matcher: ["/auth/login", "/auth/register", "/dashboard*", "/profile*"],
|
|
386
388
|
};
|
|
387
389
|
```
|
|
388
390
|
|
|
@@ -390,9 +392,12 @@ Route resolution order inside the middleware:
|
|
|
390
392
|
|
|
391
393
|
1. `guestOnly` — if authenticated, redirect to `redirectAuthenticatedTo`
|
|
392
394
|
2. `public` — always allow through
|
|
393
|
-
3. `protected` — require valid session, redirect to
|
|
395
|
+
3. `protected` — require valid session, redirect to `loginPath` if missing
|
|
394
396
|
|
|
395
|
-
|
|
397
|
+
Two things to keep in mind:
|
|
398
|
+
|
|
399
|
+
- Wildcard patterns use `*` at the end: `"/dashboard*"` matches `/dashboard`, `/dashboard/`, and `/dashboard/settings`
|
|
400
|
+
- The `matcher` in `export const config` controls which routes Next.js runs the middleware on at all — make sure it covers both your protected and guest-only routes
|
|
396
401
|
|
|
397
402
|
---
|
|
398
403
|
|
|
@@ -42,7 +42,7 @@ interface AuthConfig<User = unknown> {
|
|
|
42
42
|
/** Paths that require authentication */
|
|
43
43
|
protected: string[];
|
|
44
44
|
/**
|
|
45
|
-
* Paths only accessible when NOT authenticated (e.g. /login, /register).
|
|
45
|
+
* Paths only accessible when NOT authenticated (e.g. /auth/login, /auth/register).
|
|
46
46
|
* Authenticated users are redirected to `redirectAuthenticatedTo`.
|
|
47
47
|
*/
|
|
48
48
|
guestOnly?: string[];
|
|
@@ -51,6 +51,11 @@ interface AuthConfig<User = unknown> {
|
|
|
51
51
|
* @default "/dashboard"
|
|
52
52
|
*/
|
|
53
53
|
redirectAuthenticatedTo?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Path to redirect unauthenticated users to when they hit a protected route.
|
|
56
|
+
* @default "/login"
|
|
57
|
+
*/
|
|
58
|
+
loginPath?: string;
|
|
54
59
|
};
|
|
55
60
|
token: {
|
|
56
61
|
storage: "cookie" | "memory";
|
|
@@ -42,7 +42,7 @@ interface AuthConfig<User = unknown> {
|
|
|
42
42
|
/** Paths that require authentication */
|
|
43
43
|
protected: string[];
|
|
44
44
|
/**
|
|
45
|
-
* Paths only accessible when NOT authenticated (e.g. /login, /register).
|
|
45
|
+
* Paths only accessible when NOT authenticated (e.g. /auth/login, /auth/register).
|
|
46
46
|
* Authenticated users are redirected to `redirectAuthenticatedTo`.
|
|
47
47
|
*/
|
|
48
48
|
guestOnly?: string[];
|
|
@@ -51,6 +51,11 @@ interface AuthConfig<User = unknown> {
|
|
|
51
51
|
* @default "/dashboard"
|
|
52
52
|
*/
|
|
53
53
|
redirectAuthenticatedTo?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Path to redirect unauthenticated users to when they hit a protected route.
|
|
56
|
+
* @default "/login"
|
|
57
|
+
*/
|
|
58
|
+
loginPath?: string;
|
|
54
59
|
};
|
|
55
60
|
token: {
|
|
56
61
|
storage: "cookie" | "memory";
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AuthConfig, b as AuthTokens, a as AuthSession, L as LoginInput, E as ExpiryInput } from './index-
|
|
2
|
-
export { c as ExpiryStrategy, d as LoginResponse } from './index-
|
|
1
|
+
import { A as AuthConfig, b as AuthTokens, a as AuthSession, L as LoginInput, E as ExpiryInput } from './index-CejL5heu.mjs';
|
|
2
|
+
export { c as ExpiryStrategy, d as LoginResponse } from './index-CejL5heu.mjs';
|
|
3
3
|
export { AuthProvider, UseAuthReturn, UseRequireAuthOptions, useAuth, useRequireAuth, useSession } from './react/index.mjs';
|
|
4
4
|
import 'react/jsx-runtime';
|
|
5
5
|
import 'react';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AuthConfig, b as AuthTokens, a as AuthSession, L as LoginInput, E as ExpiryInput } from './index-
|
|
2
|
-
export { c as ExpiryStrategy, d as LoginResponse } from './index-
|
|
1
|
+
import { A as AuthConfig, b as AuthTokens, a as AuthSession, L as LoginInput, E as ExpiryInput } from './index-CejL5heu.js';
|
|
2
|
+
export { c as ExpiryStrategy, d as LoginResponse } from './index-CejL5heu.js';
|
|
3
3
|
export { AuthProvider, UseAuthReturn, UseRequireAuthOptions, useAuth, useRequireAuth, useSession } from './react/index.js';
|
|
4
4
|
import 'react/jsx-runtime';
|
|
5
5
|
import 'react';
|
package/dist/react/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { A as AuthConfig, a as AuthSession, L as LoginInput } from '../index-
|
|
3
|
+
import { A as AuthConfig, a as AuthSession, L as LoginInput } from '../index-CejL5heu.mjs';
|
|
4
4
|
|
|
5
5
|
interface AuthProviderProps<User = unknown> {
|
|
6
6
|
config: AuthConfig<User>;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { A as AuthConfig, a as AuthSession, L as LoginInput } from '../index-
|
|
3
|
+
import { A as AuthConfig, a as AuthSession, L as LoginInput } from '../index-CejL5heu.js';
|
|
4
4
|
|
|
5
5
|
interface AuthProviderProps<User = unknown> {
|
|
6
6
|
config: AuthConfig<User>;
|
package/dist/server/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AuthConfig, a as AuthSession } from '../index-
|
|
1
|
+
import { A as AuthConfig, a as AuthSession } from '../index-CejL5heu.mjs';
|
|
2
2
|
|
|
3
3
|
type NextRequest$1 = {
|
|
4
4
|
cookies: {
|
|
@@ -61,7 +61,7 @@ declare function withAuth<User = unknown>(config: AuthConfig<User>, handler: Rou
|
|
|
61
61
|
* export const middleware = authMiddleware(authConfig);
|
|
62
62
|
*
|
|
63
63
|
* export const config = {
|
|
64
|
-
* matcher: ["/
|
|
64
|
+
* matcher: ["/auth/login", "/auth/register", "/dashboard*", "/profile*"],
|
|
65
65
|
* };
|
|
66
66
|
*/
|
|
67
67
|
declare function authMiddleware<User = unknown>(authConfig: AuthConfig<User>): (request: {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AuthConfig, a as AuthSession } from '../index-
|
|
1
|
+
import { A as AuthConfig, a as AuthSession } from '../index-CejL5heu.js';
|
|
2
2
|
|
|
3
3
|
type NextRequest$1 = {
|
|
4
4
|
cookies: {
|
|
@@ -61,7 +61,7 @@ declare function withAuth<User = unknown>(config: AuthConfig<User>, handler: Rou
|
|
|
61
61
|
* export const middleware = authMiddleware(authConfig);
|
|
62
62
|
*
|
|
63
63
|
* export const config = {
|
|
64
|
-
* matcher: ["/
|
|
64
|
+
* matcher: ["/auth/login", "/auth/register", "/dashboard*", "/profile*"],
|
|
65
65
|
* };
|
|
66
66
|
*/
|
|
67
67
|
declare function authMiddleware<User = unknown>(authConfig: AuthConfig<User>): (request: {
|
package/dist/server/index.js
CHANGED
|
@@ -234,16 +234,17 @@ function authMiddleware(authConfig) {
|
|
|
234
234
|
return NextResponse.next();
|
|
235
235
|
}
|
|
236
236
|
const publicRoutes = authConfig.routes?.public ?? [];
|
|
237
|
-
if (
|
|
237
|
+
if (matchesAny(pathname, publicRoutes)) {
|
|
238
238
|
return NextResponse.next();
|
|
239
239
|
}
|
|
240
240
|
const protectedRoutes = authConfig.routes?.protected ?? [];
|
|
241
|
-
const requiresAuth = protectedRoutes.length === 0 ||
|
|
241
|
+
const requiresAuth = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
|
|
242
242
|
if (!requiresAuth) {
|
|
243
243
|
return NextResponse.next();
|
|
244
244
|
}
|
|
245
245
|
if (!isAuthenticated) {
|
|
246
|
-
|
|
246
|
+
const loginPath = authConfig.routes?.loginPath ?? "/login";
|
|
247
|
+
return NextResponse.redirect(new URL(loginPath, request.nextUrl.origin));
|
|
247
248
|
}
|
|
248
249
|
return NextResponse.next();
|
|
249
250
|
};
|
|
@@ -261,23 +262,18 @@ async function checkSession(cookieValue, secret) {
|
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
function isGuestOnlyRoute(pathname, routes) {
|
|
264
|
-
return
|
|
265
|
+
return matchesAny(pathname, routes);
|
|
265
266
|
}
|
|
266
|
-
function
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
function isProtectedRoute(pathname, protectedRoutes) {
|
|
270
|
-
return protectedRoutes.some((route) => matchRoute(pathname, route));
|
|
267
|
+
function matchesAny(pathname, patterns) {
|
|
268
|
+
return patterns.some((pattern) => matchRoute(pathname, pattern));
|
|
271
269
|
}
|
|
272
270
|
function matchRoute(pathname, pattern) {
|
|
273
271
|
if (pattern.endsWith("*")) {
|
|
274
|
-
|
|
272
|
+
const base = pattern.slice(0, -1);
|
|
273
|
+
return pathname === base || pathname.startsWith(base + "/") || pathname.startsWith(base);
|
|
275
274
|
}
|
|
276
275
|
return pathname === pattern;
|
|
277
276
|
}
|
|
278
|
-
function redirectToLogin(request, NextResponse) {
|
|
279
|
-
return NextResponse.redirect(new URL("/login", request.nextUrl.origin));
|
|
280
|
-
}
|
|
281
277
|
|
|
282
278
|
exports.authMiddleware = authMiddleware;
|
|
283
279
|
exports.getServerSession = getServerSession;
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +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.js","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"]}
|
|
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.js","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: [\"/auth/login\", \"/auth/register\", \"/dashboard*\", \"/profile*\"],\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 const isAuthenticated = await checkSession(cookieValue, authConfig.secret);\n\n // ── Guest-only routes ────────────────────────────────────────────────────\n // Accessible only when NOT authenticated.\n // Authenticated users are redirected to redirectAuthenticatedTo.\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 const publicRoutes = authConfig.routes?.public ?? [];\n if (matchesAny(pathname, publicRoutes)) {\n return NextResponse.next();\n }\n\n // ── Protected routes ─────────────────────────────────────────────────────\n const protectedRoutes = authConfig.routes?.protected ?? [];\n const requiresAuth =\n protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);\n\n if (!requiresAuth) {\n return NextResponse.next();\n }\n\n if (!isAuthenticated) {\n // Use the configured login endpoint path, falling back to \"/login\"\n const loginPath = authConfig.routes?.loginPath ?? \"/login\";\n return NextResponse.redirect(new URL(loginPath, request.nextUrl.origin));\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 const now = Date.now();\n const refreshExpired = tokens.refreshTokenExpiresAt\n ? now >= tokens.refreshTokenExpiresAt\n : false;\n return !refreshExpired;\n } catch {\n return false;\n }\n}\n\n// ─── Route matchers ───────────────────────────────────────────────────────────\n\nfunction isGuestOnlyRoute(pathname: string, routes: string[]): boolean {\n return matchesAny(pathname, routes);\n}\n\n/**\n * Matches a pathname against a list of patterns.\n * Supports wildcards: \"/dashboard*\" matches \"/dashboard\", \"/dashboard/\", \"/dashboard/settings\"\n */\nfunction matchesAny(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchRoute(pathname, pattern));\n}\n\nfunction matchRoute(pathname: string, pattern: string): boolean {\n if (pattern.endsWith(\"*\")) {\n const base = pattern.slice(0, -1); // e.g. \"/dashboard\"\n // matches \"/dashboard\", \"/dashboard/\", \"/dashboard/anything\"\n return pathname === base || pathname.startsWith(base + \"/\") || pathname.startsWith(base);\n }\n return pathname === pattern;\n}\n"]}
|
package/dist/server/index.mjs
CHANGED
|
@@ -232,16 +232,17 @@ function authMiddleware(authConfig) {
|
|
|
232
232
|
return NextResponse.next();
|
|
233
233
|
}
|
|
234
234
|
const publicRoutes = authConfig.routes?.public ?? [];
|
|
235
|
-
if (
|
|
235
|
+
if (matchesAny(pathname, publicRoutes)) {
|
|
236
236
|
return NextResponse.next();
|
|
237
237
|
}
|
|
238
238
|
const protectedRoutes = authConfig.routes?.protected ?? [];
|
|
239
|
-
const requiresAuth = protectedRoutes.length === 0 ||
|
|
239
|
+
const requiresAuth = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
|
|
240
240
|
if (!requiresAuth) {
|
|
241
241
|
return NextResponse.next();
|
|
242
242
|
}
|
|
243
243
|
if (!isAuthenticated) {
|
|
244
|
-
|
|
244
|
+
const loginPath = authConfig.routes?.loginPath ?? "/login";
|
|
245
|
+
return NextResponse.redirect(new URL(loginPath, request.nextUrl.origin));
|
|
245
246
|
}
|
|
246
247
|
return NextResponse.next();
|
|
247
248
|
};
|
|
@@ -259,23 +260,18 @@ async function checkSession(cookieValue, secret) {
|
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
function isGuestOnlyRoute(pathname, routes) {
|
|
262
|
-
return
|
|
263
|
+
return matchesAny(pathname, routes);
|
|
263
264
|
}
|
|
264
|
-
function
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
function isProtectedRoute(pathname, protectedRoutes) {
|
|
268
|
-
return protectedRoutes.some((route) => matchRoute(pathname, route));
|
|
265
|
+
function matchesAny(pathname, patterns) {
|
|
266
|
+
return patterns.some((pattern) => matchRoute(pathname, pattern));
|
|
269
267
|
}
|
|
270
268
|
function matchRoute(pathname, pattern) {
|
|
271
269
|
if (pattern.endsWith("*")) {
|
|
272
|
-
|
|
270
|
+
const base = pattern.slice(0, -1);
|
|
271
|
+
return pathname === base || pathname.startsWith(base + "/") || pathname.startsWith(base);
|
|
273
272
|
}
|
|
274
273
|
return pathname === pattern;
|
|
275
274
|
}
|
|
276
|
-
function redirectToLogin(request, NextResponse) {
|
|
277
|
-
return NextResponse.redirect(new URL("/login", request.nextUrl.origin));
|
|
278
|
-
}
|
|
279
275
|
|
|
280
276
|
export { authMiddleware, getServerSession, withAuth };
|
|
281
277
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +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":";AAKA,IAAM,IAAA,GAAO,SAAA;AAGb,SAAS,cAAA,GAAiB;AACxB,EAAA,OAAO,IAAI,WAAA,EAAY;AACzB;AAEA,SAAS,cAAA,GAAiB;AACxB,EAAA,OAAO,IAAI,WAAA,EAAY;AACzB;AAEA,eAAe,UAAU,MAAA,EAAoC;AAC3D,EAAA,MAAM,GAAA,GAAM,cAAA,EAAe,CAAE,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,EAAA,EAAI,GAAG,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA;AACvE,EAAA,OAAO,MAAA,CAAO,OAAO,SAAA,CAAU,KAAA,EAAO,KAAK,EAAE,IAAA,EAAM,IAAA,EAAK,EAAG,KAAA,EAAO;AAAA,IAChE,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAuBA,eAAsB,OAAA,CAAQ,MAAc,MAAA,EAAiC;AAC3E,EAAA,MAAM,CAAC,KAAA,EAAO,SAAS,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACzC,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,SAAA,EAAW;AACxB,IAAA,MAAM,IAAI,MAAM,oCAAoC,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,MAAM,CAAA;AAClC,EAAA,MAAM,OAAA,GAAU,eAAe,KAAK,CAAA;AACpC,EAAA,MAAM,EAAA,GAAK,QAAQ,MAAA,CAAO,KAAA;AAAA,IACxB,OAAA,CAAQ,UAAA;AAAA,IACR,OAAA,CAAQ,aAAa,OAAA,CAAQ;AAAA,GAC/B;AAEA,EAAA,MAAM,WAAA,GAAc,eAAe,SAAS,CAAA;AAC5C,EAAA,MAAM,YAAA,GAAe,YAAY,MAAA,CAAO,KAAA;AAAA,IACtC,WAAA,CAAY,UAAA;AAAA,IACZ,WAAA,CAAY,aAAa,WAAA,CAAY;AAAA,GACvC;AAEA,EAAA,MAAM,WAAA,GAAc,MAAM,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,EAAE,IAAA,EAAM,IAAA,EAAM,EAAA,EAAG,EAAG,GAAA,EAAK,YAAY,CAAA;AAErF,EAAA,OAAO,cAAA,EAAe,CAAE,MAAA,CAAO,WAAW,CAAA;AAC5C;AAWA,SAAS,eAAe,GAAA,EAAyB;AAC/C,EAAA,MAAM,MAAA,GAAS,IAAI,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,MAAM,GAAG,CAAA;AACvD,EAAA,MAAM,MAAA,GAAS,KAAK,MAAM,CAAA;AAC1B,EAAA,OAAO,UAAA,CAAW,KAAK,MAAA,EAAQ,CAAC,MAAM,CAAA,CAAE,UAAA,CAAW,CAAC,CAAC,CAAA;AACvD;;;AChFA,IAAM,QAAA,GAAmC;AAAA,EACvC,CAAA,EAAG,CAAA;AAAA,EACH,CAAA,EAAG,EAAA;AAAA,EACH,CAAA,EAAG,IAAA;AAAA,EACH,CAAA,EAAG,KAAA;AAAA,EACH,CAAA,EAAG;AACL,CAAA;AAUO,SAAS,YAAY,KAAA,EAA6B;AACvD,EAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,KAAU,IAAA,EAAM;AACzC,IAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,EACzD;AAEA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,KAAA,IAAS,CAAA,EAAG,MAAM,IAAI,MAAM,qCAAqC,CAAA;AACrE,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAG3B,EAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA,EAAG;AACzB,IAAA,OAAO,QAAA,CAAS,SAAS,EAAE,CAAA;AAAA,EAC7B;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,gCAAgC,CAAA;AAC5D,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,qCAAqC,KAAK,CAAA,oEAAA;AAAA,KAE5C;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,KAAA,CAAM,CAAC,CAAC,CAAA;AACjC,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,CAAC,CAAA,CAAE,WAAA,EAAY;AAClC,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,QAAA,CAAS,IAAI,CAAC,CAAA;AAC1C;AAKO,SAAS,eAAA,CACd,KAAA,EACA,eAAA,GAAkB,GAAA,EACV;AACR,EAAA,IAAI;AACF,IAAA,OAAO,YAAY,KAAK,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAMO,SAAS,wBAAA,CACd,QAAA,EACA,YAAA,EACA,QAAA,GAA2B,QAAA,EACnB;AACR,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAErB,EAAA,MAAM,WAAA,GACJ,QAAA,CAAS,oBAAA,IAAwB,QAAA,CAAS,SAAA,IAAa,MAAA;AAEzD,EAAA,IAAI,aAAa,SAAA,EAAW;AAC1B,IAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,OAAO,GAAA,GAAM,WAAA,CAAY,WAAW,CAAA,GAAI,GAAA;AAAA,EAC1C;AAEA,EAAA,IAAI,aAAa,QAAA,EAAU;AACzB,IAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,OAAO,GAAA,GAAM,WAAA,CAAY,YAAY,CAAA,GAAI,GAAA;AAAA,EAC3C;AAGA,EAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,IAAA,OAAO,GAAA,GAAM,eAAA,CAAgB,WAAW,CAAA,GAAI,GAAA;AAAA,EAC9C;AACA,EAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,IAAA,OAAO,GAAA,GAAM,eAAA,CAAgB,YAAY,CAAA,GAAI,GAAA;AAAA,EAC/C;AAGA,EAAA,OAAO,MAAM,GAAA,GAAM,GAAA;AACrB;AAKO,SAAS,yBAAA,CACd,QAAA,EACA,YAAA,EACA,QAAA,GAA2B,QAAA,EACP;AACpB,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,MAAM,cAAc,QAAA,CAAS,qBAAA;AAE7B,EAAA,IAAI,aAAa,SAAA,EAAW;AAC1B,IAAA,OAAO,gBAAgB,MAAA,GACnB,GAAA,GAAM,WAAA,CAAY,WAAW,IAAI,GAAA,GACjC,MAAA;AAAA,EACN;AAEA,EAAA,IAAI,aAAa,QAAA,EAAU;AACzB,IAAA,OAAO,iBAAiB,MAAA,GACpB,GAAA,GAAM,WAAA,CAAY,YAAY,IAAI,GAAA,GAClC,MAAA;AAAA,EACN;AAGA,EAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,IAAA,OAAO,GAAA,GAAM,eAAA,CAAgB,WAAW,CAAA,GAAI,GAAA;AAAA,EAC9C;AACA,EAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,IAAA,OAAO,GAAA,GAAM,eAAA,CAAgB,YAAY,CAAA,GAAI,GAAA;AAAA,EAC/C;AAEA,EAAA,OAAO,MAAA;AACT;;;AC7GA,eAAsB,gBAAA,CACpB,KACA,MAAA,EAC4B;AAC5B,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,KAAA,CAAM,UAAA,IAAc,yBAAA;AAC9C,EAAA,MAAM,WAAA,GAAc,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,EAAG,KAAA;AAEjD,EAAA,IAAI,CAAC,WAAA,EAAa;AAChB,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,iBAAiB,KAAA,EAAM;AAAA,EAC5D;AAEA,EAAA,IAAI,MAAA,GAA4B,IAAA;AAEhC,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,WAAA,EAAa,OAAO,MAAM,CAAA;AACrD,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,iBAAiB,KAAA,EAAM;AAAA,EAC5D;AAEA,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,MAAM,SAAA,GAAA,CAAa,MAAA,CAAO,gBAAA,IAAoB,EAAA,IAAM,GAAA;AACpD,EAAA,MAAM,aAAA,GAAgB,GAAA,IAAO,MAAA,CAAO,oBAAA,GAAuB,SAAA;AAC3D,EAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,qBAAA,GAC1B,GAAA,IAAO,OAAO,qBAAA,GACd,KAAA;AAGJ,EAAA,IAAI,iBAAiB,cAAA,EAAgB;AACnC,IAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,iBAAiB,KAAA,EAAM;AAAA,EAC5D;AAGA,EAAA,IAAI,aAAA,IAAiB,CAAC,cAAA,EAAgB;AACpC,IAAA,MAAM,SAAA,GAAY,MAAM,aAAA,CAAoB,MAAA,EAAQ,MAAM,CAAA;AAC1D,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,iBAAiB,KAAA,EAAM;AAAA,IAC5D;AACA,IAAA,MAAA,GAAS,SAAA;AAAA,EACX;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAgB,MAAA,CAAO,aAAa,MAAM,CAAA;AAE7D,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,MAAA;AAAA,IACA,eAAA,EAAiB;AAAA,GACnB;AACF;AAIA,eAAe,aAAA,CACb,QACA,MAAA,EAC4B;AAC5B,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,OAAO,OAAA,IAAW,KAAA;AAClC,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD,IAAA,MAAM,cAAc,MAAA,CAAO,SAAA,CAAU,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAE9D,IAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,GAAG,OAAO,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA,EAAI;AAAA,MACrD,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,YAAA,EAAc,MAAA,CAAO,cAAc;AAAA,KAC3D,CAAA;AAED,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,IAAA;AAEpB,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,IAAA,MAAM,QAAA,GAAW,MAAA,CAAO,MAAA,EAAQ,QAAA,IAAY,QAAA;AAE5C,IAAA,OAAO;AAAA,MACL,aAAa,IAAA,CAAK,WAAA;AAAA,MAClB,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,oBAAA,EAAsB,wBAAA;AAAA,QACpB,IAAA;AAAA,QACA,OAAO,MAAA,EAAQ,oBAAA;AAAA,QACf;AAAA,OACF;AAAA,MACA,qBAAA,EAAuB,yBAAA;AAAA,QACrB,IAAA;AAAA,QACA,OAAO,MAAA,EAAQ,qBAAA;AAAA,QACf;AAAA;AACF,KACF;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,eAAe,SAAA,CACb,aACA,MAAA,EACsB;AACtB,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,EAAA,EAAI,OAAO,IAAA;AAEjC,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,OAAO,OAAA,IAAW,KAAA;AAClC,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD,IAAA,MAAM,SAAS,MAAA,CAAO,SAAA,CAAU,EAAA,CAAG,OAAA,CAAQ,OAAO,EAAE,CAAA;AAEpD,IAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,GAAG,OAAO,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA,EAAI;AAAA,MAChD,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,WAAW,CAAA,CAAA;AAAG,KACnD,CAAA;AAED,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,IAAA;AACpB,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;AChHO,SAAS,QAAA,CACd,MAAA,EACA,OAAA,EACA,OAAA,GAAmC,EAAC,EACpC;AACA,EAAA,OAAO,OAAO,GAAA,KAAwC;AACpD,IAAA,MAAM,OAAA,GAAU,MAAM,gBAAA,CAAuB,GAAA,EAAK,MAAM,CAAA;AAExD,IAAA,IAAI,CAAC,QAAQ,eAAA,EAAiB;AAC5B,MAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,QAAA;AACzC,MAAA,MAAM,QAAA,GAAW,IAAI,GAAA,CAAI,UAAA,EAAY,WAAW,GAAA,CAAI,OAAA,CAAQ,QAAQ,CAAA,CAAE,CAAA;AACtE,MAAA,OAAO,QAAA,CAAS,SAAS,QAAQ,CAAA;AAAA,IACnC;AAEA,IAAA,OAAO,OAAA,CAAQ,KAAK,OAAO,CAAA;AAAA,EAC7B,CAAA;AACF;;;AC1BO,SAAS,eAA+B,UAAA,EAA8B;AAC3E,EAAA,OAAO,eAAe,WAAW,OAAA,EAIX;AACpB,IAAA,MAAM,EAAE,YAAA,EAAa,GAAI,MAAM,OAAO,aAAa,CAAA;AAEnD,IAAA,MAAM,QAAA,GAAW,QAAQ,OAAA,CAAQ,QAAA;AACjC,IAAA,MAAM,UAAA,GAAa,UAAA,CAAW,KAAA,CAAM,UAAA,IAAc,yBAAA;AAClD,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,EAAG,KAAA;AAGrD,IAAA,MAAM,eAAA,GAAkB,MAAM,YAAA,CAAa,WAAA,EAAa,WAAW,MAAM,CAAA;AAIzE,IAAA,MAAM,eAAA,GAAkB,UAAA,CAAW,MAAA,EAAQ,SAAA,IAAa,EAAC;AACzD,IAAA,IAAI,gBAAA,CAAiB,QAAA,EAAU,eAAe,CAAA,EAAG;AAC/C,MAAA,IAAI,eAAA,EAAiB;AACnB,QAAA,MAAM,UAAA,GAAa,UAAA,CAAW,MAAA,EAAQ,uBAAA,IAA2B,YAAA;AACjE,QAAA,OAAO,YAAA,CAAa,SAAS,IAAI,GAAA,CAAI,YAAY,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,MAC1E;AACA,MAAA,OAAO,aAAa,IAAA,EAAK;AAAA,IAC3B;AAIA,IAAA,MAAM,YAAA,GAAe,UAAA,CAAW,MAAA,EAAQ,MAAA,IAAU,EAAC;AACnD,IAAA,IAAI,aAAA,CAAc,QAAA,EAAU,YAAY,CAAA,EAAG;AACzC,MAAA,OAAO,aAAa,IAAA,EAAK;AAAA,IAC3B;AAGA,IAAA,MAAM,eAAA,GAAkB,UAAA,CAAW,MAAA,EAAQ,SAAA,IAAa,EAAC;AACzD,IAAA,MAAM,eACJ,eAAA,CAAgB,MAAA,KAAW,CAAA,IAAK,gBAAA,CAAiB,UAAU,eAAe,CAAA;AAE5E,IAAA,IAAI,CAAC,YAAA,EAAc;AACjB,MAAA,OAAO,aAAa,IAAA,EAAK;AAAA,IAC3B;AAEA,IAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,MAAA,OAAO,eAAA,CAAgB,SAAS,YAAY,CAAA;AAAA,IAC9C;AAEA,IAAA,OAAO,aAAa,IAAA,EAAK;AAAA,EAC3B,CAAA;AACF;AAIA,eAAe,YAAA,CACb,aACA,MAAA,EACkB;AAClB,EAAA,IAAI,CAAC,aAAa,OAAO,KAAA;AAEzB,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,WAAA,EAAa,MAAM,CAAA;AAC9C,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAE9B,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,qBAAA,GAC1B,GAAA,IAAO,OAAO,qBAAA,GACd,KAAA;AAEJ,IAAA,OAAO,CAAC,cAAA;AAAA,EACV,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAIA,SAAS,gBAAA,CAAiB,UAAkB,MAAA,EAA2B;AACrE,EAAA,OAAO,OAAO,IAAA,CAAK,CAAC,UAAU,UAAA,CAAW,QAAA,EAAU,KAAK,CAAC,CAAA;AAC3D;AAEA,SAAS,aAAA,CAAc,UAAkB,YAAA,EAAiC;AACxE,EAAA,OAAO,aAAa,IAAA,CAAK,CAAC,UAAU,UAAA,CAAW,QAAA,EAAU,KAAK,CAAC,CAAA;AACjE;AAEA,SAAS,gBAAA,CAAiB,UAAkB,eAAA,EAAoC;AAC9E,EAAA,OAAO,gBAAgB,IAAA,CAAK,CAAC,UAAU,UAAA,CAAW,QAAA,EAAU,KAAK,CAAC,CAAA;AACpE;AAEA,SAAS,UAAA,CAAW,UAAkB,OAAA,EAA0B;AAC9D,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AACzB,IAAA,OAAO,SAAS,UAAA,CAAW,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA;AAAA,EACjD;AACA,EAAA,OAAO,QAAA,KAAa,OAAA;AACtB;AAEA,SAAS,eAAA,CACP,SACA,YAAA,EACU;AACV,EAAA,OAAO,YAAA,CAAa,SAAS,IAAI,GAAA,CAAI,UAAU,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAC,CAAA;AACxE","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"]}
|
|
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: [\"/auth/login\", \"/auth/register\", \"/dashboard*\", \"/profile*\"],\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 const isAuthenticated = await checkSession(cookieValue, authConfig.secret);\n\n // ── Guest-only routes ────────────────────────────────────────────────────\n // Accessible only when NOT authenticated.\n // Authenticated users are redirected to redirectAuthenticatedTo.\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 const publicRoutes = authConfig.routes?.public ?? [];\n if (matchesAny(pathname, publicRoutes)) {\n return NextResponse.next();\n }\n\n // ── Protected routes ─────────────────────────────────────────────────────\n const protectedRoutes = authConfig.routes?.protected ?? [];\n const requiresAuth =\n protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);\n\n if (!requiresAuth) {\n return NextResponse.next();\n }\n\n if (!isAuthenticated) {\n // Use the configured login endpoint path, falling back to \"/login\"\n const loginPath = authConfig.routes?.loginPath ?? \"/login\";\n return NextResponse.redirect(new URL(loginPath, request.nextUrl.origin));\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 const now = Date.now();\n const refreshExpired = tokens.refreshTokenExpiresAt\n ? now >= tokens.refreshTokenExpiresAt\n : false;\n return !refreshExpired;\n } catch {\n return false;\n }\n}\n\n// ─── Route matchers ───────────────────────────────────────────────────────────\n\nfunction isGuestOnlyRoute(pathname: string, routes: string[]): boolean {\n return matchesAny(pathname, routes);\n}\n\n/**\n * Matches a pathname against a list of patterns.\n * Supports wildcards: \"/dashboard*\" matches \"/dashboard\", \"/dashboard/\", \"/dashboard/settings\"\n */\nfunction matchesAny(pathname: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchRoute(pathname, pattern));\n}\n\nfunction matchRoute(pathname: string, pattern: string): boolean {\n if (pattern.endsWith(\"*\")) {\n const base = pattern.slice(0, -1); // e.g. \"/dashboard\"\n // matches \"/dashboard\", \"/dashboard/\", \"/dashboard/anything\"\n return pathname === base || pathname.startsWith(base + \"/\") || pathname.startsWith(base);\n }\n return pathname === pattern;\n}\n"]}
|