najm-auth 2.0.3 → 2.0.5

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.
@@ -9,121 +9,324 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/client/server/getSession.ts
13
- var getSession_exports = {};
14
- __export(getSession_exports, {
15
- AuthConfigError: () => AuthConfigError,
16
- AuthTransportError: () => AuthTransportError,
17
- NoSessionError: () => NoSessionError,
18
- getSession: () => getSession
19
- });
20
- import { createHmac, timingSafeEqual } from "crypto";
21
- function defaultBaseURL() {
22
- const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
23
- if (explicit) return explicit;
24
- const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
25
- return `${origin.replace(/\/$/, "")}/api`;
12
+ // src/client/sessionCookie.ts
13
+ function resolveSessionSecret(explicit) {
14
+ if (explicit !== void 0) return explicit || void 0;
15
+ if (typeof process === "undefined") return void 0;
16
+ return process.env.NAJM_SESSION_SECRET || process.env.JWT_ACCESS_SECRET || void 0;
17
+ }
18
+ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_AGE_SECONDS, now = Date.now()) {
19
+ if (!Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0) return null;
20
+ try {
21
+ const data = JSON.parse(payload);
22
+ if (!isRecord(data) || !isValidUser(data.user)) return null;
23
+ if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
24
+ if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
25
+ if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
26
+ const issuedAt = data.iat;
27
+ if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
28
+ if (now - issuedAt >= maxAgeSeconds * 1e3) return null;
29
+ return {
30
+ user: data.user,
31
+ roles: [...data.roles],
32
+ permissions: [...data.permissions],
33
+ sessionVersion: data.sessionVersion,
34
+ iat: issuedAt
35
+ };
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ async function verifySessionCookie(rawCookieValue, options) {
41
+ if (!rawCookieValue || !options.secret || !globalThis.crypto?.subtle) return null;
42
+ for (const signedValue of cookieValueCandidates(rawCookieValue)) {
43
+ const lastDot = signedValue.lastIndexOf(".");
44
+ if (lastDot <= 0 || lastDot === signedValue.length - 1) continue;
45
+ const payload = signedValue.slice(0, lastDot);
46
+ const signature = signedValue.slice(lastDot + 1);
47
+ if (!await verifyHmac(payload, signature, options.secret)) continue;
48
+ return parseSessionCookiePayload(
49
+ payload,
50
+ options.maxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
51
+ options.now
52
+ );
53
+ }
54
+ return null;
26
55
  }
27
- function getSessionSecret(config) {
28
- return config.sessionSecret ?? (typeof process !== "undefined" ? process.env.NAJM_SESSION_SECRET : void 0) ?? (typeof process !== "undefined" ? process.env.JWT_ACCESS_SECRET : void 0);
56
+ function readCookieValue(cookieHeader, name) {
57
+ for (const part of cookieHeader.split(";")) {
58
+ const separator = part.indexOf("=");
59
+ if (separator === -1) continue;
60
+ if (part.slice(0, separator).trim() === name) {
61
+ const value = part.slice(separator + 1).trim();
62
+ return value || void 0;
63
+ }
64
+ }
65
+ return void 0;
29
66
  }
30
- function verifyHmac(payload, signature, secret) {
31
- const expected = createHmac("sha256", secret).update(payload).digest("base64url");
32
- if (expected.length !== signature.length) return false;
67
+ async function verifyHmac(payload, signature, secret) {
68
+ if (signature.length !== HMAC_SHA256_BASE64URL_LENGTH) return false;
69
+ const signatureBytes = decodeBase64Url(signature);
70
+ if (!signatureBytes || signatureBytes.length !== 32) return false;
33
71
  try {
34
- return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
72
+ const encoder = new TextEncoder();
73
+ const key = await globalThis.crypto.subtle.importKey(
74
+ "raw",
75
+ encoder.encode(secret),
76
+ { name: "HMAC", hash: "SHA-256" },
77
+ false,
78
+ ["verify"]
79
+ );
80
+ return await globalThis.crypto.subtle.verify(
81
+ "HMAC",
82
+ key,
83
+ signatureBytes,
84
+ encoder.encode(payload)
85
+ );
35
86
  } catch {
36
87
  return false;
37
88
  }
38
89
  }
39
- function parseSessionCookie(raw, secret) {
40
- const lastDot = raw.lastIndexOf(".");
41
- if (lastDot === -1) return null;
42
- const payload = raw.substring(0, lastDot);
43
- const signature = raw.substring(lastDot + 1);
44
- if (!verifyHmac(payload, signature, secret)) return null;
90
+ function cookieValueCandidates(raw) {
91
+ const candidates = [raw];
45
92
  try {
46
- const decoded = decodeURIComponent(payload);
47
- const data = JSON.parse(decoded);
48
- if (Date.now() - data.iat > SESSION_COOKIE_MAX_AGE_MS) return null;
49
- const { user, roles, permissions } = data;
50
- return {
51
- user,
52
- roles: roles ?? (user.role ? [user.role] : void 0),
53
- permissions: permissions ?? void 0
54
- };
93
+ const decoded = decodeURIComponent(raw);
94
+ if (decoded !== raw) candidates.push(decoded);
55
95
  } catch {
56
- return null;
57
96
  }
97
+ return candidates;
98
+ }
99
+ function decodeBase64Url(value) {
100
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
101
+ const bytes = [];
102
+ let accumulator = 0;
103
+ let bits = 0;
104
+ for (const char of value) {
105
+ const digit = BASE64URL_ALPHABET.indexOf(char);
106
+ if (digit === -1) return null;
107
+ accumulator = accumulator << 6 | digit;
108
+ bits += 6;
109
+ if (bits >= 8) {
110
+ bits -= 8;
111
+ bytes.push(accumulator >> bits & 255);
112
+ accumulator &= (1 << bits) - 1;
113
+ }
114
+ }
115
+ if (bits > 0 && accumulator !== 0) return null;
116
+ return new Uint8Array(bytes);
58
117
  }
59
- function buildSession(body) {
60
- if (!body?.data) return null;
61
- const { roles, permissions, ...user } = body.data;
118
+ function isRecord(value) {
119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
120
+ }
121
+ function isValidUser(value) {
122
+ return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
123
+ }
124
+ function isStringArray(value) {
125
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
126
+ }
127
+ var DEFAULT_SESSION_MAX_AGE_SECONDS, MAX_CLOCK_SKEW_MS, HMAC_SHA256_BASE64URL_LENGTH, BASE64URL_ALPHABET;
128
+ var init_sessionCookie = __esm({
129
+ "src/client/sessionCookie.ts"() {
130
+ DEFAULT_SESSION_MAX_AGE_SECONDS = 300;
131
+ MAX_CLOCK_SKEW_MS = 3e4;
132
+ HMAC_SHA256_BASE64URL_LENGTH = 43;
133
+ BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
134
+ __name(resolveSessionSecret, "resolveSessionSecret");
135
+ __name(parseSessionCookiePayload, "parseSessionCookiePayload");
136
+ __name(verifySessionCookie, "verifySessionCookie");
137
+ __name(readCookieValue, "readCookieValue");
138
+ __name(verifyHmac, "verifyHmac");
139
+ __name(cookieValueCandidates, "cookieValueCandidates");
140
+ __name(decodeBase64Url, "decodeBase64Url");
141
+ __name(isRecord, "isRecord");
142
+ __name(isValidUser, "isValidUser");
143
+ __name(isStringArray, "isStringArray");
144
+ }
145
+ });
146
+
147
+ // src/client/sessionRecovery.ts
148
+ async function requestSessionRecovery(options) {
149
+ if (!isCookieName(options.refreshCookieName) || !isCookieName(options.sessionCookieName)) {
150
+ return { status: "unavailable" };
151
+ }
152
+ if (!isCookieValue(options.refreshCookieValue)) {
153
+ return { status: "invalid" };
154
+ }
155
+ if (!isSafeRecoveryEndpoint(options.endpoint)) {
156
+ return { status: "unavailable" };
157
+ }
158
+ let response;
159
+ try {
160
+ response = await fetch(options.endpoint, {
161
+ method: "POST",
162
+ headers: {
163
+ Accept: "application/json",
164
+ Cookie: `${options.refreshCookieName}=${options.refreshCookieValue}`,
165
+ "X-Najm-Session-Recovery": "1"
166
+ },
167
+ cache: "no-store",
168
+ redirect: "manual"
169
+ });
170
+ } catch {
171
+ return { status: "unavailable" };
172
+ }
173
+ if (!response.ok) {
174
+ const status = response.status;
175
+ return {
176
+ status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
177
+ httpStatus: status
178
+ };
179
+ }
180
+ const setCookie = response.headers.get("set-cookie");
181
+ if (!setCookie) return { status: "unavailable", httpStatus: response.status };
182
+ const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
183
+ if (!sessionCookieValue) {
184
+ return { status: "unavailable", httpStatus: response.status };
185
+ }
186
+ const claims = await verifySessionCookie(sessionCookieValue, {
187
+ secret: options.sessionSecret,
188
+ maxAgeSeconds: options.sessionMaxAge
189
+ });
190
+ if (!claims) return { status: "unavailable", httpStatus: response.status };
62
191
  return {
63
- user,
64
- roles: roles ?? (user.role ? [user.role] : void 0),
65
- permissions: permissions ?? user.permissions
192
+ status: "recovered",
193
+ claims,
194
+ setCookie,
195
+ sessionCookieValue
66
196
  };
67
197
  }
198
+ function authEndpoint(baseURL, authPrefix, suffix, requestURL) {
199
+ const normalizedBase = baseURL.replace(/\/+$/, "");
200
+ const normalizedPrefix = `/${authPrefix.replace(/^\/+|\/+$/g, "")}`;
201
+ const normalizedSuffix = `/${suffix.replace(/^\/+/, "")}`;
202
+ const value = `${normalizedBase}${normalizedPrefix}${normalizedSuffix}`;
203
+ return requestURL ? new URL(value, requestURL).toString() : value;
204
+ }
205
+ function replaceCookieValue(cookieHeader, name, value) {
206
+ const parts = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => part.slice(0, part.indexOf("=")).trim() !== name);
207
+ parts.push(`${name}=${value}`);
208
+ return parts.join("; ");
209
+ }
210
+ function readSetCookieValue(setCookie, name) {
211
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
212
+ const match = setCookie.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;]*)`));
213
+ return match?.[1] || void 0;
214
+ }
215
+ function isCookieName(value) {
216
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
217
+ }
218
+ function isCookieValue(value) {
219
+ return value.length > 0 && !/[\r\n;]/.test(value);
220
+ }
221
+ function isSafeRecoveryEndpoint(value) {
222
+ try {
223
+ const url = new URL(value);
224
+ if (url.username || url.password) return false;
225
+ if (url.protocol === "https:") return true;
226
+ if (url.protocol !== "http:") return false;
227
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ var init_sessionRecovery = __esm({
233
+ "src/client/sessionRecovery.ts"() {
234
+ init_sessionCookie();
235
+ __name(requestSessionRecovery, "requestSessionRecovery");
236
+ __name(authEndpoint, "authEndpoint");
237
+ __name(replaceCookieValue, "replaceCookieValue");
238
+ __name(readSetCookieValue, "readSetCookieValue");
239
+ __name(isCookieName, "isCookieName");
240
+ __name(isCookieValue, "isCookieValue");
241
+ __name(isSafeRecoveryEndpoint, "isSafeRecoveryEndpoint");
242
+ }
243
+ });
244
+
245
+ // src/client/server/getSession.ts
246
+ var getSession_exports = {};
247
+ __export(getSession_exports, {
248
+ AuthConfigError: () => AuthConfigError,
249
+ AuthTransportError: () => AuthTransportError,
250
+ NoSessionError: () => NoSessionError,
251
+ getSession: () => getSession
252
+ });
253
+ function defaultBaseURL() {
254
+ const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
255
+ if (explicit) return explicit;
256
+ const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
257
+ return `${origin.replace(/\/$/, "")}/api`;
258
+ }
68
259
  async function getSession(config = {}) {
69
260
  const cookieName = config.cookieName ?? "refreshToken";
70
261
  const sessionCookieName = config.sessionCookieName ?? "najm.session";
71
262
  const baseURL = config.baseURL ?? defaultBaseURL();
72
263
  const prefix = config.authPrefix ?? "/auth";
73
264
  const strict = config.mode === "strict";
74
- let cookieHeader = "";
75
265
  let sessionCookieValue;
76
- let hasRefreshCookie = false;
266
+ let refreshCookieValue;
77
267
  try {
78
268
  const mod = await import("next/headers");
79
269
  const cookieStore = await mod.cookies();
80
- const sessionCookie = cookieStore.get(sessionCookieName);
81
- if (sessionCookie) sessionCookieValue = sessionCookie.value;
82
- if (typeof cookieStore.get === "function") {
83
- hasRefreshCookie = !!cookieStore.get(cookieName);
84
- }
85
- cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ");
86
- } catch (err) {
270
+ sessionCookieValue = cookieStore.get(sessionCookieName)?.value;
271
+ refreshCookieValue = cookieStore.get(cookieName)?.value;
272
+ } catch {
87
273
  if (strict) throw new AuthConfigError("Failed to read cookies from Next.js headers()");
88
274
  return null;
89
275
  }
90
- if (sessionCookieValue) {
91
- const secret = getSessionSecret(config);
92
- if (secret) {
93
- const session = parseSessionCookie(sessionCookieValue, secret);
94
- if (session) return session;
276
+ const secret = resolveSessionSecret(config.sessionSecret);
277
+ if (sessionCookieValue && secret) {
278
+ const claims = await verifySessionCookie(sessionCookieValue, {
279
+ secret,
280
+ maxAgeSeconds: config.sessionMaxAge
281
+ });
282
+ if (claims) {
283
+ return {
284
+ user: claims.user,
285
+ roles: claims.roles,
286
+ permissions: claims.permissions
287
+ };
95
288
  }
96
289
  }
97
- if (!hasRefreshCookie) {
98
- if (strict) throw new NoSessionError("No refresh token cookie");
290
+ if (!secret) {
291
+ if (strict) throw new AuthConfigError("Session cookie secret is not configured");
99
292
  return null;
100
293
  }
101
- if (!cookieHeader) {
102
- if (strict) throw new NoSessionError("Empty cookie header");
294
+ if (!refreshCookieValue || config.recoveryURL === false) {
295
+ if (strict) throw new NoSessionError("No recoverable refresh session");
103
296
  return null;
104
297
  }
105
- try {
106
- const res = await fetch(`${baseURL}${prefix}/me`, {
107
- headers: { Cookie: cookieHeader, Accept: "application/json" },
108
- cache: "no-store"
109
- });
110
- if (!res.ok) {
111
- if (strict) throw new AuthTransportError(`/auth/me returned ${res.status}`, res.status);
112
- return null;
298
+ const endpoint = config.recoveryURL ? new URL(config.recoveryURL, baseURL).toString() : authEndpoint(baseURL, prefix, "/session/recover");
299
+ const recovery = await requestSessionRecovery({
300
+ endpoint,
301
+ refreshCookieName: cookieName,
302
+ refreshCookieValue,
303
+ sessionCookieName,
304
+ sessionSecret: secret,
305
+ sessionMaxAge: config.sessionMaxAge
306
+ });
307
+ if (recovery.status === "recovered") {
308
+ return {
309
+ user: recovery.claims.user,
310
+ roles: recovery.claims.roles,
311
+ permissions: recovery.claims.permissions
312
+ };
313
+ }
314
+ if (strict) {
315
+ if (recovery.status === "invalid") {
316
+ throw new NoSessionError("Refresh session is invalid or revoked");
113
317
  }
114
- const body = await res.json();
115
- const session = buildSession(body);
116
- if (!session && strict) throw new NoSessionError("No user data in /auth/me response");
117
- return session;
118
- } catch (err) {
119
- if (err instanceof NoSessionError || err instanceof AuthTransportError) throw err;
120
- if (strict) throw new AuthTransportError(`Failed to fetch /auth/me: ${err.message}`);
121
- return null;
318
+ throw new AuthTransportError(
319
+ "Session recovery endpoint was unavailable or returned an invalid session",
320
+ recovery.httpStatus
321
+ );
122
322
  }
323
+ return null;
123
324
  }
124
- var NoSessionError, AuthConfigError, AuthTransportError, SESSION_COOKIE_MAX_AGE_MS;
325
+ var NoSessionError, AuthConfigError, AuthTransportError;
125
326
  var init_getSession = __esm({
126
327
  "src/client/server/getSession.ts"() {
328
+ init_sessionCookie();
329
+ init_sessionRecovery();
127
330
  NoSessionError = class extends Error {
128
331
  static {
129
332
  __name(this, "NoSessionError");
@@ -156,11 +359,6 @@ var init_getSession = __esm({
156
359
  code = "AUTH_TRANSPORT_ERROR";
157
360
  };
158
361
  __name(defaultBaseURL, "defaultBaseURL");
159
- __name(getSessionSecret, "getSessionSecret");
160
- __name(verifyHmac, "verifyHmac");
161
- SESSION_COOKIE_MAX_AGE_MS = 3e5;
162
- __name(parseSessionCookie, "parseSessionCookie");
163
- __name(buildSession, "buildSession");
164
362
  __name(getSession, "getSession");
165
363
  }
166
364
  });
@@ -355,6 +553,8 @@ function createServerClient(config) {
355
553
  __name(createServerClient, "createServerClient");
356
554
 
357
555
  // src/client/server/withAuthMiddleware.ts
556
+ init_sessionCookie();
557
+ init_sessionRecovery();
358
558
  function withAuthMiddleware(config) {
359
559
  const {
360
560
  protectedRoutes = [],
@@ -362,54 +562,86 @@ function withAuthMiddleware(config) {
362
562
  loginRoute = "/login",
363
563
  roleRoutes = {},
364
564
  cookieName = "refreshToken",
565
+ apiBaseURL = "/api",
566
+ authPrefix = "/auth",
365
567
  sessionCookieName = "najm.session",
366
- verifyAlways = false
568
+ sessionSecret,
569
+ sessionMaxAge,
570
+ verifyAlways = false,
571
+ recoveryURL
367
572
  } = config;
368
573
  return /* @__PURE__ */ __name(async function middleware(request) {
369
574
  const { NextResponse } = await import("next/server");
370
- const redirectToLogin = /* @__PURE__ */ __name((pathname2, clearCookies) => {
575
+ const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
371
576
  const loginUrl = new URL(loginRoute, request.url);
372
- loginUrl.searchParams.set("from", pathname2);
577
+ loginUrl.searchParams.set("from", returnPath2);
373
578
  const res = NextResponse.redirect(loginUrl);
374
- if (clearCookies) {
579
+ if (clearCookies.includes("refresh")) {
375
580
  res.cookies.delete(cookieName);
581
+ }
582
+ if (clearCookies.includes("session")) {
376
583
  res.cookies.delete(sessionCookieName);
377
584
  }
378
585
  return res;
379
586
  }, "redirectToLogin");
380
587
  const url = new URL(request.url);
381
588
  const pathname = url.pathname;
589
+ const returnPath = `${url.pathname}${url.search}`;
382
590
  if (matchesAny(pathname, publicRoutes)) {
383
591
  return NextResponse.next();
384
592
  }
385
593
  const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
386
594
  if (!isProtected) return NextResponse.next();
387
595
  const cookie = request.headers.get("cookie") ?? "";
388
- const hasToken = cookieRegex(cookieName).test(cookie);
389
- if (!hasToken) {
390
- return redirectToLogin(pathname, true);
596
+ const sessionCookie = readCookieValue(cookie, sessionCookieName);
597
+ const secret = resolveSessionSecret(sessionSecret);
598
+ if (!secret) {
599
+ return redirectToLogin(returnPath, ["session"]);
600
+ }
601
+ let session = sessionCookie ? await verifySessionCookie(sessionCookie, {
602
+ secret,
603
+ maxAgeSeconds: sessionMaxAge
604
+ }) : null;
605
+ let recovery = null;
606
+ if (!session || verifyAlways) {
607
+ const refreshCookie = readCookieValue(cookie, cookieName);
608
+ if (!refreshCookie || recoveryURL === false) {
609
+ return redirectToLogin(returnPath, ["refresh", "session"]);
610
+ }
611
+ const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
612
+ recovery = await requestSessionRecovery({
613
+ endpoint,
614
+ refreshCookieName: cookieName,
615
+ refreshCookieValue: refreshCookie,
616
+ sessionCookieName,
617
+ sessionSecret: secret,
618
+ sessionMaxAge
619
+ });
620
+ if (recovery.status !== "recovered") {
621
+ return redirectToLogin(
622
+ returnPath,
623
+ recovery.status === "invalid" ? ["refresh", "session"] : ["session"]
624
+ );
625
+ }
626
+ session = recovery.claims;
391
627
  }
392
628
  const requiredRoles = findMatchingRoles(pathname, roleRoutes);
393
- const needsVerify = verifyAlways || !!requiredRoles;
394
- if (needsVerify) {
395
- const verifyURL = config.verifyURL ?? `${url.origin}/api/auth/me`;
396
- try {
397
- const res = await fetch(verifyURL, {
398
- headers: { "Cookie": cookie, "Accept": "application/json" }
399
- });
400
- if (!res.ok) {
401
- return redirectToLogin(pathname, true);
402
- }
403
- if (requiredRoles) {
404
- const body = await res.json();
405
- const userRole = body?.data?.role;
406
- if (!userRole || !requiredRoles.includes(userRole)) {
407
- return new NextResponse(null, { status: 403 });
408
- }
409
- }
410
- } catch {
411
- return redirectToLogin(pathname, true);
629
+ if (requiredRoles && !session.roles.some((role) => requiredRoles.includes(role))) {
630
+ const forbidden = new NextResponse(null, { status: 403 });
631
+ if (recovery?.status === "recovered") {
632
+ forbidden.headers.append("Set-Cookie", recovery.setCookie);
412
633
  }
634
+ return forbidden;
635
+ }
636
+ if (recovery?.status === "recovered") {
637
+ const requestHeaders = new Headers(request.headers);
638
+ requestHeaders.set(
639
+ "cookie",
640
+ replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
641
+ );
642
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
643
+ response.headers.append("Set-Cookie", recovery.setCookie);
644
+ return response;
413
645
  }
414
646
  return NextResponse.next();
415
647
  }, "middleware");
@@ -425,10 +657,6 @@ function matchPattern(pathname, pattern) {
425
657
  return new RegExp(`^${regex}$`).test(pathname);
426
658
  }
427
659
  __name(matchPattern, "matchPattern");
428
- function cookieRegex(name) {
429
- return new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=[^;]`);
430
- }
431
- __name(cookieRegex, "cookieRegex");
432
660
  function findMatchingRoles(pathname, roleRoutes) {
433
661
  for (const [pattern, roles] of Object.entries(roleRoutes)) {
434
662
  if (matchPattern(pathname, pattern)) return roles;
@@ -935,27 +1163,6 @@ function createAuthClient(config) {
935
1163
  __name(createAuthClient, "createAuthClient");
936
1164
 
937
1165
  // src/client/server/defineAuth.ts
938
- function matchesAny2(pathname, patterns) {
939
- return patterns.some((p) => matchPattern2(pathname, p));
940
- }
941
- __name(matchesAny2, "matchesAny");
942
- function matchPattern2(pathname, pattern) {
943
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
944
- const regex = escaped.replace(/\/:[^/]+\*/g, "(?:/.*)?").replace(/\/\\\*$/g, "(?:/.*)?").replace(/\\\*/g, "(?:/.*)?").replace(/\//g, "\\/");
945
- return new RegExp(`^${regex}$`).test(pathname);
946
- }
947
- __name(matchPattern2, "matchPattern");
948
- function cookieRegex2(name) {
949
- return new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=[^;]`);
950
- }
951
- __name(cookieRegex2, "cookieRegex");
952
- function findMatchingRoles2(pathname, roleRoutes) {
953
- for (const [pattern, roles] of Object.entries(roleRoutes)) {
954
- if (matchPattern2(pathname, pattern)) return roles;
955
- }
956
- return null;
957
- }
958
- __name(findMatchingRoles2, "findMatchingRoles");
959
1166
  function defineAuth(authConfig = {}) {
960
1167
  const {
961
1168
  apiBaseURL = "/api",
@@ -967,6 +1174,8 @@ function defineAuth(authConfig = {}) {
967
1174
  cookieName = "refreshToken",
968
1175
  sessionCookieName = "najm.session",
969
1176
  sessionSecret,
1177
+ sessionMaxAge,
1178
+ recoveryURL,
970
1179
  matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
971
1180
  verifyAlways = false,
972
1181
  refreshThreshold,
@@ -981,7 +1190,9 @@ function defineAuth(authConfig = {}) {
981
1190
  authPrefix,
982
1191
  cookieName,
983
1192
  sessionCookieName,
984
- sessionSecret
1193
+ sessionSecret,
1194
+ sessionMaxAge,
1195
+ recoveryURL
985
1196
  };
986
1197
  let _client = null;
987
1198
  const getClient = /* @__PURE__ */ __name(() => {
@@ -1023,54 +1234,20 @@ function defineAuth(authConfig = {}) {
1023
1234
  throw err;
1024
1235
  }
1025
1236
  }, "requireSession");
1026
- const middleware = /* @__PURE__ */ __name(async (request) => {
1027
- const { NextResponse } = await import("next/server");
1028
- const url = new URL(request.url);
1029
- const pathname = url.pathname;
1030
- const redirectToLogin = /* @__PURE__ */ __name((clearCookies) => {
1031
- const loginUrl = new URL(loginRoute, request.url);
1032
- loginUrl.searchParams.set("from", pathname);
1033
- const res = NextResponse.redirect(loginUrl);
1034
- if (clearCookies) {
1035
- res.cookies.delete(cookieName);
1036
- res.cookies.delete(sessionCookieName);
1037
- }
1038
- return res;
1039
- }, "redirectToLogin");
1040
- if (matchesAny2(pathname, publicRoutes)) {
1041
- return NextResponse.next();
1042
- }
1043
- const isProtected = protectedRoutes.length === 0 || matchesAny2(pathname, protectedRoutes);
1044
- if (!isProtected) return NextResponse.next();
1045
- const cookie = request.headers.get("cookie") ?? "";
1046
- const hasToken = cookieRegex2(cookieName).test(cookie);
1047
- if (!hasToken) {
1048
- return redirectToLogin(true);
1049
- }
1050
- const requiredRoles = findMatchingRoles2(pathname, roleRoutes);
1051
- const needsVerify = verifyAlways || !!requiredRoles;
1052
- if (needsVerify) {
1053
- const verifyURL = `${url.origin}${apiBaseURL}${authPrefix}/me`;
1054
- try {
1055
- const res = await fetch(verifyURL, {
1056
- headers: { Cookie: cookie, Accept: "application/json" }
1057
- });
1058
- if (!res.ok) {
1059
- return redirectToLogin(true);
1060
- }
1061
- if (requiredRoles) {
1062
- const body = await res.json();
1063
- const userRole = body?.data?.role;
1064
- if (!userRole || !requiredRoles.includes(userRole)) {
1065
- return new NextResponse(null, { status: 403 });
1066
- }
1067
- }
1068
- } catch {
1069
- return redirectToLogin(true);
1070
- }
1071
- }
1072
- return NextResponse.next();
1073
- }, "middleware");
1237
+ const middleware = withAuthMiddleware({
1238
+ protectedRoutes,
1239
+ publicRoutes,
1240
+ loginRoute,
1241
+ roleRoutes,
1242
+ cookieName,
1243
+ apiBaseURL,
1244
+ authPrefix,
1245
+ sessionCookieName,
1246
+ sessionSecret,
1247
+ sessionMaxAge,
1248
+ recoveryURL,
1249
+ verifyAlways
1250
+ });
1074
1251
  const protect = /* @__PURE__ */ __name((Page, options) => {
1075
1252
  return /* @__PURE__ */ __name(async function ProtectedPage(props) {
1076
1253
  const session = await getSession2();