@mesob/auth-hono 0.5.2 → 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)
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
+ )
767
+ );
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;
780
+ }
781
+ await Promise.all(
782
+ hashedTokens.map((t) => sc.kv.delete(sessionCacheKey(config, t)))
783
+ );
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
527
793
  });
528
- };
794
+ await deleteSessionCacheKeys(config, tokens);
795
+ }
529
796
 
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
- ""
538
- );
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);
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;
550
853
  }
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
- })
568
- );
569
- return `${saltHex}:${toHex(derivedKey)}`;
570
- };
571
- var verifyPassword = async (password, hashed) => {
572
- if (!hashed) {
573
- 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;
574
866
  }
575
- const [saltHex, keyHex] = hashed.split(":");
576
- if (!(saltHex && keyHex)) {
577
- return false;
867
+ }
868
+ function isIpv4MappedIpv6(bytes) {
869
+ for (let i = 0; i < 10; i++) {
870
+ if (bytes[i] !== 0) {
871
+ return false;
872
+ }
578
873
  }
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;
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;
592
880
  }
593
- let result = 0;
594
- for (let i = 0; i < derived.length; i++) {
595
- result |= derived.charCodeAt(i) ^ keyHex.charCodeAt(i);
881
+ const double = s.includes("::");
882
+ if (double && s.split("::").length > 2) {
883
+ return null;
596
884
  }
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
- );
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;
604
895
  }
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;
896
+ const pad = double ? 8 - partsLen : 0;
897
+ const all = [...left, ...Array(pad).fill("0"), ...right];
898
+ if (all.length !== 8) {
899
+ return null;
627
900
  }
628
- if ("code" in error || "query" in error || "detail" in error) {
629
- return true;
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;
630
912
  }
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");
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);
634
919
  }
