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.
- package/README.md +8 -2
- package/dist/client/edge.d.ts +19 -6
- package/dist/client/edge.js +281 -30
- package/dist/client/server/index.d.ts +27 -40
- package/dist/client/server/index.js +359 -182
- package/dist/index.d.ts +31 -2
- package/dist/index.js +134 -30
- package/package.json +1 -1
|
@@ -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/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
40
|
-
const
|
|
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(
|
|
47
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
266
|
+
let refreshCookieValue;
|
|
77
267
|
try {
|
|
78
268
|
const mod = await import("next/headers");
|
|
79
269
|
const cookieStore = await mod.cookies();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 (!
|
|
98
|
-
if (strict) throw new
|
|
290
|
+
if (!secret) {
|
|
291
|
+
if (strict) throw new AuthConfigError("Session cookie secret is not configured");
|
|
99
292
|
return null;
|
|
100
293
|
}
|
|
101
|
-
if (!
|
|
102
|
-
if (strict) throw new NoSessionError("
|
|
294
|
+
if (!refreshCookieValue || config.recoveryURL === false) {
|
|
295
|
+
if (strict) throw new NoSessionError("No recoverable refresh session");
|
|
103
296
|
return null;
|
|
104
297
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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((
|
|
575
|
+
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
371
576
|
const loginUrl = new URL(loginRoute, request.url);
|
|
372
|
-
loginUrl.searchParams.set("from",
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 =
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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();
|