najm-auth 1.1.23 → 1.1.24
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/{FetchClient-DadnuVF7.d.ts → NajmAuthClient-D08--i69.d.ts} +84 -1
- package/dist/client/index.d.ts +2 -3
- package/dist/client/react/index.d.ts +1 -2
- package/dist/client/server/index.d.ts +22 -3
- package/dist/client/server/index.js +441 -1
- package/package.json +1 -1
- package/dist/NajmAuthClient-CZHKl4ri.d.ts +0 -86
|
@@ -153,4 +153,87 @@ declare class FetchClient {
|
|
|
153
153
|
private parseBody;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
interface HydrateSession {
|
|
157
|
+
user: AuthUser | null;
|
|
158
|
+
accessToken?: string | null;
|
|
159
|
+
roles?: string[];
|
|
160
|
+
permissions?: string[];
|
|
161
|
+
}
|
|
162
|
+
declare class NajmAuthClient {
|
|
163
|
+
private config;
|
|
164
|
+
private static readonly MAX_REFRESH_FAILURES;
|
|
165
|
+
private static readonly CIRCUIT_RESET_MS;
|
|
166
|
+
private state;
|
|
167
|
+
private refreshTimer;
|
|
168
|
+
private refreshCircuitTimer;
|
|
169
|
+
private refreshPromise;
|
|
170
|
+
private fetchUserPromise;
|
|
171
|
+
private refreshFailures;
|
|
172
|
+
private _hydrated;
|
|
173
|
+
private listeners;
|
|
174
|
+
private eventListeners;
|
|
175
|
+
private tabSync;
|
|
176
|
+
api: FetchClient;
|
|
177
|
+
private readonly prefix;
|
|
178
|
+
private readonly threshold;
|
|
179
|
+
constructor(config: AuthClientConfig);
|
|
180
|
+
login(credentials: {
|
|
181
|
+
email: string;
|
|
182
|
+
password: string;
|
|
183
|
+
}): Promise<AuthUser>;
|
|
184
|
+
register(data: Record<string, unknown>): Promise<AuthUser>;
|
|
185
|
+
logout(): Promise<void>;
|
|
186
|
+
refresh(): Promise<void>;
|
|
187
|
+
fetchUser(): Promise<AuthUser | null>;
|
|
188
|
+
private _doFetchUser;
|
|
189
|
+
forgotPassword(data: {
|
|
190
|
+
email: string;
|
|
191
|
+
}): Promise<void>;
|
|
192
|
+
changePassword(data: {
|
|
193
|
+
currentPassword: string;
|
|
194
|
+
newPassword: string;
|
|
195
|
+
}): Promise<void>;
|
|
196
|
+
resetPassword(data: {
|
|
197
|
+
token: string;
|
|
198
|
+
newPassword: string;
|
|
199
|
+
}): Promise<void>;
|
|
200
|
+
can(permission: string): boolean;
|
|
201
|
+
hasRole(role: string): boolean;
|
|
202
|
+
hasAnyRole(roles: string[]): boolean;
|
|
203
|
+
hasPermission(permission: string): boolean;
|
|
204
|
+
getUser(): AuthUser | null;
|
|
205
|
+
getAccessToken(): string | null;
|
|
206
|
+
isAuthenticated(): boolean;
|
|
207
|
+
getState(): AuthState;
|
|
208
|
+
/**
|
|
209
|
+
* Hydrate the client with a session resolved server-side.
|
|
210
|
+
* Use to skip the initial loading flicker on SSR-rendered pages.
|
|
211
|
+
* Safe to call multiple times — subsequent calls are no-ops.
|
|
212
|
+
*/
|
|
213
|
+
hydrate(session: HydrateSession | null): void;
|
|
214
|
+
isHydrated(): boolean;
|
|
215
|
+
on<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
|
|
216
|
+
off<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
|
|
217
|
+
subscribe(listener: (state: AuthState) => void): () => void;
|
|
218
|
+
destroy(): void;
|
|
219
|
+
private _refreshWithCircuit;
|
|
220
|
+
private _doRefresh;
|
|
221
|
+
private handleUnauthorized;
|
|
222
|
+
private applyTokens;
|
|
223
|
+
private scheduleRefresh;
|
|
224
|
+
private clearRefreshTimer;
|
|
225
|
+
private clearRefreshCircuitTimer;
|
|
226
|
+
private resetRefreshFailures;
|
|
227
|
+
private registerRefreshFailure;
|
|
228
|
+
private resetState;
|
|
229
|
+
private getSyncPayload;
|
|
230
|
+
private handleTabMessage;
|
|
231
|
+
private notify;
|
|
232
|
+
private emit;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Factory function to create an auth client.
|
|
236
|
+
*/
|
|
237
|
+
declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
|
|
238
|
+
|
|
239
|
+
export { AuthError as A, type DecodedToken as D, FetchClient as F, type HydrateSession as H, NajmAuthClient as N, type RetryConfig as R, type SyncPayload as S, type TabSyncMessage as T, type AuthClientConfig as a, type AuthEventMap as b, createAuthClient as c, type AuthState as d, type AuthUser as e, type AuthEvent as f, type AuthEventHandler as g, type ServerResponse as h, type TokenPair as i, type RequestOptions as j };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export { a as AuthClientConfig, A as AuthError, e as AuthEvent, f as AuthEventHandler, b as AuthEventMap, c as AuthState, d as AuthUser, F as FetchClient, i as RequestOptions, R as RetryConfig, g as ServerResponse, h as TokenPair } from '../FetchClient-DadnuVF7.js';
|
|
1
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-D08--i69.js';
|
|
2
|
+
export { a as AuthClientConfig, A as AuthError, f as AuthEvent, g as AuthEventHandler, b as AuthEventMap, d as AuthState, e as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, j as RequestOptions, R as RetryConfig, h as ServerResponse, i as TokenPair, c as createAuthClient } from '../NajmAuthClient-D08--i69.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Decode a JWT token payload without verification.
|
|
@@ -1,8 +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 } from '../../NajmAuthClient-
|
|
5
|
-
import { c as AuthState, d as AuthUser, A as AuthError, e as AuthEvent, b as AuthEventMap } from '../../FetchClient-DadnuVF7.js';
|
|
4
|
+
import { N as NajmAuthClient, H as HydrateSession, d as AuthState, e as AuthUser, A as AuthError, f as AuthEvent, b as AuthEventMap } from '../../NajmAuthClient-D08--i69.js';
|
|
6
5
|
|
|
7
6
|
interface AuthProviderProps {
|
|
8
7
|
client: NajmAuthClient;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { e as AuthUser, F as FetchClient, R as RetryConfig, N as NajmAuthClient } from '../../NajmAuthClient-D08--i69.js';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
3
|
|
|
4
4
|
interface GetServerSessionOptions {
|
|
@@ -202,6 +202,18 @@ interface DefineAuthConfig {
|
|
|
202
202
|
apiBaseURL?: string;
|
|
203
203
|
/** Auth prefix appended to apiBaseURL (default: '/auth') */
|
|
204
204
|
authPrefix?: string;
|
|
205
|
+
/** Refresh token cookie name (default: 'refreshToken') */
|
|
206
|
+
cookieName?: string;
|
|
207
|
+
/** Proactive refresh at this fraction of token lifetime (default: 0.8) */
|
|
208
|
+
refreshThreshold?: number;
|
|
209
|
+
/** Enable multi-tab sync via BroadcastChannel (default: true) */
|
|
210
|
+
tabSync?: boolean;
|
|
211
|
+
/** BroadcastChannel name (default: 'najm-auth') */
|
|
212
|
+
channelName?: string;
|
|
213
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
214
|
+
timeout?: number;
|
|
215
|
+
/** Network retry configuration */
|
|
216
|
+
retry?: RetryConfig;
|
|
205
217
|
/** Route to redirect unauthenticated users (default: '/login') */
|
|
206
218
|
loginRoute?: string;
|
|
207
219
|
/** Route to redirect after login (default: '/dashboard') */
|
|
@@ -212,8 +224,6 @@ interface DefineAuthConfig {
|
|
|
212
224
|
protectedRoutes?: string[];
|
|
213
225
|
/** Routes restricted to specific roles: { '/admin/:path*': ['admin'] } */
|
|
214
226
|
roleRoutes?: Record<string, string[]>;
|
|
215
|
-
/** Refresh token cookie name (default: 'refreshToken') */
|
|
216
|
-
cookieName?: string;
|
|
217
227
|
/** Session cookie name (default: 'najm.session') */
|
|
218
228
|
sessionCookieName?: string;
|
|
219
229
|
/** Secret for verifying session cookie HMAC. Falls back to env vars. */
|
|
@@ -222,6 +232,15 @@ interface DefineAuthConfig {
|
|
|
222
232
|
matcher?: string[];
|
|
223
233
|
}
|
|
224
234
|
interface AuthKit {
|
|
235
|
+
/**
|
|
236
|
+
* Browser auth client — lazily instantiated on first access.
|
|
237
|
+
*
|
|
238
|
+
* Safe to touch from server/edge runtimes (constructor is runtime-guarded),
|
|
239
|
+
* but intended for client-side use via `<AuthProvider client={auth.client}>`.
|
|
240
|
+
*/
|
|
241
|
+
readonly client: NajmAuthClient;
|
|
242
|
+
/** Shortcut for `client.api` — the underlying FetchClient with auth attached. */
|
|
243
|
+
readonly api: FetchClient;
|
|
225
244
|
/** Resolve session — reads signed cookie first (instant), falls back to /auth/me */
|
|
226
245
|
getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
|
|
227
246
|
/** Require session — throws if unauthenticated */
|
|
@@ -460,6 +460,421 @@ function withAuth(Page, options = {}) {
|
|
|
460
460
|
}
|
|
461
461
|
__name(withAuth, "withAuth");
|
|
462
462
|
|
|
463
|
+
// src/client/tokenDecoder.ts
|
|
464
|
+
function decodeToken(token) {
|
|
465
|
+
try {
|
|
466
|
+
const parts = token.split(".");
|
|
467
|
+
if (parts.length !== 3) return null;
|
|
468
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
469
|
+
const json = typeof atob === "function" ? atob(payload) : Buffer.from(payload, "base64").toString("utf-8");
|
|
470
|
+
return JSON.parse(json);
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
__name(decodeToken, "decodeToken");
|
|
476
|
+
function getTokenTTL(decoded) {
|
|
477
|
+
if (!decoded.exp) return 0;
|
|
478
|
+
return Math.max(0, decoded.exp - Date.now() / 1e3);
|
|
479
|
+
}
|
|
480
|
+
__name(getTokenTTL, "getTokenTTL");
|
|
481
|
+
|
|
482
|
+
// src/client/NajmAuthClient.ts
|
|
483
|
+
init_permissions();
|
|
484
|
+
|
|
485
|
+
// src/client/tabSync.ts
|
|
486
|
+
var TabSync = class {
|
|
487
|
+
constructor(channelName, onMessage) {
|
|
488
|
+
this.onMessage = onMessage;
|
|
489
|
+
this.channel = new BroadcastChannel(channelName);
|
|
490
|
+
this.channel.onmessage = (e) => this.onMessage(e.data);
|
|
491
|
+
}
|
|
492
|
+
static {
|
|
493
|
+
__name(this, "TabSync");
|
|
494
|
+
}
|
|
495
|
+
channel;
|
|
496
|
+
broadcastSync(state) {
|
|
497
|
+
this.channel.postMessage({ type: "sync", state });
|
|
498
|
+
}
|
|
499
|
+
broadcastLogout() {
|
|
500
|
+
this.channel.postMessage({ type: "logout" });
|
|
501
|
+
}
|
|
502
|
+
destroy() {
|
|
503
|
+
this.channel.close();
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/client/NajmAuthClient.ts
|
|
508
|
+
var INITIAL_STATE = {
|
|
509
|
+
user: null,
|
|
510
|
+
accessToken: null,
|
|
511
|
+
isAuthenticated: false,
|
|
512
|
+
isLoading: false,
|
|
513
|
+
roles: [],
|
|
514
|
+
permissions: []
|
|
515
|
+
};
|
|
516
|
+
var NajmAuthClient = class _NajmAuthClient {
|
|
517
|
+
constructor(config) {
|
|
518
|
+
this.config = config;
|
|
519
|
+
this.prefix = config.authPrefix ?? "/auth";
|
|
520
|
+
this.threshold = config.refreshThreshold ?? 0.8;
|
|
521
|
+
this.api = new FetchClient({
|
|
522
|
+
baseURL: config.baseURL,
|
|
523
|
+
credentials: "include",
|
|
524
|
+
timeout: config.timeout ?? 3e4,
|
|
525
|
+
getToken: /* @__PURE__ */ __name(() => this.state.accessToken, "getToken"),
|
|
526
|
+
onUnauthorized: /* @__PURE__ */ __name(() => this.handleUnauthorized(), "onUnauthorized"),
|
|
527
|
+
retry: config.retry
|
|
528
|
+
});
|
|
529
|
+
if (config.tabSync !== false && typeof BroadcastChannel !== "undefined") {
|
|
530
|
+
const name = config.channelName ?? "najm-auth";
|
|
531
|
+
this.tabSync = new TabSync(name, (msg) => this.handleTabMessage(msg));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
static {
|
|
535
|
+
__name(this, "NajmAuthClient");
|
|
536
|
+
}
|
|
537
|
+
static MAX_REFRESH_FAILURES = 3;
|
|
538
|
+
static CIRCUIT_RESET_MS = 6e4;
|
|
539
|
+
state = { ...INITIAL_STATE };
|
|
540
|
+
refreshTimer = null;
|
|
541
|
+
refreshCircuitTimer = null;
|
|
542
|
+
refreshPromise = null;
|
|
543
|
+
fetchUserPromise = null;
|
|
544
|
+
refreshFailures = 0;
|
|
545
|
+
_hydrated = false;
|
|
546
|
+
// Subscriptions (for React useSyncExternalStore)
|
|
547
|
+
listeners = /* @__PURE__ */ new Set();
|
|
548
|
+
eventListeners = /* @__PURE__ */ new Map();
|
|
549
|
+
// Multi-tab sync
|
|
550
|
+
tabSync = null;
|
|
551
|
+
// Public fetch client
|
|
552
|
+
api;
|
|
553
|
+
prefix;
|
|
554
|
+
threshold;
|
|
555
|
+
// =========================================================================
|
|
556
|
+
// Auth Operations
|
|
557
|
+
// =========================================================================
|
|
558
|
+
async login(credentials) {
|
|
559
|
+
const res = await this.api.post(
|
|
560
|
+
`${this.prefix}/login`,
|
|
561
|
+
{ body: credentials, skipAuth: true }
|
|
562
|
+
);
|
|
563
|
+
this.applyTokens(res.data);
|
|
564
|
+
if (res.data.user) {
|
|
565
|
+
this.state = { ...this.state, user: res.data.user };
|
|
566
|
+
this.notify();
|
|
567
|
+
} else {
|
|
568
|
+
await this.fetchUser();
|
|
569
|
+
}
|
|
570
|
+
this.tabSync?.broadcastSync(this.getSyncPayload());
|
|
571
|
+
this.emit("login", this.state.user);
|
|
572
|
+
return this.state.user;
|
|
573
|
+
}
|
|
574
|
+
async register(data) {
|
|
575
|
+
const res = await this.api.post(
|
|
576
|
+
`${this.prefix}/register`,
|
|
577
|
+
{ body: data, skipAuth: true }
|
|
578
|
+
);
|
|
579
|
+
return res.data;
|
|
580
|
+
}
|
|
581
|
+
async logout() {
|
|
582
|
+
this.resetState();
|
|
583
|
+
this.tabSync?.broadcastLogout();
|
|
584
|
+
this.emit("logout", null);
|
|
585
|
+
try {
|
|
586
|
+
await this.api.post(`${this.prefix}/logout`);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
this.emit("logoutError", err);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
async refresh() {
|
|
592
|
+
if (this.refreshFailures >= _NajmAuthClient.MAX_REFRESH_FAILURES) {
|
|
593
|
+
throw new Error("Session expired (circuit open)");
|
|
594
|
+
}
|
|
595
|
+
if (!this.refreshPromise) {
|
|
596
|
+
this.refreshPromise = this._refreshWithCircuit().finally(() => {
|
|
597
|
+
this.refreshPromise = null;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return this.refreshPromise;
|
|
601
|
+
}
|
|
602
|
+
async fetchUser() {
|
|
603
|
+
if (!this.fetchUserPromise) {
|
|
604
|
+
this.fetchUserPromise = this._doFetchUser().finally(() => {
|
|
605
|
+
this.fetchUserPromise = null;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return this.fetchUserPromise;
|
|
609
|
+
}
|
|
610
|
+
async _doFetchUser() {
|
|
611
|
+
try {
|
|
612
|
+
const res = await this.api.get(`${this.prefix}/me`);
|
|
613
|
+
this.state = { ...this.state, user: res.data };
|
|
614
|
+
this.notify();
|
|
615
|
+
this.emit("userUpdated", res.data);
|
|
616
|
+
return res.data;
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async forgotPassword(data) {
|
|
622
|
+
await this.api.post(`${this.prefix}/forgot-password`, { body: data, skipAuth: true });
|
|
623
|
+
}
|
|
624
|
+
async changePassword(data) {
|
|
625
|
+
await this.api.post(`${this.prefix}/change-password`, { body: data });
|
|
626
|
+
this.resetState();
|
|
627
|
+
this.tabSync?.broadcastLogout();
|
|
628
|
+
this.emit("logout", null);
|
|
629
|
+
}
|
|
630
|
+
async resetPassword(data) {
|
|
631
|
+
await this.api.post(`${this.prefix}/reset-password`, { body: data, skipAuth: true });
|
|
632
|
+
}
|
|
633
|
+
// =========================================================================
|
|
634
|
+
// Permissions (decoded from JWT — instant, no round-trip)
|
|
635
|
+
// =========================================================================
|
|
636
|
+
can(permission) {
|
|
637
|
+
return matchPermission(this.state.permissions, permission);
|
|
638
|
+
}
|
|
639
|
+
hasRole(role) {
|
|
640
|
+
return hasRole(this.state.roles, role);
|
|
641
|
+
}
|
|
642
|
+
hasAnyRole(roles) {
|
|
643
|
+
return hasAnyRole(this.state.roles, roles);
|
|
644
|
+
}
|
|
645
|
+
hasPermission(permission) {
|
|
646
|
+
return this.can(permission);
|
|
647
|
+
}
|
|
648
|
+
// =========================================================================
|
|
649
|
+
// State Access
|
|
650
|
+
// =========================================================================
|
|
651
|
+
getUser() {
|
|
652
|
+
return this.state.user;
|
|
653
|
+
}
|
|
654
|
+
getAccessToken() {
|
|
655
|
+
return this.state.accessToken;
|
|
656
|
+
}
|
|
657
|
+
isAuthenticated() {
|
|
658
|
+
return this.state.isAuthenticated;
|
|
659
|
+
}
|
|
660
|
+
getState() {
|
|
661
|
+
return this.state;
|
|
662
|
+
}
|
|
663
|
+
// =========================================================================
|
|
664
|
+
// Hydration (SSR)
|
|
665
|
+
// =========================================================================
|
|
666
|
+
/**
|
|
667
|
+
* Hydrate the client with a session resolved server-side.
|
|
668
|
+
* Use to skip the initial loading flicker on SSR-rendered pages.
|
|
669
|
+
* Safe to call multiple times — subsequent calls are no-ops.
|
|
670
|
+
*/
|
|
671
|
+
hydrate(session) {
|
|
672
|
+
if (this._hydrated) return;
|
|
673
|
+
this._hydrated = true;
|
|
674
|
+
this.resetRefreshFailures();
|
|
675
|
+
if (!session || !session.user) {
|
|
676
|
+
this.state = { ...INITIAL_STATE };
|
|
677
|
+
this.notify();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.state = {
|
|
681
|
+
...this.state,
|
|
682
|
+
user: session.user,
|
|
683
|
+
accessToken: session.accessToken ?? null,
|
|
684
|
+
isAuthenticated: true,
|
|
685
|
+
isLoading: false,
|
|
686
|
+
roles: session.roles ?? (session.user.role ? [session.user.role] : []),
|
|
687
|
+
permissions: session.permissions ?? session.user.permissions ?? []
|
|
688
|
+
};
|
|
689
|
+
if (session.accessToken) {
|
|
690
|
+
const decoded = decodeToken(session.accessToken);
|
|
691
|
+
if (decoded) {
|
|
692
|
+
if (!session.roles && decoded.roles) this.state.roles = decoded.roles;
|
|
693
|
+
if (!session.permissions && decoded.permissions) this.state.permissions = decoded.permissions;
|
|
694
|
+
this.scheduleRefresh(decoded);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
this.notify();
|
|
698
|
+
}
|
|
699
|
+
isHydrated() {
|
|
700
|
+
return this._hydrated;
|
|
701
|
+
}
|
|
702
|
+
// =========================================================================
|
|
703
|
+
// Events
|
|
704
|
+
// =========================================================================
|
|
705
|
+
on(event, handler) {
|
|
706
|
+
if (!this.eventListeners.has(event)) this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
707
|
+
this.eventListeners.get(event).add(handler);
|
|
708
|
+
}
|
|
709
|
+
off(event, handler) {
|
|
710
|
+
this.eventListeners.get(event)?.delete(handler);
|
|
711
|
+
}
|
|
712
|
+
// =========================================================================
|
|
713
|
+
// Subscribe (for React useSyncExternalStore)
|
|
714
|
+
// =========================================================================
|
|
715
|
+
subscribe(listener) {
|
|
716
|
+
this.listeners.add(listener);
|
|
717
|
+
return () => this.listeners.delete(listener);
|
|
718
|
+
}
|
|
719
|
+
// =========================================================================
|
|
720
|
+
// Cleanup
|
|
721
|
+
// =========================================================================
|
|
722
|
+
destroy() {
|
|
723
|
+
this.clearRefreshTimer();
|
|
724
|
+
this.clearRefreshCircuitTimer();
|
|
725
|
+
this.tabSync?.destroy();
|
|
726
|
+
this.tabSync = null;
|
|
727
|
+
this.refreshFailures = 0;
|
|
728
|
+
this.state = { ...INITIAL_STATE };
|
|
729
|
+
this.listeners.clear();
|
|
730
|
+
this.eventListeners.clear();
|
|
731
|
+
}
|
|
732
|
+
// =========================================================================
|
|
733
|
+
// Internals
|
|
734
|
+
// =========================================================================
|
|
735
|
+
async _refreshWithCircuit() {
|
|
736
|
+
try {
|
|
737
|
+
await this._doRefresh();
|
|
738
|
+
this.resetRefreshFailures();
|
|
739
|
+
} catch (err) {
|
|
740
|
+
const shouldOpenCircuit = this.registerRefreshFailure(err);
|
|
741
|
+
if (shouldOpenCircuit) {
|
|
742
|
+
this.resetState();
|
|
743
|
+
this.emit("sessionExpired", null);
|
|
744
|
+
if (err instanceof AuthError && err.status === 401) {
|
|
745
|
+
throw new Error("Session expired");
|
|
746
|
+
}
|
|
747
|
+
throw new Error("Session expired (circuit open)");
|
|
748
|
+
}
|
|
749
|
+
throw err;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async _doRefresh() {
|
|
753
|
+
const res = await this.api.post(
|
|
754
|
+
`${this.prefix}/refresh`,
|
|
755
|
+
{ skipAuth: true }
|
|
756
|
+
);
|
|
757
|
+
this.applyTokens(res.data);
|
|
758
|
+
this.tabSync?.broadcastSync(this.getSyncPayload());
|
|
759
|
+
this.emit("tokenRefresh", null);
|
|
760
|
+
}
|
|
761
|
+
async handleUnauthorized() {
|
|
762
|
+
try {
|
|
763
|
+
await this.refresh();
|
|
764
|
+
return this.state.accessToken;
|
|
765
|
+
} catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
applyTokens(tokens) {
|
|
770
|
+
this.resetRefreshFailures();
|
|
771
|
+
const decoded = decodeToken(tokens.accessToken);
|
|
772
|
+
this.state = {
|
|
773
|
+
...this.state,
|
|
774
|
+
accessToken: tokens.accessToken,
|
|
775
|
+
isAuthenticated: true,
|
|
776
|
+
isLoading: false,
|
|
777
|
+
roles: decoded?.roles ?? [],
|
|
778
|
+
permissions: decoded?.permissions ?? []
|
|
779
|
+
};
|
|
780
|
+
if (decoded) this.scheduleRefresh(decoded);
|
|
781
|
+
this.notify();
|
|
782
|
+
}
|
|
783
|
+
scheduleRefresh(decoded) {
|
|
784
|
+
this.clearRefreshTimer();
|
|
785
|
+
const ttl = getTokenTTL(decoded);
|
|
786
|
+
if (ttl <= 0) return;
|
|
787
|
+
const delay = ttl * this.threshold * 1e3;
|
|
788
|
+
this.refreshTimer = setTimeout(() => this.refresh().catch(() => {
|
|
789
|
+
}), delay);
|
|
790
|
+
}
|
|
791
|
+
clearRefreshTimer() {
|
|
792
|
+
if (this.refreshTimer) {
|
|
793
|
+
clearTimeout(this.refreshTimer);
|
|
794
|
+
this.refreshTimer = null;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
clearRefreshCircuitTimer() {
|
|
798
|
+
if (this.refreshCircuitTimer) {
|
|
799
|
+
clearTimeout(this.refreshCircuitTimer);
|
|
800
|
+
this.refreshCircuitTimer = null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
resetRefreshFailures() {
|
|
804
|
+
this.refreshFailures = 0;
|
|
805
|
+
this.clearRefreshCircuitTimer();
|
|
806
|
+
}
|
|
807
|
+
registerRefreshFailure(err) {
|
|
808
|
+
if (err instanceof AuthError && err.status === 401) {
|
|
809
|
+
this.refreshFailures = _NajmAuthClient.MAX_REFRESH_FAILURES;
|
|
810
|
+
} else {
|
|
811
|
+
this.refreshFailures += 1;
|
|
812
|
+
}
|
|
813
|
+
if (this.refreshFailures >= _NajmAuthClient.MAX_REFRESH_FAILURES) {
|
|
814
|
+
this.clearRefreshCircuitTimer();
|
|
815
|
+
this.refreshCircuitTimer = setTimeout(() => {
|
|
816
|
+
this.refreshFailures = 0;
|
|
817
|
+
this.refreshCircuitTimer = null;
|
|
818
|
+
}, _NajmAuthClient.CIRCUIT_RESET_MS);
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
resetState() {
|
|
824
|
+
this.clearRefreshTimer();
|
|
825
|
+
this.state = { ...INITIAL_STATE };
|
|
826
|
+
this.notify();
|
|
827
|
+
}
|
|
828
|
+
getSyncPayload() {
|
|
829
|
+
return {
|
|
830
|
+
accessToken: this.state.accessToken,
|
|
831
|
+
user: this.state.user,
|
|
832
|
+
roles: this.state.roles,
|
|
833
|
+
permissions: this.state.permissions,
|
|
834
|
+
isAuthenticated: this.state.isAuthenticated
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
handleTabMessage(msg) {
|
|
838
|
+
switch (msg.type) {
|
|
839
|
+
case "logout":
|
|
840
|
+
this.clearRefreshTimer();
|
|
841
|
+
this.state = { ...INITIAL_STATE };
|
|
842
|
+
this.notify();
|
|
843
|
+
this.emit("logout", null);
|
|
844
|
+
break;
|
|
845
|
+
case "sync":
|
|
846
|
+
this.state = {
|
|
847
|
+
...this.state,
|
|
848
|
+
accessToken: msg.state.accessToken,
|
|
849
|
+
user: msg.state.user,
|
|
850
|
+
roles: msg.state.roles,
|
|
851
|
+
permissions: msg.state.permissions,
|
|
852
|
+
isAuthenticated: msg.state.isAuthenticated,
|
|
853
|
+
isLoading: false
|
|
854
|
+
};
|
|
855
|
+
if (msg.state.accessToken) {
|
|
856
|
+
const decoded = decodeToken(msg.state.accessToken);
|
|
857
|
+
if (decoded) this.scheduleRefresh(decoded);
|
|
858
|
+
}
|
|
859
|
+
this.notify();
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
notify() {
|
|
864
|
+
this.listeners.forEach((l) => l(this.state));
|
|
865
|
+
}
|
|
866
|
+
emit(event, data) {
|
|
867
|
+
this.eventListeners.get(event)?.forEach((h) => h(data));
|
|
868
|
+
if (event !== "stateChange") {
|
|
869
|
+
this.eventListeners.get("stateChange")?.forEach((h) => h(this.state));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
function createAuthClient(config) {
|
|
874
|
+
return new NajmAuthClient(config);
|
|
875
|
+
}
|
|
876
|
+
__name(createAuthClient, "createAuthClient");
|
|
877
|
+
|
|
463
878
|
// src/client/server/defineAuth.ts
|
|
464
879
|
function matchesAny2(pathname, patterns) {
|
|
465
880
|
return patterns.some((p) => matchPattern2(pathname, p));
|
|
@@ -493,7 +908,12 @@ function defineAuth(authConfig = {}) {
|
|
|
493
908
|
cookieName = "refreshToken",
|
|
494
909
|
sessionCookieName = "najm.session",
|
|
495
910
|
sessionSecret,
|
|
496
|
-
matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"]
|
|
911
|
+
matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
|
|
912
|
+
refreshThreshold,
|
|
913
|
+
tabSync,
|
|
914
|
+
channelName,
|
|
915
|
+
timeout,
|
|
916
|
+
retry
|
|
497
917
|
} = authConfig;
|
|
498
918
|
const sessionConfig = {
|
|
499
919
|
baseURL: void 0,
|
|
@@ -503,6 +923,20 @@ function defineAuth(authConfig = {}) {
|
|
|
503
923
|
sessionCookieName,
|
|
504
924
|
sessionSecret
|
|
505
925
|
};
|
|
926
|
+
let _client = null;
|
|
927
|
+
const getClient = /* @__PURE__ */ __name(() => {
|
|
928
|
+
if (_client) return _client;
|
|
929
|
+
_client = createAuthClient({
|
|
930
|
+
baseURL: apiBaseURL,
|
|
931
|
+
authPrefix,
|
|
932
|
+
refreshThreshold,
|
|
933
|
+
tabSync,
|
|
934
|
+
channelName,
|
|
935
|
+
timeout,
|
|
936
|
+
retry
|
|
937
|
+
});
|
|
938
|
+
return _client;
|
|
939
|
+
}, "getClient");
|
|
506
940
|
const getSession2 = /* @__PURE__ */ __name(async (opts) => {
|
|
507
941
|
const { getSession: resolveSession } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
|
|
508
942
|
return resolveSession({ ...sessionConfig, ...opts });
|
|
@@ -596,6 +1030,12 @@ function defineAuth(authConfig = {}) {
|
|
|
596
1030
|
}, "ProtectedPage");
|
|
597
1031
|
}, "protect");
|
|
598
1032
|
return {
|
|
1033
|
+
get client() {
|
|
1034
|
+
return getClient();
|
|
1035
|
+
},
|
|
1036
|
+
get api() {
|
|
1037
|
+
return getClient().api;
|
|
1038
|
+
},
|
|
599
1039
|
getSession: getSession2,
|
|
600
1040
|
requireSession,
|
|
601
1041
|
middleware,
|
package/package.json
CHANGED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { F as FetchClient, a as AuthClientConfig, d as AuthUser, c as AuthState, e as AuthEvent, f as AuthEventHandler } from './FetchClient-DadnuVF7.js';
|
|
2
|
-
|
|
3
|
-
interface HydrateSession {
|
|
4
|
-
user: AuthUser | null;
|
|
5
|
-
accessToken?: string | null;
|
|
6
|
-
roles?: string[];
|
|
7
|
-
permissions?: string[];
|
|
8
|
-
}
|
|
9
|
-
declare class NajmAuthClient {
|
|
10
|
-
private config;
|
|
11
|
-
private static readonly MAX_REFRESH_FAILURES;
|
|
12
|
-
private static readonly CIRCUIT_RESET_MS;
|
|
13
|
-
private state;
|
|
14
|
-
private refreshTimer;
|
|
15
|
-
private refreshCircuitTimer;
|
|
16
|
-
private refreshPromise;
|
|
17
|
-
private fetchUserPromise;
|
|
18
|
-
private refreshFailures;
|
|
19
|
-
private _hydrated;
|
|
20
|
-
private listeners;
|
|
21
|
-
private eventListeners;
|
|
22
|
-
private tabSync;
|
|
23
|
-
api: FetchClient;
|
|
24
|
-
private readonly prefix;
|
|
25
|
-
private readonly threshold;
|
|
26
|
-
constructor(config: AuthClientConfig);
|
|
27
|
-
login(credentials: {
|
|
28
|
-
email: string;
|
|
29
|
-
password: string;
|
|
30
|
-
}): Promise<AuthUser>;
|
|
31
|
-
register(data: Record<string, unknown>): Promise<AuthUser>;
|
|
32
|
-
logout(): Promise<void>;
|
|
33
|
-
refresh(): Promise<void>;
|
|
34
|
-
fetchUser(): Promise<AuthUser | null>;
|
|
35
|
-
private _doFetchUser;
|
|
36
|
-
forgotPassword(data: {
|
|
37
|
-
email: string;
|
|
38
|
-
}): Promise<void>;
|
|
39
|
-
changePassword(data: {
|
|
40
|
-
currentPassword: string;
|
|
41
|
-
newPassword: string;
|
|
42
|
-
}): Promise<void>;
|
|
43
|
-
resetPassword(data: {
|
|
44
|
-
token: string;
|
|
45
|
-
newPassword: string;
|
|
46
|
-
}): Promise<void>;
|
|
47
|
-
can(permission: string): boolean;
|
|
48
|
-
hasRole(role: string): boolean;
|
|
49
|
-
hasAnyRole(roles: string[]): boolean;
|
|
50
|
-
hasPermission(permission: string): boolean;
|
|
51
|
-
getUser(): AuthUser | null;
|
|
52
|
-
getAccessToken(): string | null;
|
|
53
|
-
isAuthenticated(): boolean;
|
|
54
|
-
getState(): AuthState;
|
|
55
|
-
/**
|
|
56
|
-
* Hydrate the client with a session resolved server-side.
|
|
57
|
-
* Use to skip the initial loading flicker on SSR-rendered pages.
|
|
58
|
-
* Safe to call multiple times — subsequent calls are no-ops.
|
|
59
|
-
*/
|
|
60
|
-
hydrate(session: HydrateSession | null): void;
|
|
61
|
-
isHydrated(): boolean;
|
|
62
|
-
on<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
|
|
63
|
-
off<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
|
|
64
|
-
subscribe(listener: (state: AuthState) => void): () => void;
|
|
65
|
-
destroy(): void;
|
|
66
|
-
private _refreshWithCircuit;
|
|
67
|
-
private _doRefresh;
|
|
68
|
-
private handleUnauthorized;
|
|
69
|
-
private applyTokens;
|
|
70
|
-
private scheduleRefresh;
|
|
71
|
-
private clearRefreshTimer;
|
|
72
|
-
private clearRefreshCircuitTimer;
|
|
73
|
-
private resetRefreshFailures;
|
|
74
|
-
private registerRefreshFailure;
|
|
75
|
-
private resetState;
|
|
76
|
-
private getSyncPayload;
|
|
77
|
-
private handleTabMessage;
|
|
78
|
-
private notify;
|
|
79
|
-
private emit;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Factory function to create an auth client.
|
|
83
|
-
*/
|
|
84
|
-
declare function createAuthClient(config: AuthClientConfig): NajmAuthClient;
|
|
85
|
-
|
|
86
|
-
export { type HydrateSession as H, NajmAuthClient as N, createAuthClient as c };
|