@mesob/auth-hono 0.5.0 → 0.5.3

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/index.js CHANGED
@@ -327,6 +327,232 @@ var createDatabase = (connectionString) => {
327
327
  import { OpenAPIHono as OpenAPIHono17 } from "@hono/zod-openapi";
328
328
  import { getCookie as getCookie3 } from "hono/cookie";
329
329
 
330
+ // src/lib/cookie.ts
331
+ import { deleteCookie as honoDel, setCookie as honoSet } from "hono/cookie";
332
+ var isProduction = process.env.NODE_ENV === "production";
333
+ var getSessionKeyNamespace = (config) => {
334
+ const p = config.prefix?.trim();
335
+ return p && p.length > 0 ? p : "msb";
336
+ };
337
+ var getSessionCookieName = (config) => {
338
+ const prefix = config.prefix?.trim() || "";
339
+ const baseName = "session_token";
340
+ if (prefix) {
341
+ return `${prefix}_${baseName}`;
342
+ }
343
+ return isProduction ? "__Host-session_token" : baseName;
344
+ };
345
+ var setSessionCookie = (c, token, config, options) => {
346
+ const cookieName = getSessionCookieName(config);
347
+ const cookieOptions = {
348
+ httpOnly: true,
349
+ secure: isProduction,
350
+ sameSite: "Lax",
351
+ path: options.path || "/",
352
+ expires: options.expires,
353
+ ...isProduction && { domain: void 0 }
354
+ // __Host- requires no domain
355
+ };
356
+ honoSet(c, cookieName, token, cookieOptions);
357
+ };
358
+ var deleteSessionCookie = (c, config) => {
359
+ const cookieName = getSessionCookieName(config);
360
+ honoDel(c, cookieName, {
361
+ httpOnly: true,
362
+ secure: isProduction,
363
+ sameSite: "Lax",
364
+ path: "/",
365
+ expires: /* @__PURE__ */ new Date(0)
366
+ });
367
+ };
368
+
369
+ // src/lib/crypto.ts
370
+ import { scrypt } from "@noble/hashes/scrypt.js";
371
+ import { randomBytes } from "@noble/hashes/utils.js";
372
+ var encoder = new TextEncoder();
373
+ var randomHex = (bytes) => {
374
+ const arr = randomBytes(bytes);
375
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join(
376
+ ""
377
+ );
378
+ };
379
+ var toHex = (buffer) => {
380
+ return Array.from(
381
+ buffer,
382
+ (b) => b.toString(16).padStart(2, "0")
383
+ ).join("");
384
+ };
385
+ var hexToBytes = (hex) => {
386
+ const bytes = new Uint8Array(hex.length / 2);
387
+ for (let i = 0; i < hex.length; i += 2) {
388
+ bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
389
+ }
390
+ return bytes;
391
+ };
392
+ var SCRYPT_KEYLEN = 64;
393
+ var SCRYPT_COST = 16384;
394
+ var SCRYPT_BLOCK_SIZE = 8;
395
+ var SCRYPT_PARALLELISM = 1;
396
+ var hashPassword = async (password) => {
397
+ const salt = randomBytes(16);
398
+ const saltHex = toHex(salt);
399
+ const passwordBytes = encoder.encode(password);
400
+ const derivedKey = await Promise.resolve(
401
+ scrypt(passwordBytes, salt, {
402
+ N: SCRYPT_COST,
403
+ r: SCRYPT_BLOCK_SIZE,
404
+ p: SCRYPT_PARALLELISM,
405
+ dkLen: SCRYPT_KEYLEN
406
+ })
407
+ );
408
+ return `${saltHex}:${toHex(derivedKey)}`;
409
+ };
410
+ var verifyPassword = async (password, hashed) => {
411
+ if (!hashed) {
412
+ return false;
413
+ }
414
+ const [saltHex, keyHex] = hashed.split(":");
415
+ if (!(saltHex && keyHex)) {
416
+ return false;
417
+ }
418
+ const salt = hexToBytes(saltHex);
419
+ const passwordBytes = encoder.encode(password);
420
+ const derivedKey = await Promise.resolve(
421
+ scrypt(passwordBytes, salt, {
422
+ N: SCRYPT_COST,
423
+ r: SCRYPT_BLOCK_SIZE,
424
+ p: SCRYPT_PARALLELISM,
425
+ dkLen: SCRYPT_KEYLEN
426
+ })
427
+ );
428
+ const derived = toHex(derivedKey);
429
+ if (derived.length !== keyHex.length) {
430
+ return false;
431
+ }
432
+ let result = 0;
433
+ for (let i = 0; i < derived.length; i++) {
434
+ result |= derived.charCodeAt(i) ^ keyHex.charCodeAt(i);
435
+ }
436
+ return result === 0;
437
+ };
438
+ var hashToken = async (token, secret) => {
439
+ if (!secret || secret.length === 0) {
440
+ throw new Error(
441
+ "AUTH_SECRET is required and must be a non-empty string. Please set AUTH_SECRET environment variable."
442
+ );
443
+ }
444
+ const key = await crypto.subtle.importKey(
445
+ "raw",
446
+ encoder.encode(secret),
447
+ { name: "HMAC", hash: "SHA-256" },
448
+ false,
449
+ ["sign"]
450
+ );
451
+ const signature = await crypto.subtle.sign(
452
+ "HMAC",
453
+ key,
454
+ encoder.encode(token)
455
+ );
456
+ return toHex(new Uint8Array(signature));
457
+ };
458
+ var generateToken = (bytes = 48) => randomHex(bytes);
459
+
460
+ // src/lib/error-handler.ts
461
+ import { logger } from "@mesob/common";
462
+ import { HTTPException } from "hono/http-exception";
463
+ var isDatabaseError = (error) => {
464
+ if (typeof error !== "object" || error === null) {
465
+ return false;
466
+ }
467
+ if ("code" in error || "query" in error || "detail" in error) {
468
+ return true;
469
+ }
470
+ if (error instanceof Error) {
471
+ const message = error.message.toLowerCase();
472
+ return message.includes("failed query") || message.includes("relation") || message.includes("column") || message.includes("syntax error") || message.includes("duplicate key") || message.includes("foreign key") || message.includes("null value");
473
+ }
474
+ return false;
475
+ };
476
+ var sanitizeDatabaseError = (error) => {
477
+ const code = error.code;
478
+ if (code === "23505") {
479
+ return "Resource already exists";
480
+ }
481
+ if (code === "23503") {
482
+ return "Referenced resource not found";
483
+ }
484
+ if (code === "23502") {
485
+ return "Required field is missing";
486
+ }
487
+ if (code === "42P01") {
488
+ return "Resource not found";
489
+ }
490
+ if (code === "42703") {
491
+ return "Invalid request";
492
+ }
493
+ if (code === "23514") {
494
+ return "Validation failed";
495
+ }
496
+ return "An error occurred while processing your request";
497
+ };
498
+ var isDatabaseErrorMessage = (message) => {
499
+ const lowerMessage = message.toLowerCase();
500
+ return lowerMessage.includes("failed query") || lowerMessage.includes("select") || lowerMessage.includes("insert") || lowerMessage.includes("update") || lowerMessage.includes("delete") || lowerMessage.includes("from") || lowerMessage.includes("where") || lowerMessage.includes("limit") || lowerMessage.includes("params:") || lowerMessage.includes("query") || message.includes('"iam".') || message.includes('"tenants"') || message.includes('"users"') || message.includes('"sessions"') || message.includes('"accounts"') || lowerMessage.includes("relation") || lowerMessage.includes("column") || lowerMessage.includes("syntax error") || lowerMessage.includes("database") || lowerMessage.includes("postgres") || lowerMessage.includes("sql");
501
+ };
502
+ var handleError = (error, c) => {
503
+ logger.error("API Error:", {
504
+ error,
505
+ path: c.req.path,
506
+ method: c.req.method,
507
+ url: c.req.url
508
+ });
509
+ if (error instanceof HTTPException) {
510
+ const message = isDatabaseErrorMessage(error.message) ? "An error occurred while processing your request" : error.message;
511
+ return c.json({ error: message }, error.status);
512
+ }
513
+ if (isDatabaseError(error)) {
514
+ const userMessage = sanitizeDatabaseError(error);
515
+ logger.error("Database error details:", {
516
+ code: error.code,
517
+ message: error.message,
518
+ detail: error.detail,
519
+ query: error.query,
520
+ parameters: error.parameters
521
+ });
522
+ return c.json({ error: userMessage }, 500);
523
+ }
524
+ if (error instanceof Error) {
525
+ const message = error.message;
526
+ const lowerMessage = message.toLowerCase();
527
+ const isDatabaseError2 = lowerMessage.includes("failed query") || lowerMessage.includes("select") || lowerMessage.includes("insert") || lowerMessage.includes("update") || lowerMessage.includes("delete") || lowerMessage.includes("from") || lowerMessage.includes("where") || lowerMessage.includes("limit") || lowerMessage.includes("params:") || lowerMessage.includes("query") || message.includes('"iam".') || message.includes('"tenants"') || message.includes('"users"') || message.includes('"sessions"') || message.includes('"accounts"') || lowerMessage.includes("relation") || lowerMessage.includes("column") || lowerMessage.includes("syntax error") || lowerMessage.includes("duplicate key") || lowerMessage.includes("foreign key") || lowerMessage.includes("null value") || lowerMessage.includes("database") || lowerMessage.includes("postgres") || lowerMessage.includes("sql");
528
+ if (isDatabaseError2) {
529
+ logger.error("SQL/database error detected:", {
530
+ message: error.message,
531
+ stack: error.stack,
532
+ name: error.name
533
+ });
534
+ return c.json(
535
+ { error: "An error occurred while processing your request" },
536
+ 500
537
+ );
538
+ }
539
+ logger.error("Error details:", {
540
+ message: error.message,
541
+ stack: error.stack,
542
+ name: error.name
543
+ });
544
+ return c.json(
545
+ { error: "An error occurred while processing your request" },
546
+ 500
547
+ );
548
+ }
549
+ logger.error("Unknown error:", error);
550
+ return c.json(
551
+ { error: "An error occurred while processing your request" },
552
+ 500
553
+ );
554
+ };
555
+
330
556
  // src/db/orm/session.ts
331
557
  import { and, eq, gt } from "drizzle-orm";
332
558
  var fetchSessionByToken = async ({
@@ -384,6 +610,19 @@ var deleteSession = async ({
384
610
  )
385
611
  );
386
612
  };
613
+ var listHashedTokensForUser = async ({
614
+ database,
615
+ userId,
616
+ tenantId
617
+ }) => {
618
+ const rows = await database.select({ token: sessionsInIam.token }).from(sessionsInIam).where(
619
+ and(
620
+ eq(sessionsInIam.userId, userId),
621
+ eq(sessionsInIam.tenantId, tenantId)
622
+ )
623
+ );
624
+ return rows.map((r) => r.token);
625
+ };
387
626
 
388
627
  // src/db/orm/tenant.ts
389
628
  import { and as and2, eq as eq2, sql as sql2 } from "drizzle-orm";
@@ -492,226 +731,335 @@ var fetchUserWithRoles = async ({
492
731
  return userResult || null;
493
732
  };
494
733
 
495
- // src/lib/cookie.ts
496
- import { deleteCookie as honoDel, setCookie as honoSet } from "hono/cookie";
497
- var isProduction = process.env.NODE_ENV === "production";
498
- var getSessionCookieName = (config) => {
499
- const prefix = config.cookie?.prefix || "";
500
- const baseName = "session_token";
501
- if (prefix) {
502
- return `${prefix}_${baseName}`;
734
+ // src/lib/session-cache.ts
735
+ var sessionCacheKey = (config, hashedToken) => `${getSessionKeyNamespace(config)}:sc:v1:${hashedToken}`;
736
+ async function readSessionCache(config, hashedToken) {
737
+ const sc = config.sessionCache;
738
+ if (!sc?.enabled) {
739
+ return null;
503
740
  }
504
- return isProduction ? "__Host-session_token" : baseName;
505
- };
506
- var setSessionCookie = (c, token, config, options) => {
507
- const cookieName = getSessionCookieName(config);
508
- const cookieOptions = {
509
- httpOnly: true,
510
- secure: isProduction,
511
- sameSite: "Lax",
512
- path: options.path || "/",
513
- expires: options.expires,
514
- ...isProduction && { domain: void 0 }
515
- // __Host- requires no domain
516
- };
517
- honoSet(c, cookieName, token, cookieOptions);
518
- };
519
- var deleteSessionCookie = (c, config) => {
520
- const cookieName = getSessionCookieName(config);
521
- honoDel(c, cookieName, {
522
- httpOnly: true,
523
- secure: isProduction,
524
- sameSite: "Lax",
525
- path: "/",
526
- expires: /* @__PURE__ */ new Date(0)
527
- });
528
- };
529
-
530
- // src/lib/crypto.ts
531
- import { scrypt } from "@noble/hashes/scrypt.js";
532
- import { randomBytes } from "@noble/hashes/utils.js";
533
- var encoder = new TextEncoder();
534
- var randomHex = (bytes) => {
535
- const arr = randomBytes(bytes);
536
- return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join(
537
- ""
741
+ const raw = await sc.kv.get(sessionCacheKey(config, hashedToken));
742
+ if (!raw) {
743
+ return null;
744
+ }
745
+ try {
746
+ const parsed = JSON.parse(raw);
747
+ if (!parsed?.session?.expiresAt || new Date(parsed.session.expiresAt) <= /* @__PURE__ */ new Date()) {
748
+ await sc.kv.delete(sessionCacheKey(config, hashedToken));
749
+ return null;
750
+ }
751
+ return parsed;
752
+ } catch {
753
+ await sc.kv.delete(sessionCacheKey(config, hashedToken));
754
+ return null;
755
+ }
756
+ }
757
+ async function writeSessionCache(config, hashedToken, payload) {
758
+ const sc = config.sessionCache;
759
+ if (!sc?.enabled) {
760
+ return;
761
+ }
762
+ const ttl = sc.ttlSeconds ?? Math.max(
763
+ 60,
764
+ Math.ceil(
765
+ (new Date(payload.session.expiresAt).getTime() - Date.now()) / 1e3
766
+ )
538
767
  );
539
- };
540
- var toHex = (buffer) => {
541
- return Array.from(
542
- buffer,
543
- (b) => b.toString(16).padStart(2, "0")
544
- ).join("");
545
- };
546
- var hexToBytes = (hex) => {
547
- const bytes = new Uint8Array(hex.length / 2);
548
- for (let i = 0; i < hex.length; i += 2) {
549
- bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
768
+ await sc.kv.put(
769
+ sessionCacheKey(config, hashedToken),
770
+ JSON.stringify(payload),
771
+ {
772
+ expirationTtl: Math.min(Math.max(ttl, 60), 2147483647)
773
+ }
774
+ );
775
+ }
776
+ async function deleteSessionCacheKeys(config, hashedTokens) {
777
+ const sc = config.sessionCache;
778
+ if (!sc?.enabled || hashedTokens.length === 0) {
779
+ return;
550
780
  }
551
- return bytes;
552
- };
553
- var SCRYPT_KEYLEN = 64;
554
- var SCRYPT_COST = 16384;
555
- var SCRYPT_BLOCK_SIZE = 8;
556
- var SCRYPT_PARALLELISM = 1;
557
- var hashPassword = async (password) => {
558
- const salt = randomBytes(16);
559
- const saltHex = toHex(salt);
560
- const passwordBytes = encoder.encode(password);
561
- const derivedKey = await Promise.resolve(
562
- scrypt(passwordBytes, salt, {
563
- N: SCRYPT_COST,
564
- r: SCRYPT_BLOCK_SIZE,
565
- p: SCRYPT_PARALLELISM,
566
- dkLen: SCRYPT_KEYLEN
567
- })
781
+ await Promise.all(
782
+ hashedTokens.map((t) => sc.kv.delete(sessionCacheKey(config, t)))
568
783
  );
569
- return `${saltHex}:${toHex(derivedKey)}`;
570
- };
571
- var verifyPassword = async (password, hashed) => {
572
- if (!hashed) {
573
- return false;
784
+ }
785
+ async function invalidateSessionCacheForHashedToken(config, hashedToken) {
786
+ await deleteSessionCacheKeys(config, [hashedToken]);
787
+ }
788
+ async function invalidateSessionCacheForUser(config, database, userId, tenantId) {
789
+ const tokens = await listHashedTokensForUser({
790
+ database,
791
+ userId,
792
+ tenantId
793
+ });
794
+ await deleteSessionCacheKeys(config, tokens);
795
+ }
796
+
797
+ // src/lib/load-session-pair.ts
798
+ async function loadSessionPair(database, config, hashedToken) {
799
+ const cached = await readSessionCache(config, hashedToken);
800
+ if (cached) {
801
+ return { session: cached.session, user: cached.user };
802
+ }
803
+ const session = await fetchSessionByToken({
804
+ database,
805
+ hashedToken,
806
+ tenantId: config.tenant?.tenantId || "tenant"
807
+ });
808
+ if (!session) {
809
+ return null;
810
+ }
811
+ const user = await fetchUserWithRoles({
812
+ database,
813
+ userId: session.userId,
814
+ tenantId: session.tenantId
815
+ });
816
+ if (!user) {
817
+ await deleteSession({
818
+ database,
819
+ sessionId: session.id,
820
+ tenantId: session.tenantId
821
+ });
822
+ return null;
823
+ }
824
+ await writeSessionCache(config, hashedToken, { session, user });
825
+ return { session, user };
826
+ }
827
+
828
+ // src/lib/normalize-rate-limit-ip.ts
829
+ function parseIpv4Dotted(s) {
830
+ const p = s.split(".");
831
+ if (p.length !== 4) {
832
+ return null;
833
+ }
834
+ const b = new Uint8Array(4);
835
+ for (let i = 0; i < 4; i++) {
836
+ if (!/^\d{1,3}$/.test(p[i])) {
837
+ return null;
838
+ }
839
+ const n = Number(p[i]);
840
+ if (!Number.isInteger(n) || n < 0 || n > 255) {
841
+ return null;
842
+ }
843
+ b[i] = n;
844
+ }
845
+ return b;
846
+ }
847
+ function ipv4Key(bytes) {
848
+ return `4:${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`;
849
+ }
850
+ function maskIpv6InPlace(bytes, prefixBits) {
851
+ if (prefixBits >= 128) {
852
+ return;
574
853
  }
575
- const [saltHex, keyHex] = hashed.split(":");
576
- if (!(saltHex && keyHex)) {
577
- return false;
854
+ const whole = Math.floor(prefixBits / 8);
855
+ const rem = prefixBits % 8;
856
+ if (rem === 0) {
857
+ for (let i = whole; i < 16; i++) {
858
+ bytes[i] = 0;
859
+ }
860
+ } else {
861
+ for (let i = whole + 1; i < 16; i++) {
862
+ bytes[i] = 0;
863
+ }
864
+ const keep = 255 << 8 - rem & 255;
865
+ bytes[whole] &= keep;
578
866
  }
579
- const salt = hexToBytes(saltHex);
580
- const passwordBytes = encoder.encode(password);
581
- const derivedKey = await Promise.resolve(
582
- scrypt(passwordBytes, salt, {
583
- N: SCRYPT_COST,
584
- r: SCRYPT_BLOCK_SIZE,
585
- p: SCRYPT_PARALLELISM,
586
- dkLen: SCRYPT_KEYLEN
587
- })
588
- );
589
- const derived = toHex(derivedKey);
590
- if (derived.length !== keyHex.length) {
591
- return false;
867
+ }
868
+ function isIpv4MappedIpv6(bytes) {
869
+ for (let i = 0; i < 10; i++) {
870
+ if (bytes[i] !== 0) {
871
+ return false;
872
+ }
592
873
  }
593
- let result = 0;
594
- for (let i = 0; i < derived.length; i++) {
595
- result |= derived.charCodeAt(i) ^ keyHex.charCodeAt(i);
874
+ return bytes[10] === 255 && bytes[11] === 255;
875
+ }
876
+ function parseIpv6HextetsToBytes(s) {
877
+ const buf = new Uint8Array(16);
878
+ if (s === "") {
879
+ return buf;
596
880
  }
597
- return result === 0;
598
- };
599
- var hashToken = async (token, secret) => {
600
- if (!secret || secret.length === 0) {
601
- throw new Error(
602
- "AUTH_SECRET is required and must be a non-empty string. Please set AUTH_SECRET environment variable."
603
- );
881
+ const double = s.includes("::");
882
+ if (double && s.split("::").length > 2) {
883
+ return null;
604
884
  }
605
- const key = await crypto.subtle.importKey(
606
- "raw",
607
- encoder.encode(secret),
608
- { name: "HMAC", hash: "SHA-256" },
609
- false,
610
- ["sign"]
611
- );
612
- const signature = await crypto.subtle.sign(
613
- "HMAC",
614
- key,
615
- encoder.encode(token)
616
- );
617
- return toHex(new Uint8Array(signature));
618
- };
619
- var generateToken = (bytes = 48) => randomHex(bytes);
620
-
621
- // src/lib/error-handler.ts
622
- import { logger } from "@mesob/common";
623
- import { HTTPException } from "hono/http-exception";
624
- var isDatabaseError = (error) => {
625
- if (typeof error !== "object" || error === null) {
626
- return false;
885
+ const [leftRaw, rightRaw] = double ? s.split("::", 2) : [s, ""];
886
+ const left = leftRaw ? leftRaw.split(":").filter(Boolean) : [];
887
+ const right = rightRaw ? rightRaw.split(":").filter(Boolean) : [];
888
+ const partsLen = left.length + right.length;
889
+ if (double) {
890
+ if (partsLen > 8) {
891
+ return null;
892
+ }
893
+ } else if (partsLen !== 8) {
894
+ return null;
627
895
  }
628
- if ("code" in error || "query" in error || "detail" in error) {
629
- return true;
896
+ const pad = double ? 8 - partsLen : 0;
897
+ const all = [...left, ...Array(pad).fill("0"), ...right];
898
+ if (all.length !== 8) {
899
+ return null;
630
900
  }
631
- if (error instanceof Error) {
632
- const message = error.message.toLowerCase();
633
- return message.includes("failed query") || message.includes("relation") || message.includes("column") || message.includes("syntax error") || message.includes("duplicate key") || message.includes("foreign key") || message.includes("null value");
901
+ for (let i = 0; i < 8; i++) {
902
+ const h = all[i];
903
+ if (!h || h.includes(".")) {
904
+ return null;
905
+ }
906
+ const v = Number.parseInt(h, 16);
907
+ if (v > 65535 || Number.isNaN(v)) {
908
+ return null;
909
+ }
910
+ buf[i * 2] = v >> 8;
911
+ buf[i * 2 + 1] = v & 255;
634
912
  }
635
- return false;
636
- };
637
- var sanitizeDatabaseError = (error) => {
638
- const code = error.code;
639
- if (code === "23505") {
640
- return "Resource already exists";
913
+ return buf;
914
+ }
915
+ function parseIpv6ToBytes(address) {
916
+ let a = address.trim().toLowerCase();
917
+ if (a.startsWith("[") && a.endsWith("]")) {
918
+ a = a.slice(1, -1);
641
919
  }
642
- if (code === "23503") {
643
- return "Referenced resource not found";
920
+ const zi = a.indexOf("%");
921
+ if (zi >= 0) {
922
+ a = a.slice(0, zi);
644
923
  }
645
- if (code === "23502") {
646
- return "Required field is missing";
924
+ if (a === "") {
925
+ return null;
647
926
  }
648
- if (code === "42P01") {
649
- return "Resource not found";
927
+ if (!a.includes(":")) {
928
+ const v4 = parseIpv4Dotted(a);
929
+ return v4 ? v4 : null;
930
+ }
931
+ const lastColon = a.lastIndexOf(":");
932
+ if (lastColon > 0) {
933
+ const maybeV4 = a.slice(lastColon + 1);
934
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(maybeV4)) {
935
+ const v4b = parseIpv4Dotted(maybeV4);
936
+ if (!v4b) {
937
+ return null;
938
+ }
939
+ const prefix = a.slice(0, lastColon);
940
+ const w1 = v4b[0] << 8 | v4b[1];
941
+ const w2 = v4b[2] << 8 | v4b[3];
942
+ const tailHex = `${w1.toString(16)}:${w2.toString(16)}`;
943
+ let merged;
944
+ if (!prefix) {
945
+ merged = tailHex;
946
+ } else if (prefix.endsWith(":")) {
947
+ merged = `${prefix}${tailHex}`;
948
+ } else {
949
+ merged = `${prefix}:${tailHex}`;
950
+ }
951
+ return parseIpv6HextetsToBytes(merged);
952
+ }
650
953
  }
651
- if (code === "42703") {
652
- return "Invalid request";
954
+ return parseIpv6HextetsToBytes(a);
955
+ }
956
+ function bytesToHex(bytes) {
957
+ let o = "";
958
+ for (let i = 0; i < bytes.length; i++) {
959
+ o += bytes[i].toString(16).padStart(2, "0");
653
960
  }
654
- if (code === "23514") {
655
- return "Validation failed";
961
+ return o;
962
+ }
963
+ function rateLimitClientKey(raw, ipv6SubnetBits) {
964
+ const s = raw.trim();
965
+ if (!s || s === "unknown") {
966
+ return "unknown";
656
967
  }
657
- return "An error occurred while processing your request";
658
- };
659
- var isDatabaseErrorMessage = (message) => {
660
- const lowerMessage = message.toLowerCase();
661
- return lowerMessage.includes("failed query") || lowerMessage.includes("select") || lowerMessage.includes("insert") || lowerMessage.includes("update") || lowerMessage.includes("delete") || lowerMessage.includes("from") || lowerMessage.includes("where") || lowerMessage.includes("limit") || lowerMessage.includes("params:") || lowerMessage.includes("query") || message.includes('"iam".') || message.includes('"tenants"') || message.includes('"users"') || message.includes('"sessions"') || message.includes('"accounts"') || lowerMessage.includes("relation") || lowerMessage.includes("column") || lowerMessage.includes("syntax error") || lowerMessage.includes("database") || lowerMessage.includes("postgres") || lowerMessage.includes("sql");
662
- };
663
- var handleError = (error, c) => {
664
- logger.error("API Error:", {
665
- error,
666
- path: c.req.path,
667
- method: c.req.method,
668
- url: c.req.url
669
- });
670
- if (error instanceof HTTPException) {
671
- const message = isDatabaseErrorMessage(error.message) ? "An error occurred while processing your request" : error.message;
672
- return c.json({ error: message }, error.status);
968
+ const prefixBits = Number.isFinite(ipv6SubnetBits) && ipv6SubnetBits >= 1 && ipv6SubnetBits <= 128 ? Math.floor(ipv6SubnetBits) : 64;
969
+ if (!s.includes(":")) {
970
+ const v4 = parseIpv4Dotted(s);
971
+ return v4 ? ipv4Key(v4) : `raw:${s}`;
673
972
  }
674
- if (isDatabaseError(error)) {
675
- const userMessage = sanitizeDatabaseError(error);
676
- logger.error("Database error details:", {
677
- code: error.code,
678
- message: error.message,
679
- detail: error.detail,
680
- query: error.query,
681
- parameters: error.parameters
682
- });
683
- return c.json({ error: userMessage }, 500);
973
+ const bytes = parseIpv6ToBytes(s);
974
+ if (!bytes) {
975
+ return `raw:${s}`;
684
976
  }
685
- if (error instanceof Error) {
686
- const message = error.message;
687
- const lowerMessage = message.toLowerCase();
688
- const isDatabaseError2 = lowerMessage.includes("failed query") || lowerMessage.includes("select") || lowerMessage.includes("insert") || lowerMessage.includes("update") || lowerMessage.includes("delete") || lowerMessage.includes("from") || lowerMessage.includes("where") || lowerMessage.includes("limit") || lowerMessage.includes("params:") || lowerMessage.includes("query") || message.includes('"iam".') || message.includes('"tenants"') || message.includes('"users"') || message.includes('"sessions"') || message.includes('"accounts"') || lowerMessage.includes("relation") || lowerMessage.includes("column") || lowerMessage.includes("syntax error") || lowerMessage.includes("duplicate key") || lowerMessage.includes("foreign key") || lowerMessage.includes("null value") || lowerMessage.includes("database") || lowerMessage.includes("postgres") || lowerMessage.includes("sql");
689
- if (isDatabaseError2) {
690
- logger.error("SQL/database error detected:", {
691
- message: error.message,
692
- stack: error.stack,
693
- name: error.name
694
- });
977
+ if (isIpv4MappedIpv6(bytes)) {
978
+ return ipv4Key(bytes.subarray(12, 16));
979
+ }
980
+ maskIpv6InPlace(bytes, prefixBits);
981
+ return `6:${bytesToHex(bytes)}`;
982
+ }
983
+
984
+ // src/lib/auth-rate-limit.ts
985
+ var AUTH_RATE_LIMIT_POST_PATHS = /* @__PURE__ */ new Set([
986
+ "/check-account",
987
+ "/sign-in",
988
+ "/sign-up",
989
+ "/password/forgot",
990
+ "/password/reset",
991
+ "/password/set",
992
+ "/email/verification/request",
993
+ "/email/verification/confirm",
994
+ "/phone/verification/request",
995
+ "/phone/verification/confirm"
996
+ ]);
997
+ var DEFAULT_MESSAGE = "Too many requests. Please wait before trying again.";
998
+ var RL_PREFIX = "msb:rl:v1:";
999
+ function getClientIp(c, headerName) {
1000
+ const primary = c.req.header(headerName)?.trim();
1001
+ if (primary) {
1002
+ return primary;
1003
+ }
1004
+ const forwarded = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
1005
+ if (forwarded) {
1006
+ return forwarded;
1007
+ }
1008
+ return c.req.header("x-real-ip")?.trim() || "unknown";
1009
+ }
1010
+ function rateLimitKey(path, windowStart, ip) {
1011
+ const bucket = path.replaceAll(/[^a-z0-9/-]/gi, "_");
1012
+ return `${RL_PREFIX}${bucket}:${ip}:${windowStart}`;
1013
+ }
1014
+ async function checkAuthRateLimit(c, config) {
1015
+ const rl = config.rateLimit;
1016
+ if (!rl?.enabled) {
1017
+ return { limited: false };
1018
+ }
1019
+ if (c.req.method !== "POST") {
1020
+ return { limited: false };
1021
+ }
1022
+ const path = c.req.path;
1023
+ if (!AUTH_RATE_LIMIT_POST_PATHS.has(path)) {
1024
+ return { limited: false };
1025
+ }
1026
+ const rawIp = getClientIp(c, rl.ipHeader ?? "cf-connecting-ip");
1027
+ const ipKey = rateLimitClientKey(rawIp, rl.ipv6Subnet ?? 64);
1028
+ const now = Math.floor(Date.now() / 1e3);
1029
+ const windowStart = Math.floor(now / rl.window) * rl.window;
1030
+ const key = rateLimitKey(path, windowStart, ipKey);
1031
+ const msg = rl.message ?? DEFAULT_MESSAGE;
1032
+ const raw = await rl.kv.get(key);
1033
+ let count = 0;
1034
+ if (raw) {
1035
+ try {
1036
+ const parsed = JSON.parse(raw);
1037
+ count = typeof parsed.n === "number" ? parsed.n : 0;
1038
+ } catch {
1039
+ count = 0;
1040
+ }
1041
+ }
1042
+ if (count >= rl.max) {
1043
+ return { limited: true, message: msg };
1044
+ }
1045
+ await rl.kv.put(key, JSON.stringify({ n: count + 1 }), {
1046
+ expirationTtl: Math.min(rl.window * 2, 2147483647)
1047
+ });
1048
+ return { limited: false };
1049
+ }
1050
+
1051
+ // src/middlewares/auth-rate-limit-middleware.ts
1052
+ var createAuthRateLimitMiddleware = (config) => {
1053
+ return async (c, next) => {
1054
+ const result = await checkAuthRateLimit(c, config);
1055
+ if (result.limited) {
695
1056
  return c.json(
696
- { error: "An error occurred while processing your request" },
697
- 500
1057
+ { error: result.message, code: "RATE_LIMITED" },
1058
+ 429
698
1059
  );
699
1060
  }
700
- logger.error("Error details:", {
701
- message: error.message,
702
- stack: error.stack,
703
- name: error.name
704
- });
705
- return c.json(
706
- { error: "An error occurred while processing your request" },
707
- 500
708
- );
709
- }
710
- logger.error("Unknown error:", error);
711
- return c.json(
712
- { error: "An error occurred while processing your request" },
713
- 500
714
- );
1061
+ await next();
1062
+ };
715
1063
  };
716
1064
 
717
1065
  // src/routes/index.ts
@@ -1283,6 +1631,19 @@ var checkAccountHandler = async (c) => {
1283
1631
  const resolvedTenantId = ensureTenantId(config, tenantId);
1284
1632
  const { username } = body;
1285
1633
  const isEmail = username.includes("@");
1634
+ const disabledChannelResponse = {
1635
+ exists: false,
1636
+ verified: false,
1637
+ hasPassword: false,
1638
+ requiresPasswordSetup: false,
1639
+ account: null
1640
+ };
1641
+ if (isEmail && !config.email.enabled) {
1642
+ return c.json(disabledChannelResponse, 200);
1643
+ }
1644
+ if (!(isEmail || config.phone.enabled)) {
1645
+ return c.json(disabledChannelResponse, 200);
1646
+ }
1286
1647
  const userTypeFilter = sql4`${usersInIam.userType} @> ARRAY[${config.userType}]::text[]`;
1287
1648
  const whereClause = isEmail ? and6(
1288
1649
  eq6(usersInIam.tenantId, resolvedTenantId),
@@ -1818,6 +2179,7 @@ var signOutHandler = async (c) => {
1818
2179
  )
1819
2180
  );
1820
2181
  }
2182
+ await invalidateSessionCacheForHashedToken(config, hashedToken);
1821
2183
  deleteSessionCookie(c, config);
1822
2184
  return c.json({ message: "Signed out" }, 200);
1823
2185
  };
@@ -1846,6 +2208,20 @@ var SignUpError = class extends Error {
1846
2208
  this.status = status;
1847
2209
  }
1848
2210
  };
2211
+ var normalizeSignupDomain = (value) => {
2212
+ return value.trim().toLowerCase().replace(/^@/, "");
2213
+ };
2214
+ var isAllowedSignupEmail = (email, allowedDomains) => {
2215
+ const domain = email.split("@")[1]?.toLowerCase();
2216
+ if (!domain) {
2217
+ return false;
2218
+ }
2219
+ const normalizedDomains = allowedDomains.map(normalizeSignupDomain).filter(Boolean);
2220
+ if (normalizedDomains.length === 0) {
2221
+ return true;
2222
+ }
2223
+ return normalizedDomains.includes(domain);
2224
+ };
1849
2225
  var signUpHandler = async (c) => {
1850
2226
  const body = c.req.valid("json");
1851
2227
  const config = c.get("config");
@@ -1857,7 +2233,19 @@ var signUpHandler = async (c) => {
1857
2233
  if (!identifier) {
1858
2234
  return c.json({ error: "Either email or phone is required" }, 409);
1859
2235
  }
2236
+ if (config.signUp && !config.signUp.enabled) {
2237
+ return c.json({ error: "Sign up is disabled" }, 403);
2238
+ }
1860
2239
  const isEmail = identifier.includes("@");
2240
+ if (isEmail && config.signUp && !config.signUp.emailEnabled) {
2241
+ return c.json({ error: "Email sign up is disabled" }, 403);
2242
+ }
2243
+ if (!isEmail && config.signUp && !config.signUp.phoneEnabled) {
2244
+ return c.json({ error: "Phone sign up is disabled" }, 403);
2245
+ }
2246
+ if (isEmail && config.signUp?.allowedEmailDomains && !isAllowedSignupEmail(identifier, config.signUp.allowedEmailDomains)) {
2247
+ return c.json({ error: "Email domain is not allowed for sign up" }, 403);
2248
+ }
1861
2249
  if (phone) {
1862
2250
  const phoneValidator = createPhoneField(config);
1863
2251
  if (!phoneValidator.validate(phone)) {
@@ -2578,6 +2966,12 @@ var emailVerificationConfirmHandler = async (c) => {
2578
2966
  if (result.status === "error") {
2579
2967
  return c.json({ error: result.error }, 400);
2580
2968
  }
2969
+ await invalidateSessionCacheForUser(
2970
+ config,
2971
+ database,
2972
+ result.user.id,
2973
+ resolvedTenantId
2974
+ );
2581
2975
  setSessionCookie(c, result.sessionToken, config, {
2582
2976
  expires: new Date(result.expiresAt)
2583
2977
  });
@@ -2833,6 +3227,12 @@ var changePasswordHandler = async (c) => {
2833
3227
  eq18(accountsInIam.provider, "credentials")
2834
3228
  )
2835
3229
  );
3230
+ await invalidateSessionCacheForUser(
3231
+ config,
3232
+ database,
3233
+ userId,
3234
+ resolvedTenantId
3235
+ );
2836
3236
  const currentSessionToken = getCookie2(c, getSessionCookieName(config));
2837
3237
  if (currentSessionToken) {
2838
3238
  const hashedToken = await hashToken(currentSessionToken, config.secret);
@@ -3113,6 +3513,12 @@ var resetPasswordHandler = async (c) => {
3113
3513
  eq20(accountsInIam.provider, "credentials")
3114
3514
  )
3115
3515
  );
3516
+ await invalidateSessionCacheForUser(
3517
+ config,
3518
+ database,
3519
+ verification.userId,
3520
+ resolvedTenantId
3521
+ );
3116
3522
  await database.delete(sessionsInIam).where(
3117
3523
  and20(
3118
3524
  eq20(sessionsInIam.tenantId, resolvedTenantId),
@@ -3952,6 +4358,12 @@ var phoneVerificationConfirmHandler = async (c) => {
3952
4358
  if (result.status === "error") {
3953
4359
  return c.json({ error: result.error }, 400);
3954
4360
  }
4361
+ await invalidateSessionCacheForUser(
4362
+ config,
4363
+ database,
4364
+ result.user.id,
4365
+ resolvedTenantId
4366
+ );
3955
4367
  if (!result.session) {
3956
4368
  return c.json(
3957
4369
  {
@@ -3987,6 +4399,14 @@ var phoneVerificationRequestHandler = async (c) => {
3987
4399
  return c.json({ error: "Phone authentication is disabled" }, 400);
3988
4400
  }
3989
4401
  const { phone, context } = body;
4402
+ if (context === "sign-up" && config.signUp) {
4403
+ if (!config.signUp.enabled) {
4404
+ return c.json({ error: "Sign up is disabled" }, 400);
4405
+ }
4406
+ if (!config.signUp.phoneEnabled) {
4407
+ return c.json({ error: "Phone sign up is disabled" }, 400);
4408
+ }
4409
+ }
3990
4410
  if (!phone) {
3991
4411
  return c.json({ error: "Phone required" }, 400);
3992
4412
  }
@@ -4350,6 +4770,12 @@ var updateProfileHandler = async (c) => {
4350
4770
  if (!updatedUser) {
4351
4771
  return c.json({ error: "User not found" }, 404);
4352
4772
  }
4773
+ await invalidateSessionCacheForUser(
4774
+ config,
4775
+ database,
4776
+ userId,
4777
+ resolvedTenantId
4778
+ );
4353
4779
  return c.json({ user: normalizeUser(updatedUser) }, 200);
4354
4780
  };
4355
4781
 
@@ -4370,6 +4796,12 @@ var updateEmailHandler = async (c) => {
4370
4796
  return c.json({ error: AUTH_ERRORS.UNAUTHORIZED }, 401);
4371
4797
  }
4372
4798
  const resolvedTenantId = ensureTenantId(config, tenantId);
4799
+ await invalidateSessionCacheForUser(
4800
+ config,
4801
+ database,
4802
+ userId,
4803
+ resolvedTenantId
4804
+ );
4373
4805
  if (user.email && session?.id) {
4374
4806
  await database.delete(sessionsInIam).where(
4375
4807
  and28(
@@ -4435,6 +4867,12 @@ var updatePhoneHandler = async (c) => {
4435
4867
  return c.json({ error: AUTH_ERRORS.UNAUTHORIZED }, 401);
4436
4868
  }
4437
4869
  const resolvedTenantId = ensureTenantId(config, tenantId);
4870
+ await invalidateSessionCacheForUser(
4871
+ config,
4872
+ database,
4873
+ userId,
4874
+ resolvedTenantId
4875
+ );
4438
4876
  const phoneValidator = createPhoneField(config);
4439
4877
  if (!phoneValidator.validate(body.phone)) {
4440
4878
  return c.json({ error: "Invalid phone number format" }, 400);
@@ -6100,12 +6538,14 @@ var listSessionsHandler = async (c) => {
6100
6538
  import { and as and45, eq as eq46 } from "drizzle-orm";
6101
6539
  var revokeAllSessionsHandler = async (c) => {
6102
6540
  const { userId } = c.req.valid("param");
6541
+ const config = c.get("config");
6103
6542
  const database = c.get("database");
6104
6543
  const tenantId = c.get("tenantId");
6105
6544
  const [user] = await database.select().from(usersInIam).where(and45(eq46(usersInIam.id, userId), eq46(usersInIam.tenantId, tenantId))).limit(1);
6106
6545
  if (!user) {
6107
6546
  return c.json({ error: "User not found" }, 404);
6108
6547
  }
6548
+ await invalidateSessionCacheForUser(config, database, userId, tenantId);
6109
6549
  await database.delete(sessionsInIam).where(
6110
6550
  and45(
6111
6551
  eq46(sessionsInIam.tenantId, tenantId),
@@ -6125,6 +6565,8 @@ var revokeSessionHandler = async (c) => {
6125
6565
  if (!existing) {
6126
6566
  return c.json({ error: "Session not found" }, 404);
6127
6567
  }
6568
+ const config = c.get("config");
6569
+ await deleteSessionCacheKeys(config, [existing.token]);
6128
6570
  await database.delete(sessionsInIam).where(and46(eq47(sessionsInIam.id, id), eq47(sessionsInIam.tenantId, tenantId)));
6129
6571
  return c.json({ message: "Session revoked" }, 200);
6130
6572
  };
@@ -6728,6 +7170,12 @@ var assignUserRoleHandler = async (c) => {
6728
7170
  userId: body.userId,
6729
7171
  roleId: body.roleId
6730
7172
  }).returning();
7173
+ await invalidateSessionCacheForUser(
7174
+ config,
7175
+ database,
7176
+ body.userId,
7177
+ resolvedTenantId
7178
+ );
6731
7179
  return c.json({ userRole }, 201);
6732
7180
  } catch (error) {
6733
7181
  if (error && typeof error === "object" && "code" in error && error.code === "23505") {
@@ -6758,6 +7206,7 @@ var listUserRolesHandler = async (c) => {
6758
7206
  import { and as and49, eq as eq54 } from "drizzle-orm";
6759
7207
  var revokeUserRoleHandler = async (c) => {
6760
7208
  const { id } = c.req.valid("param");
7209
+ const config = c.get("config");
6761
7210
  const database = c.get("database");
6762
7211
  const tenantId = c.get("tenantId");
6763
7212
  const [existing] = await database.select().from(userRolesInIam).where(
@@ -6769,6 +7218,12 @@ var revokeUserRoleHandler = async (c) => {
6769
7218
  await database.delete(userRolesInIam).where(
6770
7219
  and49(eq54(userRolesInIam.id, id), eq54(userRolesInIam.tenantId, tenantId))
6771
7220
  );
7221
+ await invalidateSessionCacheForUser(
7222
+ config,
7223
+ database,
7224
+ existing.userId,
7225
+ tenantId
7226
+ );
6772
7227
  return c.json({ message: "Role revoked from user" }, 200);
6773
7228
  };
6774
7229
 
@@ -6900,6 +7355,7 @@ import { and as and50, eq as eq55, sql as sql25 } from "drizzle-orm";
6900
7355
  var banUserHandler = async (c) => {
6901
7356
  const { id } = c.req.valid("param");
6902
7357
  const body = c.req.valid("json");
7358
+ const config = c.get("config");
6903
7359
  const database = c.get("database");
6904
7360
  const tenantId = c.get("tenantId");
6905
7361
  const [existing] = await database.select().from(usersInIam).where(and50(eq55(usersInIam.id, id), eq55(usersInIam.tenantId, tenantId))).limit(1);
@@ -6918,6 +7374,7 @@ var banUserHandler = async (c) => {
6918
7374
  if (!userWithRoles) {
6919
7375
  return c.json({ error: "User not found" }, 404);
6920
7376
  }
7377
+ await invalidateSessionCacheForUser(config, database, id, tenantId);
6921
7378
  return c.json({ user: normalizeUser(userWithRoles) }, 200);
6922
7379
  };
6923
7380
 
@@ -7501,6 +7958,7 @@ import { and as and56, eq as eq61, inArray as inArray7, sql as sql28 } from "dri
7501
7958
  var updateUserHandler = async (c) => {
7502
7959
  const { id } = c.req.valid("param");
7503
7960
  const body = c.req.valid("json");
7961
+ const config = c.get("config");
7504
7962
  const database = c.get("database");
7505
7963
  const tenantId = c.get("tenantId");
7506
7964
  const [existing] = await database.select().from(usersInIam).where(and56(eq61(usersInIam.id, id), eq61(usersInIam.tenantId, tenantId))).limit(1);
@@ -7600,6 +8058,7 @@ var updateUserHandler = async (c) => {
7600
8058
  userId: id,
7601
8059
  tenantId
7602
8060
  });
8061
+ await invalidateSessionCacheForUser(config, database, id, tenantId);
7603
8062
  return c.json(
7604
8063
  {
7605
8064
  user: normalizeUser(userWithRoles ?? updated)
@@ -8178,26 +8637,14 @@ var loadSession = async ({
8178
8637
  }) => {
8179
8638
  try {
8180
8639
  const hashedToken = await hashToken(sessionToken, config.secret);
8181
- const session = await fetchSessionByToken({
8182
- database,
8183
- hashedToken,
8184
- tenantId: config.tenant?.tenantId || "tenant"
8185
- });
8186
- if (!session) {
8187
- return;
8188
- }
8189
- const user = await fetchUserWithRoles({
8190
- database,
8191
- userId: session.userId,
8192
- tenantId: session.tenantId
8193
- });
8194
- if (user) {
8640
+ const pair = await loadSessionPair(database, config, hashedToken);
8641
+ if (pair) {
8195
8642
  setAuthContext({
8196
8643
  c,
8197
8644
  config,
8198
8645
  database,
8199
- user,
8200
- session
8646
+ user: pair.user,
8647
+ session: pair.session
8201
8648
  });
8202
8649
  }
8203
8650
  } catch {
@@ -8235,6 +8682,9 @@ var createAuthRoutes = ({
8235
8682
  app.onError((error, c) => {
8236
8683
  return handleError(error, c);
8237
8684
  });
8685
+ if (config.rateLimit?.enabled) {
8686
+ app.use("*", createAuthRateLimitMiddleware(config));
8687
+ }
8238
8688
  app.use(
8239
8689
  "*",
8240
8690
  createAuthMiddleware({
@@ -8457,12 +8907,9 @@ var createGetSession = (database, config) => {
8457
8907
  }
8458
8908
  try {
8459
8909
  const hashedToken = await hashToken(sessionToken, config.secret);
8460
- const session = await fetchSessionByToken({
8461
- database,
8462
- hashedToken,
8463
- tenantId: config.tenant?.tenantId || "tenant"
8464
- });
8465
- if (!session) {
8910
+ const pair = await loadSessionPair(database, config, hashedToken);
8911
+ if (!pair) {
8912
+ await invalidateSessionCacheForHashedToken(config, hashedToken);
8466
8913
  deleteSessionCookie(c, config);
8467
8914
  return {
8468
8915
  session: null,
@@ -8471,25 +8918,7 @@ var createGetSession = (database, config) => {
8471
8918
  status: "invalid_session"
8472
8919
  };
8473
8920
  }
8474
- const user = await fetchUserWithRoles({
8475
- database,
8476
- userId: session.userId,
8477
- tenantId: session.tenantId
8478
- });
8479
- if (!user) {
8480
- await deleteSession({
8481
- database,
8482
- sessionId: session.id,
8483
- tenantId: session.tenantId
8484
- });
8485
- deleteSessionCookie(c, config);
8486
- return {
8487
- session: null,
8488
- user: null,
8489
- sessionToken: null,
8490
- status: "user_not_found"
8491
- };
8492
- }
8921
+ let { session, user } = pair;
8493
8922
  const rememberMe = session.meta?.rememberMe !== false;
8494
8923
  const updateAge = getSessionUpdateAge({
8495
8924
  sessionConfig: config.session,
@@ -8509,12 +8938,8 @@ var createGetSession = (database, config) => {
8509
8938
  setSessionCookie(c, sessionToken, config, {
8510
8939
  expires: new Date(newExpiresAt)
8511
8940
  });
8512
- return {
8513
- session: { ...session, expiresAt: newExpiresAt },
8514
- user,
8515
- sessionToken,
8516
- status: "valid"
8517
- };
8941
+ session = { ...session, expiresAt: newExpiresAt };
8942
+ await writeSessionCache(config, hashedToken, { session, user });
8518
8943
  }
8519
8944
  return { session, user, sessionToken, status: "valid" };
8520
8945
  } catch {
@@ -8550,9 +8975,7 @@ var defaultAuthConfig = {
8550
8975
  docs: {
8551
8976
  enabled: true
8552
8977
  },
8553
- cookie: {
8554
- prefix: "msb"
8555
- },
8978
+ prefix: "msb",
8556
8979
  session: {
8557
8980
  expiresIn: "7d",
8558
8981
  rememberMeExpiresIn: "30d",
@@ -8566,10 +8989,18 @@ var defaultAuthConfig = {
8566
8989
  ...defaultConfig,
8567
8990
  phoneRegex: defaultPhoneRegex
8568
8991
  },
8992
+ signUp: {
8993
+ enabled: true,
8994
+ emailEnabled: true,
8995
+ phoneEnabled: true,
8996
+ allowedEmailDomains: []
8997
+ },
8569
8998
  security: {
8570
8999
  maxLoginAttempts: 5,
8571
9000
  lockoutDuration: "15m"
8572
- }
9001
+ },
9002
+ sessionCache: { enabled: false },
9003
+ rateLimit: { enabled: false }
8573
9004
  };
8574
9005
 
8575
9006
  // src/lib/cleanup.ts
@@ -8602,6 +9033,21 @@ var createMesobAuth = (authConfig) => {
8602
9033
  "AUTH_SECRET is required and must be a non-empty string. Please set AUTH_SECRET environment variable."
8603
9034
  );
8604
9035
  }
9036
+ if (config.sessionCache?.enabled && !config.sessionCache.kv) {
9037
+ throw new Error("sessionCache.kv is required when sessionCache.enabled");
9038
+ }
9039
+ if (config.rateLimit?.enabled) {
9040
+ if (!config.rateLimit.kv) {
9041
+ throw new Error("rateLimit.kv is required when rateLimit.enabled");
9042
+ }
9043
+ if (config.rateLimit.window < 1 || config.rateLimit.max < 1) {
9044
+ throw new Error("rateLimit.window and rateLimit.max must be >= 1");
9045
+ }
9046
+ const ipv6s = config.rateLimit.ipv6Subnet;
9047
+ if (ipv6s !== void 0 && (!Number.isFinite(ipv6s) || ipv6s < 1 || ipv6s > 128)) {
9048
+ throw new Error("rateLimit.ipv6Subnet must be between 1 and 128");
9049
+ }
9050
+ }
8605
9051
  const database = createDatabase(config.connectionString);
8606
9052
  const routesApp = createAuthRoutes({ config, database });
8607
9053
  const basePath = config.basePath || "";
@@ -8630,14 +9076,23 @@ var createMesobAuth = (authConfig) => {
8630
9076
  };
8631
9077
  };
8632
9078
  export {
9079
+ AUTH_RATE_LIMIT_POST_PATHS,
8633
9080
  cleanupExpiredData,
8634
9081
  cleanupExpiredSessions,
8635
9082
  cleanupExpiredVerifications,
9083
+ createAuthRateLimitMiddleware,
8636
9084
  createDatabase,
8637
9085
  createMesobAuth,
8638
9086
  createSessionMiddleware,
8639
9087
  createTenantMiddleware,
9088
+ deleteSessionCacheKeys,
9089
+ getSessionCookieName,
9090
+ getSessionKeyNamespace,
8640
9091
  hasPermission,
8641
- hasPermissionThrow
9092
+ hasPermissionThrow,
9093
+ invalidateSessionCacheForHashedToken,
9094
+ invalidateSessionCacheForUser,
9095
+ rateLimitClientKey,
9096
+ sessionCacheKey
8642
9097
  };
8643
9098
  //# sourceMappingURL=index.js.map