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