@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-CDgzxZzO.d.ts → index-v_uxUd8A.d.ts} +45 -4
- package/dist/index.d.ts +10 -3
- package/dist/index.js +651 -249
- 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 +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;
|
|
550
853
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
881
|
+
const double = s.includes("::");
|
|
882
|
+
if (double && s.split("::").length > 2) {
|
|
883
|
+
return null;
|
|
596
884
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
629
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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 (
|
|
643
|
-
return
|
|
924
|
+
if (a === "") {
|
|
925
|
+
return null;
|
|
644
926
|
}
|
|
645
|
-
if (
|
|
646
|
-
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
|
|
655
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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 (
|
|
675
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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:
|
|
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
|
|
@@ -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
|
|
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) {
|
|
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
|
|
8508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8560
|
-
|
|
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
|
-
|
|
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
|