najm-auth 3.0.0 → 3.1.1
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 +179 -11
- package/dist/NajmAuthClient-DqGucYXi.d.ts +120 -0
- package/dist/client/edge.d.ts +3 -18
- package/dist/client/index.d.ts +3 -2
- package/dist/client/react/index.d.ts +2 -1
- package/dist/client/server/index.d.ts +15 -75
- package/dist/client/server/index.js +77 -53
- package/dist/client/server/react.d.ts +48 -0
- package/dist/client/server/react.js +71 -0
- package/dist/client/server/reactClientGuard.d.ts +2 -0
- package/dist/client/server/reactClientGuard.js +4 -0
- package/dist/getSession-BthP85UA.d.ts +68 -0
- package/dist/sessionRecovery-D5Fa0yZ1.d.ts +18 -0
- package/dist/types-BaSfgxqE.d.ts +166 -0
- package/package.json +10 -2
- package/dist/NajmAuthClient-D2fSvQ_H.d.ts +0 -283
|
@@ -356,7 +356,8 @@ __export(getSession_exports, {
|
|
|
356
356
|
AuthConfigError: () => AuthConfigError,
|
|
357
357
|
AuthTransportError: () => AuthTransportError,
|
|
358
358
|
NoSessionError: () => NoSessionError,
|
|
359
|
-
getSession: () => getSession
|
|
359
|
+
getSession: () => getSession,
|
|
360
|
+
resolveSessionOutcome: () => resolveSessionOutcome
|
|
360
361
|
});
|
|
361
362
|
function defaultBaseURL() {
|
|
362
363
|
const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
|
|
@@ -381,12 +382,11 @@ function requestOriginFromHeaders(headers) {
|
|
|
381
382
|
return void 0;
|
|
382
383
|
}
|
|
383
384
|
}
|
|
384
|
-
async function
|
|
385
|
+
async function resolveSessionOutcome(config = {}) {
|
|
385
386
|
const cookieName2 = config.cookieName ?? "refreshToken";
|
|
386
387
|
const sessionCookieName = config.sessionCookieName ?? "najm.session";
|
|
387
388
|
const baseURL = config.baseURL ?? defaultBaseURL();
|
|
388
389
|
const prefix = config.authPrefix ?? "/auth";
|
|
389
|
-
const strict = config.mode === "strict";
|
|
390
390
|
const internalRecoveryURL = resolveInternalRecoveryURL(config.internalRecoveryURL);
|
|
391
391
|
let sessionCookieValue;
|
|
392
392
|
let refreshCookieValue;
|
|
@@ -400,8 +400,7 @@ async function getSession(config = {}) {
|
|
|
400
400
|
requestOrigin = requestOriginFromHeaders(await mod.headers());
|
|
401
401
|
}
|
|
402
402
|
} catch {
|
|
403
|
-
|
|
404
|
-
return null;
|
|
403
|
+
return failed(new AuthConfigError("Failed to read cookies from Next.js headers()"));
|
|
405
404
|
}
|
|
406
405
|
const secret = resolveSessionSecret(config.sessionSecret);
|
|
407
406
|
if (sessionCookieValue && secret) {
|
|
@@ -410,24 +409,21 @@ async function getSession(config = {}) {
|
|
|
410
409
|
maxAgeSeconds: config.sessionMaxAge
|
|
411
410
|
});
|
|
412
411
|
if (claims) {
|
|
413
|
-
return {
|
|
412
|
+
return authenticated({
|
|
414
413
|
user: claims.user,
|
|
415
414
|
roles: claims.roles,
|
|
416
415
|
permissions: claims.permissions
|
|
417
|
-
};
|
|
416
|
+
});
|
|
418
417
|
}
|
|
419
418
|
}
|
|
420
419
|
if (!secret) {
|
|
421
|
-
|
|
422
|
-
return null;
|
|
420
|
+
return failed(new AuthConfigError("Session cookie secret is not configured"));
|
|
423
421
|
}
|
|
424
422
|
if (!refreshCookieValue || config.recoveryURL === false && !internalRecoveryURL) {
|
|
425
|
-
|
|
426
|
-
return null;
|
|
423
|
+
return unauthenticated("No recoverable refresh session");
|
|
427
424
|
}
|
|
428
425
|
if (!requestOrigin) {
|
|
429
|
-
|
|
430
|
-
return null;
|
|
426
|
+
return failed(new AuthConfigError("Incoming request origin is unavailable"));
|
|
431
427
|
}
|
|
432
428
|
const endpoint = internalRecoveryURL ?? (config.recoveryURL ? new URL(config.recoveryURL, requestOrigin).toString() : authEndpoint(baseURL, prefix, "/session/recover", requestOrigin));
|
|
433
429
|
const recovery = await requestSessionRecovery({
|
|
@@ -442,24 +438,27 @@ async function getSession(config = {}) {
|
|
|
442
438
|
onFailure: config.onRecoveryFailure
|
|
443
439
|
});
|
|
444
440
|
if (recovery.status === "recovered") {
|
|
445
|
-
return {
|
|
441
|
+
return authenticated({
|
|
446
442
|
user: recovery.claims.user,
|
|
447
443
|
roles: recovery.claims.roles,
|
|
448
444
|
permissions: recovery.claims.permissions
|
|
449
|
-
};
|
|
445
|
+
});
|
|
450
446
|
}
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
throw new NoSessionError("Refresh session is invalid or revoked");
|
|
454
|
-
}
|
|
455
|
-
throw new AuthTransportError(
|
|
456
|
-
"Session recovery endpoint was unavailable or returned an invalid session",
|
|
457
|
-
recovery.httpStatus
|
|
458
|
-
);
|
|
447
|
+
if (recovery.status === "invalid") {
|
|
448
|
+
return unauthenticated("Refresh session is invalid or revoked");
|
|
459
449
|
}
|
|
450
|
+
return failed(new AuthTransportError(
|
|
451
|
+
"Session recovery endpoint was unavailable or returned an invalid session",
|
|
452
|
+
recovery.httpStatus
|
|
453
|
+
));
|
|
454
|
+
}
|
|
455
|
+
async function getSession(config = {}) {
|
|
456
|
+
const outcome = await resolveSessionOutcome(config);
|
|
457
|
+
if (outcome.status === "authenticated") return outcome.session;
|
|
458
|
+
if (config.mode === "strict") throw outcome.error;
|
|
460
459
|
return null;
|
|
461
460
|
}
|
|
462
|
-
var NoSessionError, AuthConfigError, AuthTransportError;
|
|
461
|
+
var NoSessionError, AuthConfigError, AuthTransportError, authenticated, unauthenticated, failed;
|
|
463
462
|
var init_getSession = __esm({
|
|
464
463
|
"src/client/server/getSession.ts"() {
|
|
465
464
|
init_sessionCookie();
|
|
@@ -498,6 +497,19 @@ var init_getSession = __esm({
|
|
|
498
497
|
__name(defaultBaseURL, "defaultBaseURL");
|
|
499
498
|
__name(firstForwardedValue, "firstForwardedValue");
|
|
500
499
|
__name(requestOriginFromHeaders, "requestOriginFromHeaders");
|
|
500
|
+
authenticated = /* @__PURE__ */ __name((session) => ({
|
|
501
|
+
status: "authenticated",
|
|
502
|
+
session
|
|
503
|
+
}), "authenticated");
|
|
504
|
+
unauthenticated = /* @__PURE__ */ __name((message) => ({
|
|
505
|
+
status: "unauthenticated",
|
|
506
|
+
error: new NoSessionError(message)
|
|
507
|
+
}), "unauthenticated");
|
|
508
|
+
failed = /* @__PURE__ */ __name((error) => ({
|
|
509
|
+
status: "failed",
|
|
510
|
+
error
|
|
511
|
+
}), "failed");
|
|
512
|
+
__name(resolveSessionOutcome, "resolveSessionOutcome");
|
|
501
513
|
__name(getSession, "getSession");
|
|
502
514
|
}
|
|
503
515
|
});
|
|
@@ -949,10 +961,10 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
949
961
|
if (setup) {
|
|
950
962
|
return { ...setup };
|
|
951
963
|
}
|
|
952
|
-
const
|
|
953
|
-
this.applyTokens(
|
|
954
|
-
if (
|
|
955
|
-
this.state = { ...this.state, user:
|
|
964
|
+
const authenticated2 = res.data;
|
|
965
|
+
this.applyTokens(authenticated2);
|
|
966
|
+
if (authenticated2.user) {
|
|
967
|
+
this.state = { ...this.state, user: authenticated2.user };
|
|
956
968
|
this.notify();
|
|
957
969
|
} else {
|
|
958
970
|
await this.fetchUser();
|
|
@@ -1325,6 +1337,29 @@ function createAuthClient(config) {
|
|
|
1325
1337
|
}
|
|
1326
1338
|
__name(createAuthClient, "createAuthClient");
|
|
1327
1339
|
|
|
1340
|
+
// src/client/server/internals.ts
|
|
1341
|
+
var REACT_SERVER_INTERNALS = Symbol.for("najm-auth.reactServerInternals");
|
|
1342
|
+
function redirectsToLogin(outcome) {
|
|
1343
|
+
if (outcome.status === "unauthenticated") return true;
|
|
1344
|
+
const { code, status } = outcome.error;
|
|
1345
|
+
return code === "AUTH_TRANSPORT_ERROR" && (status === 401 || status === 403);
|
|
1346
|
+
}
|
|
1347
|
+
__name(redirectsToLogin, "redirectsToLogin");
|
|
1348
|
+
function heldRoles(session) {
|
|
1349
|
+
return session.roles ?? (session.user.role ? [session.user.role] : []);
|
|
1350
|
+
}
|
|
1351
|
+
__name(heldRoles, "heldRoles");
|
|
1352
|
+
function attachReactServerInternals(kit, internals) {
|
|
1353
|
+
Object.defineProperty(kit, REACT_SERVER_INTERNALS, {
|
|
1354
|
+
value: internals,
|
|
1355
|
+
enumerable: false,
|
|
1356
|
+
configurable: false,
|
|
1357
|
+
writable: false
|
|
1358
|
+
});
|
|
1359
|
+
return kit;
|
|
1360
|
+
}
|
|
1361
|
+
__name(attachReactServerInternals, "attachReactServerInternals");
|
|
1362
|
+
|
|
1328
1363
|
// src/client/server/defineAuth.ts
|
|
1329
1364
|
function defineAuth(authConfig = {}) {
|
|
1330
1365
|
const {
|
|
@@ -1379,32 +1414,22 @@ function defineAuth(authConfig = {}) {
|
|
|
1379
1414
|
const { getSession: resolveSession } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
|
|
1380
1415
|
return resolveSession({ ...sessionConfig, ...opts });
|
|
1381
1416
|
}, "getSession");
|
|
1417
|
+
const resolveSessionOutcome2 = /* @__PURE__ */ __name(async () => {
|
|
1418
|
+
const { resolveSessionOutcome: resolve } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
|
|
1419
|
+
return resolve(sessionConfig);
|
|
1420
|
+
}, "resolveSessionOutcome");
|
|
1382
1421
|
const requireSession = /* @__PURE__ */ __name(async () => {
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
} = sessionModule;
|
|
1389
|
-
try {
|
|
1390
|
-
const session = await resolveSession({ ...sessionConfig, mode: "strict" });
|
|
1391
|
-
return session;
|
|
1392
|
-
} catch (err) {
|
|
1393
|
-
if (err instanceof NoSessionError2) {
|
|
1394
|
-
const { redirect } = await import("next/navigation");
|
|
1395
|
-
redirect(loginRoute);
|
|
1396
|
-
}
|
|
1397
|
-
if (err instanceof AuthTransportError2 && (err.status === 401 || err.status === 403)) {
|
|
1398
|
-
const { redirect } = await import("next/navigation");
|
|
1399
|
-
redirect(loginRoute);
|
|
1400
|
-
}
|
|
1401
|
-
throw err;
|
|
1422
|
+
const outcome = await resolveSessionOutcome2();
|
|
1423
|
+
if (outcome.status === "authenticated") return outcome.session;
|
|
1424
|
+
if (redirectsToLogin(outcome)) {
|
|
1425
|
+
const { redirect } = await import("next/navigation");
|
|
1426
|
+
redirect(loginRoute);
|
|
1402
1427
|
}
|
|
1428
|
+
throw outcome.error;
|
|
1403
1429
|
}, "requireSession");
|
|
1404
1430
|
const requireRole = /* @__PURE__ */ __name(async (roles) => {
|
|
1405
1431
|
const session = await requireSession();
|
|
1406
|
-
|
|
1407
|
-
if (!held.some((role) => roles.includes(role))) {
|
|
1432
|
+
if (!heldRoles(session).some((role) => roles.includes(role))) {
|
|
1408
1433
|
const { redirect } = await import("next/navigation");
|
|
1409
1434
|
redirect(forbiddenRoute);
|
|
1410
1435
|
}
|
|
@@ -1434,8 +1459,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1434
1459
|
redirect(loginRoute);
|
|
1435
1460
|
}
|
|
1436
1461
|
if (options?.role) {
|
|
1437
|
-
|
|
1438
|
-
if (!userRoles.includes(options.role)) {
|
|
1462
|
+
if (!heldRoles(session).includes(options.role)) {
|
|
1439
1463
|
const { redirect } = await import("next/navigation");
|
|
1440
1464
|
redirect(forbiddenRoute);
|
|
1441
1465
|
}
|
|
@@ -1451,7 +1475,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1451
1475
|
return Page({ session, ...props });
|
|
1452
1476
|
}, "ProtectedPage");
|
|
1453
1477
|
}, "protect");
|
|
1454
|
-
return {
|
|
1478
|
+
return attachReactServerInternals({
|
|
1455
1479
|
get client() {
|
|
1456
1480
|
return getClient();
|
|
1457
1481
|
},
|
|
@@ -1464,7 +1488,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1464
1488
|
middleware,
|
|
1465
1489
|
config: { matcher },
|
|
1466
1490
|
protect
|
|
1467
|
-
};
|
|
1491
|
+
}, { resolveSessionOutcome: resolveSessionOutcome2, loginRoute, forbiddenRoute });
|
|
1468
1492
|
}
|
|
1469
1493
|
__name(defineAuth, "defineAuth");
|
|
1470
1494
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { S as ServerSession } from '../../getSession-BthP85UA.js';
|
|
2
|
+
import '../../types-BaSfgxqE.js';
|
|
3
|
+
import '../../sessionRecovery-D5Fa0yZ1.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `defineAuth()` result.
|
|
7
|
+
*
|
|
8
|
+
* Declared structurally rather than as `AuthKit` on purpose: each entry point
|
|
9
|
+
* bundles its own declarations, and `AuthKit` reaches classes whose private
|
|
10
|
+
* members would make the two copies nominally incompatible.
|
|
11
|
+
*/
|
|
12
|
+
interface ReactServerAuthSource {
|
|
13
|
+
getSession(): Promise<ServerSession | null>;
|
|
14
|
+
requireSession(): Promise<ServerSession>;
|
|
15
|
+
requireRole(roles: string[]): Promise<ServerSession>;
|
|
16
|
+
}
|
|
17
|
+
interface ReactServerAuth {
|
|
18
|
+
/** The session for this request, or `null` when there is none. */
|
|
19
|
+
getSession(): Promise<ServerSession | null>;
|
|
20
|
+
/**
|
|
21
|
+
* The session for this request, redirecting to `loginRoute` when the visitor
|
|
22
|
+
* is unauthenticated.
|
|
23
|
+
*
|
|
24
|
+
* A recovery endpoint that is unreachable or a missing session secret is an
|
|
25
|
+
* operational fault, not an anonymous visitor: those stay visible errors
|
|
26
|
+
* rather than becoming a login redirect that hides the outage.
|
|
27
|
+
*/
|
|
28
|
+
requireSession(): Promise<ServerSession>;
|
|
29
|
+
/**
|
|
30
|
+
* The session for this request, redirecting to `forbiddenRoute` when it holds
|
|
31
|
+
* none of `roles`. Not the login route — the visitor is already
|
|
32
|
+
* authenticated, so signing in again cannot change the answer.
|
|
33
|
+
*/
|
|
34
|
+
requireRole(roles: string[]): Promise<ServerSession>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create the app's single request-scoped session accessor.
|
|
38
|
+
*
|
|
39
|
+
* Call this exactly once, at module scope, in a module the whole app imports.
|
|
40
|
+
* Calling it inside a layout, page, or component creates a fresh memoized
|
|
41
|
+
* resolver per call and shares nothing.
|
|
42
|
+
*
|
|
43
|
+
* Redirect routes and error classification come from the `defineAuth()` config
|
|
44
|
+
* that produced `auth`; they are not re-accepted here.
|
|
45
|
+
*/
|
|
46
|
+
declare function createReactServerAuth(auth: ReactServerAuthSource): ReactServerAuth;
|
|
47
|
+
|
|
48
|
+
export { type ReactServerAuth, type ReactServerAuthSource, createReactServerAuth };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/client/server/react.ts
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
// src/client/server/internals.ts
|
|
8
|
+
var REACT_SERVER_INTERNALS = Symbol.for("najm-auth.reactServerInternals");
|
|
9
|
+
function redirectsToLogin(outcome) {
|
|
10
|
+
if (outcome.status === "unauthenticated") return true;
|
|
11
|
+
const { code, status } = outcome.error;
|
|
12
|
+
return code === "AUTH_TRANSPORT_ERROR" && (status === 401 || status === 403);
|
|
13
|
+
}
|
|
14
|
+
__name(redirectsToLogin, "redirectsToLogin");
|
|
15
|
+
function heldRoles(session) {
|
|
16
|
+
return session.roles ?? (session.user.role ? [session.user.role] : []);
|
|
17
|
+
}
|
|
18
|
+
__name(heldRoles, "heldRoles");
|
|
19
|
+
function readReactServerInternals(auth) {
|
|
20
|
+
if (auth === null || typeof auth !== "object" && typeof auth !== "function") return void 0;
|
|
21
|
+
return auth[REACT_SERVER_INTERNALS];
|
|
22
|
+
}
|
|
23
|
+
__name(readReactServerInternals, "readReactServerInternals");
|
|
24
|
+
|
|
25
|
+
// src/client/server/react.ts
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"najm-auth/client/server/react is a React Server Component module and cannot be imported from a Client Component. Import najm-auth/client/react instead."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function createReactServerAuth(auth) {
|
|
32
|
+
const internals = readReactServerInternals(auth);
|
|
33
|
+
if (!internals) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"createReactServerAuth() expects the object returned by defineAuth() from najm-auth/client/server. Check that both imports resolve to the same installed najm-auth version."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const cache2 = React.cache;
|
|
39
|
+
if (typeof cache2 !== "function") {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"najm-auth/client/server/react requires a React version that exports cache() (React 18.3 or newer). Upgrade react, or use auth.getSession() directly."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const resolve = cache2(internals.resolveSessionOutcome);
|
|
45
|
+
const getSession = /* @__PURE__ */ __name(async () => {
|
|
46
|
+
const outcome = await resolve();
|
|
47
|
+
return outcome.status === "authenticated" ? outcome.session : null;
|
|
48
|
+
}, "getSession");
|
|
49
|
+
const requireSession = /* @__PURE__ */ __name(async () => {
|
|
50
|
+
const outcome = await resolve();
|
|
51
|
+
if (outcome.status === "authenticated") return outcome.session;
|
|
52
|
+
if (redirectsToLogin(outcome)) {
|
|
53
|
+
const { redirect } = await import("next/navigation");
|
|
54
|
+
redirect(internals.loginRoute);
|
|
55
|
+
}
|
|
56
|
+
throw outcome.error;
|
|
57
|
+
}, "requireSession");
|
|
58
|
+
const requireRole = /* @__PURE__ */ __name(async (roles) => {
|
|
59
|
+
const session = await requireSession();
|
|
60
|
+
if (!heldRoles(session).some((role) => roles.includes(role))) {
|
|
61
|
+
const { redirect } = await import("next/navigation");
|
|
62
|
+
redirect(internals.forbiddenRoute);
|
|
63
|
+
}
|
|
64
|
+
return session;
|
|
65
|
+
}, "requireRole");
|
|
66
|
+
return { getSession, requireSession, requireRole };
|
|
67
|
+
}
|
|
68
|
+
__name(createReactServerAuth, "createReactServerAuth");
|
|
69
|
+
export {
|
|
70
|
+
createReactServerAuth
|
|
71
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// src/client/server/reactClientGuard.ts
|
|
2
|
+
throw new Error(
|
|
3
|
+
"najm-auth/client/server/react is a React Server Component module. It cannot be imported from a Client Component, the Edge runtime, or any browser bundle. Use najm-auth/client/react for client components, and najm-auth/client/server for proxy and Edge code."
|
|
4
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { A as AuthUser } from './types-BaSfgxqE.js';
|
|
2
|
+
import { S as SessionRecoveryFailure } from './sessionRecovery-D5Fa0yZ1.js';
|
|
3
|
+
|
|
4
|
+
interface ServerSession {
|
|
5
|
+
user: AuthUser;
|
|
6
|
+
roles?: string[];
|
|
7
|
+
permissions?: string[];
|
|
8
|
+
}
|
|
9
|
+
interface GetSessionConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Base URL for auth endpoints.
|
|
12
|
+
* Defaults to `NEXT_PUBLIC_API_URL` or the same-origin `/api` path.
|
|
13
|
+
*/
|
|
14
|
+
baseURL?: string;
|
|
15
|
+
/** Auth route prefix appended to baseURL (default: '/auth'). */
|
|
16
|
+
authPrefix?: string;
|
|
17
|
+
/** Refresh token cookie name (default: 'refreshToken'). */
|
|
18
|
+
cookieName?: string;
|
|
19
|
+
/** Signed session cookie name (default: 'najm.session'). */
|
|
20
|
+
sessionCookieName?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Secret used to verify the session cookie HMAC signature.
|
|
23
|
+
* Falls back to NAJM_SESSION_SECRET or JWT_ACCESS_SECRET env vars.
|
|
24
|
+
*/
|
|
25
|
+
sessionSecret?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum accepted session-cookie age in seconds.
|
|
28
|
+
* Must match the auth plugin's `session.maxAge`. Default: 300.
|
|
29
|
+
*/
|
|
30
|
+
sessionMaxAge?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Session-recovery endpoint. Defaults to
|
|
33
|
+
* `${baseURL}${authPrefix}/session/recover`. Set to false to disable fallback.
|
|
34
|
+
*/
|
|
35
|
+
recoveryURL?: string | false;
|
|
36
|
+
/** Loopback-only recovery endpoint for self-hosted reverse-proxy setups. */
|
|
37
|
+
internalRecoveryURL?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Error handling mode:
|
|
40
|
+
* - 'nullable' (default): returns null on any failure
|
|
41
|
+
* - 'strict': throws typed errors for debugging
|
|
42
|
+
*/
|
|
43
|
+
mode?: 'nullable' | 'strict';
|
|
44
|
+
/** Secret-free diagnostic hook for failed recovery attempts. */
|
|
45
|
+
onRecoveryFailure?: (failure: SessionRecoveryFailure) => void;
|
|
46
|
+
}
|
|
47
|
+
declare class NoSessionError extends Error {
|
|
48
|
+
readonly code = "NO_SESSION";
|
|
49
|
+
constructor(message?: string);
|
|
50
|
+
}
|
|
51
|
+
declare class AuthConfigError extends Error {
|
|
52
|
+
readonly code = "AUTH_CONFIG_ERROR";
|
|
53
|
+
constructor(message: string);
|
|
54
|
+
}
|
|
55
|
+
declare class AuthTransportError extends Error {
|
|
56
|
+
readonly status?: number;
|
|
57
|
+
readonly code = "AUTH_TRANSPORT_ERROR";
|
|
58
|
+
constructor(message: string, status?: number);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a session in a Next.js Server Component, Route Handler, or Server
|
|
62
|
+
* Action. Recovery returns claims for the current render but cannot persist
|
|
63
|
+
* response cookies during Server Component rendering; middleware performs that
|
|
64
|
+
* persistence for protected navigation.
|
|
65
|
+
*/
|
|
66
|
+
declare function getSession(config?: GetSessionConfig): Promise<ServerSession | null>;
|
|
67
|
+
|
|
68
|
+
export { AuthConfigError as A, type GetSessionConfig as G, NoSessionError as N, type ServerSession as S, AuthTransportError as a, getSession as g };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type SessionRecoveryFailureReason = 'invalid-cookie-name' | 'invalid-refresh-cookie' | 'invalid-endpoint' | 'fetch-error' | 'http-status' | 'missing-set-cookie' | 'session-cookie-parse' | 'session-cookie-hmac' | 'session-cookie-payload';
|
|
2
|
+
interface SessionRecoveryErrorDetails {
|
|
3
|
+
name: string;
|
|
4
|
+
message: string;
|
|
5
|
+
code?: string;
|
|
6
|
+
cause?: {
|
|
7
|
+
name: string;
|
|
8
|
+
message: string;
|
|
9
|
+
code?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
interface SessionRecoveryFailure {
|
|
13
|
+
reason: SessionRecoveryFailureReason;
|
|
14
|
+
httpStatus?: number;
|
|
15
|
+
error?: SessionRecoveryErrorDetails;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { SessionRecoveryFailure as S, SessionRecoveryErrorDetails as a, SessionRecoveryFailureReason as b };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User data from the auth server
|
|
3
|
+
*/
|
|
4
|
+
interface AuthUser {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
role?: string | null;
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Full auth state snapshot
|
|
14
|
+
*/
|
|
15
|
+
interface AuthState {
|
|
16
|
+
user: AuthUser | null;
|
|
17
|
+
accessToken: string | null;
|
|
18
|
+
isAuthenticated: boolean;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
roles: string[];
|
|
21
|
+
permissions: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decoded JWT payload (client-side, no verification)
|
|
25
|
+
*/
|
|
26
|
+
interface DecodedToken {
|
|
27
|
+
userId: string;
|
|
28
|
+
jti?: string;
|
|
29
|
+
sessionVersion?: number;
|
|
30
|
+
roles?: string[];
|
|
31
|
+
permissions?: string[];
|
|
32
|
+
exp?: number;
|
|
33
|
+
iat?: number;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Server response envelope
|
|
38
|
+
*/
|
|
39
|
+
interface ServerResponse<T = unknown> {
|
|
40
|
+
data: T;
|
|
41
|
+
message?: string;
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Token pair from login/refresh (internal — refresh token is httpOnly cookie)
|
|
46
|
+
*/
|
|
47
|
+
interface TokenPair {
|
|
48
|
+
accessToken: string;
|
|
49
|
+
refreshToken?: string;
|
|
50
|
+
accessTokenExpiresAt?: number;
|
|
51
|
+
refreshTokenExpiresAt?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Login credentials. `identifier` accepts an email address or a phone number;
|
|
55
|
+
* `email` stays supported for existing callers.
|
|
56
|
+
*/
|
|
57
|
+
interface LoginCredentials {
|
|
58
|
+
identifier?: string;
|
|
59
|
+
email?: string;
|
|
60
|
+
password: string;
|
|
61
|
+
/** Persist the auth cookies past the browser closing. */
|
|
62
|
+
rememberMe?: boolean;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
/** The account must replace its credential before it gets a session. */
|
|
66
|
+
interface CredentialSetupPending {
|
|
67
|
+
nextStep: 'credential_setup';
|
|
68
|
+
setupRequired: true;
|
|
69
|
+
purpose: string;
|
|
70
|
+
expiresAt: string;
|
|
71
|
+
}
|
|
72
|
+
interface AuthenticatedLogin {
|
|
73
|
+
nextStep: 'authenticated';
|
|
74
|
+
user: AuthUser;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Login answer. Branch on `nextStep`: `credential_setup` carries no tokens and
|
|
78
|
+
* leaves the client unauthenticated.
|
|
79
|
+
*/
|
|
80
|
+
type LoginResult = AuthenticatedLogin | CredentialSetupPending;
|
|
81
|
+
/**
|
|
82
|
+
* Retry configuration
|
|
83
|
+
*/
|
|
84
|
+
interface RetryConfig {
|
|
85
|
+
maxRetries?: number;
|
|
86
|
+
backoff?: 'exponential' | 'linear';
|
|
87
|
+
baseDelay?: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Auth client configuration
|
|
91
|
+
*/
|
|
92
|
+
interface AuthClientConfig {
|
|
93
|
+
/** API base URL (e.g., '/api' or 'https://api.example.com') */
|
|
94
|
+
baseURL: string;
|
|
95
|
+
/** Auth endpoints prefix (default: '/auth') */
|
|
96
|
+
authPrefix?: string;
|
|
97
|
+
/** Proactive refresh at this fraction of token lifetime (default: 0.8) */
|
|
98
|
+
refreshThreshold?: number;
|
|
99
|
+
/** Enable multi-tab sync via BroadcastChannel (default: true) */
|
|
100
|
+
tabSync?: boolean;
|
|
101
|
+
/** BroadcastChannel name (default: 'najm-auth') */
|
|
102
|
+
channelName?: string;
|
|
103
|
+
/** Network retry configuration */
|
|
104
|
+
retry?: RetryConfig;
|
|
105
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
106
|
+
timeout?: number;
|
|
107
|
+
}
|
|
108
|
+
type OAuthProvider = 'google';
|
|
109
|
+
interface OAuthLoginOptions {
|
|
110
|
+
/** Same-origin frontend path after OAuth completes. */
|
|
111
|
+
returnTo?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Auth event types
|
|
115
|
+
*/
|
|
116
|
+
interface AuthEventMap {
|
|
117
|
+
login: AuthUser;
|
|
118
|
+
logout: null;
|
|
119
|
+
/** Emitted when server-side logout invalidation fails (state was already cleared) */
|
|
120
|
+
logoutError: unknown;
|
|
121
|
+
tokenRefresh: null;
|
|
122
|
+
sessionExpired: null;
|
|
123
|
+
stateChange: AuthState;
|
|
124
|
+
userUpdated: AuthUser;
|
|
125
|
+
}
|
|
126
|
+
type AuthEvent = keyof AuthEventMap;
|
|
127
|
+
type AuthEventHandler<K extends AuthEvent = AuthEvent> = (data: AuthEventMap[K]) => void;
|
|
128
|
+
/**
|
|
129
|
+
* Tab sync message types
|
|
130
|
+
*/
|
|
131
|
+
type TabSyncMessage = {
|
|
132
|
+
type: 'logout';
|
|
133
|
+
} | {
|
|
134
|
+
type: 'sync';
|
|
135
|
+
state: SyncPayload;
|
|
136
|
+
};
|
|
137
|
+
interface SyncPayload {
|
|
138
|
+
accessToken: string | null;
|
|
139
|
+
user: AuthUser | null;
|
|
140
|
+
roles: string[];
|
|
141
|
+
permissions: string[];
|
|
142
|
+
isAuthenticated: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* FetchClient request options
|
|
146
|
+
*/
|
|
147
|
+
interface RequestOptions {
|
|
148
|
+
body?: unknown;
|
|
149
|
+
headers?: Record<string, string>;
|
|
150
|
+
signal?: AbortSignal;
|
|
151
|
+
timeout?: number;
|
|
152
|
+
/** Skip auth header attachment (for public endpoints like /login, /register) */
|
|
153
|
+
skipAuth?: boolean;
|
|
154
|
+
/** @internal Prevents 401-refresh loop after a single retry */
|
|
155
|
+
_retried?: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Auth error thrown by the client
|
|
159
|
+
*/
|
|
160
|
+
declare class AuthError extends Error {
|
|
161
|
+
status: number;
|
|
162
|
+
body?: unknown;
|
|
163
|
+
constructor(status: number, message: string, body?: unknown);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { type AuthUser as A, type CredentialSetupPending as C, type DecodedToken as D, type LoginCredentials as L, type OAuthProvider as O, type RetryConfig as R, type SyncPayload as S, type TabSyncMessage as T, type RequestOptions as a, type AuthClientConfig as b, type LoginResult as c, type OAuthLoginOptions as d, type AuthState as e, type AuthEvent as f, type AuthEventHandler as g, AuthError as h, type AuthEventMap as i, type AuthenticatedLogin as j, type ServerResponse as k, type TokenPair as l };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "najm-auth",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Authentication and authorization library for najm framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
"import": "./dist/client/server/index.js",
|
|
35
35
|
"default": "./dist/client/server/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./client/server/react": {
|
|
38
|
+
"types": "./dist/client/server/react.d.ts",
|
|
39
|
+
"react-server": "./dist/client/server/react.js",
|
|
40
|
+
"browser": "./dist/client/server/reactClientGuard.js",
|
|
41
|
+
"import": "./dist/client/server/react.js",
|
|
42
|
+
"default": "./dist/client/server/react.js"
|
|
43
|
+
},
|
|
37
44
|
"./identity/ma": {
|
|
38
45
|
"types": "./dist/identity/ma.d.ts",
|
|
39
46
|
"import": "./dist/identity/ma.js",
|
|
@@ -52,7 +59,8 @@
|
|
|
52
59
|
},
|
|
53
60
|
"scripts": {
|
|
54
61
|
"build": "tsup",
|
|
55
|
-
"test": "bun test",
|
|
62
|
+
"test": "bun test && bun run test:rsc",
|
|
63
|
+
"test:rsc": "bun test --conditions react-server test/react-server-auth.test.ts",
|
|
56
64
|
"test:next16": "bun run build && bun integration/next16-proxy/run.ts",
|
|
57
65
|
"test:watch": "bun test --watch",
|
|
58
66
|
"start": "bun --watch src/main.ts",
|