635
- return false;
636
- };
637
- var sanitizeDatabaseError = (error) => {
638
- const code = error.code;
639
- if (code === "23505") {
640
- return "Resource already exists";
920
+ const zi = a.indexOf("%");
921
+ if (zi >= 0) {
922
+ a = a.slice(0, zi);
641
923
  }
642
- if (code === "23503") {
643
- return "Referenced resource not found";
924
+ if (a === "") {
925
+ return null;
644
926
  }
645
- if (code === "23502") {
646
- return "Required field is missing";
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
+ }
647
953
  }
648
- if (code === "42P01") {
649
- return "Resource not found";
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");
650
960
  }
651
- if (code === "42703") {
652
- return "Invalid request";
961
+ return o;
962
+ }
963
+ function rateLimitClientKey(raw, ipv6SubnetBits) {
964
+ const s = raw.trim();
965
+ if (!s || s === "unknown") {
966
+ return "unknown";
653
967
  }
654
- if (code === "23514") {
655
- return "Validation failed";
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}`;
656
972
  }
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);
973
+ const bytes = parseIpv6ToBytes(s);
974
+ if (!bytes) {
975
+ return `raw:${s}`;
673
976
  }
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);
977
+ if (isIpv4MappedIpv6(bytes)) {
978
+ return ipv4Key(bytes.subarray(12, 16));
684
979
  }
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
- });
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
@@ -1831,6 +2179,7 @@ var signOutHandler = async (c) => {
1831
2179
  )
1832
2180
  );
1833
2181
  }
2182
+ await invalidateSessionCacheForHashedToken(config, hashedToken);
1834
2183
  deleteSessionCookie(c, config);
1835
2184
  return c.json({ message: "Signed out" }, 200);
1836
2185
  };
@@ -2617,6 +2966,12 @@ var emailVerificationConfirmHandler = async (c) => {
2617
2966
  if (result.status === "error") {
2618
2967
  return c.json({ error: result.error }, 400);
2619
2968
  }
2969
+ await invalidateSessionCacheForUser(
2970
+ config,
2971
+ database,
2972
+ result.user.id,
2973
+ resolvedTenantId
2974
+ );
2620
2975
  setSessionCookie(c, result.sessionToken, config, {
2621
2976
  expires: new Date(result.expiresAt)
2622
2977
  });
@@ -2872,6 +3227,12 @@ var changePasswordHandler = async (c) => {
2872
3227
  eq18(accountsInIam.provider, "credentials")
2873
3228
  )
2874
3229
  );
3230
+ await invalidateSessionCacheForUser(
3231
+ config,
3232
+ database,
3233
+ userId,
3234
+ resolvedTenantId
3235
+ );
2875
3236
  const currentSessionToken = getCookie2(c, getSessionCookieName(config));
2876
3237
  if (currentSessionToken) {
2877
3238
  const hashedToken = await hashToken(currentSessionToken, config.secret);
@@ -3152,6 +3513,12 @@ var resetPasswordHandler = async (c) => {
3152
3513
  eq20(accountsInIam.provider, "credentials")
3153
3514
  )
3154
3515
  );
3516
+ await invalidateSessionCacheForUser(
3517
+ config,
3518
+ database,
3519
+ verification.userId,
3520
+ resolvedTenantId
3521
+ );
3155
3522
  await database.delete(sessionsInIam).where(
3156
3523
  and20(
3157
3524
  eq20(sessionsInIam.tenantId, resolvedTenantId),
@@ -3991,6 +4358,12 @@ var phoneVerificationConfirmHandler = async (c) => {
3991
4358
  if (result.status === "error") {
3992
4359
  return c.json({ error: result.error }, 400);
3993
4360
  }
4361
+ await invalidateSessionCacheForUser(
4362
+ config,
4363
+ database,
4364
+ result.user.id,
4365
+ resolvedTenantId
4366
+ );
3994
4367
  if (!result.session) {
3995
4368
  return c.json(
3996
4369
  {
@@ -4397,6 +4770,12 @@ var updateProfileHandler = async (c) => {
4397
4770
  if (!updatedUser) {
4398
4771
  return c.json({ error: "User not found" }, 404);
4399
4772
  }
4773
+ await invalidateSessionCacheForUser(
4774
+ config,
4775
+ database,
4776
+ userId,
4777
+ resolvedTenantId
4778
+ );
4400
4779
  return c.json({ user: normalizeUser(updatedUser) }, 200);
4401
4780
  };
4402
4781
 
@@ -4417,6 +4796,12 @@ var updateEmailHandler = async (c) => {
4417
4796
  return c.json({ error: AUTH_ERRORS.UNAUTHORIZED }, 401);
4418
4797
  }
4419
4798
  const resolvedTenantId = ensureTenantId(config, tenantId);
4799
+ await invalidateSessionCacheForUser(
4800
+ config,
4801
+ database,
4802
+ userId,
4803
+ resolvedTenantId
4804
+ );
4420
4805
  if (user.email && session?.id) {
4421
4806
  await database.delete(sessionsInIam).where(
4422
4807
  and28(
@@ -4482,6 +4867,12 @@ var updatePhoneHandler = async (c) => {
4482
4867
  return c.json({ error: AUTH_ERRORS.UNAUTHORIZED }, 401);
4483
4868
  }
4484
4869
  const resolvedTenantId = ensureTenantId(config, tenantId);
4870
+ await invalidateSessionCacheForUser(
4871
+ config,
4872
+ database,
4873
+ userId,
4874
+ resolvedTenantId
4875
+ );
4485
4876
  const phoneValidator = createPhoneField(config);
4486
4877
  if (!phoneValidator.validate(body.phone)) {
4487
4878
  return c.json({ error: "Invalid phone number format" }, 400);
@@ -6147,12 +6538,14 @@ var listSessionsHandler = async (c) => {
6147
6538
  import { and as and45, eq as eq46 } from "drizzle-orm";
6148
6539
  var revokeAllSessionsHandler = async (c) => {
6149
6540
  const { userId } = c.req.valid("param");
6541
+ const config = c.get("config");
6150
6542
  const database = c.get("database");
6151
6543
  const tenantId = c.get("tenantId");
6152
6544
  const [user] = await database.select().from(usersInIam).where(and45(eq46(usersInIam.id, userId), eq46(usersInIam.tenantId, tenantId))).limit(1);
6153
6545
  if (!user) {
6154
6546
  return c.json({ error: "User not found" }, 404);
6155
6547
  }
6548
+ await invalidateSessionCacheForUser(config, database, userId, tenantId);
6156
6549
  await database.delete(sessionsInIam).where(
6157
6550
  and45(
6158
6551
  eq46(sessionsInIam.tenantId, tenantId),
@@ -6172,6 +6565,8 @@ var revokeSessionHandler = async (c) => {
6172
6565
  if (!existing) {
6173
6566
  return c.json({ error: "Session not found" }, 404);
6174
6567
  }
6568
+ const config = c.get("config");
6569
+ await deleteSessionCacheKeys(config, [existing.token]);
6175
6570
  await database.delete(sessionsInIam).where(and46(eq47(sessionsInIam.id, id), eq47(sessionsInIam.tenantId, tenantId)));
6176
6571
  return c.json({ message: "Session revoked" }, 200);
6177
6572
  };
@@ -6775,6 +7170,12 @@ var assignUserRoleHandler = async (c) => {
6775
7170
  userId: body.userId,
6776
7171
  roleId: body.roleId
6777
7172
  }).returning();
7173
+ await invalidateSessionCacheForUser(
7174
+ config,
7175
+ database,
7176
+ body.userId,
7177
+ resolvedTenantId
7178
+ );
6778
7179
  return c.json({ userRole }, 201);
6779
7180
  } catch (error) {
6780
7181
  if (error && typeof error === "object" && "code" in error && error.code === "23505") {
@@ -6805,6 +7206,7 @@ var listUserRolesHandler = async (c) => {
6805
7206
  import { and as and49, eq as eq54 } from "drizzle-orm";
6806
7207
  var revokeUserRoleHandler = async (c) => {
6807
7208
  const { id } = c.req.valid("param");
7209
+ const config = c.get("config");
6808
7210
  const database = c.get("database");
6809
7211
  const tenantId = c.get("tenantId");
6810
7212
  const [existing] = await database.select().from(userRolesInIam).where(
@@ -6816,6 +7218,12 @@ var revokeUserRoleHandler = async (c) => {
6816
7218
  await database.delete(userRolesInIam).where(
6817
7219
  and49(eq54(userRolesInIam.id, id), eq54(userRolesInIam.tenantId, tenantId))
6818
7220
  );
7221
+ await invalidateSessionCacheForUser(
7222
+ config,
7223
+ database,
7224
+ existing.userId,
7225
+ tenantId
7226
+ );
6819
7227
  return c.json({ message: "Role revoked from user" }, 200);
6820
7228
  };
6821
7229
 
@@ -6947,6 +7355,7 @@ import { and as and50, eq as eq55, sql as sql25 } from "drizzle-orm";
6947
7355
  var banUserHandler = async (c) => {
6948
7356
  const { id } = c.req.valid("param");
6949
7357
  const body = c.req.valid("json");
7358
+ const config = c.get("config");
6950
7359
  const database = c.get("database");
6951
7360
  const tenantId = c.get("tenantId");
6952
7361
  const [existing] = await database.select().from(usersInIam).where(and50(eq55(usersInIam.id, id), eq55(usersInIam.tenantId, tenantId))).limit(1);
@@ -6965,6 +7374,7 @@ var banUserHandler = async (c) => {
6965
7374
  if (!userWithRoles) {
6966
7375
  return c.json({ error: "User not found" }, 404);
6967
7376
  }
7377
+ await invalidateSessionCacheForUser(config, database, id, tenantId);
6968
7378
  return c.json({ user: normalizeUser(userWithRoles) }, 200);
6969
7379
  };
6970
7380
 
@@ -7548,6 +7958,7 @@ import { and as and56, eq as eq61, inArray as inArray7, sql as sql28 } from "dri
7548
7958
  var updateUserHandler = async (c) => {
7549
7959
  const { id } = c.req.valid("param");
7550
7960
  const body = c.req.valid("json");
7961
+ const config = c.get("config");
7551
7962
  const database = c.get("database");
7552
7963
  const tenantId = c.get("tenantId");
7553
7964
  const [existing] = await database.select().from(usersInIam).where(and56(eq61(usersInIam.id, id), eq61(usersInIam.tenantId, tenantId))).limit(1);
@@ -7647,6 +8058,7 @@ var updateUserHandler = async (c) => {
7647
8058
  userId: id,
7648
8059
  tenantId
7649
8060
  });
8061
+ await invalidateSessionCacheForUser(config, database, id, tenantId);
7650
8062
  return c.json(
7651
8063
  {
7652
8064
  user: normalizeUser(userWithRoles ?? updated)
@@ -8225,26 +8637,14 @@ var loadSession = async ({
8225
8637
  }) => {
8226
8638
  try {
8227
8639
  const hashedToken = await hashToken(sessionToken, config.secret);
8228
- const session = await fetchSessionByToken({
8229
- database,
8230
- hashedToken,
8231
- tenantId: config.tenant?.tenantId || "tenant"
8232
- });
8233
- if (!session) {
8234
- return;
8235
- }
8236
- const user = await fetchUserWithRoles({
8237
- database,
8238
- userId: session.userId,
8239
- tenantId: session.tenantId
8240
- });
8241
- if (user) {
8640
+ const pair = await loadSessionPair(database, config, hashedToken);
8641
+ if (pair) {
8242
8642
  setAuthContext({
8243
8643
  c,
8244
8644
  config,
8245
8645
  database,
8246
- user,
8247
- session
8646
+ user: pair.user,
8647
+ session: pair.session
8248
8648
  });
8249
8649
  }
8250
8650
  } catch {
@@ -8282,6 +8682,9 @@ var createAuthRoutes = ({
8282
8682
  app.onError((error, c) => {
8283
8683
  return handleError(error, c);
8284
8684
  });
8685
+ if (config.rateLimit?.enabled) {
8686
+ app.use("*", createAuthRateLimitMiddleware(config));
8687
+ }
8285
8688
  app.use(
8286
8689
  "*",
8287
8690
  createAuthMiddleware({
@@ -8504,12 +8907,9 @@ var createGetSession = (database, config) => {
8504
8907
  }
8505
8908
  try {
8506
8909
  const hashedToken = await hashToken(sessionToken, config.secret);
8507
- const session = await fetchSessionByToken({
8508
- database,
8509
- hashedToken,
8510
- tenantId: config.tenant?.tenantId || "tenant"
8511
- });
8512
- if (!session) {
8910
+ const pair = await loadSessionPair(database, config, hashedToken);
8911
+ if (!pair) {
8912
+ await invalidateSessionCacheForHashedToken(config, hashedToken);
8513
8913
  deleteSessionCookie(c, config);
8514
8914
  return {
8515
8915
  session: null,
@@ -8518,25 +8918,7 @@ var createGetSession = (database, config) => {
8518
8918
  status: "invalid_session"
8519
8919
  };
8520
8920
  }
8521
- const user = await fetchUserWithRoles({
8522
- database,
8523
- userId: session.userId,
8524
- tenantId: session.tenantId
8525
- });
8526
- if (!user) {
8527
- await deleteSession({
8528
- database,
8529
- sessionId: session.id,
8530
- tenantId: session.tenantId
8531
- });
8532
- deleteSessionCookie(c, config);
8533
- return {
8534
- session: null,
8535
- user: null,
8536
- sessionToken: null,
8537
- status: "user_not_found"
8538
- };
8539
- }
8921
+ let { session, user } = pair;
8540
8922
  const rememberMe = session.meta?.rememberMe !== false;
8541
8923
  const updateAge = getSessionUpdateAge({
8542
8924
  sessionConfig: config.session,
@@ -8556,12 +8938,8 @@ var createGetSession = (database, config) => {
8556
8938
  setSessionCookie(c, sessionToken, config, {
8557
8939
  expires: new Date(newExpiresAt)
8558
8940
  });
8559
- return {
8560
- session: { ...session, expiresAt: newExpiresAt },
8561
- user,
8562
- sessionToken,
8563
- status: "valid"
8564
- };
8941
+ session = { ...session, expiresAt: newExpiresAt };
8942
+ await writeSessionCache(config, hashedToken, { session, user });
8565
8943
  }
8566
8944
  return { session, user, sessionToken, status: "valid" };
8567
8945
  } catch {
@@ -8597,9 +8975,7 @@ var defaultAuthConfig = {
8597
8975
  docs: {
8598
8976
  enabled: true
8599
8977
  },
8600
- cookie: {
8601
- prefix: "msb"
8602
- },
8978
+ prefix: "msb",
8603
8979
  session: {
8604
8980
  expiresIn: "7d",
8605
8981
  rememberMeExpiresIn: "30d",
@@ -8622,7 +8998,9 @@ var defaultAuthConfig = {
8622
8998
  security: {
8623
8999
  maxLoginAttempts: 5,
8624
9000
  lockoutDuration: "15m"
8625
- }
9001
+ },
9002
+ sessionCache: { enabled: false },
9003
+ rateLimit: { enabled: false }
8626
9004
  };
8627
9005
 
8628
9006
  // src/lib/cleanup.ts
@@ -8655,6 +9033,21 @@ var createMesobAuth = (authConfig) => {
8655
9033
  "AUTH_SECRET is required and must be a non-empty string. Please set AUTH_SECRET environment variable."
8656
9034
  );
8657
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
+ }
8658
9051
  const database = createDatabase(config.connectionString);
8659
9052
  const routesApp = createAuthRoutes({ config, database });
8660
9053
  const basePath = config.basePath || "";
@@ -8683,14 +9076,23 @@ var createMesobAuth = (authConfig) => {
8683
9076
  };
8684
9077
  };
8685
9078
  export {
9079
+ AUTH_RATE_LIMIT_POST_PATHS,
8686
9080
  cleanupExpiredData,
8687
9081
  cleanupExpiredSessions,
8688
9082
  cleanupExpiredVerifications,
9083
+ createAuthRateLimitMiddleware,
8689
9084
  createDatabase,
8690
9085
  createMesobAuth,
8691
9086
  createSessionMiddleware,
8692
9087
  createTenantMiddleware,
9088
+ deleteSessionCacheKeys,
9089
+ getSessionCookieName,
9090
+ getSessionKeyNamespace,
8693
9091
  hasPermission,
8694
- hasPermissionThrow
9092
+ hasPermissionThrow,
9093
+ invalidateSessionCacheForHashedToken,
9094
+ invalidateSessionCacheForUser,
9095
+ rateLimitClientKey,
9096
+ sessionCacheKey
8695
9097
  };
8696
9098
  //# sourceMappingURL=index.js.map