najm-auth 2.0.13 → 2.0.15
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/dist/{NajmAuthClient-0yzFb9CR.d.ts → NajmAuthClient-Cn9bObLB.d.ts} +8 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +11 -0
- package/dist/client/react/index.d.ts +1 -1
- package/dist/client/react/index.js +36 -33
- package/dist/client/server/index.d.ts +95 -2
- package/dist/client/server/index.js +186 -12
- package/package.json +3 -3
|
@@ -222,6 +222,14 @@ declare class NajmAuthClient {
|
|
|
222
222
|
*/
|
|
223
223
|
hydrate(session: HydrateSession | null): void;
|
|
224
224
|
isHydrated(): boolean;
|
|
225
|
+
/**
|
|
226
|
+
* A fresh client with the same config — unhydrated, and without tab sync.
|
|
227
|
+
*
|
|
228
|
+
* Server rendering needs one client per request. A single process serves
|
|
229
|
+
* every user, so the hydration latch on a shared client would otherwise pin
|
|
230
|
+
* every later render to the first request's session.
|
|
231
|
+
*/
|
|
232
|
+
fork(): NajmAuthClient;
|
|
225
233
|
on<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
|
|
226
234
|
off<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
|
|
227
235
|
subscribe(listener: (state: AuthState) => void): () => void;
|
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 { 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-
|
|
1
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-Cn9bObLB.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-Cn9bObLB.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Decode a JWT token payload without verification.
|
package/dist/client/index.js
CHANGED
|
@@ -422,6 +422,16 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
422
422
|
isHydrated() {
|
|
423
423
|
return this._hydrated;
|
|
424
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* A fresh client with the same config — unhydrated, and without tab sync.
|
|
427
|
+
*
|
|
428
|
+
* Server rendering needs one client per request. A single process serves
|
|
429
|
+
* every user, so the hydration latch on a shared client would otherwise pin
|
|
430
|
+
* every later render to the first request's session.
|
|
431
|
+
*/
|
|
432
|
+
fork() {
|
|
433
|
+
return new _NajmAuthClient({ ...this.config, tabSync: false });
|
|
434
|
+
}
|
|
425
435
|
// =========================================================================
|
|
426
436
|
// Events
|
|
427
437
|
// =========================================================================
|
|
@@ -505,6 +515,7 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
505
515
|
}
|
|
506
516
|
scheduleRefresh(decoded) {
|
|
507
517
|
this.clearRefreshTimer();
|
|
518
|
+
if (typeof window === "undefined") return;
|
|
508
519
|
const ttl = getTokenTTL(decoded);
|
|
509
520
|
if (ttl <= 0) return;
|
|
510
521
|
const delay = ttl * this.threshold * 1e3;
|
|
@@ -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, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-
|
|
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-Cn9bObLB.js';
|
|
5
5
|
|
|
6
6
|
interface AuthProviderProps {
|
|
7
7
|
client: NajmAuthClient;
|
|
@@ -3,7 +3,7 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
4
|
|
|
5
5
|
// src/client/react/AuthProvider.tsx
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
|
|
8
8
|
// src/client/react/context.ts
|
|
9
9
|
import { createContext, useContext } from "react";
|
|
@@ -49,12 +49,15 @@ function AuthProvider({
|
|
|
49
49
|
initialSession,
|
|
50
50
|
autoRefresh = true
|
|
51
51
|
}) {
|
|
52
|
+
const [active] = useState(
|
|
53
|
+
() => typeof window === "undefined" && initialSession !== void 0 ? client.fork() : client
|
|
54
|
+
);
|
|
52
55
|
const hydrated = useRef(false);
|
|
53
56
|
if (!hydrated.current && initialSession !== void 0) {
|
|
54
|
-
|
|
57
|
+
active.hydrate(initialSession);
|
|
55
58
|
hydrated.current = true;
|
|
56
59
|
}
|
|
57
|
-
return /* @__PURE__ */ jsxs(AuthClientContext.Provider, { value:
|
|
60
|
+
return /* @__PURE__ */ jsxs(AuthClientContext.Provider, { value: active, children: [
|
|
58
61
|
autoRefresh ? /* @__PURE__ */ jsx(AutoRefresh, {}) : null,
|
|
59
62
|
children
|
|
60
63
|
] });
|
|
@@ -98,11 +101,11 @@ function useUser() {
|
|
|
98
101
|
__name(useUser, "useUser");
|
|
99
102
|
|
|
100
103
|
// src/client/react/useSession.ts
|
|
101
|
-
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
104
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
102
105
|
function useSession(opts) {
|
|
103
106
|
const client = useAuthClient();
|
|
104
107
|
const state = useAuth();
|
|
105
|
-
const [isLoading, setIsLoading] =
|
|
108
|
+
const [isLoading, setIsLoading] = useState2(() => !client.isHydrated());
|
|
106
109
|
const didInit = useRef2(false);
|
|
107
110
|
useEffect2(() => {
|
|
108
111
|
if (didInit.current) return;
|
|
@@ -145,11 +148,11 @@ function usePermissions() {
|
|
|
145
148
|
__name(usePermissions, "usePermissions");
|
|
146
149
|
|
|
147
150
|
// src/client/react/useLogin.ts
|
|
148
|
-
import { useCallback, useState as
|
|
151
|
+
import { useCallback, useState as useState3 } from "react";
|
|
149
152
|
function useLogin(opts) {
|
|
150
153
|
const client = useAuthClient();
|
|
151
|
-
const [isLoading, setIsLoading] =
|
|
152
|
-
const [error, setError] =
|
|
154
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
155
|
+
const [error, setError] = useState3(null);
|
|
153
156
|
const login = useCallback(async (credentials) => {
|
|
154
157
|
setIsLoading(true);
|
|
155
158
|
setError(null);
|
|
@@ -169,11 +172,11 @@ function useLogin(opts) {
|
|
|
169
172
|
__name(useLogin, "useLogin");
|
|
170
173
|
|
|
171
174
|
// src/client/react/useRegister.ts
|
|
172
|
-
import { useCallback as useCallback2, useState as
|
|
175
|
+
import { useCallback as useCallback2, useState as useState4 } from "react";
|
|
173
176
|
function useRegister(opts) {
|
|
174
177
|
const client = useAuthClient();
|
|
175
|
-
const [isLoading, setIsLoading] =
|
|
176
|
-
const [error, setError] =
|
|
178
|
+
const [isLoading, setIsLoading] = useState4(false);
|
|
179
|
+
const [error, setError] = useState4(null);
|
|
177
180
|
const register = useCallback2(async (data) => {
|
|
178
181
|
setIsLoading(true);
|
|
179
182
|
setError(null);
|
|
@@ -193,10 +196,10 @@ function useRegister(opts) {
|
|
|
193
196
|
__name(useRegister, "useRegister");
|
|
194
197
|
|
|
195
198
|
// src/client/react/useLogout.ts
|
|
196
|
-
import { useCallback as useCallback3, useState as
|
|
199
|
+
import { useCallback as useCallback3, useState as useState5 } from "react";
|
|
197
200
|
function useLogout(opts) {
|
|
198
201
|
const client = useAuthClient();
|
|
199
|
-
const [isLoading, setIsLoading] =
|
|
202
|
+
const [isLoading, setIsLoading] = useState5(false);
|
|
200
203
|
const logout = useCallback3(async () => {
|
|
201
204
|
setIsLoading(true);
|
|
202
205
|
try {
|
|
@@ -218,12 +221,12 @@ function useLogout(opts) {
|
|
|
218
221
|
__name(useLogout, "useLogout");
|
|
219
222
|
|
|
220
223
|
// src/client/react/useForgotPassword.ts
|
|
221
|
-
import { useCallback as useCallback4, useState as
|
|
224
|
+
import { useCallback as useCallback4, useState as useState6 } from "react";
|
|
222
225
|
function useForgotPassword(opts) {
|
|
223
226
|
const client = useAuthClient();
|
|
224
|
-
const [isLoading, setIsLoading] =
|
|
225
|
-
const [error, setError] =
|
|
226
|
-
const [isSuccess, setIsSuccess] =
|
|
227
|
+
const [isLoading, setIsLoading] = useState6(false);
|
|
228
|
+
const [error, setError] = useState6(null);
|
|
229
|
+
const [isSuccess, setIsSuccess] = useState6(false);
|
|
227
230
|
const forgotPassword = useCallback4(async (data) => {
|
|
228
231
|
setIsLoading(true);
|
|
229
232
|
setError(null);
|
|
@@ -249,12 +252,12 @@ function useForgotPassword(opts) {
|
|
|
249
252
|
__name(useForgotPassword, "useForgotPassword");
|
|
250
253
|
|
|
251
254
|
// src/client/react/useResetPassword.ts
|
|
252
|
-
import { useCallback as useCallback5, useState as
|
|
255
|
+
import { useCallback as useCallback5, useState as useState7 } from "react";
|
|
253
256
|
function useResetPassword(opts) {
|
|
254
257
|
const client = useAuthClient();
|
|
255
|
-
const [isLoading, setIsLoading] =
|
|
256
|
-
const [error, setError] =
|
|
257
|
-
const [isSuccess, setIsSuccess] =
|
|
258
|
+
const [isLoading, setIsLoading] = useState7(false);
|
|
259
|
+
const [error, setError] = useState7(null);
|
|
260
|
+
const [isSuccess, setIsSuccess] = useState7(false);
|
|
258
261
|
const resetPassword = useCallback5(async (data) => {
|
|
259
262
|
setIsLoading(true);
|
|
260
263
|
setError(null);
|
|
@@ -280,12 +283,12 @@ function useResetPassword(opts) {
|
|
|
280
283
|
__name(useResetPassword, "useResetPassword");
|
|
281
284
|
|
|
282
285
|
// src/client/react/useChangePassword.ts
|
|
283
|
-
import { useCallback as useCallback6, useState as
|
|
286
|
+
import { useCallback as useCallback6, useState as useState8 } from "react";
|
|
284
287
|
function useChangePassword(opts) {
|
|
285
288
|
const client = useAuthClient();
|
|
286
|
-
const [isLoading, setIsLoading] =
|
|
287
|
-
const [error, setError] =
|
|
288
|
-
const [isSuccess, setIsSuccess] =
|
|
289
|
+
const [isLoading, setIsLoading] = useState8(false);
|
|
290
|
+
const [error, setError] = useState8(null);
|
|
291
|
+
const [isSuccess, setIsSuccess] = useState8(false);
|
|
289
292
|
const changePassword = useCallback6(async (data) => {
|
|
290
293
|
setIsLoading(true);
|
|
291
294
|
setError(null);
|
|
@@ -311,11 +314,11 @@ function useChangePassword(opts) {
|
|
|
311
314
|
__name(useChangePassword, "useChangePassword");
|
|
312
315
|
|
|
313
316
|
// src/client/react/useGoogleLogin.ts
|
|
314
|
-
import { useCallback as useCallback7, useState as
|
|
317
|
+
import { useCallback as useCallback7, useState as useState9 } from "react";
|
|
315
318
|
function useGoogleLogin(options) {
|
|
316
319
|
const client = useAuthClient();
|
|
317
|
-
const [isRedirecting, setIsRedirecting] =
|
|
318
|
-
const [error, setError] =
|
|
320
|
+
const [isRedirecting, setIsRedirecting] = useState9(false);
|
|
321
|
+
const [error, setError] = useState9(null);
|
|
319
322
|
const fail = useCallback7((value) => {
|
|
320
323
|
const next = value instanceof Error ? value : new Error(String(value));
|
|
321
324
|
setIsRedirecting(false);
|
|
@@ -345,12 +348,12 @@ function useGoogleLogin(options) {
|
|
|
345
348
|
__name(useGoogleLogin, "useGoogleLogin");
|
|
346
349
|
|
|
347
350
|
// src/client/react/useOAuthCallback.ts
|
|
348
|
-
import { useCallback as useCallback8, useRef as useRef3, useState as
|
|
351
|
+
import { useCallback as useCallback8, useRef as useRef3, useState as useState10 } from "react";
|
|
349
352
|
function useOAuthCallback(options) {
|
|
350
353
|
const client = useAuthClient();
|
|
351
354
|
const promise = useRef3(null);
|
|
352
|
-
const [isLoading, setIsLoading] =
|
|
353
|
-
const [error, setError] =
|
|
355
|
+
const [isLoading, setIsLoading] = useState10(false);
|
|
356
|
+
const [error, setError] = useState10(null);
|
|
354
357
|
const complete = useCallback8(async () => {
|
|
355
358
|
if (promise.current) return promise.current;
|
|
356
359
|
setIsLoading(true);
|
|
@@ -388,7 +391,7 @@ function useAuthEvent(event, handler) {
|
|
|
388
391
|
__name(useAuthEvent, "useAuthEvent");
|
|
389
392
|
|
|
390
393
|
// src/client/react/useAuthEvents.ts
|
|
391
|
-
import { useEffect as useEffect4, useRef as useRef5, useState as
|
|
394
|
+
import { useEffect as useEffect4, useRef as useRef5, useState as useState11 } from "react";
|
|
392
395
|
var ALL_EVENTS = [
|
|
393
396
|
"login",
|
|
394
397
|
"logout",
|
|
@@ -401,7 +404,7 @@ var ALL_EVENTS = [
|
|
|
401
404
|
function useAuthEvents(opts) {
|
|
402
405
|
const client = useAuthClient();
|
|
403
406
|
const max = opts?.maxEntries ?? 100;
|
|
404
|
-
const [entries, setEntries] =
|
|
407
|
+
const [entries, setEntries] = useState11([]);
|
|
405
408
|
const maxRef = useRef5(max);
|
|
406
409
|
maxRef.current = max;
|
|
407
410
|
useEffect4(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-
|
|
1
|
+
import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-Cn9bObLB.js';
|
|
2
2
|
import { SessionRecoveryFailure } from '../edge.js';
|
|
3
3
|
export { SessionRecoveryErrorDetails, SessionRecoveryFailureReason, withAuthMiddleware } from '../edge.js';
|
|
4
4
|
import 'next/server';
|
|
@@ -169,6 +169,16 @@ interface DefineAuthConfig {
|
|
|
169
169
|
loginRoute?: string;
|
|
170
170
|
/** Route to redirect after login (default: '/dashboard') */
|
|
171
171
|
afterLoginRoute?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Where an *authenticated* user goes when their role is not allowed
|
|
174
|
+
* (default: '/forbidden').
|
|
175
|
+
*
|
|
176
|
+
* Distinct from `loginRoute` on purpose. Sending them to the login form says
|
|
177
|
+
* "prove who you are" to someone who already has; they log in again, land
|
|
178
|
+
* back on the same page, and get bounced again. A forbidden page is the only
|
|
179
|
+
* response that terminates.
|
|
180
|
+
*/
|
|
181
|
+
forbiddenRoute?: string;
|
|
172
182
|
/** Routes that are always public (glob patterns) */
|
|
173
183
|
publicRoutes?: string[];
|
|
174
184
|
/** Routes that require authentication (glob patterns) */
|
|
@@ -212,6 +222,15 @@ interface AuthKit {
|
|
|
212
222
|
getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
|
|
213
223
|
/** Require session — throws if unauthenticated */
|
|
214
224
|
requireSession: () => Promise<ServerSession>;
|
|
225
|
+
/**
|
|
226
|
+
* Require one of `roles` — redirects to `loginRoute` when unauthenticated and
|
|
227
|
+
* to `forbiddenRoute` when authenticated as the wrong role.
|
|
228
|
+
*
|
|
229
|
+
* ```ts
|
|
230
|
+
* const session = await auth.requireRole(['admin', 'operator']);
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
requireRole: (roles: string[]) => Promise<ServerSession>;
|
|
215
234
|
/** Generated Next.js middleware function */
|
|
216
235
|
middleware: (request: Request) => Promise<Response>;
|
|
217
236
|
/** Next.js middleware config with matcher */
|
|
@@ -232,4 +251,78 @@ interface AuthKit {
|
|
|
232
251
|
}
|
|
233
252
|
declare function defineAuth(authConfig?: DefineAuthConfig): AuthKit;
|
|
234
253
|
|
|
235
|
-
|
|
254
|
+
interface SafeRedirectOptions {
|
|
255
|
+
/** Where to send anything rejected. Defaults to `/dashboard`. */
|
|
256
|
+
fallback?: string;
|
|
257
|
+
/**
|
|
258
|
+
* Path prefixes that are never a valid destination. Defaults to `/api`,
|
|
259
|
+
* `/login` and `/_next`.
|
|
260
|
+
*
|
|
261
|
+
* `/login` is on the list because bouncing back to it is the redirect loop
|
|
262
|
+
* this parameter causes most often: a user who just authenticated is sent
|
|
263
|
+
* straight back to the form they came from.
|
|
264
|
+
*/
|
|
265
|
+
blockedPrefixes?: string[];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Reduces an untrusted `?next=` value to a path that is safe to redirect to.
|
|
269
|
+
*
|
|
270
|
+
* Only same-origin *paths* survive. An absolute URL is rejected outright rather
|
|
271
|
+
* than parsed and compared, because the comparison is where this goes wrong:
|
|
272
|
+
* `//evil.test` is a protocol-relative URL that browsers resolve off-site while
|
|
273
|
+
* a naive `startsWith('/')` check reads it as local. Anything that is not a
|
|
274
|
+
* single leading slash followed by a path is refused.
|
|
275
|
+
*
|
|
276
|
+
* ```ts
|
|
277
|
+
* redirect(getSafeRedirectPath(searchParams.next, { fallback: '/home' }));
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
|
|
281
|
+
|
|
282
|
+
type RequestHandler = (request: Request) => Promise<Response>;
|
|
283
|
+
interface AuthCookiePersistenceOptions {
|
|
284
|
+
/**
|
|
285
|
+
* Cookies whose lifetime this rewrites. Defaults to the two najm-auth issues.
|
|
286
|
+
* Anything not named here is passed through untouched.
|
|
287
|
+
*/
|
|
288
|
+
authCookieNames?: string[];
|
|
289
|
+
/** Where the one-bit choice is stored. Defaults to `najm.remember`. */
|
|
290
|
+
rememberCookieName?: string;
|
|
291
|
+
/** How long a remembered choice lasts. Defaults to 7 days. */
|
|
292
|
+
maxAgeSeconds?: number;
|
|
293
|
+
/** Paths whose JSON body carries `rememberMe`. Defaults to `/api/auth/login`. */
|
|
294
|
+
loginPaths?: string[];
|
|
295
|
+
/** Paths that end a session and clear the choice. Defaults to `/api/auth/logout`. */
|
|
296
|
+
logoutPaths?: string[];
|
|
297
|
+
/** Paths that reissue cookies and must reapply the stored choice. */
|
|
298
|
+
refreshPaths?: string[];
|
|
299
|
+
/**
|
|
300
|
+
* Recognizes a response that has *not* issued a usable session because the
|
|
301
|
+
* user must still set up credentials.
|
|
302
|
+
*
|
|
303
|
+
* Such a response may carry auth cookies anyway, and persisting them would
|
|
304
|
+
* leave a half-authenticated browser that skips the setup step on reload.
|
|
305
|
+
* Returning `true` strips them and clears the stored choice.
|
|
306
|
+
*/
|
|
307
|
+
isSetupResponse?: (payload: unknown) => boolean;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Strips the lifetime attributes so the browser drops the cookie when it closes.
|
|
311
|
+
*
|
|
312
|
+
* Only the named auth cookies are touched — rewriting an unrelated `Set-Cookie`
|
|
313
|
+
* from the same response would be a silent side effect on someone else's state.
|
|
314
|
+
*/
|
|
315
|
+
declare function makeSessionCookie(setCookie: string, authCookieNames?: string[]): string;
|
|
316
|
+
/**
|
|
317
|
+
* Wraps a request handler so the auth cookies it issues match the user's
|
|
318
|
+
* "remember me" choice.
|
|
319
|
+
*
|
|
320
|
+
* ```ts
|
|
321
|
+
* // app/api/[...route]/route.ts
|
|
322
|
+
* const handler = withAuthCookiePersistence((req) => server.fetch(req));
|
|
323
|
+
* export { handler as GET, handler as POST };
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
declare function withAuthCookiePersistence(handler: RequestHandler, options?: AuthCookiePersistenceOptions): RequestHandler;
|
|
327
|
+
|
|
328
|
+
export { AuthConfigError, type AuthCookiePersistenceOptions, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type SafeRedirectOptions, type ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, getSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
|
|
@@ -382,7 +382,7 @@ function requestOriginFromHeaders(headers) {
|
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
async function getSession(config = {}) {
|
|
385
|
-
const
|
|
385
|
+
const cookieName2 = config.cookieName ?? "refreshToken";
|
|
386
386
|
const sessionCookieName = config.sessionCookieName ?? "najm.session";
|
|
387
387
|
const baseURL = config.baseURL ?? defaultBaseURL();
|
|
388
388
|
const prefix = config.authPrefix ?? "/auth";
|
|
@@ -395,7 +395,7 @@ async function getSession(config = {}) {
|
|
|
395
395
|
const mod = await import("next/headers");
|
|
396
396
|
const cookieStore = await mod.cookies();
|
|
397
397
|
sessionCookieValue = cookieStore.get(sessionCookieName)?.value;
|
|
398
|
-
refreshCookieValue = cookieStore.get(
|
|
398
|
+
refreshCookieValue = cookieStore.get(cookieName2)?.value;
|
|
399
399
|
if (typeof mod.headers === "function") {
|
|
400
400
|
requestOrigin = requestOriginFromHeaders(await mod.headers());
|
|
401
401
|
}
|
|
@@ -434,7 +434,7 @@ async function getSession(config = {}) {
|
|
|
434
434
|
endpoint,
|
|
435
435
|
requestOrigin,
|
|
436
436
|
allowLoopbackEndpoint: internalRecoveryURL !== void 0,
|
|
437
|
-
refreshCookieName:
|
|
437
|
+
refreshCookieName: cookieName2,
|
|
438
438
|
refreshCookieValue,
|
|
439
439
|
sessionCookieName,
|
|
440
440
|
sessionSecret: secret,
|
|
@@ -700,7 +700,7 @@ function withAuthMiddleware(config) {
|
|
|
700
700
|
publicRoutes = [],
|
|
701
701
|
loginRoute = "/login",
|
|
702
702
|
roleRoutes = {},
|
|
703
|
-
cookieName = "refreshToken",
|
|
703
|
+
cookieName: cookieName2 = "refreshToken",
|
|
704
704
|
apiBaseURL = "/api",
|
|
705
705
|
authPrefix = "/auth",
|
|
706
706
|
sessionCookieName = "najm.session",
|
|
@@ -719,7 +719,7 @@ function withAuthMiddleware(config) {
|
|
|
719
719
|
loginUrl.searchParams.set("from", returnPath2);
|
|
720
720
|
const res = NextResponse.redirect(loginUrl);
|
|
721
721
|
if (clearCookies.includes("refresh")) {
|
|
722
|
-
res.cookies.delete(
|
|
722
|
+
res.cookies.delete(cookieName2);
|
|
723
723
|
}
|
|
724
724
|
if (clearCookies.includes("session")) {
|
|
725
725
|
res.cookies.delete(sessionCookieName);
|
|
@@ -746,7 +746,7 @@ function withAuthMiddleware(config) {
|
|
|
746
746
|
}) : null;
|
|
747
747
|
let recovery = null;
|
|
748
748
|
if (!session || verifyAlways) {
|
|
749
|
-
const refreshCookie = readCookieValue(cookie,
|
|
749
|
+
const refreshCookie = readCookieValue(cookie, cookieName2);
|
|
750
750
|
if (!refreshCookie || recoveryURL === false && !resolvedInternalRecoveryURL) {
|
|
751
751
|
return redirectToLogin(returnPath, ["refresh", "session"]);
|
|
752
752
|
}
|
|
@@ -755,7 +755,7 @@ function withAuthMiddleware(config) {
|
|
|
755
755
|
endpoint,
|
|
756
756
|
requestOrigin: url.origin,
|
|
757
757
|
allowLoopbackEndpoint: resolvedInternalRecoveryURL !== void 0,
|
|
758
|
-
refreshCookieName:
|
|
758
|
+
refreshCookieName: cookieName2,
|
|
759
759
|
refreshCookieValue: refreshCookie,
|
|
760
760
|
sessionCookieName,
|
|
761
761
|
sessionSecret: secret,
|
|
@@ -1119,6 +1119,16 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
1119
1119
|
isHydrated() {
|
|
1120
1120
|
return this._hydrated;
|
|
1121
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* A fresh client with the same config — unhydrated, and without tab sync.
|
|
1124
|
+
*
|
|
1125
|
+
* Server rendering needs one client per request. A single process serves
|
|
1126
|
+
* every user, so the hydration latch on a shared client would otherwise pin
|
|
1127
|
+
* every later render to the first request's session.
|
|
1128
|
+
*/
|
|
1129
|
+
fork() {
|
|
1130
|
+
return new _NajmAuthClient({ ...this.config, tabSync: false });
|
|
1131
|
+
}
|
|
1122
1132
|
// =========================================================================
|
|
1123
1133
|
// Events
|
|
1124
1134
|
// =========================================================================
|
|
@@ -1202,6 +1212,7 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
1202
1212
|
}
|
|
1203
1213
|
scheduleRefresh(decoded) {
|
|
1204
1214
|
this.clearRefreshTimer();
|
|
1215
|
+
if (typeof window === "undefined") return;
|
|
1205
1216
|
const ttl = getTokenTTL(decoded);
|
|
1206
1217
|
if (ttl <= 0) return;
|
|
1207
1218
|
const delay = ttl * this.threshold * 1e3;
|
|
@@ -1313,10 +1324,11 @@ function defineAuth(authConfig = {}) {
|
|
|
1313
1324
|
apiBaseURL = "/api",
|
|
1314
1325
|
authPrefix = "/auth",
|
|
1315
1326
|
loginRoute = "/login",
|
|
1327
|
+
forbiddenRoute = "/forbidden",
|
|
1316
1328
|
publicRoutes = [],
|
|
1317
1329
|
protectedRoutes = [],
|
|
1318
1330
|
roleRoutes = {},
|
|
1319
|
-
cookieName = "refreshToken",
|
|
1331
|
+
cookieName: cookieName2 = "refreshToken",
|
|
1320
1332
|
sessionCookieName = "najm.session",
|
|
1321
1333
|
sessionSecret,
|
|
1322
1334
|
sessionMaxAge,
|
|
@@ -1334,7 +1346,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1334
1346
|
const sessionConfig = {
|
|
1335
1347
|
baseURL: apiBaseURL,
|
|
1336
1348
|
authPrefix,
|
|
1337
|
-
cookieName,
|
|
1349
|
+
cookieName: cookieName2,
|
|
1338
1350
|
sessionCookieName,
|
|
1339
1351
|
sessionSecret,
|
|
1340
1352
|
sessionMaxAge,
|
|
@@ -1382,12 +1394,21 @@ function defineAuth(authConfig = {}) {
|
|
|
1382
1394
|
throw err;
|
|
1383
1395
|
}
|
|
1384
1396
|
}, "requireSession");
|
|
1397
|
+
const requireRole = /* @__PURE__ */ __name(async (roles) => {
|
|
1398
|
+
const session = await requireSession();
|
|
1399
|
+
const held = session.roles ?? (session.user.role ? [session.user.role] : []);
|
|
1400
|
+
if (!held.some((role) => roles.includes(role))) {
|
|
1401
|
+
const { redirect } = await import("next/navigation");
|
|
1402
|
+
redirect(forbiddenRoute);
|
|
1403
|
+
}
|
|
1404
|
+
return session;
|
|
1405
|
+
}, "requireRole");
|
|
1385
1406
|
const middleware = withAuthMiddleware({
|
|
1386
1407
|
protectedRoutes,
|
|
1387
1408
|
publicRoutes,
|
|
1388
1409
|
loginRoute,
|
|
1389
1410
|
roleRoutes,
|
|
1390
|
-
cookieName,
|
|
1411
|
+
cookieName: cookieName2,
|
|
1391
1412
|
apiBaseURL,
|
|
1392
1413
|
authPrefix,
|
|
1393
1414
|
sessionCookieName,
|
|
@@ -1409,7 +1430,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1409
1430
|
const userRoles = session.roles ?? (session.user.role ? [session.user.role] : []);
|
|
1410
1431
|
if (!userRoles.includes(options.role)) {
|
|
1411
1432
|
const { redirect } = await import("next/navigation");
|
|
1412
|
-
redirect(
|
|
1433
|
+
redirect(forbiddenRoute);
|
|
1413
1434
|
}
|
|
1414
1435
|
}
|
|
1415
1436
|
if (options?.permission) {
|
|
@@ -1417,7 +1438,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1417
1438
|
const perms = session.permissions ?? session.user.permissions ?? [];
|
|
1418
1439
|
if (!matchPermission2(perms, options.permission)) {
|
|
1419
1440
|
const { redirect } = await import("next/navigation");
|
|
1420
|
-
redirect(
|
|
1441
|
+
redirect(forbiddenRoute);
|
|
1421
1442
|
}
|
|
1422
1443
|
}
|
|
1423
1444
|
return Page({ session, ...props });
|
|
@@ -1432,20 +1453,173 @@ function defineAuth(authConfig = {}) {
|
|
|
1432
1453
|
},
|
|
1433
1454
|
getSession: getSession2,
|
|
1434
1455
|
requireSession,
|
|
1456
|
+
requireRole,
|
|
1435
1457
|
middleware,
|
|
1436
1458
|
config: { matcher },
|
|
1437
1459
|
protect
|
|
1438
1460
|
};
|
|
1439
1461
|
}
|
|
1440
1462
|
__name(defineAuth, "defineAuth");
|
|
1463
|
+
|
|
1464
|
+
// src/client/server/safeRedirect.ts
|
|
1465
|
+
var DEFAULT_BLOCKED_PREFIXES = ["/api", "/login", "/_next"];
|
|
1466
|
+
var ASSET_EXTENSIONS = /\.(?:css|gif|ico|jpe?g|js|json|map|png|svg|webmanifest|webp)$/i;
|
|
1467
|
+
function getSafeRedirectPath(value, options = {}) {
|
|
1468
|
+
const {
|
|
1469
|
+
fallback = "/dashboard",
|
|
1470
|
+
blockedPrefixes = DEFAULT_BLOCKED_PREFIXES
|
|
1471
|
+
} = typeof options === "string" ? { fallback: options } : options;
|
|
1472
|
+
const path = Array.isArray(value) ? value[0] : value;
|
|
1473
|
+
if (!path || !path.startsWith("/") || // Protocol-relative: the browser treats `//host/x` as off-site.
|
|
1474
|
+
path.startsWith("//") || // A backslash is normalized to a forward slash by some browsers, so
|
|
1475
|
+
// `/\evil.test` is another way to spell the case above.
|
|
1476
|
+
path.startsWith("/\\") || blockedPrefixes.some((prefix) => path.startsWith(prefix)) || ASSET_EXTENSIONS.test(path.split("?")[0] ?? path)) {
|
|
1477
|
+
return fallback;
|
|
1478
|
+
}
|
|
1479
|
+
return path;
|
|
1480
|
+
}
|
|
1481
|
+
__name(getSafeRedirectPath, "getSafeRedirectPath");
|
|
1482
|
+
|
|
1483
|
+
// src/client/server/authCookiePersistence.ts
|
|
1484
|
+
var DEFAULTS = {
|
|
1485
|
+
authCookieNames: ["refreshToken", "najm.session"],
|
|
1486
|
+
rememberCookieName: "najm.remember",
|
|
1487
|
+
maxAgeSeconds: 7 * 24 * 60 * 60,
|
|
1488
|
+
loginPaths: ["/api/auth/login"],
|
|
1489
|
+
logoutPaths: ["/api/auth/logout"],
|
|
1490
|
+
refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"]
|
|
1491
|
+
};
|
|
1492
|
+
function cookieValue(header, name) {
|
|
1493
|
+
for (const part of header.split(";")) {
|
|
1494
|
+
const separator = part.indexOf("=");
|
|
1495
|
+
if (separator < 0) continue;
|
|
1496
|
+
if (part.slice(0, separator).trim() !== name) continue;
|
|
1497
|
+
return part.slice(separator + 1).trim();
|
|
1498
|
+
}
|
|
1499
|
+
return void 0;
|
|
1500
|
+
}
|
|
1501
|
+
__name(cookieValue, "cookieValue");
|
|
1502
|
+
function cookieName(setCookie) {
|
|
1503
|
+
const separator = setCookie.indexOf("=");
|
|
1504
|
+
return separator < 0 ? "" : setCookie.slice(0, separator).trim();
|
|
1505
|
+
}
|
|
1506
|
+
__name(cookieName, "cookieName");
|
|
1507
|
+
function makeSessionCookie(setCookie, authCookieNames = DEFAULTS.authCookieNames) {
|
|
1508
|
+
if (!authCookieNames.includes(cookieName(setCookie))) return setCookie;
|
|
1509
|
+
return setCookie.split(";").filter((part) => !/^\s*(?:expires|max-age)=/i.test(part)).join(";");
|
|
1510
|
+
}
|
|
1511
|
+
__name(makeSessionCookie, "makeSessionCookie");
|
|
1512
|
+
function isDeletionCookie(setCookie) {
|
|
1513
|
+
if (/^[^=]+=\s*(?:;|$)/.test(setCookie)) return true;
|
|
1514
|
+
if (/(?:^|;)\s*max-age=0(?:;|$)/i.test(setCookie)) return true;
|
|
1515
|
+
const expires = /(?:^|;)\s*expires=([^;]+)/i.exec(setCookie)?.[1];
|
|
1516
|
+
return expires ? new Date(expires).getTime() <= Date.now() : false;
|
|
1517
|
+
}
|
|
1518
|
+
__name(isDeletionCookie, "isDeletionCookie");
|
|
1519
|
+
function rememberCookie(name, mode, secure, maxAgeSeconds) {
|
|
1520
|
+
const attributes = [
|
|
1521
|
+
`${name}=${mode === "persistent" ? "1" : "0"}`,
|
|
1522
|
+
"Path=/",
|
|
1523
|
+
"HttpOnly",
|
|
1524
|
+
"SameSite=Lax"
|
|
1525
|
+
];
|
|
1526
|
+
if (secure) attributes.push("Secure");
|
|
1527
|
+
if (mode === "persistent") attributes.push(`Max-Age=${maxAgeSeconds}`);
|
|
1528
|
+
return attributes.join("; ");
|
|
1529
|
+
}
|
|
1530
|
+
__name(rememberCookie, "rememberCookie");
|
|
1531
|
+
function clearedRememberCookie(name, secure) {
|
|
1532
|
+
return [
|
|
1533
|
+
`${name}=`,
|
|
1534
|
+
"Path=/",
|
|
1535
|
+
"HttpOnly",
|
|
1536
|
+
"SameSite=Lax",
|
|
1537
|
+
...secure ? ["Secure"] : [],
|
|
1538
|
+
"Max-Age=0"
|
|
1539
|
+
].join("; ");
|
|
1540
|
+
}
|
|
1541
|
+
__name(clearedRememberCookie, "clearedRememberCookie");
|
|
1542
|
+
function withAuthCookiePersistence(handler, options = {}) {
|
|
1543
|
+
const {
|
|
1544
|
+
authCookieNames = DEFAULTS.authCookieNames,
|
|
1545
|
+
rememberCookieName = DEFAULTS.rememberCookieName,
|
|
1546
|
+
maxAgeSeconds = DEFAULTS.maxAgeSeconds,
|
|
1547
|
+
loginPaths = DEFAULTS.loginPaths,
|
|
1548
|
+
logoutPaths = DEFAULTS.logoutPaths,
|
|
1549
|
+
refreshPaths = DEFAULTS.refreshPaths,
|
|
1550
|
+
isSetupResponse
|
|
1551
|
+
} = options;
|
|
1552
|
+
const resolveAction = /* @__PURE__ */ __name(async (request) => {
|
|
1553
|
+
const { pathname } = new URL(request.url);
|
|
1554
|
+
if (loginPaths.includes(pathname)) {
|
|
1555
|
+
const body = await request.clone().json().catch(() => null);
|
|
1556
|
+
return {
|
|
1557
|
+
type: "apply",
|
|
1558
|
+
mode: body?.rememberMe === true ? "persistent" : "session"
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (logoutPaths.includes(pathname)) return { type: "clear" };
|
|
1562
|
+
if (refreshPaths.includes(pathname)) {
|
|
1563
|
+
const remembered = cookieValue(
|
|
1564
|
+
request.headers.get("cookie") ?? "",
|
|
1565
|
+
rememberCookieName
|
|
1566
|
+
);
|
|
1567
|
+
if (remembered === "0") return { type: "apply", mode: "session" };
|
|
1568
|
+
if (remembered === "1") return { type: "apply", mode: "persistent" };
|
|
1569
|
+
}
|
|
1570
|
+
return null;
|
|
1571
|
+
}, "resolveAction");
|
|
1572
|
+
const applyAction = /* @__PURE__ */ __name((response, action, secure) => {
|
|
1573
|
+
const headers = new Headers(response.headers);
|
|
1574
|
+
const setCookies = headers.getSetCookie();
|
|
1575
|
+
headers.delete("set-cookie");
|
|
1576
|
+
for (const setCookie of setCookies) {
|
|
1577
|
+
if (action.type === "setup" && authCookieNames.includes(cookieName(setCookie)) && !isDeletionCookie(setCookie)) {
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
headers.append(
|
|
1581
|
+
"set-cookie",
|
|
1582
|
+
action.type === "apply" && action.mode === "session" ? makeSessionCookie(setCookie, authCookieNames) : setCookie
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
headers.append(
|
|
1586
|
+
"set-cookie",
|
|
1587
|
+
action.type === "clear" || action.type === "setup" ? clearedRememberCookie(rememberCookieName, secure) : rememberCookie(rememberCookieName, action.mode, secure, maxAgeSeconds)
|
|
1588
|
+
);
|
|
1589
|
+
return new Response(response.body, {
|
|
1590
|
+
headers,
|
|
1591
|
+
status: response.status,
|
|
1592
|
+
statusText: response.statusText
|
|
1593
|
+
});
|
|
1594
|
+
}, "applyAction");
|
|
1595
|
+
return async (request) => {
|
|
1596
|
+
let action = await resolveAction(request);
|
|
1597
|
+
const response = await handler(request);
|
|
1598
|
+
if (!response.ok) return response;
|
|
1599
|
+
if (action?.type === "apply" && isSetupResponse) {
|
|
1600
|
+
const payload = await response.clone().json().catch(() => null);
|
|
1601
|
+
if (isSetupResponse(payload)) action = { type: "setup" };
|
|
1602
|
+
}
|
|
1603
|
+
if (!action) return response;
|
|
1604
|
+
return applyAction(
|
|
1605
|
+
response,
|
|
1606
|
+
action,
|
|
1607
|
+
new URL(request.url).protocol === "https:"
|
|
1608
|
+
);
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
__name(withAuthCookiePersistence, "withAuthCookiePersistence");
|
|
1441
1612
|
export {
|
|
1442
1613
|
AuthConfigError,
|
|
1443
1614
|
AuthTransportError,
|
|
1444
1615
|
NoSessionError,
|
|
1445
1616
|
createServerClient,
|
|
1446
1617
|
defineAuth,
|
|
1618
|
+
getSafeRedirectPath,
|
|
1447
1619
|
getServerSession,
|
|
1448
1620
|
getSession,
|
|
1621
|
+
makeSessionCookie,
|
|
1449
1622
|
withAuth,
|
|
1623
|
+
withAuthCookiePersistence,
|
|
1450
1624
|
withAuthMiddleware
|
|
1451
1625
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "najm-auth",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"description": "Authentication and authorization library for najm framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -78,10 +78,10 @@
|
|
|
78
78
|
"dependencies": {
|
|
79
79
|
"bcryptjs": "^3.0.2",
|
|
80
80
|
"najm-cookies": "^2.0.2",
|
|
81
|
-
"najm-core": "^2.0.
|
|
81
|
+
"najm-core": "^2.0.5",
|
|
82
82
|
"najm-database": "^2.0.3",
|
|
83
83
|
"najm-guard": "^2.0.2",
|
|
84
|
-
"najm-i18n": "^2.0.
|
|
84
|
+
"najm-i18n": "^2.0.3",
|
|
85
85
|
"najm-cache": "^2.0.2",
|
|
86
86
|
"najm-email": "^2.0.2",
|
|
87
87
|
"najm-rate": "^2.0.2",
|