najm-auth 2.0.4 → 2.0.6
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 +11 -2
- package/dist/{NajmAuthClient-B9dGk9MH.d.ts → NajmAuthClient-0yzFb9CR.d.ts} +1 -1
- package/dist/client/edge.d.ts +12 -2
- package/dist/client/edge.js +152 -12
- package/dist/client/index.d.ts +2 -2
- package/dist/client/react/index.d.ts +1 -1
- package/dist/client/react/index.js +1 -1
- package/dist/client/server/index.d.ts +22 -41
- package/dist/client/server/index.js +234 -68
- package/dist/index.d.ts +29 -0
- package/dist/index.js +101 -31
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -167,6 +167,7 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
|
|
|
167
167
|
| `POST` | `/auth/register` | Register new user | None |
|
|
168
168
|
| `POST` | `/auth/login` | Login with email/password | None |
|
|
169
169
|
| `POST` | `/auth/refresh` | Refresh access token (cookie) | None (uses refresh cookie) |
|
|
170
|
+
| `POST` | `/auth/session/recover` | Reissue signed session without token rotation | Refresh cookie + recovery header |
|
|
170
171
|
| `POST` | `/auth/logout` | Logout and revoke tokens | ✅ Required |
|
|
171
172
|
| `GET` | `/auth/me` | Get current user profile | ✅ Required |
|
|
172
173
|
| `POST` | `/auth/forgot-password` | Request password reset | None |
|
|
@@ -600,6 +601,7 @@ limits are active when `auth()` is registered.
|
|
|
600
601
|
| `POST /auth/register` | 5 | 15 minutes | IP |
|
|
601
602
|
| `POST /auth/login` | 5 | 15 minutes | IP |
|
|
602
603
|
| `POST /auth/refresh` | 15 | 15 minutes | Cookie fingerprint |
|
|
604
|
+
| `POST /auth/session/recover` | 120 | 1 minute | Cookie fingerprint |
|
|
603
605
|
| `POST /auth/logout` | 10 | 15 minutes | User ID |
|
|
604
606
|
| `GET /auth/me` | 30 | 1 minute | User ID |
|
|
605
607
|
| `POST /auth/forgot-password` | 3 | 15 minutes | IP |
|
|
@@ -675,8 +677,15 @@ throw new HttpError(403, 'Insufficient permissions for this action');
|
|
|
675
677
|
- Login uses a dummy password hash for missing users to reduce timing leaks.
|
|
676
678
|
- Forgot-password responses avoid email enumeration.
|
|
677
679
|
- Auth routes register `najm-rate` and ship route-level brute-force limits.
|
|
678
|
-
- Session cookies are signed
|
|
679
|
-
|
|
680
|
+
- Session cookies are signed and short-lived; server auth resolution checks
|
|
681
|
+
their session version.
|
|
682
|
+
- Expired signed sessions recover through authoritative, non-rotating refresh
|
|
683
|
+
validation; middleware verifies the reissued HMAC before using its claims.
|
|
684
|
+
- Server-side recovery sends only the configured refresh cookie and accepts
|
|
685
|
+
relative or exact same-origin endpoints. URL credentials and any
|
|
686
|
+
scheme/hostname/port change are rejected before the network request.
|
|
687
|
+
- `verifyAlways` forces that authoritative check on every protected request;
|
|
688
|
+
the default bounds cached role/status staleness to `session.maxAge`.
|
|
680
689
|
|
|
681
690
|
### Password Reset Tokens
|
|
682
691
|
|
|
@@ -247,4 +247,4 @@ declare class NajmAuthClient {
|
|
|
247
247
|
*/
|
|
248
248
|
declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
|
|
249
249
|
|
|
250
|
-
export {
|
|
250
|
+
export { type AuthClientConfig as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type OAuthLoginOptions as O, type RequestOptions as R, type SyncPayload as S, type TabSyncMessage as T, AuthError as a, type AuthEvent as b, type AuthEventHandler as c, type AuthEventMap as d, type AuthState as e, type AuthUser as f, type OAuthProvider as g, type RetryConfig as h, type ServerResponse as i, type TokenPair as j, createAuthClient as k };
|
package/dist/client/edge.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ interface AuthMiddlewareConfig {
|
|
|
11
11
|
roleRoutes?: Record<string, string[]>;
|
|
12
12
|
/** Refresh token cookie name (default: 'refreshToken') */
|
|
13
13
|
cookieName?: string;
|
|
14
|
+
/** API base URL used by session recovery (default: '/api'). */
|
|
15
|
+
apiBaseURL?: string;
|
|
16
|
+
/** Auth endpoint prefix used by session recovery (default: '/auth'). */
|
|
17
|
+
authPrefix?: string;
|
|
14
18
|
/** Signed session cookie name (default: 'najm.session') */
|
|
15
19
|
sessionCookieName?: string;
|
|
16
20
|
/** @deprecated Session cookies are verified locally at the Edge. */
|
|
@@ -20,10 +24,16 @@ interface AuthMiddlewareConfig {
|
|
|
20
24
|
/** Must match the auth plugin's session.maxAge. Default: 300 seconds. */
|
|
21
25
|
sessionMaxAge?: number;
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* Force authoritative refresh-session validation on every protected request.
|
|
28
|
+
* This reissues the signed session cookie without rotating refresh tokens.
|
|
25
29
|
*/
|
|
26
30
|
verifyAlways?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Session-recovery endpoint. Relative values resolve against the request
|
|
33
|
+
* origin. Defaults to `${apiBaseURL}${authPrefix}/session/recover`.
|
|
34
|
+
* Set to false to disable automatic recovery.
|
|
35
|
+
*/
|
|
36
|
+
recoveryURL?: string | false;
|
|
27
37
|
}
|
|
28
38
|
/**
|
|
29
39
|
* Create a Next.js middleware function that protects routes based on auth state.
|
package/dist/client/edge.js
CHANGED
|
@@ -131,6 +131,106 @@ function isStringArray(value) {
|
|
|
131
131
|
}
|
|
132
132
|
__name(isStringArray, "isStringArray");
|
|
133
133
|
|
|
134
|
+
// src/client/sessionRecovery.ts
|
|
135
|
+
async function requestSessionRecovery(options) {
|
|
136
|
+
if (!isCookieName(options.refreshCookieName) || !isCookieName(options.sessionCookieName)) {
|
|
137
|
+
return { status: "unavailable" };
|
|
138
|
+
}
|
|
139
|
+
if (!isCookieValue(options.refreshCookieValue)) {
|
|
140
|
+
return { status: "invalid" };
|
|
141
|
+
}
|
|
142
|
+
const endpoint = sameOriginRecoveryEndpoint(
|
|
143
|
+
options.endpoint,
|
|
144
|
+
options.requestOrigin
|
|
145
|
+
);
|
|
146
|
+
if (!endpoint) {
|
|
147
|
+
return { status: "unavailable" };
|
|
148
|
+
}
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = await fetch(endpoint, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
Accept: "application/json",
|
|
155
|
+
Cookie: `${options.refreshCookieName}=${options.refreshCookieValue}`,
|
|
156
|
+
"X-Najm-Session-Recovery": "1"
|
|
157
|
+
},
|
|
158
|
+
cache: "no-store",
|
|
159
|
+
redirect: "manual"
|
|
160
|
+
});
|
|
161
|
+
} catch {
|
|
162
|
+
return { status: "unavailable" };
|
|
163
|
+
}
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
const status = response.status;
|
|
166
|
+
return {
|
|
167
|
+
status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
|
|
168
|
+
httpStatus: status
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const setCookie = response.headers.get("set-cookie");
|
|
172
|
+
if (!setCookie) return { status: "unavailable", httpStatus: response.status };
|
|
173
|
+
const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
|
|
174
|
+
if (!sessionCookieValue) {
|
|
175
|
+
return { status: "unavailable", httpStatus: response.status };
|
|
176
|
+
}
|
|
177
|
+
const claims = await verifySessionCookie(sessionCookieValue, {
|
|
178
|
+
secret: options.sessionSecret,
|
|
179
|
+
maxAgeSeconds: options.sessionMaxAge
|
|
180
|
+
});
|
|
181
|
+
if (!claims) return { status: "unavailable", httpStatus: response.status };
|
|
182
|
+
return {
|
|
183
|
+
status: "recovered",
|
|
184
|
+
claims,
|
|
185
|
+
setCookie,
|
|
186
|
+
sessionCookieValue
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
__name(requestSessionRecovery, "requestSessionRecovery");
|
|
190
|
+
function authEndpoint(baseURL, authPrefix, suffix, requestURL) {
|
|
191
|
+
const normalizedBase = baseURL.replace(/\/+$/, "");
|
|
192
|
+
const normalizedPrefix = `/${authPrefix.replace(/^\/+|\/+$/g, "")}`;
|
|
193
|
+
const normalizedSuffix = `/${suffix.replace(/^\/+/, "")}`;
|
|
194
|
+
const value = `${normalizedBase}${normalizedPrefix}${normalizedSuffix}`;
|
|
195
|
+
return requestURL ? new URL(value, requestURL).toString() : value;
|
|
196
|
+
}
|
|
197
|
+
__name(authEndpoint, "authEndpoint");
|
|
198
|
+
function replaceCookieValue(cookieHeader, name, value) {
|
|
199
|
+
const parts = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => part.slice(0, part.indexOf("=")).trim() !== name);
|
|
200
|
+
parts.push(`${name}=${value}`);
|
|
201
|
+
return parts.join("; ");
|
|
202
|
+
}
|
|
203
|
+
__name(replaceCookieValue, "replaceCookieValue");
|
|
204
|
+
function readSetCookieValue(setCookie, name) {
|
|
205
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
206
|
+
const match = setCookie.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;]*)`));
|
|
207
|
+
return match?.[1] || void 0;
|
|
208
|
+
}
|
|
209
|
+
__name(readSetCookieValue, "readSetCookieValue");
|
|
210
|
+
function isCookieName(value) {
|
|
211
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
|
|
212
|
+
}
|
|
213
|
+
__name(isCookieName, "isCookieName");
|
|
214
|
+
function isCookieValue(value) {
|
|
215
|
+
return value.length > 0 && /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/.test(value);
|
|
216
|
+
}
|
|
217
|
+
__name(isCookieValue, "isCookieValue");
|
|
218
|
+
function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
|
|
219
|
+
try {
|
|
220
|
+
const trusted = new URL(requestOrigin);
|
|
221
|
+
if (trusted.protocol !== "https:" && trusted.protocol !== "http:" || trusted.username || trusted.password) {
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
const resolved = new URL(endpoint, trusted.origin);
|
|
225
|
+
if (resolved.username || resolved.password) return void 0;
|
|
226
|
+
if (resolved.origin !== trusted.origin) return void 0;
|
|
227
|
+
return resolved.toString();
|
|
228
|
+
} catch {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
__name(sameOriginRecoveryEndpoint, "sameOriginRecoveryEndpoint");
|
|
233
|
+
|
|
134
234
|
// src/client/server/withAuthMiddleware.ts
|
|
135
235
|
function withAuthMiddleware(config) {
|
|
136
236
|
const {
|
|
@@ -139,26 +239,31 @@ function withAuthMiddleware(config) {
|
|
|
139
239
|
loginRoute = "/login",
|
|
140
240
|
roleRoutes = {},
|
|
141
241
|
cookieName = "refreshToken",
|
|
242
|
+
apiBaseURL = "/api",
|
|
243
|
+
authPrefix = "/auth",
|
|
142
244
|
sessionCookieName = "najm.session",
|
|
143
245
|
sessionSecret,
|
|
144
246
|
sessionMaxAge,
|
|
145
|
-
verifyAlways = false
|
|
247
|
+
verifyAlways = false,
|
|
248
|
+
recoveryURL
|
|
146
249
|
} = config;
|
|
147
|
-
void verifyAlways;
|
|
148
250
|
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
149
251
|
const { NextResponse } = await import("next/server");
|
|
150
|
-
const redirectToLogin = /* @__PURE__ */ __name((
|
|
252
|
+
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
151
253
|
const loginUrl = new URL(loginRoute, request.url);
|
|
152
|
-
loginUrl.searchParams.set("from",
|
|
254
|
+
loginUrl.searchParams.set("from", returnPath2);
|
|
153
255
|
const res = NextResponse.redirect(loginUrl);
|
|
154
|
-
if (clearCookies) {
|
|
256
|
+
if (clearCookies.includes("refresh")) {
|
|
155
257
|
res.cookies.delete(cookieName);
|
|
258
|
+
}
|
|
259
|
+
if (clearCookies.includes("session")) {
|
|
156
260
|
res.cookies.delete(sessionCookieName);
|
|
157
261
|
}
|
|
158
262
|
return res;
|
|
159
263
|
}, "redirectToLogin");
|
|
160
264
|
const url = new URL(request.url);
|
|
161
265
|
const pathname = url.pathname;
|
|
266
|
+
const returnPath = `${url.pathname}${url.search}`;
|
|
162
267
|
if (matchesAny(pathname, publicRoutes)) {
|
|
163
268
|
return NextResponse.next();
|
|
164
269
|
}
|
|
@@ -167,19 +272,54 @@ function withAuthMiddleware(config) {
|
|
|
167
272
|
const cookie = request.headers.get("cookie") ?? "";
|
|
168
273
|
const sessionCookie = readCookieValue(cookie, sessionCookieName);
|
|
169
274
|
const secret = resolveSessionSecret(sessionSecret);
|
|
170
|
-
if (!
|
|
171
|
-
return redirectToLogin(
|
|
275
|
+
if (!secret) {
|
|
276
|
+
return redirectToLogin(returnPath, ["session"]);
|
|
172
277
|
}
|
|
173
|
-
|
|
278
|
+
let session = sessionCookie ? await verifySessionCookie(sessionCookie, {
|
|
174
279
|
secret,
|
|
175
280
|
maxAgeSeconds: sessionMaxAge
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
|
|
281
|
+
}) : null;
|
|
282
|
+
let recovery = null;
|
|
283
|
+
if (!session || verifyAlways) {
|
|
284
|
+
const refreshCookie = readCookieValue(cookie, cookieName);
|
|
285
|
+
if (!refreshCookie || recoveryURL === false) {
|
|
286
|
+
return redirectToLogin(returnPath, ["refresh", "session"]);
|
|
287
|
+
}
|
|
288
|
+
const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
|
|
289
|
+
recovery = await requestSessionRecovery({
|
|
290
|
+
endpoint,
|
|
291
|
+
requestOrigin: url.origin,
|
|
292
|
+
refreshCookieName: cookieName,
|
|
293
|
+
refreshCookieValue: refreshCookie,
|
|
294
|
+
sessionCookieName,
|
|
295
|
+
sessionSecret: secret,
|
|
296
|
+
sessionMaxAge
|
|
297
|
+
});
|
|
298
|
+
if (recovery.status !== "recovered") {
|
|
299
|
+
return redirectToLogin(
|
|
300
|
+
returnPath,
|
|
301
|
+
recovery.status === "invalid" ? ["refresh", "session"] : ["session"]
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
session = recovery.claims;
|
|
179
305
|
}
|
|
180
306
|
const requiredRoles = findMatchingRoles(pathname, roleRoutes);
|
|
181
307
|
if (requiredRoles && !session.roles.some((role) => requiredRoles.includes(role))) {
|
|
182
|
-
|
|
308
|
+
const forbidden = new NextResponse(null, { status: 403 });
|
|
309
|
+
if (recovery?.status === "recovered") {
|
|
310
|
+
forbidden.headers.append("Set-Cookie", recovery.setCookie);
|
|
311
|
+
}
|
|
312
|
+
return forbidden;
|
|
313
|
+
}
|
|
314
|
+
if (recovery?.status === "recovered") {
|
|
315
|
+
const requestHeaders = new Headers(request.headers);
|
|
316
|
+
requestHeaders.set(
|
|
317
|
+
"cookie",
|
|
318
|
+
replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
|
|
319
|
+
);
|
|
320
|
+
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
321
|
+
response.headers.append("Set-Cookie", recovery.setCookie);
|
|
322
|
+
return response;
|
|
183
323
|
}
|
|
184
324
|
return NextResponse.next();
|
|
185
325
|
}, "middleware");
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-
|
|
2
|
-
export {
|
|
1
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-0yzFb9CR.js';
|
|
2
|
+
export { A as AuthClientConfig, a as AuthError, b as AuthEvent, c as AuthEventHandler, d as AuthEventMap, e as AuthState, f as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, O as OAuthLoginOptions, g as OAuthProvider, R as RequestOptions, h as RetryConfig, i as ServerResponse, j as TokenPair, k as createAuthClient } from '../NajmAuthClient-0yzFb9CR.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Decode a JWT token payload without verification.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode, CSSProperties, ReactElement } from 'react';
|
|
4
|
-
import { N as NajmAuthClient, H as HydrateSession,
|
|
4
|
+
import { N as NajmAuthClient, H as HydrateSession, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-0yzFb9CR.js';
|
|
5
5
|
|
|
6
6
|
interface AuthProviderProps {
|
|
7
7
|
client: NajmAuthClient;
|
|
@@ -7,7 +7,7 @@ import { useEffect, useRef } from "react";
|
|
|
7
7
|
|
|
8
8
|
// src/client/react/context.ts
|
|
9
9
|
import { createContext, useContext } from "react";
|
|
10
|
-
var KEY =
|
|
10
|
+
var KEY = Symbol.for("najm:auth:client:context");
|
|
11
11
|
var contextStore = globalThis;
|
|
12
12
|
function getAuthClientContext() {
|
|
13
13
|
const existing = contextStore[KEY];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-0yzFb9CR.js';
|
|
2
2
|
export { withAuthMiddleware } from '../edge.js';
|
|
3
3
|
import 'next/server';
|
|
4
4
|
|
|
@@ -72,27 +72,17 @@ interface ServerSession {
|
|
|
72
72
|
interface GetSessionConfig {
|
|
73
73
|
/**
|
|
74
74
|
* Base URL for auth endpoints.
|
|
75
|
-
* Defaults to
|
|
75
|
+
* Defaults to `NEXT_PUBLIC_API_URL` or the same-origin `/api` path.
|
|
76
76
|
*/
|
|
77
77
|
baseURL?: string;
|
|
78
|
-
/**
|
|
79
|
-
* Auth route prefix appended to baseURL (default: '/auth').
|
|
80
|
-
*/
|
|
78
|
+
/** Auth route prefix appended to baseURL (default: '/auth'). */
|
|
81
79
|
authPrefix?: string;
|
|
82
|
-
/**
|
|
83
|
-
* Refresh token cookie name to check before making the network call.
|
|
84
|
-
* If absent we skip and return null immediately.
|
|
85
|
-
* Default: 'refreshToken'
|
|
86
|
-
*/
|
|
80
|
+
/** Refresh token cookie name (default: 'refreshToken'). */
|
|
87
81
|
cookieName?: string;
|
|
88
|
-
/**
|
|
89
|
-
* Session cookie name (default: 'najm.session').
|
|
90
|
-
* When present and valid, skips the /auth/me fetch entirely.
|
|
91
|
-
*/
|
|
82
|
+
/** Signed session cookie name (default: 'najm.session'). */
|
|
92
83
|
sessionCookieName?: string;
|
|
93
84
|
/**
|
|
94
85
|
* Secret used to verify the session cookie HMAC signature.
|
|
95
|
-
* Must match the `jwt.accessSecret` used by the auth plugin.
|
|
96
86
|
* Falls back to NAJM_SESSION_SECRET or JWT_ACCESS_SECRET env vars.
|
|
97
87
|
*/
|
|
98
88
|
sessionSecret?: string;
|
|
@@ -101,6 +91,11 @@ interface GetSessionConfig {
|
|
|
101
91
|
* Must match the auth plugin's `session.maxAge`. Default: 300.
|
|
102
92
|
*/
|
|
103
93
|
sessionMaxAge?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Session-recovery endpoint. Defaults to
|
|
96
|
+
* `${baseURL}${authPrefix}/session/recover`. Set to false to disable fallback.
|
|
97
|
+
*/
|
|
98
|
+
recoveryURL?: string | false;
|
|
104
99
|
/**
|
|
105
100
|
* Error handling mode:
|
|
106
101
|
* - 'nullable' (default): returns null on any failure
|
|
@@ -122,29 +117,10 @@ declare class AuthTransportError extends Error {
|
|
|
122
117
|
constructor(message: string, status?: number);
|
|
123
118
|
}
|
|
124
119
|
/**
|
|
125
|
-
* Resolve
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* returns the session instantly with zero network calls.
|
|
130
|
-
*
|
|
131
|
-
* **Fallback**: Checks the refresh token cookie and calls `/auth/me`.
|
|
132
|
-
*
|
|
133
|
-
* @example
|
|
134
|
-
* ```tsx
|
|
135
|
-
* import { getSession } from 'najm-auth/client/server';
|
|
136
|
-
*
|
|
137
|
-
* export default async function RootLayout({ children }) {
|
|
138
|
-
* const session = await getSession();
|
|
139
|
-
* return (
|
|
140
|
-
* <html><body>
|
|
141
|
-
* <AuthProvider client={authClient} initialSession={session}>
|
|
142
|
-
* {children}
|
|
143
|
-
* </AuthProvider>
|
|
144
|
-
* </body></html>
|
|
145
|
-
* );
|
|
146
|
-
* }
|
|
147
|
-
* ```
|
|
120
|
+
* Resolve a session in a Next.js Server Component, Route Handler, or Server
|
|
121
|
+
* Action. Recovery returns claims for the current render but cannot persist
|
|
122
|
+
* response cookies during Server Component rendering; middleware performs that
|
|
123
|
+
* persistence for protected navigation.
|
|
148
124
|
*/
|
|
149
125
|
declare function getSession(config?: GetSessionConfig): Promise<ServerSession | null>;
|
|
150
126
|
|
|
@@ -200,11 +176,16 @@ interface DefineAuthConfig {
|
|
|
200
176
|
sessionSecret?: string;
|
|
201
177
|
/** Must match the auth plugin's session.maxAge. Default: 300 seconds. */
|
|
202
178
|
sessionMaxAge?: number;
|
|
179
|
+
/**
|
|
180
|
+
* Session-recovery endpoint. Defaults to
|
|
181
|
+
* `${apiBaseURL}${authPrefix}/session/recover`; false disables recovery.
|
|
182
|
+
*/
|
|
183
|
+
recoveryURL?: string | false;
|
|
203
184
|
/** Next.js middleware matcher (default: exclude _next, favicon, api) */
|
|
204
185
|
matcher?: string[];
|
|
205
186
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
187
|
+
* Force authoritative refresh-session validation on every protected request.
|
|
188
|
+
* Recovery reissues the signed cookie without rotating refresh tokens.
|
|
208
189
|
*/
|
|
209
190
|
verifyAlways?: boolean;
|
|
210
191
|
}
|
|
@@ -218,7 +199,7 @@ interface AuthKit {
|
|
|
218
199
|
readonly client: NajmAuthClient;
|
|
219
200
|
/** Shortcut for `client.api` — the underlying FetchClient with auth attached. */
|
|
220
201
|
readonly api: FetchClient;
|
|
221
|
-
/** Resolve session —
|
|
202
|
+
/** Resolve session — signed-cookie first, then non-rotating recovery. */
|
|
222
203
|
getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
|
|
223
204
|
/** Require session — throws if unauthenticated */
|
|
224
205
|
requireSession: () => Promise<ServerSession>;
|
|
@@ -144,6 +144,111 @@ var init_sessionCookie = __esm({
|
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
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
|
+
const endpoint = sameOriginRecoveryEndpoint(
|
|
156
|
+
options.endpoint,
|
|
157
|
+
options.requestOrigin
|
|
158
|
+
);
|
|
159
|
+
if (!endpoint) {
|
|
160
|
+
return { status: "unavailable" };
|
|
161
|
+
}
|
|
162
|
+
let response;
|
|
163
|
+
try {
|
|
164
|
+
response = await fetch(endpoint, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
Accept: "application/json",
|
|
168
|
+
Cookie: `${options.refreshCookieName}=${options.refreshCookieValue}`,
|
|
169
|
+
"X-Najm-Session-Recovery": "1"
|
|
170
|
+
},
|
|
171
|
+
cache: "no-store",
|
|
172
|
+
redirect: "manual"
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
return { status: "unavailable" };
|
|
176
|
+
}
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const status = response.status;
|
|
179
|
+
return {
|
|
180
|
+
status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
|
|
181
|
+
httpStatus: status
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const setCookie = response.headers.get("set-cookie");
|
|
185
|
+
if (!setCookie) return { status: "unavailable", httpStatus: response.status };
|
|
186
|
+
const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
|
|
187
|
+
if (!sessionCookieValue) {
|
|
188
|
+
return { status: "unavailable", httpStatus: response.status };
|
|
189
|
+
}
|
|
190
|
+
const claims = await verifySessionCookie(sessionCookieValue, {
|
|
191
|
+
secret: options.sessionSecret,
|
|
192
|
+
maxAgeSeconds: options.sessionMaxAge
|
|
193
|
+
});
|
|
194
|
+
if (!claims) return { status: "unavailable", httpStatus: response.status };
|
|
195
|
+
return {
|
|
196
|
+
status: "recovered",
|
|
197
|
+
claims,
|
|
198
|
+
setCookie,
|
|
199
|
+
sessionCookieValue
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function authEndpoint(baseURL, authPrefix, suffix, requestURL) {
|
|
203
|
+
const normalizedBase = baseURL.replace(/\/+$/, "");
|
|
204
|
+
const normalizedPrefix = `/${authPrefix.replace(/^\/+|\/+$/g, "")}`;
|
|
205
|
+
const normalizedSuffix = `/${suffix.replace(/^\/+/, "")}`;
|
|
206
|
+
const value = `${normalizedBase}${normalizedPrefix}${normalizedSuffix}`;
|
|
207
|
+
return requestURL ? new URL(value, requestURL).toString() : value;
|
|
208
|
+
}
|
|
209
|
+
function replaceCookieValue(cookieHeader, name, value) {
|
|
210
|
+
const parts = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => part.slice(0, part.indexOf("=")).trim() !== name);
|
|
211
|
+
parts.push(`${name}=${value}`);
|
|
212
|
+
return parts.join("; ");
|
|
213
|
+
}
|
|
214
|
+
function readSetCookieValue(setCookie, name) {
|
|
215
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
216
|
+
const match = setCookie.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;]*)`));
|
|
217
|
+
return match?.[1] || void 0;
|
|
218
|
+
}
|
|
219
|
+
function isCookieName(value) {
|
|
220
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
|
|
221
|
+
}
|
|
222
|
+
function isCookieValue(value) {
|
|
223
|
+
return value.length > 0 && /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/.test(value);
|
|
224
|
+
}
|
|
225
|
+
function sameOriginRecoveryEndpoint(endpoint, requestOrigin) {
|
|
226
|
+
try {
|
|
227
|
+
const trusted = new URL(requestOrigin);
|
|
228
|
+
if (trusted.protocol !== "https:" && trusted.protocol !== "http:" || trusted.username || trusted.password) {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
const resolved = new URL(endpoint, trusted.origin);
|
|
232
|
+
if (resolved.username || resolved.password) return void 0;
|
|
233
|
+
if (resolved.origin !== trusted.origin) return void 0;
|
|
234
|
+
return resolved.toString();
|
|
235
|
+
} catch {
|
|
236
|
+
return void 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
var init_sessionRecovery = __esm({
|
|
240
|
+
"src/client/sessionRecovery.ts"() {
|
|
241
|
+
init_sessionCookie();
|
|
242
|
+
__name(requestSessionRecovery, "requestSessionRecovery");
|
|
243
|
+
__name(authEndpoint, "authEndpoint");
|
|
244
|
+
__name(replaceCookieValue, "replaceCookieValue");
|
|
245
|
+
__name(readSetCookieValue, "readSetCookieValue");
|
|
246
|
+
__name(isCookieName, "isCookieName");
|
|
247
|
+
__name(isCookieValue, "isCookieValue");
|
|
248
|
+
__name(sameOriginRecoveryEndpoint, "sameOriginRecoveryEndpoint");
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
147
252
|
// src/client/server/getSession.ts
|
|
148
253
|
var getSession_exports = {};
|
|
149
254
|
__export(getSession_exports, {
|
|
@@ -155,17 +260,25 @@ __export(getSession_exports, {
|
|
|
155
260
|
function defaultBaseURL() {
|
|
156
261
|
const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
|
|
157
262
|
if (explicit) return explicit;
|
|
158
|
-
const
|
|
159
|
-
return
|
|
263
|
+
const publicUrl = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL : void 0;
|
|
264
|
+
return publicUrl || "/api";
|
|
160
265
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
266
|
+
function firstForwardedValue(value) {
|
|
267
|
+
const first = value?.split(",")[0]?.trim();
|
|
268
|
+
return first || void 0;
|
|
269
|
+
}
|
|
270
|
+
function requestOriginFromHeaders(headers) {
|
|
271
|
+
const host = firstForwardedValue(headers.get("x-forwarded-host")) ?? firstForwardedValue(headers.get("host"));
|
|
272
|
+
if (!host) return void 0;
|
|
273
|
+
const protocol = firstForwardedValue(headers.get("x-forwarded-proto")) ?? "https";
|
|
274
|
+
if (protocol !== "https" && protocol !== "http") return void 0;
|
|
275
|
+
try {
|
|
276
|
+
const url = new URL(`${protocol}://${host}`);
|
|
277
|
+
if (url.username || url.password) return void 0;
|
|
278
|
+
return url.origin;
|
|
279
|
+
} catch {
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
169
282
|
}
|
|
170
283
|
async function getSession(config = {}) {
|
|
171
284
|
const cookieName = config.cookieName ?? "refreshToken";
|
|
@@ -173,73 +286,80 @@ async function getSession(config = {}) {
|
|
|
173
286
|
const baseURL = config.baseURL ?? defaultBaseURL();
|
|
174
287
|
const prefix = config.authPrefix ?? "/auth";
|
|
175
288
|
const strict = config.mode === "strict";
|
|
176
|
-
let cookieHeader = "";
|
|
177
289
|
let sessionCookieValue;
|
|
178
|
-
let
|
|
290
|
+
let refreshCookieValue;
|
|
291
|
+
let requestOrigin;
|
|
179
292
|
try {
|
|
180
293
|
const mod = await import("next/headers");
|
|
181
294
|
const cookieStore = await mod.cookies();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (typeof
|
|
185
|
-
|
|
295
|
+
sessionCookieValue = cookieStore.get(sessionCookieName)?.value;
|
|
296
|
+
refreshCookieValue = cookieStore.get(cookieName)?.value;
|
|
297
|
+
if (typeof mod.headers === "function") {
|
|
298
|
+
requestOrigin = requestOriginFromHeaders(await mod.headers());
|
|
186
299
|
}
|
|
187
|
-
|
|
188
|
-
} catch (err) {
|
|
300
|
+
} catch {
|
|
189
301
|
if (strict) throw new AuthConfigError("Failed to read cookies from Next.js headers()");
|
|
190
302
|
return null;
|
|
191
303
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!secret) {
|
|
195
|
-
if (strict) throw new AuthConfigError("Session cookie secret is not configured");
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
304
|
+
const secret = resolveSessionSecret(config.sessionSecret);
|
|
305
|
+
if (sessionCookieValue && secret) {
|
|
198
306
|
const claims = await verifySessionCookie(sessionCookieValue, {
|
|
199
307
|
secret,
|
|
200
308
|
maxAgeSeconds: config.sessionMaxAge
|
|
201
309
|
});
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
310
|
+
if (claims) {
|
|
311
|
+
return {
|
|
312
|
+
user: claims.user,
|
|
313
|
+
roles: claims.roles,
|
|
314
|
+
permissions: claims.permissions
|
|
315
|
+
};
|
|
205
316
|
}
|
|
206
|
-
return {
|
|
207
|
-
user: claims.user,
|
|
208
|
-
roles: claims.roles,
|
|
209
|
-
permissions: claims.permissions
|
|
210
|
-
};
|
|
211
317
|
}
|
|
212
|
-
if (!
|
|
213
|
-
if (strict) throw new
|
|
318
|
+
if (!secret) {
|
|
319
|
+
if (strict) throw new AuthConfigError("Session cookie secret is not configured");
|
|
214
320
|
return null;
|
|
215
321
|
}
|
|
216
|
-
if (!
|
|
217
|
-
if (strict) throw new NoSessionError("
|
|
322
|
+
if (!refreshCookieValue || config.recoveryURL === false) {
|
|
323
|
+
if (strict) throw new NoSessionError("No recoverable refresh session");
|
|
218
324
|
return null;
|
|
219
325
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
headers: { Cookie: cookieHeader, Accept: "application/json" },
|
|
223
|
-
cache: "no-store"
|
|
224
|
-
});
|
|
225
|
-
if (!res.ok) {
|
|
226
|
-
if (strict) throw new AuthTransportError(`/auth/me returned ${res.status}`, res.status);
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
const body = await res.json();
|
|
230
|
-
const session = buildSession(body);
|
|
231
|
-
if (!session && strict) throw new NoSessionError("No user data in /auth/me response");
|
|
232
|
-
return session;
|
|
233
|
-
} catch (err) {
|
|
234
|
-
if (err instanceof NoSessionError || err instanceof AuthTransportError) throw err;
|
|
235
|
-
if (strict) throw new AuthTransportError(`Failed to fetch /auth/me: ${err.message}`);
|
|
326
|
+
if (!requestOrigin) {
|
|
327
|
+
if (strict) throw new AuthConfigError("Incoming request origin is unavailable");
|
|
236
328
|
return null;
|
|
237
329
|
}
|
|
330
|
+
const endpoint = config.recoveryURL ? new URL(config.recoveryURL, requestOrigin).toString() : authEndpoint(baseURL, prefix, "/session/recover", requestOrigin);
|
|
331
|
+
const recovery = await requestSessionRecovery({
|
|
332
|
+
endpoint,
|
|
333
|
+
requestOrigin,
|
|
334
|
+
refreshCookieName: cookieName,
|
|
335
|
+
refreshCookieValue,
|
|
336
|
+
sessionCookieName,
|
|
337
|
+
sessionSecret: secret,
|
|
338
|
+
sessionMaxAge: config.sessionMaxAge
|
|
339
|
+
});
|
|
340
|
+
if (recovery.status === "recovered") {
|
|
341
|
+
return {
|
|
342
|
+
user: recovery.claims.user,
|
|
343
|
+
roles: recovery.claims.roles,
|
|
344
|
+
permissions: recovery.claims.permissions
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (strict) {
|
|
348
|
+
if (recovery.status === "invalid") {
|
|
349
|
+
throw new NoSessionError("Refresh session is invalid or revoked");
|
|
350
|
+
}
|
|
351
|
+
throw new AuthTransportError(
|
|
352
|
+
"Session recovery endpoint was unavailable or returned an invalid session",
|
|
353
|
+
recovery.httpStatus
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
238
357
|
}
|
|
239
358
|
var NoSessionError, AuthConfigError, AuthTransportError;
|
|
240
359
|
var init_getSession = __esm({
|
|
241
360
|
"src/client/server/getSession.ts"() {
|
|
242
361
|
init_sessionCookie();
|
|
362
|
+
init_sessionRecovery();
|
|
243
363
|
NoSessionError = class extends Error {
|
|
244
364
|
static {
|
|
245
365
|
__name(this, "NoSessionError");
|
|
@@ -272,7 +392,8 @@ var init_getSession = __esm({
|
|
|
272
392
|
code = "AUTH_TRANSPORT_ERROR";
|
|
273
393
|
};
|
|
274
394
|
__name(defaultBaseURL, "defaultBaseURL");
|
|
275
|
-
__name(
|
|
395
|
+
__name(firstForwardedValue, "firstForwardedValue");
|
|
396
|
+
__name(requestOriginFromHeaders, "requestOriginFromHeaders");
|
|
276
397
|
__name(getSession, "getSession");
|
|
277
398
|
}
|
|
278
399
|
});
|
|
@@ -468,6 +589,7 @@ __name(createServerClient, "createServerClient");
|
|
|
468
589
|
|
|
469
590
|
// src/client/server/withAuthMiddleware.ts
|
|
470
591
|
init_sessionCookie();
|
|
592
|
+
init_sessionRecovery();
|
|
471
593
|
function withAuthMiddleware(config) {
|
|
472
594
|
const {
|
|
473
595
|
protectedRoutes = [],
|
|
@@ -475,26 +597,31 @@ function withAuthMiddleware(config) {
|
|
|
475
597
|
loginRoute = "/login",
|
|
476
598
|
roleRoutes = {},
|
|
477
599
|
cookieName = "refreshToken",
|
|
600
|
+
apiBaseURL = "/api",
|
|
601
|
+
authPrefix = "/auth",
|
|
478
602
|
sessionCookieName = "najm.session",
|
|
479
603
|
sessionSecret,
|
|
480
604
|
sessionMaxAge,
|
|
481
|
-
verifyAlways = false
|
|
605
|
+
verifyAlways = false,
|
|
606
|
+
recoveryURL
|
|
482
607
|
} = config;
|
|
483
|
-
void verifyAlways;
|
|
484
608
|
return /* @__PURE__ */ __name(async function middleware(request) {
|
|
485
609
|
const { NextResponse } = await import("next/server");
|
|
486
|
-
const redirectToLogin = /* @__PURE__ */ __name((
|
|
610
|
+
const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
|
|
487
611
|
const loginUrl = new URL(loginRoute, request.url);
|
|
488
|
-
loginUrl.searchParams.set("from",
|
|
612
|
+
loginUrl.searchParams.set("from", returnPath2);
|
|
489
613
|
const res = NextResponse.redirect(loginUrl);
|
|
490
|
-
if (clearCookies) {
|
|
614
|
+
if (clearCookies.includes("refresh")) {
|
|
491
615
|
res.cookies.delete(cookieName);
|
|
616
|
+
}
|
|
617
|
+
if (clearCookies.includes("session")) {
|
|
492
618
|
res.cookies.delete(sessionCookieName);
|
|
493
619
|
}
|
|
494
620
|
return res;
|
|
495
621
|
}, "redirectToLogin");
|
|
496
622
|
const url = new URL(request.url);
|
|
497
623
|
const pathname = url.pathname;
|
|
624
|
+
const returnPath = `${url.pathname}${url.search}`;
|
|
498
625
|
if (matchesAny(pathname, publicRoutes)) {
|
|
499
626
|
return NextResponse.next();
|
|
500
627
|
}
|
|
@@ -503,19 +630,54 @@ function withAuthMiddleware(config) {
|
|
|
503
630
|
const cookie = request.headers.get("cookie") ?? "";
|
|
504
631
|
const sessionCookie = readCookieValue(cookie, sessionCookieName);
|
|
505
632
|
const secret = resolveSessionSecret(sessionSecret);
|
|
506
|
-
if (!
|
|
507
|
-
return redirectToLogin(
|
|
633
|
+
if (!secret) {
|
|
634
|
+
return redirectToLogin(returnPath, ["session"]);
|
|
508
635
|
}
|
|
509
|
-
|
|
636
|
+
let session = sessionCookie ? await verifySessionCookie(sessionCookie, {
|
|
510
637
|
secret,
|
|
511
638
|
maxAgeSeconds: sessionMaxAge
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
|
|
639
|
+
}) : null;
|
|
640
|
+
let recovery = null;
|
|
641
|
+
if (!session || verifyAlways) {
|
|
642
|
+
const refreshCookie = readCookieValue(cookie, cookieName);
|
|
643
|
+
if (!refreshCookie || recoveryURL === false) {
|
|
644
|
+
return redirectToLogin(returnPath, ["refresh", "session"]);
|
|
645
|
+
}
|
|
646
|
+
const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
|
|
647
|
+
recovery = await requestSessionRecovery({
|
|
648
|
+
endpoint,
|
|
649
|
+
requestOrigin: url.origin,
|
|
650
|
+
refreshCookieName: cookieName,
|
|
651
|
+
refreshCookieValue: refreshCookie,
|
|
652
|
+
sessionCookieName,
|
|
653
|
+
sessionSecret: secret,
|
|
654
|
+
sessionMaxAge
|
|
655
|
+
});
|
|
656
|
+
if (recovery.status !== "recovered") {
|
|
657
|
+
return redirectToLogin(
|
|
658
|
+
returnPath,
|
|
659
|
+
recovery.status === "invalid" ? ["refresh", "session"] : ["session"]
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
session = recovery.claims;
|
|
515
663
|
}
|
|
516
664
|
const requiredRoles = findMatchingRoles(pathname, roleRoutes);
|
|
517
665
|
if (requiredRoles && !session.roles.some((role) => requiredRoles.includes(role))) {
|
|
518
|
-
|
|
666
|
+
const forbidden = new NextResponse(null, { status: 403 });
|
|
667
|
+
if (recovery?.status === "recovered") {
|
|
668
|
+
forbidden.headers.append("Set-Cookie", recovery.setCookie);
|
|
669
|
+
}
|
|
670
|
+
return forbidden;
|
|
671
|
+
}
|
|
672
|
+
if (recovery?.status === "recovered") {
|
|
673
|
+
const requestHeaders = new Headers(request.headers);
|
|
674
|
+
requestHeaders.set(
|
|
675
|
+
"cookie",
|
|
676
|
+
replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
|
|
677
|
+
);
|
|
678
|
+
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
679
|
+
response.headers.append("Set-Cookie", recovery.setCookie);
|
|
680
|
+
return response;
|
|
519
681
|
}
|
|
520
682
|
return NextResponse.next();
|
|
521
683
|
}, "middleware");
|
|
@@ -1049,6 +1211,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1049
1211
|
sessionCookieName = "najm.session",
|
|
1050
1212
|
sessionSecret,
|
|
1051
1213
|
sessionMaxAge,
|
|
1214
|
+
recoveryURL,
|
|
1052
1215
|
matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
|
|
1053
1216
|
verifyAlways = false,
|
|
1054
1217
|
refreshThreshold,
|
|
@@ -1058,13 +1221,13 @@ function defineAuth(authConfig = {}) {
|
|
|
1058
1221
|
retry
|
|
1059
1222
|
} = authConfig;
|
|
1060
1223
|
const sessionConfig = {
|
|
1061
|
-
baseURL:
|
|
1062
|
-
// resolved at call time from env/defaults
|
|
1224
|
+
baseURL: apiBaseURL,
|
|
1063
1225
|
authPrefix,
|
|
1064
1226
|
cookieName,
|
|
1065
1227
|
sessionCookieName,
|
|
1066
1228
|
sessionSecret,
|
|
1067
|
-
sessionMaxAge
|
|
1229
|
+
sessionMaxAge,
|
|
1230
|
+
recoveryURL
|
|
1068
1231
|
};
|
|
1069
1232
|
let _client = null;
|
|
1070
1233
|
const getClient = /* @__PURE__ */ __name(() => {
|
|
@@ -1112,9 +1275,12 @@ function defineAuth(authConfig = {}) {
|
|
|
1112
1275
|
loginRoute,
|
|
1113
1276
|
roleRoutes,
|
|
1114
1277
|
cookieName,
|
|
1278
|
+
apiBaseURL,
|
|
1279
|
+
authPrefix,
|
|
1115
1280
|
sessionCookieName,
|
|
1116
1281
|
sessionSecret,
|
|
1117
1282
|
sessionMaxAge,
|
|
1283
|
+
recoveryURL,
|
|
1118
1284
|
verifyAlways
|
|
1119
1285
|
});
|
|
1120
1286
|
const protect = /* @__PURE__ */ __name((Page, options) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { User, NewUser, RoleEntity, NewRoleEntity, Permission, NewPermission, Ro
|
|
|
9
9
|
export { NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
|
|
10
10
|
import { CacheService } from 'najm-cache';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
+
import { Context } from 'hono';
|
|
12
13
|
import { GuardResult } from 'najm-guard';
|
|
13
14
|
import 'drizzle-orm';
|
|
14
15
|
import 'drizzle-orm/pg-core';
|
|
@@ -279,6 +280,7 @@ var auth = {
|
|
|
279
280
|
passwordReset: "Password has been reset successfully",
|
|
280
281
|
accountInviteSent: "Invitation sent successfully",
|
|
281
282
|
tokenRefreshed: "Token refreshed successfully",
|
|
283
|
+
sessionRecovered: "Session recovered successfully",
|
|
282
284
|
oauthLogin: "Google sign-in successful",
|
|
283
285
|
oauthLinked: "Google account linked successfully"
|
|
284
286
|
},
|
|
@@ -396,6 +398,7 @@ declare const AUTH_LOCALES: {
|
|
|
396
398
|
passwordReset: string;
|
|
397
399
|
accountInviteSent: string;
|
|
398
400
|
tokenRefreshed: string;
|
|
401
|
+
sessionRecovered: string;
|
|
399
402
|
oauthLogin: string;
|
|
400
403
|
oauthLinked: string;
|
|
401
404
|
};
|
|
@@ -932,7 +935,21 @@ declare class TokenService {
|
|
|
932
935
|
* the active session. Reuse detection and revocation belong to the
|
|
933
936
|
* rotation path only.
|
|
934
937
|
*/
|
|
938
|
+
private resolveRefreshSessionFromCookie;
|
|
935
939
|
resolveUserFromCookie(): Promise<string>;
|
|
940
|
+
/**
|
|
941
|
+
* Resolve authoritative claims for signed-session recovery.
|
|
942
|
+
*
|
|
943
|
+
* This deliberately bypasses the 30-second user cache so status, role, and
|
|
944
|
+
* permission changes are reflected when the short session snapshot expires.
|
|
945
|
+
* It validates but never rotates or consumes the refresh token.
|
|
946
|
+
*/
|
|
947
|
+
recoverSessionFromCookie(): Promise<{
|
|
948
|
+
user: any;
|
|
949
|
+
roles: any[];
|
|
950
|
+
permissions: any;
|
|
951
|
+
sessionVersion: number;
|
|
952
|
+
}>;
|
|
936
953
|
getUser(auth: string): Promise<any>;
|
|
937
954
|
getUserById(userId: string): Promise<any>;
|
|
938
955
|
private hashToken;
|
|
@@ -1009,6 +1026,7 @@ declare class TokenService {
|
|
|
1009
1026
|
accessTokenExpiresAt: number;
|
|
1010
1027
|
refreshTokenExpiresAt: number;
|
|
1011
1028
|
}>;
|
|
1029
|
+
private requireActiveRefreshUser;
|
|
1012
1030
|
/** Revoke every refresh session for a user (password change/reset, logout-all). */
|
|
1013
1031
|
revokeAllForUser(userId: string): Promise<any>;
|
|
1014
1032
|
/** Revoke a single refresh session (one family). */
|
|
@@ -1242,6 +1260,14 @@ declare class AuthService {
|
|
|
1242
1260
|
user: SanitizedUser;
|
|
1243
1261
|
}>;
|
|
1244
1262
|
refreshTokens(): Promise<TokenPair>;
|
|
1263
|
+
/**
|
|
1264
|
+
* Reissue the short-lived signed session snapshot from a fully validated
|
|
1265
|
+
* refresh session. This path never creates or returns access/refresh tokens
|
|
1266
|
+
* and never rotates the refresh family.
|
|
1267
|
+
*/
|
|
1268
|
+
recoverSession(): Promise<{
|
|
1269
|
+
recovered: true;
|
|
1270
|
+
}>;
|
|
1245
1271
|
logoutUser(userId: string, authorization?: string): Promise<{
|
|
1246
1272
|
data: any;
|
|
1247
1273
|
message: string;
|
|
@@ -1310,6 +1336,9 @@ declare class AuthController {
|
|
|
1310
1336
|
emailSent: boolean;
|
|
1311
1337
|
}>;
|
|
1312
1338
|
refreshTokens(): Promise<TokenPair>;
|
|
1339
|
+
recoverSession(recoveryRequest: string | undefined, ctx: Context): Promise<{
|
|
1340
|
+
recovered: true;
|
|
1341
|
+
}>;
|
|
1313
1342
|
logoutUser(userId: string, authorization?: string): Promise<{
|
|
1314
1343
|
data: any;
|
|
1315
1344
|
message: string;
|
package/dist/index.js
CHANGED
|
@@ -6,16 +6,16 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/AuthPlugin.ts
|
|
9
|
-
import { Err as
|
|
9
|
+
import { Err as Err11, plugin } from "najm-core";
|
|
10
10
|
import { cache } from "najm-cache";
|
|
11
11
|
|
|
12
12
|
// src/auth.tokens.ts
|
|
13
|
-
var AUTH_CONFIG =
|
|
14
|
-
var AUTH_SCHEMA =
|
|
15
|
-
var AUTH_USER =
|
|
16
|
-
var AUTH_ROLE =
|
|
17
|
-
var AUTH_PERMISSIONS =
|
|
18
|
-
var AUTH_ENCRYPTION_KEY =
|
|
13
|
+
var AUTH_CONFIG = Symbol.for("najm:auth:config");
|
|
14
|
+
var AUTH_SCHEMA = Symbol.for("najm:auth:schema");
|
|
15
|
+
var AUTH_USER = Symbol.for("najm:auth:user");
|
|
16
|
+
var AUTH_ROLE = Symbol.for("najm:auth:role");
|
|
17
|
+
var AUTH_PERMISSIONS = Symbol.for("najm:auth:permissions");
|
|
18
|
+
var AUTH_ENCRYPTION_KEY = Symbol.for("najm:auth:encryption-key");
|
|
19
19
|
|
|
20
20
|
// src/schema/pg.ts
|
|
21
21
|
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex } from "drizzle-orm/pg-core";
|
|
@@ -431,9 +431,9 @@ CookieManager = __decorate2([
|
|
|
431
431
|
], CookieManager);
|
|
432
432
|
|
|
433
433
|
// src/auth/AuthController.ts
|
|
434
|
-
import { Controller } from "najm-core";
|
|
434
|
+
import { Controller, Err as Err9 } from "najm-core";
|
|
435
435
|
import { Get, Post, ResMsg } from "najm-core";
|
|
436
|
-
import { Body, User as User2, Headers } from "najm-core";
|
|
436
|
+
import { Body, User as User2, Headers, Ctx } from "najm-core";
|
|
437
437
|
|
|
438
438
|
// src/auth/AuthService.ts
|
|
439
439
|
import { Injectable as Injectable8, Inject as Inject8 } from "najm-core";
|
|
@@ -1638,7 +1638,7 @@ var TokenService = class TokenService2 {
|
|
|
1638
1638
|
* the active session. Reuse detection and revocation belong to the
|
|
1639
1639
|
* rotation path only.
|
|
1640
1640
|
*/
|
|
1641
|
-
async
|
|
1641
|
+
async resolveRefreshSessionFromCookie() {
|
|
1642
1642
|
const refreshToken = this.cookieManager.getRefreshToken();
|
|
1643
1643
|
if (!refreshToken) {
|
|
1644
1644
|
Err6(this.t("errors.refreshTokenMissing"));
|
|
@@ -1650,14 +1650,35 @@ var TokenService = class TokenService2 {
|
|
|
1650
1650
|
}
|
|
1651
1651
|
const presentedHash = this.hashToken(refreshToken);
|
|
1652
1652
|
if (presentedHash === stored.token) {
|
|
1653
|
-
return userId;
|
|
1653
|
+
return { userId, tokenFamily };
|
|
1654
1654
|
}
|
|
1655
1655
|
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && new Date(stored.previousValidUntil).getTime() > Date.now() && !stored.previousUsedAt;
|
|
1656
1656
|
if (canRecover) {
|
|
1657
|
-
return userId;
|
|
1657
|
+
return { userId, tokenFamily };
|
|
1658
1658
|
}
|
|
1659
1659
|
Err6(this.t("errors.refreshTokenInvalid"));
|
|
1660
1660
|
}
|
|
1661
|
+
async resolveUserFromCookie() {
|
|
1662
|
+
return (await this.resolveRefreshSessionFromCookie()).userId;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Resolve authoritative claims for signed-session recovery.
|
|
1666
|
+
*
|
|
1667
|
+
* This deliberately bypasses the 30-second user cache so status, role, and
|
|
1668
|
+
* permission changes are reflected when the short session snapshot expires.
|
|
1669
|
+
* It validates but never rotates or consumes the refresh token.
|
|
1670
|
+
*/
|
|
1671
|
+
async recoverSessionFromCookie() {
|
|
1672
|
+
const { userId, tokenFamily } = await this.resolveRefreshSessionFromCookie();
|
|
1673
|
+
const user = await this.requireActiveRefreshUser(userId, tokenFamily);
|
|
1674
|
+
const sessionVersion = await this.getUserSessionVersion(userId);
|
|
1675
|
+
return {
|
|
1676
|
+
user,
|
|
1677
|
+
roles: user.role ? [user.role] : [],
|
|
1678
|
+
permissions: Array.isArray(user.permissions) ? user.permissions : [],
|
|
1679
|
+
sessionVersion
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1661
1682
|
// ============ USER RETRIEVAL (MAIN METHOD) ============
|
|
1662
1683
|
async getUser(auth2) {
|
|
1663
1684
|
if (!auth2)
|
|
@@ -1820,6 +1841,7 @@ var TokenService = class TokenService2 {
|
|
|
1820
1841
|
Err6(this.t("errors.refreshTokenInvalid"));
|
|
1821
1842
|
}
|
|
1822
1843
|
const presentedHash = this.hashToken(refreshToken);
|
|
1844
|
+
await this.requireActiveRefreshUser(userId, tokenFamily);
|
|
1823
1845
|
if (presentedHash === stored.token) {
|
|
1824
1846
|
return this.generateTokens(userId, tokenFamily);
|
|
1825
1847
|
}
|
|
@@ -1834,6 +1856,14 @@ var TokenService = class TokenService2 {
|
|
|
1834
1856
|
await this.revokeSuspectRefreshFamily(userId, tokenFamily);
|
|
1835
1857
|
Err6(this.t("errors.refreshTokenInvalid"));
|
|
1836
1858
|
}
|
|
1859
|
+
async requireActiveRefreshUser(userId, tokenFamily) {
|
|
1860
|
+
const user = await this.tokenRepository.getUser(userId);
|
|
1861
|
+
if (!user || user.status !== "active") {
|
|
1862
|
+
await this.revokeFamily(tokenFamily);
|
|
1863
|
+
Err6(this.t("errors.refreshTokenInvalid"));
|
|
1864
|
+
}
|
|
1865
|
+
return user;
|
|
1866
|
+
}
|
|
1837
1867
|
/** Revoke every refresh session for a user (password change/reset, logout-all). */
|
|
1838
1868
|
async revokeAllForUser(userId) {
|
|
1839
1869
|
return this.tokenRepository.revokeAllForUser(userId);
|
|
@@ -2276,6 +2306,27 @@ var AuthService = class AuthService2 {
|
|
|
2276
2306
|
const { userId: _userId, tokenFamily: _tokenFamily, roles: _roles, permissions: _permissions, sessionVersion: _sv, ...tokens } = generated;
|
|
2277
2307
|
return tokens;
|
|
2278
2308
|
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Reissue the short-lived signed session snapshot from a fully validated
|
|
2311
|
+
* refresh session. This path never creates or returns access/refresh tokens
|
|
2312
|
+
* and never rotates the refresh family.
|
|
2313
|
+
*/
|
|
2314
|
+
async recoverSession() {
|
|
2315
|
+
const recovered = await this.tokenService.recoverSessionFromCookie();
|
|
2316
|
+
this.cookieManager.setSessionCookie({
|
|
2317
|
+
user: {
|
|
2318
|
+
id: recovered.user.id,
|
|
2319
|
+
email: recovered.user.email,
|
|
2320
|
+
name: recovered.user.name,
|
|
2321
|
+
role: recovered.user.role ?? void 0,
|
|
2322
|
+
status: recovered.user.status ?? void 0
|
|
2323
|
+
},
|
|
2324
|
+
roles: recovered.roles,
|
|
2325
|
+
permissions: recovered.permissions,
|
|
2326
|
+
sessionVersion: recovered.sessionVersion
|
|
2327
|
+
});
|
|
2328
|
+
return { recovered: true };
|
|
2329
|
+
}
|
|
2279
2330
|
async logoutUser(userId, authorization) {
|
|
2280
2331
|
await this.tokenService.logout(userId, authorization);
|
|
2281
2332
|
this.cookieManager.clearRefreshToken();
|
|
@@ -2604,6 +2655,14 @@ var AuthController = class AuthController2 {
|
|
|
2604
2655
|
async refreshTokens() {
|
|
2605
2656
|
return this.authService.refreshTokens();
|
|
2606
2657
|
}
|
|
2658
|
+
async recoverSession(recoveryRequest, ctx) {
|
|
2659
|
+
if (recoveryRequest !== "1") {
|
|
2660
|
+
Err9("Invalid session recovery request", 400);
|
|
2661
|
+
}
|
|
2662
|
+
ctx.header("Cache-Control", "private, no-store");
|
|
2663
|
+
ctx.header("Vary", "Cookie");
|
|
2664
|
+
return this.authService.recoverSession();
|
|
2665
|
+
}
|
|
2607
2666
|
async logoutUser(userId, authorization) {
|
|
2608
2667
|
return this.authService.logoutUser(userId, authorization);
|
|
2609
2668
|
}
|
|
@@ -2659,6 +2718,16 @@ __decorate15([
|
|
|
2659
2718
|
__metadata15("design:paramtypes", []),
|
|
2660
2719
|
__metadata15("design:returntype", Promise)
|
|
2661
2720
|
], AuthController.prototype, "refreshTokens", null);
|
|
2721
|
+
__decorate15([
|
|
2722
|
+
Post("/session/recover"),
|
|
2723
|
+
RateLimit({ limit: 120, window: "1m", key: cookieFingerprint() }),
|
|
2724
|
+
ResMsg("auth.success.sessionRecovered"),
|
|
2725
|
+
__param5(0, Headers("x-najm-session-recovery")),
|
|
2726
|
+
__param5(1, Ctx()),
|
|
2727
|
+
__metadata15("design:type", Function),
|
|
2728
|
+
__metadata15("design:paramtypes", [String, Object]),
|
|
2729
|
+
__metadata15("design:returntype", Promise)
|
|
2730
|
+
], AuthController.prototype, "recoverSession", null);
|
|
2662
2731
|
__decorate15([
|
|
2663
2732
|
Post("/logout"),
|
|
2664
2733
|
isAuth(),
|
|
@@ -3471,7 +3540,7 @@ import { Injectable as Injectable11 } from "najm-core";
|
|
|
3471
3540
|
// src/permissions/PermissionValidator.ts
|
|
3472
3541
|
import { Injectable as Injectable10 } from "najm-core";
|
|
3473
3542
|
import { I18n as I18n7 } from "najm-i18n";
|
|
3474
|
-
import { Err as
|
|
3543
|
+
import { Err as Err10 } from "najm-core";
|
|
3475
3544
|
var __decorate21 = function(decorators, target, key, desc) {
|
|
3476
3545
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3477
3546
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -3500,7 +3569,7 @@ var PermissionValidator = class PermissionValidator2 {
|
|
|
3500
3569
|
async checkPermissionExists(id) {
|
|
3501
3570
|
const permission = await this.permissionRepository.getById(id);
|
|
3502
3571
|
if (!permission) {
|
|
3503
|
-
|
|
3572
|
+
Err10(this.t("errors.notFound"), 404);
|
|
3504
3573
|
}
|
|
3505
3574
|
return permission;
|
|
3506
3575
|
}
|
|
@@ -3510,7 +3579,7 @@ var PermissionValidator = class PermissionValidator2 {
|
|
|
3510
3579
|
async checkPermissionExistsByName(name) {
|
|
3511
3580
|
const permission = await this.permissionRepository.getByName(name);
|
|
3512
3581
|
if (!permission) {
|
|
3513
|
-
|
|
3582
|
+
Err10(this.t("errors.notFound"), 404);
|
|
3514
3583
|
}
|
|
3515
3584
|
return permission;
|
|
3516
3585
|
}
|
|
@@ -3522,7 +3591,7 @@ var PermissionValidator = class PermissionValidator2 {
|
|
|
3522
3591
|
return;
|
|
3523
3592
|
const existingPermission = await this.permissionRepository.getByName(name);
|
|
3524
3593
|
if (existingPermission && existingPermission.id !== excludeId) {
|
|
3525
|
-
|
|
3594
|
+
Err10(this.t("errors.nameExists"), 409);
|
|
3526
3595
|
}
|
|
3527
3596
|
}
|
|
3528
3597
|
/**
|
|
@@ -3545,7 +3614,7 @@ var PermissionValidator = class PermissionValidator2 {
|
|
|
3545
3614
|
await this.checkPermissionExists(permissionId);
|
|
3546
3615
|
const hasPermission = await this.permissionRepository.checkRoleHasPermission(roleId, permissionId);
|
|
3547
3616
|
if (hasPermission) {
|
|
3548
|
-
|
|
3617
|
+
Err10(this.t("errors.roleAlreadyHasPermission"), 409);
|
|
3549
3618
|
}
|
|
3550
3619
|
}
|
|
3551
3620
|
};
|
|
@@ -3911,9 +3980,9 @@ var revokeTokenDto = z4.object({
|
|
|
3911
3980
|
// src/ownership/scopedOwnership.ts
|
|
3912
3981
|
import { aliasedTable, eq as eq6, getTableColumns, sql as sql4 } from "drizzle-orm";
|
|
3913
3982
|
var DEFAULT_ADMIN_ROLES = ["admin"];
|
|
3914
|
-
var DRIZZLE_NAME =
|
|
3915
|
-
var DRIZZLE_BASE_NAME =
|
|
3916
|
-
var DRIZZLE_IS_ALIAS =
|
|
3983
|
+
var DRIZZLE_NAME = Symbol.for("drizzle:Name");
|
|
3984
|
+
var DRIZZLE_BASE_NAME = Symbol.for("drizzle:BaseName");
|
|
3985
|
+
var DRIZZLE_IS_ALIAS = Symbol.for("drizzle:IsAlias");
|
|
3917
3986
|
function join2(left, right) {
|
|
3918
3987
|
const table = right.table;
|
|
3919
3988
|
if (!table)
|
|
@@ -4361,9 +4430,9 @@ __name(configureOwnership, "configureOwnership");
|
|
|
4361
4430
|
// src/ownership/ScopeGuard.ts
|
|
4362
4431
|
import "reflect-metadata";
|
|
4363
4432
|
import { composeGuards as composeGuards5 } from "najm-guard";
|
|
4364
|
-
var ACTION_KEY =
|
|
4365
|
-
var TOKEN_KEY =
|
|
4366
|
-
var POLICY_KEY =
|
|
4433
|
+
var ACTION_KEY = Symbol.for("najm:guard:action");
|
|
4434
|
+
var TOKEN_KEY = Symbol.for("najm:guard:token");
|
|
4435
|
+
var POLICY_KEY = Symbol.for("najm:policy:token");
|
|
4367
4436
|
var PERM = {
|
|
4368
4437
|
list: /* @__PURE__ */ __name((n) => `read:${n}`, "list"),
|
|
4369
4438
|
read: /* @__PURE__ */ __name((n) => `read:${n}`, "read"),
|
|
@@ -4433,7 +4502,7 @@ var __metadata25 = function(k, v) {
|
|
|
4433
4502
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4434
4503
|
};
|
|
4435
4504
|
var _a16;
|
|
4436
|
-
var OWNED_META =
|
|
4505
|
+
var OWNED_META = Symbol.for("najm:owned");
|
|
4437
4506
|
var ScopeContext = class ScopeContext2 {
|
|
4438
4507
|
static {
|
|
4439
4508
|
__name(this, "ScopeContext");
|
|
@@ -4594,6 +4663,7 @@ var en_default = {
|
|
|
4594
4663
|
passwordReset: "Password has been reset successfully",
|
|
4595
4664
|
accountInviteSent: "Invitation sent successfully",
|
|
4596
4665
|
tokenRefreshed: "Token refreshed successfully",
|
|
4666
|
+
sessionRecovered: "Session recovered successfully",
|
|
4597
4667
|
oauthLogin: "Google sign-in successful",
|
|
4598
4668
|
oauthLinked: "Google account linked successfully"
|
|
4599
4669
|
},
|
|
@@ -5010,7 +5080,7 @@ OAuthAccountService = __decorate29([
|
|
|
5010
5080
|
|
|
5011
5081
|
// src/oauth/OAuthController.ts
|
|
5012
5082
|
import { createHash as createHash4 } from "crypto";
|
|
5013
|
-
import { Controller as Controller5, Ctx, Get as Get5, Post as Post5, Query as Query2, User as User5 } from "najm-core";
|
|
5083
|
+
import { Controller as Controller5, Ctx as Ctx2, Get as Get5, Post as Post5, Query as Query2, User as User5 } from "najm-core";
|
|
5014
5084
|
import { RateLimit as RateLimit2 } from "najm-rate";
|
|
5015
5085
|
|
|
5016
5086
|
// src/oauth/OAuthService.ts
|
|
@@ -5296,7 +5366,7 @@ var OAuthController = class OAuthController2 {
|
|
|
5296
5366
|
__decorate32([
|
|
5297
5367
|
Get5("/start"),
|
|
5298
5368
|
RateLimit2({ limit: 20, window: "15m", key: "ip" }),
|
|
5299
|
-
__param11(0,
|
|
5369
|
+
__param11(0, Ctx2()),
|
|
5300
5370
|
__param11(1, Query2("returnTo")),
|
|
5301
5371
|
__metadata32("design:type", Function),
|
|
5302
5372
|
__metadata32("design:paramtypes", [Object, String]),
|
|
@@ -5305,7 +5375,7 @@ __decorate32([
|
|
|
5305
5375
|
__decorate32([
|
|
5306
5376
|
Get5("/callback"),
|
|
5307
5377
|
RateLimit2({ limit: 20, window: "15m", key: callbackKey }),
|
|
5308
|
-
__param11(0,
|
|
5378
|
+
__param11(0, Ctx2()),
|
|
5309
5379
|
__param11(1, Query2("code")),
|
|
5310
5380
|
__param11(2, Query2("state")),
|
|
5311
5381
|
__param11(3, Query2("error")),
|
|
@@ -5360,9 +5430,9 @@ var resolveGoogleConfig = /* @__PURE__ */ __name((config) => {
|
|
|
5360
5430
|
const clientId = google.clientId ?? process.env.GOOGLE_CLIENT_ID ?? "";
|
|
5361
5431
|
const clientSecret = google.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET ?? "";
|
|
5362
5432
|
if (!clientId)
|
|
5363
|
-
throw
|
|
5433
|
+
throw Err11.configRequired("auth.oauth.google", "GOOGLE_CLIENT_ID");
|
|
5364
5434
|
if (!clientSecret)
|
|
5365
|
-
throw
|
|
5435
|
+
throw Err11.configRequired("auth.oauth.google", "GOOGLE_CLIENT_SECRET");
|
|
5366
5436
|
const frontendUrl = config?.frontendUrl ?? process.env.FRONTEND_URL ?? "http://localhost:3000";
|
|
5367
5437
|
const callbackUrl = google.callbackUrl ?? process.env.GOOGLE_CALLBACK_URL ?? `${frontendUrl.replace(/\/$/, "")}/api/auth/oauth/google/callback`;
|
|
5368
5438
|
let callback;
|
|
@@ -5421,10 +5491,10 @@ var resolveAuthConfig = /* @__PURE__ */ __name((config) => {
|
|
|
5421
5491
|
}
|
|
5422
5492
|
};
|
|
5423
5493
|
if (!finalConfig.jwt.accessSecret) {
|
|
5424
|
-
throw
|
|
5494
|
+
throw Err11.configRequired("auth", "JWT_ACCESS_SECRET");
|
|
5425
5495
|
}
|
|
5426
5496
|
if (!finalConfig.jwt.refreshSecret) {
|
|
5427
|
-
throw
|
|
5497
|
+
throw Err11.configRequired("auth", "JWT_REFRESH_SECRET");
|
|
5428
5498
|
}
|
|
5429
5499
|
return finalConfig;
|
|
5430
5500
|
}, "resolveAuthConfig");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "najm-auth",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "Authentication and authorization library for najm framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"@testing-library/react": "^16.3.2",
|
|
68
68
|
"@types/jsonwebtoken": "^9.0.10",
|
|
69
69
|
"@types/node": "^25.0.2",
|
|
70
|
-
"drizzle-kit": "^0.31.
|
|
71
|
-
"drizzle-orm": "^0.45.
|
|
72
|
-
"happy-dom": "^
|
|
70
|
+
"drizzle-kit": "^0.31.10",
|
|
71
|
+
"drizzle-orm": "^0.45.2",
|
|
72
|
+
"happy-dom": "^20.11.1",
|
|
73
73
|
"tsup": "^8.5.1",
|
|
74
74
|
"typescript": "^5.9.3"
|
|
75
75
|
},
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"zod": "^4.2.1"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"drizzle-orm": "^0.45.
|
|
97
|
-
"hono": "^4.
|
|
96
|
+
"drizzle-orm": "^0.45.2",
|
|
97
|
+
"hono": "^4.12.18",
|
|
98
98
|
"react": ">=18",
|
|
99
99
|
"next": ">=14"
|
|
100
100
|
},
|