@mesob/auth-hono 0.5.2 → 0.5.4
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-CDgzxZzO.d.ts → index-v_uxUd8A.d.ts} +45 -4
- package/dist/index.d.ts +10 -3
- package/dist/index.js +655 -250
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-rate-limit.d.ts +21 -0
- package/dist/lib/auth-rate-limit.js +228 -0
- package/dist/lib/auth-rate-limit.js.map +1 -0
- package/dist/lib/cookie.d.ts +4 -2
- package/dist/lib/cookie.js +6 -1
- package/dist/lib/cookie.js.map +1 -1
- package/dist/lib/has-role-permission.d.ts +1 -1
- package/dist/lib/iam-seed.d.ts +1 -1
- package/dist/lib/load-session-pair.d.ts +16 -0
- package/dist/lib/load-session-pair.js +505 -0
- package/dist/lib/load-session-pair.js.map +1 -0
- package/dist/lib/normalize-auth-response.d.ts +5 -2
- package/dist/lib/normalize-auth-response.js +4 -1
- package/dist/lib/normalize-auth-response.js.map +1 -1
- package/dist/lib/normalize-rate-limit-ip.d.ts +5 -0
- package/dist/lib/normalize-rate-limit-ip.js +159 -0
- package/dist/lib/normalize-rate-limit-ip.js.map +1 -0
- package/dist/lib/normalize-user.d.ts +1 -1
- package/dist/lib/openapi-config.d.ts +1 -1
- package/dist/lib/phone-validation.d.ts +1 -1
- package/dist/lib/session-cache.d.ts +22 -0
- package/dist/lib/session-cache.js +396 -0
- package/dist/lib/session-cache.js.map +1 -0
- package/dist/lib/session.d.ts +1 -1
- package/dist/lib/tenant.d.ts +1 -1
- package/package.json +2 -2
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/
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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/
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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;
|
|
853
|
+
}
|
|
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;
|
|
550
866
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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;
|
|
867
|
+
}
|
|
868
|
+
function isIpv4MappedIpv6(bytes) {
|
|
869
|
+
for (let i = 0; i < 10; i++) {
|
|
870
|
+
if (bytes[i] !== 0) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
574
873
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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;
|
|
578
880
|
}
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
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;
|
|
881
|
+
const double = s.includes("::");
|
|
882
|
+
if (double && s.split("::").length > 2) {
|
|
883
|
+
return null;
|
|
592
884
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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;
|
|
596
895
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
throw new Error(
|
|
602
|
-
"AUTH_SECRET is required and must be a non-empty string. Please set AUTH_SECRET environment variable."
|
|
603
|
-
);
|
|
896
|
+
const pad = double ? 8 - partsLen : 0;
|
|
897
|
+
const all = [...left, ...Array(pad).fill("0"), ...right];
|
|
898
|
+
if (all.length !== 8) {
|
|
899
|
+
return null;
|
|
604
900
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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;
|
|
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;
|
|
627
912
|
}
|
|
628
|
-
|
|
629
|
-
|
|
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);
|
|
630
919
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
920
|
+
const zi = a.indexOf("%");
|
|
921
|
+
if (zi >= 0) {
|
|
922
|
+
a = a.slice(0, zi);
|
|
634
923
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
var sanitizeDatabaseError = (error) => {
|
|
638
|
-
const code = error.code;
|
|
639
|
-
if (code === "23505") {
|
|
640
|
-
return "Resource already exists";
|
|
924
|
+
if (a === "") {
|
|
925
|
+
return null;
|
|
641
926
|
}
|
|
642
|
-
if (
|
|
643
|
-
|
|
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
|
+
}
|
|
644
953
|
}
|
|
645
|
-
|
|
646
|
-
|
|
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");
|
|
647
960
|
}
|
|
648
|
-
|
|
649
|
-
|
|
961
|
+
return o;
|
|
962
|
+
}
|
|
963
|
+
function rateLimitClientKey(raw, ipv6SubnetBits) {
|
|
964
|
+
const s = raw.trim();
|
|
965
|
+
if (!s || s === "unknown") {
|
|
966
|
+
return "unknown";
|
|
650
967
|
}
|
|
651
|
-
|
|
652
|
-
|
|
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}`;
|
|
653
972
|
}
|
|
654
|
-
|
|
655
|
-
|
|
973
|
+
const bytes = parseIpv6ToBytes(s);
|
|
974
|
+
if (!bytes) {
|
|
975
|
+
return `raw:${s}`;
|
|
656
976
|
}
|
|
657
|
-
|
|
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);
|
|
977
|
+
if (isIpv4MappedIpv6(bytes)) {
|
|
978
|
+
return ipv4Key(bytes.subarray(12, 16));
|
|
673
979
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
+
}
|
|
684
1041
|
}
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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:
|
|
697
|
-
|
|
1057
|
+
{ error: result.message, code: "RATE_LIMITED" },
|
|
1058
|
+
429
|
|
698
1059
|
);
|
|
699
1060
|
}
|
|
700
|
-
|
|
701
|
-
|
|
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
|
|
@@ -932,7 +1280,10 @@ var normalizeAuthUser = (user) => ({
|
|
|
932
1280
|
phone: user.phone,
|
|
933
1281
|
image: user.image,
|
|
934
1282
|
emailVerified: user.emailVerified,
|
|
935
|
-
phoneVerified: user.phoneVerified
|
|
1283
|
+
phoneVerified: user.phoneVerified,
|
|
1284
|
+
roles: user.roles ?? null,
|
|
1285
|
+
roleCodes: user.roleCodes ?? null,
|
|
1286
|
+
permissions: user.permissions ?? null
|
|
936
1287
|
});
|
|
937
1288
|
var normalizeAuthSession = (session) => session ? {
|
|
938
1289
|
id: session.id,
|
|
@@ -1831,6 +2182,7 @@ var signOutHandler = async (c) => {
|
|
|
1831
2182
|
)
|
|
1832
2183
|
);
|
|
1833
2184
|
}
|
|
2185
|
+
await invalidateSessionCacheForHashedToken(config, hashedToken);
|
|
1834
2186
|
deleteSessionCookie(c, config);
|
|
1835
2187
|
return c.json({ message: "Signed out" }, 200);
|
|
1836
2188
|
};
|
|
@@ -2617,6 +2969,12 @@ var emailVerificationConfirmHandler = async (c) => {
|
|
|
2617
2969
|
if (result.status === "error") {
|
|
2618
2970
|
return c.json({ error: result.error }, 400);
|
|
2619
2971
|
}
|
|
2972
|
+
await invalidateSessionCacheForUser(
|
|
2973
|
+
config,
|
|
2974
|
+
database,
|
|
2975
|
+
result.user.id,
|
|
2976
|
+
resolvedTenantId
|
|
2977
|
+
);
|
|
2620
2978
|
setSessionCookie(c, result.sessionToken, config, {
|
|
2621
2979
|
expires: new Date(result.expiresAt)
|
|
2622
2980
|
});
|
|
@@ -2872,6 +3230,12 @@ var changePasswordHandler = async (c) => {
|
|
|
2872
3230
|
eq18(accountsInIam.provider, "credentials")
|
|
2873
3231
|
)
|
|
2874
3232
|
);
|
|
3233
|
+
await invalidateSessionCacheForUser(
|
|
3234
|
+
config,
|
|
3235
|
+
database,
|
|
3236
|
+
userId,
|
|
3237
|
+
resolvedTenantId
|
|
3238
|
+
);
|
|
2875
3239
|
const currentSessionToken = getCookie2(c, getSessionCookieName(config));
|
|
2876
3240
|
if (currentSessionToken) {
|
|
2877
3241
|
const hashedToken = await hashToken(currentSessionToken, config.secret);
|
|
@@ -3152,6 +3516,12 @@ var resetPasswordHandler = async (c) => {
|
|
|
3152
3516
|
eq20(accountsInIam.provider, "credentials")
|
|
3153
3517
|
)
|
|
3154
3518
|
);
|
|
3519
|
+
await invalidateSessionCacheForUser(
|
|
3520
|
+
config,
|
|
3521
|
+
database,
|
|
3522
|
+
verification.userId,
|
|
3523
|
+
resolvedTenantId
|
|
3524
|
+
);
|
|
3155
3525
|
await database.delete(sessionsInIam).where(
|
|
3156
3526
|
and20(
|
|
3157
3527
|
eq20(sessionsInIam.tenantId, resolvedTenantId),
|
|
@@ -3991,6 +4361,12 @@ var phoneVerificationConfirmHandler = async (c) => {
|
|
|
3991
4361
|
if (result.status === "error") {
|
|
3992
4362
|
return c.json({ error: result.error }, 400);
|
|
3993
4363
|
}
|
|
4364
|
+
await invalidateSessionCacheForUser(
|
|
4365
|
+
config,
|
|
4366
|
+
database,
|
|
4367
|
+
result.user.id,
|
|
4368
|
+
resolvedTenantId
|
|
4369
|
+
);
|
|
3994
4370
|
if (!result.session) {
|
|
3995
4371
|
return c.json(
|
|
3996
4372
|
{
|
|
@@ -4397,6 +4773,12 @@ var updateProfileHandler = async (c) => {
|
|
|
4397
4773
|
if (!updatedUser) {
|
|
4398
4774
|
return c.json({ error: "User not found" }, 404);
|
|
4399
4775
|
}
|
|
4776
|
+
await invalidateSessionCacheForUser(
|
|
4777
|
+
config,
|
|
4778
|
+
database,
|
|
4779
|
+
userId,
|
|
4780
|
+
resolvedTenantId
|
|
4781
|
+
);
|
|
4400
4782
|
return c.json({ user: normalizeUser(updatedUser) }, 200);
|
|
4401
4783
|
};
|
|
4402
4784
|
|
|
@@ -4417,6 +4799,12 @@ var updateEmailHandler = async (c) => {
|
|
|
4417
4799
|
return c.json({ error: AUTH_ERRORS.UNAUTHORIZED }, 401);
|
|
4418
4800
|
}
|
|
4419
4801
|
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
4802
|
+
await invalidateSessionCacheForUser(
|
|
4803
|
+
config,
|
|
4804
|
+
database,
|
|
4805
|
+
userId,
|
|
4806
|
+
resolvedTenantId
|
|
4807
|
+
);
|
|
4420
4808
|
if (user.email && session?.id) {
|
|
4421
4809
|
await database.delete(sessionsInIam).where(
|
|
4422
4810
|
and28(
|
|
@@ -4482,6 +4870,12 @@ var updatePhoneHandler = async (c) => {
|
|
|
4482
4870
|
return c.json({ error: AUTH_ERRORS.UNAUTHORIZED }, 401);
|
|
4483
4871
|
}
|
|
4484
4872
|
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
4873
|
+
await invalidateSessionCacheForUser(
|
|
4874
|
+
config,
|
|
4875
|
+
database,
|
|
4876
|
+
userId,
|
|
4877
|
+
resolvedTenantId
|
|
4878
|
+
);
|
|
4485
4879
|
const phoneValidator = createPhoneField(config);
|
|
4486
4880
|
if (!phoneValidator.validate(body.phone)) {
|
|
4487
4881
|
return c.json({ error: "Invalid phone number format" }, 400);
|
|
@@ -6147,12 +6541,14 @@ var listSessionsHandler = async (c) => {
|
|
|
6147
6541
|
import { and as and45, eq as eq46 } from "drizzle-orm";
|
|
6148
6542
|
var revokeAllSessionsHandler = async (c) => {
|
|
6149
6543
|
const { userId } = c.req.valid("param");
|
|
6544
|
+
const config = c.get("config");
|
|
6150
6545
|
const database = c.get("database");
|
|
6151
6546
|
const tenantId = c.get("tenantId");
|
|
6152
6547
|
const [user] = await database.select().from(usersInIam).where(and45(eq46(usersInIam.id, userId), eq46(usersInIam.tenantId, tenantId))).limit(1);
|
|
6153
6548
|
if (!user) {
|
|
6154
6549
|
return c.json({ error: "User not found" }, 404);
|
|
6155
6550
|
}
|
|
6551
|
+
await invalidateSessionCacheForUser(config, database, userId, tenantId);
|
|
6156
6552
|
await database.delete(sessionsInIam).where(
|
|
6157
6553
|
and45(
|
|
6158
6554
|
eq46(sessionsInIam.tenantId, tenantId),
|
|
@@ -6172,6 +6568,8 @@ var revokeSessionHandler = async (c) => {
|
|
|
6172
6568
|
if (!existing) {
|
|
6173
6569
|
return c.json({ error: "Session not found" }, 404);
|
|
6174
6570
|
}
|
|
6571
|
+
const config = c.get("config");
|
|
6572
|
+
await deleteSessionCacheKeys(config, [existing.token]);
|
|
6175
6573
|
await database.delete(sessionsInIam).where(and46(eq47(sessionsInIam.id, id), eq47(sessionsInIam.tenantId, tenantId)));
|
|
6176
6574
|
return c.json({ message: "Session revoked" }, 200);
|
|
6177
6575
|
};
|
|
@@ -6775,6 +7173,12 @@ var assignUserRoleHandler = async (c) => {
|
|
|
6775
7173
|
userId: body.userId,
|
|
6776
7174
|
roleId: body.roleId
|
|
6777
7175
|
}).returning();
|
|
7176
|
+
await invalidateSessionCacheForUser(
|
|
7177
|
+
config,
|
|
7178
|
+
database,
|
|
7179
|
+
body.userId,
|
|
7180
|
+
resolvedTenantId
|
|
7181
|
+
);
|
|
6778
7182
|
return c.json({ userRole }, 201);
|
|
6779
7183
|
} catch (error) {
|
|
6780
7184
|
if (error && typeof error === "object" && "code" in error && error.code === "23505") {
|
|
@@ -6805,6 +7209,7 @@ var listUserRolesHandler = async (c) => {
|
|
|
6805
7209
|
import { and as and49, eq as eq54 } from "drizzle-orm";
|
|
6806
7210
|
var revokeUserRoleHandler = async (c) => {
|
|
6807
7211
|
const { id } = c.req.valid("param");
|
|
7212
|
+
const config = c.get("config");
|
|
6808
7213
|
const database = c.get("database");
|
|
6809
7214
|
const tenantId = c.get("tenantId");
|
|
6810
7215
|
const [existing] = await database.select().from(userRolesInIam).where(
|
|
@@ -6816,6 +7221,12 @@ var revokeUserRoleHandler = async (c) => {
|
|
|
6816
7221
|
await database.delete(userRolesInIam).where(
|
|
6817
7222
|
and49(eq54(userRolesInIam.id, id), eq54(userRolesInIam.tenantId, tenantId))
|
|
6818
7223
|
);
|
|
7224
|
+
await invalidateSessionCacheForUser(
|
|
7225
|
+
config,
|
|
7226
|
+
database,
|
|
7227
|
+
existing.userId,
|
|
7228
|
+
tenantId
|
|
7229
|
+
);
|
|
6819
7230
|
return c.json({ message: "Role revoked from user" }, 200);
|
|
6820
7231
|
};
|
|
6821
7232
|
|
|
@@ -6947,6 +7358,7 @@ import { and as and50, eq as eq55, sql as sql25 } from "drizzle-orm";
|
|
|
6947
7358
|
var banUserHandler = async (c) => {
|
|
6948
7359
|
const { id } = c.req.valid("param");
|
|
6949
7360
|
const body = c.req.valid("json");
|
|
7361
|
+
const config = c.get("config");
|
|
6950
7362
|
const database = c.get("database");
|
|
6951
7363
|
const tenantId = c.get("tenantId");
|
|
6952
7364
|
const [existing] = await database.select().from(usersInIam).where(and50(eq55(usersInIam.id, id), eq55(usersInIam.tenantId, tenantId))).limit(1);
|
|
@@ -6965,6 +7377,7 @@ var banUserHandler = async (c) => {
|
|
|
6965
7377
|
if (!userWithRoles) {
|
|
6966
7378
|
return c.json({ error: "User not found" }, 404);
|
|
6967
7379
|
}
|
|
7380
|
+
await invalidateSessionCacheForUser(config, database, id, tenantId);
|
|
6968
7381
|
return c.json({ user: normalizeUser(userWithRoles) }, 200);
|
|
6969
7382
|
};
|
|
6970
7383
|
|
|
@@ -7548,6 +7961,7 @@ import { and as and56, eq as eq61, inArray as inArray7, sql as sql28 } from "dri
|
|
|
7548
7961
|
var updateUserHandler = async (c) => {
|
|
7549
7962
|
const { id } = c.req.valid("param");
|
|
7550
7963
|
const body = c.req.valid("json");
|
|
7964
|
+
const config = c.get("config");
|
|
7551
7965
|
const database = c.get("database");
|
|
7552
7966
|
const tenantId = c.get("tenantId");
|
|
7553
7967
|
const [existing] = await database.select().from(usersInIam).where(and56(eq61(usersInIam.id, id), eq61(usersInIam.tenantId, tenantId))).limit(1);
|
|
@@ -7647,6 +8061,7 @@ var updateUserHandler = async (c) => {
|
|
|
7647
8061
|
userId: id,
|
|
7648
8062
|
tenantId
|
|
7649
8063
|
});
|
|
8064
|
+
await invalidateSessionCacheForUser(config, database, id, tenantId);
|
|
7650
8065
|
return c.json(
|
|
7651
8066
|
{
|
|
7652
8067
|
user: normalizeUser(userWithRoles ?? updated)
|
|
@@ -8225,26 +8640,14 @@ var loadSession = async ({
|
|
|
8225
8640
|
}) => {
|
|
8226
8641
|
try {
|
|
8227
8642
|
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
8228
|
-
const
|
|
8229
|
-
|
|
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) {
|
|
8643
|
+
const pair = await loadSessionPair(database, config, hashedToken);
|
|
8644
|
+
if (pair) {
|
|
8242
8645
|
setAuthContext({
|
|
8243
8646
|
c,
|
|
8244
8647
|
config,
|
|
8245
8648
|
database,
|
|
8246
|
-
user,
|
|
8247
|
-
session
|
|
8649
|
+
user: pair.user,
|
|
8650
|
+
session: pair.session
|
|
8248
8651
|
});
|
|
8249
8652
|
}
|
|
8250
8653
|
} catch {
|
|
@@ -8282,6 +8685,9 @@ var createAuthRoutes = ({
|
|
|
8282
8685
|
app.onError((error, c) => {
|
|
8283
8686
|
return handleError(error, c);
|
|
8284
8687
|
});
|
|
8688
|
+
if (config.rateLimit?.enabled) {
|
|
8689
|
+
app.use("*", createAuthRateLimitMiddleware(config));
|
|
8690
|
+
}
|
|
8285
8691
|
app.use(
|
|
8286
8692
|
"*",
|
|
8287
8693
|
createAuthMiddleware({
|
|
@@ -8504,12 +8910,9 @@ var createGetSession = (database, config) => {
|
|
|
8504
8910
|
}
|
|
8505
8911
|
try {
|
|
8506
8912
|
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
8507
|
-
const
|
|
8508
|
-
|
|
8509
|
-
hashedToken
|
|
8510
|
-
tenantId: config.tenant?.tenantId || "tenant"
|
|
8511
|
-
});
|
|
8512
|
-
if (!session) {
|
|
8913
|
+
const pair = await loadSessionPair(database, config, hashedToken);
|
|
8914
|
+
if (!pair) {
|
|
8915
|
+
await invalidateSessionCacheForHashedToken(config, hashedToken);
|
|
8513
8916
|
deleteSessionCookie(c, config);
|
|
8514
8917
|
return {
|
|
8515
8918
|
session: null,
|
|
@@ -8518,25 +8921,7 @@ var createGetSession = (database, config) => {
|
|
|
8518
8921
|
status: "invalid_session"
|
|
8519
8922
|
};
|
|
8520
8923
|
}
|
|
8521
|
-
|
|
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
|
-
}
|
|
8924
|
+
let { session, user } = pair;
|
|
8540
8925
|
const rememberMe = session.meta?.rememberMe !== false;
|
|
8541
8926
|
const updateAge = getSessionUpdateAge({
|
|
8542
8927
|
sessionConfig: config.session,
|
|
@@ -8556,12 +8941,8 @@ var createGetSession = (database, config) => {
|
|
|
8556
8941
|
setSessionCookie(c, sessionToken, config, {
|
|
8557
8942
|
expires: new Date(newExpiresAt)
|
|
8558
8943
|
});
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
user,
|
|
8562
|
-
sessionToken,
|
|
8563
|
-
status: "valid"
|
|
8564
|
-
};
|
|
8944
|
+
session = { ...session, expiresAt: newExpiresAt };
|
|
8945
|
+
await writeSessionCache(config, hashedToken, { session, user });
|
|
8565
8946
|
}
|
|
8566
8947
|
return { session, user, sessionToken, status: "valid" };
|
|
8567
8948
|
} catch {
|
|
@@ -8597,9 +8978,7 @@ var defaultAuthConfig = {
|
|
|
8597
8978
|
docs: {
|
|
8598
8979
|
enabled: true
|
|
8599
8980
|
},
|
|
8600
|
-
|
|
8601
|
-
prefix: "msb"
|
|
8602
|
-
},
|
|
8981
|
+
prefix: "msb",
|
|
8603
8982
|
session: {
|
|
8604
8983
|
expiresIn: "7d",
|
|
8605
8984
|
rememberMeExpiresIn: "30d",
|
|
@@ -8622,7 +9001,9 @@ var defaultAuthConfig = {
|
|
|
8622
9001
|
security: {
|
|
8623
9002
|
maxLoginAttempts: 5,
|
|
8624
9003
|
lockoutDuration: "15m"
|
|
8625
|
-
}
|
|
9004
|
+
},
|
|
9005
|
+
sessionCache: { enabled: false },
|
|
9006
|
+
rateLimit: { enabled: false }
|
|
8626
9007
|
};
|
|
8627
9008
|
|
|
8628
9009
|
// src/lib/cleanup.ts
|
|
@@ -8655,6 +9036,21 @@ var createMesobAuth = (authConfig) => {
|
|
|
8655
9036
|
"AUTH_SECRET is required and must be a non-empty string. Please set AUTH_SECRET environment variable."
|
|
8656
9037
|
);
|
|
8657
9038
|
}
|
|
9039
|
+
if (config.sessionCache?.enabled && !config.sessionCache.kv) {
|
|
9040
|
+
throw new Error("sessionCache.kv is required when sessionCache.enabled");
|
|
9041
|
+
}
|
|
9042
|
+
if (config.rateLimit?.enabled) {
|
|
9043
|
+
if (!config.rateLimit.kv) {
|
|
9044
|
+
throw new Error("rateLimit.kv is required when rateLimit.enabled");
|
|
9045
|
+
}
|
|
9046
|
+
if (config.rateLimit.window < 1 || config.rateLimit.max < 1) {
|
|
9047
|
+
throw new Error("rateLimit.window and rateLimit.max must be >= 1");
|
|
9048
|
+
}
|
|
9049
|
+
const ipv6s = config.rateLimit.ipv6Subnet;
|
|
9050
|
+
if (ipv6s !== void 0 && (!Number.isFinite(ipv6s) || ipv6s < 1 || ipv6s > 128)) {
|
|
9051
|
+
throw new Error("rateLimit.ipv6Subnet must be between 1 and 128");
|
|
9052
|
+
}
|
|
9053
|
+
}
|
|
8658
9054
|
const database = createDatabase(config.connectionString);
|
|
8659
9055
|
const routesApp = createAuthRoutes({ config, database });
|
|
8660
9056
|
const basePath = config.basePath || "";
|
|
@@ -8683,14 +9079,23 @@ var createMesobAuth = (authConfig) => {
|
|
|
8683
9079
|
};
|
|
8684
9080
|
};
|
|
8685
9081
|
export {
|
|
9082
|
+
AUTH_RATE_LIMIT_POST_PATHS,
|
|
8686
9083
|
cleanupExpiredData,
|
|
8687
9084
|
cleanupExpiredSessions,
|
|
8688
9085
|
cleanupExpiredVerifications,
|
|
9086
|
+
createAuthRateLimitMiddleware,
|
|
8689
9087
|
createDatabase,
|
|
8690
9088
|
createMesobAuth,
|
|
8691
9089
|
createSessionMiddleware,
|
|
8692
9090
|
createTenantMiddleware,
|
|
9091
|
+
deleteSessionCacheKeys,
|
|
9092
|
+
getSessionCookieName,
|
|
9093
|
+
getSessionKeyNamespace,
|
|
8693
9094
|
hasPermission,
|
|
8694
|
-
hasPermissionThrow
|
|
9095
|
+
hasPermissionThrow,
|
|
9096
|
+
invalidateSessionCacheForHashedToken,
|
|
9097
|
+
invalidateSessionCacheForUser,
|
|
9098
|
+
rateLimitClientKey,
|
|
9099
|
+
sessionCacheKey
|
|
8695
9100
|
};
|
|
8696
9101
|
//# sourceMappingURL=index.js.map
|