@kb-labs/gateway-app 2.104.0 → 2.105.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +115 -25
- package/dist/index.js.map +1 -1
- package/package.json +42 -42
package/dist/index.js
CHANGED
|
@@ -85,6 +85,10 @@ var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
|
|
|
85
85
|
"/auth/refresh",
|
|
86
86
|
// User-auth public endpoints (ADR-0020, Phase 1.16).
|
|
87
87
|
"/auth/login",
|
|
88
|
+
// CLI-only analogues of /auth/login and the cookie user-refresh path —
|
|
89
|
+
// return tokens in the body instead of cookies, see user-routes.ts.
|
|
90
|
+
"/auth/login/cli",
|
|
91
|
+
"/auth/refresh/cli",
|
|
88
92
|
"/auth/activate",
|
|
89
93
|
"/auth/providers",
|
|
90
94
|
"/internal/dispatch",
|
|
@@ -256,13 +260,18 @@ var sendUnauthorized = (reply, message) => reply.code(401).send({ error: "Unauth
|
|
|
256
260
|
var createUserAuthMiddleware = (deps) => {
|
|
257
261
|
return async function userAuthMiddleware(request, reply) {
|
|
258
262
|
const cookies = request.cookies;
|
|
259
|
-
const
|
|
263
|
+
const cookieToken = cookies?.[COOKIE_ACCESS];
|
|
264
|
+
const bearerToken = cookieToken ? void 0 : extractBearerToken(request.headers.authorization);
|
|
265
|
+
const token = cookieToken ?? bearerToken;
|
|
260
266
|
if (!token) {
|
|
261
267
|
return;
|
|
262
268
|
}
|
|
263
269
|
const payload = await verifyUserAccessToken(token, deps.jwtConfig);
|
|
264
270
|
if (!payload) {
|
|
265
|
-
|
|
271
|
+
if (cookieToken) {
|
|
272
|
+
return sendUnauthorized(reply, "Invalid session");
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
266
275
|
}
|
|
267
276
|
const hostHeader = request.headers["host"];
|
|
268
277
|
const resolvedTenant = deps.tenantResolver.resolve(
|
|
@@ -481,6 +490,32 @@ function createUserRefreshFn(deps) {
|
|
|
481
490
|
}
|
|
482
491
|
};
|
|
483
492
|
}
|
|
493
|
+
async function peekLoginRateLimit(reply, ip, emailNorm, rateLimiter, cfg) {
|
|
494
|
+
const [ipPeek, emailPeek] = await Promise.all([
|
|
495
|
+
rateLimiter.peek(`rl:login:ip:${ip}`, { max: cfg.loginPerIpPerMinute, windowMs: 6e4 }),
|
|
496
|
+
rateLimiter.peek(`rl:login:email:${emailNorm}`, { max: cfg.loginPerEmailPerMinute, windowMs: 6e4 })
|
|
497
|
+
]);
|
|
498
|
+
if (!ipPeek.allowed || !emailPeek.allowed) {
|
|
499
|
+
const retryAfter = !ipPeek.allowed ? ipPeek.retryAfterSec : !emailPeek.allowed ? emailPeek.retryAfterSec : 60;
|
|
500
|
+
reply.header("Retry-After", String(retryAfter));
|
|
501
|
+
void reply.code(429).send({ error: "Too Many Requests", message: "Rate limit exceeded", retryAfterSec: retryAfter });
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
async function recordLoginFailure(reply, ip, emailNorm, rateLimiter, cfg) {
|
|
507
|
+
const [ipResult, emailResult] = await Promise.all([
|
|
508
|
+
rateLimiter.check(`rl:login:ip:${ip}`, { max: cfg.loginPerIpPerMinute, windowMs: 6e4 }),
|
|
509
|
+
rateLimiter.check(`rl:login:email:${emailNorm}`, { max: cfg.loginPerEmailPerMinute, windowMs: 6e4 })
|
|
510
|
+
]);
|
|
511
|
+
if (!ipResult.allowed || !emailResult.allowed) {
|
|
512
|
+
const retryAfter = !ipResult.allowed ? ipResult.retryAfterSec : !emailResult.allowed ? emailResult.retryAfterSec : 60;
|
|
513
|
+
reply.header("Retry-After", String(retryAfter));
|
|
514
|
+
void reply.code(429).send({ error: "Too Many Requests", message: "Rate limit exceeded", retryAfterSec: retryAfter });
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
484
519
|
function registerUserAuthRoutes(app, deps) {
|
|
485
520
|
const { userAuthService, users, sessions, invites, providers, pdp, tenantResolver, cookieOpts, inviteTtlMs, rateLimiter, authRateLimit, oauthState, oauthCallbackPerIpPerMinute } = deps;
|
|
486
521
|
const authRateLimitCfg = { loginPerIpPerMinute: 10, loginPerEmailPerMinute: 5, ...authRateLimit };
|
|
@@ -537,14 +572,8 @@ function registerUserAuthRoutes(app, deps) {
|
|
|
537
572
|
const tenantId = hostTenant ?? bodyTenantId ?? "";
|
|
538
573
|
if (rateLimiter) {
|
|
539
574
|
const emailNorm = email.toLowerCase().trim();
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
rateLimiter.peek(`rl:login:email:${emailNorm}`, { max: authRateLimitCfg.loginPerEmailPerMinute, windowMs: 6e4 })
|
|
543
|
-
]);
|
|
544
|
-
if (!ipPeek.allowed || !emailPeek.allowed) {
|
|
545
|
-
const retryAfter = !ipPeek.allowed ? ipPeek.retryAfterSec : !emailPeek.allowed ? emailPeek.retryAfterSec : 60;
|
|
546
|
-
reply.header("Retry-After", String(retryAfter));
|
|
547
|
-
return reply.code(429).send({ error: "Too Many Requests", message: "Rate limit exceeded", retryAfterSec: retryAfter });
|
|
575
|
+
if (!await peekLoginRateLimit(reply, request.ip, emailNorm, rateLimiter, authRateLimitCfg)) {
|
|
576
|
+
return;
|
|
548
577
|
}
|
|
549
578
|
}
|
|
550
579
|
try {
|
|
@@ -564,17 +593,53 @@ function registerUserAuthRoutes(app, deps) {
|
|
|
564
593
|
} catch (err) {
|
|
565
594
|
if (err instanceof AuthError) {
|
|
566
595
|
if (rateLimiter) {
|
|
567
|
-
const perIpMax = authRateLimitCfg.loginPerIpPerMinute;
|
|
568
|
-
const perEmailMax = authRateLimitCfg.loginPerEmailPerMinute;
|
|
569
596
|
const emailNorm = email.toLowerCase().trim();
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
597
|
+
if (!await recordLoginFailure(reply, request.ip, emailNorm, rateLimiter, authRateLimitCfg)) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return reply.code(401).send({ error: "invalid_credentials" });
|
|
602
|
+
}
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
app.post("/auth/login/cli", {
|
|
607
|
+
schema: { tags: ["Auth"], summary: "Login with email/password (CLI: returns tokens in body)" }
|
|
608
|
+
}, async (request, reply) => {
|
|
609
|
+
const { email, password, providerId, tenantId: bodyTenantId } = request.body ?? {};
|
|
610
|
+
if (typeof email !== "string" || !email) {
|
|
611
|
+
return reply.code(400).send({ error: "Bad Request", message: "email is required" });
|
|
612
|
+
}
|
|
613
|
+
if (typeof password !== "string" || !password) {
|
|
614
|
+
return reply.code(400).send({ error: "Bad Request", message: "password is required" });
|
|
615
|
+
}
|
|
616
|
+
const host = typeof request.headers.host === "string" ? request.headers.host : "";
|
|
617
|
+
const hostTenant = tenantResolver.resolve(host);
|
|
618
|
+
const tenantId = hostTenant ?? bodyTenantId ?? "";
|
|
619
|
+
if (rateLimiter) {
|
|
620
|
+
const emailNorm = email.toLowerCase().trim();
|
|
621
|
+
if (!await peekLoginRateLimit(reply, request.ip, emailNorm, rateLimiter, authRateLimitCfg)) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const result = await userAuthService.login(
|
|
627
|
+
{ providerId: providerId ?? "email-password", input: { email, password } },
|
|
628
|
+
tenantId,
|
|
629
|
+
{ ip: request.ip, userAgent: request.headers["user-agent"] }
|
|
630
|
+
);
|
|
631
|
+
return reply.send({
|
|
632
|
+
accessToken: result.access.token,
|
|
633
|
+
refreshToken: result.refresh.token,
|
|
634
|
+
expiresIn: accessTtl2(result),
|
|
635
|
+
tenantId
|
|
636
|
+
});
|
|
637
|
+
} catch (err) {
|
|
638
|
+
if (err instanceof AuthError) {
|
|
639
|
+
if (rateLimiter) {
|
|
640
|
+
const emailNorm = email.toLowerCase().trim();
|
|
641
|
+
if (!await recordLoginFailure(reply, request.ip, emailNorm, rateLimiter, authRateLimitCfg)) {
|
|
642
|
+
return;
|
|
578
643
|
}
|
|
579
644
|
}
|
|
580
645
|
return reply.code(401).send({ error: "invalid_credentials" });
|
|
@@ -582,6 +647,27 @@ function registerUserAuthRoutes(app, deps) {
|
|
|
582
647
|
throw err;
|
|
583
648
|
}
|
|
584
649
|
});
|
|
650
|
+
app.post("/auth/refresh/cli", {
|
|
651
|
+
schema: { tags: ["Auth"], summary: "Refresh a CLI session token pair" }
|
|
652
|
+
}, async (request, reply) => {
|
|
653
|
+
const { refreshToken } = request.body ?? {};
|
|
654
|
+
if (typeof refreshToken !== "string" || !refreshToken) {
|
|
655
|
+
return reply.code(400).send({ error: "Bad Request", message: "refreshToken is required" });
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const result = await userAuthService.refresh(refreshToken);
|
|
659
|
+
return reply.send({
|
|
660
|
+
accessToken: result.access.token,
|
|
661
|
+
refreshToken: result.refresh.token,
|
|
662
|
+
expiresIn: accessTtl2(result)
|
|
663
|
+
});
|
|
664
|
+
} catch (err) {
|
|
665
|
+
if (err instanceof AuthError) {
|
|
666
|
+
return reply.code(401).send({ error: "Unauthorized", message: "Invalid or expired refresh token" });
|
|
667
|
+
}
|
|
668
|
+
throw err;
|
|
669
|
+
}
|
|
670
|
+
});
|
|
585
671
|
app.post("/auth/logout", {
|
|
586
672
|
schema: { tags: ["Auth"], summary: "Logout and clear session cookies" }
|
|
587
673
|
}, async (request, reply) => {
|
|
@@ -4164,11 +4250,7 @@ async function bootstrap(repoRoot = process.cwd()) {
|
|
|
4164
4250
|
}
|
|
4165
4251
|
let userAuth;
|
|
4166
4252
|
{
|
|
4167
|
-
const accessTtlSec = config.auth
|
|
4168
|
-
const refreshTtlSec = config.auth?.sessionRefreshTtlSec ?? (process.env.AUTH_REFRESH_TTL_SEC ? parseInt(process.env.AUTH_REFRESH_TTL_SEC, 10) : 30 * 24 * 3600);
|
|
4169
|
-
const graceWindowMs = (config.auth?.refreshGraceWindowSec ?? 5) * 1e3;
|
|
4170
|
-
const bcryptCost = config.auth?.bcryptCost ?? 12;
|
|
4171
|
-
const cookieSecure = config.auth?.cookieSecure ?? (process.env.AUTH_COOKIE_SECURE === "false" ? false : true);
|
|
4253
|
+
const { accessTtlSec, refreshTtlSec, graceWindowMs, bcryptCost, cookieSecure } = resolveAuthRuntimeConfig(config.auth, process.env);
|
|
4172
4254
|
const tenantPattern = config.tenants?.pattern ?? "{tenant}.kblabs.ru";
|
|
4173
4255
|
const bootstrapTenantId = config.auth?.bootstrap?.tenantId ?? process.env.GATEWAY_BOOTSTRAP_TENANT_ID ?? "kblabs-cloud";
|
|
4174
4256
|
const users = new UsersStore(docs);
|
|
@@ -4362,6 +4444,14 @@ async function bootstrap(repoRoot = process.cwd()) {
|
|
|
4362
4444
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
4363
4445
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
4364
4446
|
}
|
|
4447
|
+
function resolveAuthRuntimeConfig(authConfig, env) {
|
|
4448
|
+
const accessTtlSec = env.AUTH_ACCESS_TTL_SEC ? parseInt(env.AUTH_ACCESS_TTL_SEC, 10) : authConfig?.sessionAccessTtlSec ?? 900;
|
|
4449
|
+
const refreshTtlSec = env.AUTH_REFRESH_TTL_SEC ? parseInt(env.AUTH_REFRESH_TTL_SEC, 10) : authConfig?.sessionRefreshTtlSec ?? 30 * 24 * 3600;
|
|
4450
|
+
const graceWindowMs = (authConfig?.refreshGraceWindowSec ?? 5) * 1e3;
|
|
4451
|
+
const bcryptCost = authConfig?.bcryptCost ?? 12;
|
|
4452
|
+
const cookieSecure = env.AUTH_COOKIE_SECURE === "false" ? false : authConfig?.cookieSecure ?? true;
|
|
4453
|
+
return { accessTtlSec, refreshTtlSec, graceWindowMs, bcryptCost, cookieSecure };
|
|
4454
|
+
}
|
|
4365
4455
|
function isLoopbackHost(host) {
|
|
4366
4456
|
const h = host.trim().toLowerCase();
|
|
4367
4457
|
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.startsWith("127.");
|