najm-auth 2.0.14 → 3.0.0

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.
@@ -382,7 +382,7 @@ function requestOriginFromHeaders(headers) {
382
382
  }
383
383
  }
384
384
  async function getSession(config = {}) {
385
- const cookieName = config.cookieName ?? "refreshToken";
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(cookieName)?.value;
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: cookieName,
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(cookieName);
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, cookieName);
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: cookieName,
758
+ refreshCookieName: cookieName2,
759
759
  refreshCookieValue: refreshCookie,
760
760
  sessionCookieName,
761
761
  sessionSecret: secret,
@@ -888,6 +888,10 @@ var TabSync = class {
888
888
  };
889
889
 
890
890
  // src/client/NajmAuthClient.ts
891
+ function isCredentialSetupPending(payload) {
892
+ return typeof payload === "object" && payload !== null && payload.nextStep === "credential_setup";
893
+ }
894
+ __name(isCredentialSetupPending, "isCredentialSetupPending");
891
895
  var INITIAL_STATE = {
892
896
  user: null,
893
897
  accessToken: null,
@@ -939,20 +943,23 @@ var NajmAuthClient = class _NajmAuthClient {
939
943
  // Auth Operations
940
944
  // =========================================================================
941
945
  async login(credentials) {
942
- const res = await this.api.post(
943
- `${this.prefix}/login`,
944
- { body: credentials, skipAuth: true }
945
- );
946
- this.applyTokens(res.data);
947
- if (res.data.user) {
948
- this.state = { ...this.state, user: res.data.user };
946
+ const res = await this.api.post(`${this.prefix}/login`, { body: credentials, skipAuth: true });
947
+ this.resetRefreshFailures();
948
+ const setup = isCredentialSetupPending(res) ? res : isCredentialSetupPending(res.data) ? res.data : null;
949
+ if (setup) {
950
+ return { ...setup };
951
+ }
952
+ const authenticated = res.data;
953
+ this.applyTokens(authenticated);
954
+ if (authenticated.user) {
955
+ this.state = { ...this.state, user: authenticated.user };
949
956
  this.notify();
950
957
  } else {
951
958
  await this.fetchUser();
952
959
  }
953
960
  this.tabSync?.broadcastSync(this.getSyncPayload());
954
961
  this.emit("login", this.state.user);
955
- return this.state.user;
962
+ return { nextStep: "authenticated", user: this.state.user };
956
963
  }
957
964
  async register(data) {
958
965
  const res = await this.api.post(
@@ -1324,10 +1331,11 @@ function defineAuth(authConfig = {}) {
1324
1331
  apiBaseURL = "/api",
1325
1332
  authPrefix = "/auth",
1326
1333
  loginRoute = "/login",
1334
+ forbiddenRoute = "/forbidden",
1327
1335
  publicRoutes = [],
1328
1336
  protectedRoutes = [],
1329
1337
  roleRoutes = {},
1330
- cookieName = "refreshToken",
1338
+ cookieName: cookieName2 = "refreshToken",
1331
1339
  sessionCookieName = "najm.session",
1332
1340
  sessionSecret,
1333
1341
  sessionMaxAge,
@@ -1345,7 +1353,7 @@ function defineAuth(authConfig = {}) {
1345
1353
  const sessionConfig = {
1346
1354
  baseURL: apiBaseURL,
1347
1355
  authPrefix,
1348
- cookieName,
1356
+ cookieName: cookieName2,
1349
1357
  sessionCookieName,
1350
1358
  sessionSecret,
1351
1359
  sessionMaxAge,
@@ -1393,12 +1401,21 @@ function defineAuth(authConfig = {}) {
1393
1401
  throw err;
1394
1402
  }
1395
1403
  }, "requireSession");
1404
+ const requireRole = /* @__PURE__ */ __name(async (roles) => {
1405
+ const session = await requireSession();
1406
+ const held = session.roles ?? (session.user.role ? [session.user.role] : []);
1407
+ if (!held.some((role) => roles.includes(role))) {
1408
+ const { redirect } = await import("next/navigation");
1409
+ redirect(forbiddenRoute);
1410
+ }
1411
+ return session;
1412
+ }, "requireRole");
1396
1413
  const middleware = withAuthMiddleware({
1397
1414
  protectedRoutes,
1398
1415
  publicRoutes,
1399
1416
  loginRoute,
1400
1417
  roleRoutes,
1401
- cookieName,
1418
+ cookieName: cookieName2,
1402
1419
  apiBaseURL,
1403
1420
  authPrefix,
1404
1421
  sessionCookieName,
@@ -1420,7 +1437,7 @@ function defineAuth(authConfig = {}) {
1420
1437
  const userRoles = session.roles ?? (session.user.role ? [session.user.role] : []);
1421
1438
  if (!userRoles.includes(options.role)) {
1422
1439
  const { redirect } = await import("next/navigation");
1423
- redirect(loginRoute);
1440
+ redirect(forbiddenRoute);
1424
1441
  }
1425
1442
  }
1426
1443
  if (options?.permission) {
@@ -1428,7 +1445,7 @@ function defineAuth(authConfig = {}) {
1428
1445
  const perms = session.permissions ?? session.user.permissions ?? [];
1429
1446
  if (!matchPermission2(perms, options.permission)) {
1430
1447
  const { redirect } = await import("next/navigation");
1431
- redirect(loginRoute);
1448
+ redirect(forbiddenRoute);
1432
1449
  }
1433
1450
  }
1434
1451
  return Page({ session, ...props });
@@ -1443,20 +1460,186 @@ function defineAuth(authConfig = {}) {
1443
1460
  },
1444
1461
  getSession: getSession2,
1445
1462
  requireSession,
1463
+ requireRole,
1446
1464
  middleware,
1447
1465
  config: { matcher },
1448
1466
  protect
1449
1467
  };
1450
1468
  }
1451
1469
  __name(defineAuth, "defineAuth");
1470
+
1471
+ // src/client/server/safeRedirect.ts
1472
+ var DEFAULT_BLOCKED_PREFIXES = ["/api", "/login", "/_next"];
1473
+ var ASSET_EXTENSIONS = /\.(?:css|gif|ico|jpe?g|js|json|map|png|svg|webmanifest|webp)$/i;
1474
+ function getSafeRedirectPath(value, options = {}) {
1475
+ const {
1476
+ fallback = "/dashboard",
1477
+ blockedPrefixes = DEFAULT_BLOCKED_PREFIXES
1478
+ } = typeof options === "string" ? { fallback: options } : options;
1479
+ const path = Array.isArray(value) ? value[0] : value;
1480
+ if (!path || !path.startsWith("/") || // Protocol-relative: the browser treats `//host/x` as off-site.
1481
+ path.startsWith("//") || // A backslash is normalized to a forward slash by some browsers, so
1482
+ // `/\evil.test` is another way to spell the case above.
1483
+ path.startsWith("/\\") || blockedPrefixes.some((prefix) => path.startsWith(prefix)) || ASSET_EXTENSIONS.test(path.split("?")[0] ?? path)) {
1484
+ return fallback;
1485
+ }
1486
+ return path;
1487
+ }
1488
+ __name(getSafeRedirectPath, "getSafeRedirectPath");
1489
+
1490
+ // src/client/server/authCookiePersistence.ts
1491
+ var DEFAULTS = {
1492
+ authCookieNames: ["refreshToken", "najm.session"],
1493
+ rememberCookieName: "najm.remember",
1494
+ maxAgeSeconds: 7 * 24 * 60 * 60,
1495
+ loginPaths: ["/api/auth/login"],
1496
+ logoutPaths: ["/api/auth/logout"],
1497
+ refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"],
1498
+ setupCompletionPaths: ["/api/auth/credential-setup/change"]
1499
+ };
1500
+ function isNajmSetupResponse(payload) {
1501
+ if (typeof payload !== "object" || payload === null) return false;
1502
+ const body = payload;
1503
+ if (body.nextStep === "credential_setup") return true;
1504
+ const data = body.data;
1505
+ return typeof data === "object" && data !== null && data.nextStep === "credential_setup";
1506
+ }
1507
+ __name(isNajmSetupResponse, "isNajmSetupResponse");
1508
+ function cookieValue(header, name) {
1509
+ for (const part of header.split(";")) {
1510
+ const separator = part.indexOf("=");
1511
+ if (separator < 0) continue;
1512
+ if (part.slice(0, separator).trim() !== name) continue;
1513
+ return part.slice(separator + 1).trim();
1514
+ }
1515
+ return void 0;
1516
+ }
1517
+ __name(cookieValue, "cookieValue");
1518
+ function cookieName(setCookie) {
1519
+ const separator = setCookie.indexOf("=");
1520
+ return separator < 0 ? "" : setCookie.slice(0, separator).trim();
1521
+ }
1522
+ __name(cookieName, "cookieName");
1523
+ function makeSessionCookie(setCookie, authCookieNames = DEFAULTS.authCookieNames) {
1524
+ if (!authCookieNames.includes(cookieName(setCookie))) return setCookie;
1525
+ return setCookie.split(";").filter((part) => !/^\s*(?:expires|max-age)=/i.test(part)).join(";");
1526
+ }
1527
+ __name(makeSessionCookie, "makeSessionCookie");
1528
+ function isDeletionCookie(setCookie) {
1529
+ if (/^[^=]+=\s*(?:;|$)/.test(setCookie)) return true;
1530
+ if (/(?:^|;)\s*max-age=0(?:;|$)/i.test(setCookie)) return true;
1531
+ const expires = /(?:^|;)\s*expires=([^;]+)/i.exec(setCookie)?.[1];
1532
+ return expires ? new Date(expires).getTime() <= Date.now() : false;
1533
+ }
1534
+ __name(isDeletionCookie, "isDeletionCookie");
1535
+ function rememberCookie(name, mode, secure, maxAgeSeconds) {
1536
+ const attributes = [
1537
+ `${name}=${mode === "persistent" ? "1" : "0"}`,
1538
+ "Path=/",
1539
+ "HttpOnly",
1540
+ "SameSite=Lax"
1541
+ ];
1542
+ if (secure) attributes.push("Secure");
1543
+ if (mode === "persistent") attributes.push(`Max-Age=${maxAgeSeconds}`);
1544
+ return attributes.join("; ");
1545
+ }
1546
+ __name(rememberCookie, "rememberCookie");
1547
+ function clearedRememberCookie(name, secure) {
1548
+ return [
1549
+ `${name}=`,
1550
+ "Path=/",
1551
+ "HttpOnly",
1552
+ "SameSite=Lax",
1553
+ ...secure ? ["Secure"] : [],
1554
+ "Max-Age=0"
1555
+ ].join("; ");
1556
+ }
1557
+ __name(clearedRememberCookie, "clearedRememberCookie");
1558
+ function withAuthCookiePersistence(handler, options = {}) {
1559
+ const {
1560
+ authCookieNames = DEFAULTS.authCookieNames,
1561
+ rememberCookieName = DEFAULTS.rememberCookieName,
1562
+ maxAgeSeconds = DEFAULTS.maxAgeSeconds,
1563
+ loginPaths = DEFAULTS.loginPaths,
1564
+ logoutPaths = DEFAULTS.logoutPaths,
1565
+ refreshPaths = DEFAULTS.refreshPaths,
1566
+ setupCompletionPaths = DEFAULTS.setupCompletionPaths,
1567
+ isSetupResponse
1568
+ } = options;
1569
+ const resolveAction = /* @__PURE__ */ __name(async (request) => {
1570
+ const { pathname } = new URL(request.url);
1571
+ if (loginPaths.includes(pathname)) {
1572
+ const body = await request.clone().json().catch(() => null);
1573
+ return {
1574
+ type: "apply",
1575
+ mode: body?.rememberMe === true ? "persistent" : "session"
1576
+ };
1577
+ }
1578
+ if (logoutPaths.includes(pathname)) return { type: "clear" };
1579
+ if (setupCompletionPaths.includes(pathname)) return { type: "clear" };
1580
+ if (refreshPaths.includes(pathname)) {
1581
+ const remembered = cookieValue(
1582
+ request.headers.get("cookie") ?? "",
1583
+ rememberCookieName
1584
+ );
1585
+ if (remembered === "0") return { type: "apply", mode: "session" };
1586
+ if (remembered === "1") return { type: "apply", mode: "persistent" };
1587
+ }
1588
+ return null;
1589
+ }, "resolveAction");
1590
+ const applyAction = /* @__PURE__ */ __name((response, action, secure) => {
1591
+ const headers = new Headers(response.headers);
1592
+ const setCookies = headers.getSetCookie();
1593
+ headers.delete("set-cookie");
1594
+ for (const setCookie of setCookies) {
1595
+ if (action.type === "setup" && authCookieNames.includes(cookieName(setCookie)) && !isDeletionCookie(setCookie)) {
1596
+ continue;
1597
+ }
1598
+ headers.append(
1599
+ "set-cookie",
1600
+ action.type === "apply" && action.mode === "session" ? makeSessionCookie(setCookie, authCookieNames) : setCookie
1601
+ );
1602
+ }
1603
+ headers.append(
1604
+ "set-cookie",
1605
+ action.type === "clear" || action.type === "setup" ? clearedRememberCookie(rememberCookieName, secure) : rememberCookie(rememberCookieName, action.mode, secure, maxAgeSeconds)
1606
+ );
1607
+ return new Response(response.body, {
1608
+ headers,
1609
+ status: response.status,
1610
+ statusText: response.statusText
1611
+ });
1612
+ }, "applyAction");
1613
+ return async (request) => {
1614
+ let action = await resolveAction(request);
1615
+ const response = await handler(request);
1616
+ if (!response.ok) return response;
1617
+ if (action?.type === "apply" && loginPaths.includes(new URL(request.url).pathname)) {
1618
+ const payload = await response.clone().json().catch(() => null);
1619
+ if (isNajmSetupResponse(payload) || isSetupResponse?.(payload)) {
1620
+ action = { type: "setup" };
1621
+ }
1622
+ }
1623
+ if (!action) return response;
1624
+ return applyAction(
1625
+ response,
1626
+ action,
1627
+ new URL(request.url).protocol === "https:"
1628
+ );
1629
+ };
1630
+ }
1631
+ __name(withAuthCookiePersistence, "withAuthCookiePersistence");
1452
1632
  export {
1453
1633
  AuthConfigError,
1454
1634
  AuthTransportError,
1455
1635
  NoSessionError,
1456
1636
  createServerClient,
1457
1637
  defineAuth,
1638
+ getSafeRedirectPath,
1458
1639
  getServerSession,
1459
1640
  getSession,
1641
+ makeSessionCookie,
1460
1642
  withAuth,
1643
+ withAuthCookiePersistence,
1461
1644
  withAuthMiddleware
1462
1645
  };
@@ -0,0 +1 @@
1
+ export { M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, T as TemporaryCredentialInput, i as isMoroccanCin, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone } from '../ma-sNHnUGLO.js';
@@ -0,0 +1,64 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/identity/presets.ts
5
+ var compactPhone = /* @__PURE__ */ __name((value) => value.trim().replace(/[\s().-]+/g, ""), "compactPhone");
6
+ var MOROCCO_LOCAL = /^0\d{9}$/;
7
+ var MOROCCO_NATIONAL = /^212\d{9}$/;
8
+ var MOROCCO_E164 = /^\+212\d{9}$/;
9
+ var normalizeMoroccanPhone = /* @__PURE__ */ __name((value) => {
10
+ const compact = compactPhone(value);
11
+ if (MOROCCO_LOCAL.test(compact))
12
+ return `+212${compact.slice(1)}`;
13
+ if (MOROCCO_NATIONAL.test(compact))
14
+ return `+${compact}`;
15
+ if (MOROCCO_E164.test(compact))
16
+ return compact;
17
+ return null;
18
+ }, "normalizeMoroccanPhone");
19
+ var moroccoIdentityPreset = {
20
+ name: "ma",
21
+ normalize: normalizeMoroccanPhone
22
+ };
23
+
24
+ // src/identity/temporaryCredential.ts
25
+ var EXACT_TEMPORARY_CREDENTIAL_KIND = "exact";
26
+ var exactKind = {
27
+ name: EXACT_TEMPORARY_CREDENTIAL_KIND,
28
+ normalize: /* @__PURE__ */ __name((value) => value, "normalize")
29
+ };
30
+ var MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
31
+ var MOROCCAN_CIN = /^[a-z]{1,3}\d{5,17}$/i;
32
+ function isMoroccanCin(value) {
33
+ const trimmed = value.trim();
34
+ return trimmed.length >= 8 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
35
+ }
36
+ __name(isMoroccanCin, "isMoroccanCin");
37
+ function normalizeMoroccanCin(value) {
38
+ return isMoroccanCin(value) ? value.trim().toLowerCase() : value;
39
+ }
40
+ __name(normalizeMoroccanCin, "normalizeMoroccanCin");
41
+ var moroccanCinKind = {
42
+ name: MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND,
43
+ normalize: normalizeMoroccanCin,
44
+ isTemporaryShape: isMoroccanCin
45
+ };
46
+ var KINDS = /* @__PURE__ */ new Map([
47
+ [exactKind.name, exactKind],
48
+ [moroccanCinKind.name, moroccanCinKind]
49
+ ]);
50
+ function moroccanCinTemporaryCredential(value) {
51
+ if (!isMoroccanCin(value)) {
52
+ throw new Error("moroccanCinTemporaryCredential requires a valid Moroccan CIN");
53
+ }
54
+ return { kind: MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, value };
55
+ }
56
+ __name(moroccanCinTemporaryCredential, "moroccanCinTemporaryCredential");
57
+ export {
58
+ MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND,
59
+ isMoroccanCin,
60
+ moroccanCinTemporaryCredential,
61
+ moroccoIdentityPreset,
62
+ normalizeMoroccanCin,
63
+ normalizeMoroccanPhone
64
+ };