@kb-labs/gateway-app 2.104.0 → 2.106.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 +119 -27
- 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,8 +490,34 @@ 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
|
-
const { userAuthService, users, sessions, invites, providers, pdp, tenantResolver, cookieOpts, inviteTtlMs, rateLimiter, authRateLimit, oauthState, oauthCallbackPerIpPerMinute } = deps;
|
|
520
|
+
const { userAuthService, users, sessions, invites, providers, pdp, tenantResolver, bootstrapTenantId, cookieOpts, inviteTtlMs, rateLimiter, authRateLimit, oauthState, oauthCallbackPerIpPerMinute } = deps;
|
|
486
521
|
const authRateLimitCfg = { loginPerIpPerMinute: 10, loginPerEmailPerMinute: 5, ...authRateLimit };
|
|
487
522
|
if (oauthState) {
|
|
488
523
|
registerOAuthRoutes(app, {
|
|
@@ -534,17 +569,11 @@ function registerUserAuthRoutes(app, deps) {
|
|
|
534
569
|
}
|
|
535
570
|
const host = typeof request.headers.host === "string" ? request.headers.host : "";
|
|
536
571
|
const hostTenant = tenantResolver.resolve(host);
|
|
537
|
-
const tenantId = hostTenant ?? bodyTenantId ??
|
|
572
|
+
const tenantId = hostTenant ?? bodyTenantId ?? bootstrapTenantId;
|
|
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 ?? bootstrapTenantId;
|
|
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) => {
|
|
@@ -3879,6 +3965,7 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
|
|
|
3879
3965
|
providers: userAuth.providers,
|
|
3880
3966
|
pdp: userAuth.pdp,
|
|
3881
3967
|
tenantResolver: userAuth.tenantResolver,
|
|
3968
|
+
bootstrapTenantId: userAuth.bootstrapTenantId,
|
|
3882
3969
|
cookieOpts: { cookieSecure: userAuth.cookieSecure },
|
|
3883
3970
|
accessTtlSec: userAuth.accessTtlSec,
|
|
3884
3971
|
refreshTtlSec: userAuth.refreshTtlSec,
|
|
@@ -4164,11 +4251,7 @@ async function bootstrap(repoRoot = process.cwd()) {
|
|
|
4164
4251
|
}
|
|
4165
4252
|
let userAuth;
|
|
4166
4253
|
{
|
|
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);
|
|
4254
|
+
const { accessTtlSec, refreshTtlSec, graceWindowMs, bcryptCost, cookieSecure } = resolveAuthRuntimeConfig(config.auth, process.env);
|
|
4172
4255
|
const tenantPattern = config.tenants?.pattern ?? "{tenant}.kblabs.ru";
|
|
4173
4256
|
const bootstrapTenantId = config.auth?.bootstrap?.tenantId ?? process.env.GATEWAY_BOOTSTRAP_TENANT_ID ?? "kblabs-cloud";
|
|
4174
4257
|
const users = new UsersStore(docs);
|
|
@@ -4243,6 +4326,7 @@ async function bootstrap(repoRoot = process.cwd()) {
|
|
|
4243
4326
|
providers,
|
|
4244
4327
|
pdp,
|
|
4245
4328
|
tenantResolver,
|
|
4329
|
+
bootstrapTenantId,
|
|
4246
4330
|
cookieSecure,
|
|
4247
4331
|
accessTtlSec,
|
|
4248
4332
|
refreshTtlSec,
|
|
@@ -4362,6 +4446,14 @@ async function bootstrap(repoRoot = process.cwd()) {
|
|
|
4362
4446
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
4363
4447
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
4364
4448
|
}
|
|
4449
|
+
function resolveAuthRuntimeConfig(authConfig, env) {
|
|
4450
|
+
const accessTtlSec = env.AUTH_ACCESS_TTL_SEC ? parseInt(env.AUTH_ACCESS_TTL_SEC, 10) : authConfig?.sessionAccessTtlSec ?? 900;
|
|
4451
|
+
const refreshTtlSec = env.AUTH_REFRESH_TTL_SEC ? parseInt(env.AUTH_REFRESH_TTL_SEC, 10) : authConfig?.sessionRefreshTtlSec ?? 30 * 24 * 3600;
|
|
4452
|
+
const graceWindowMs = (authConfig?.refreshGraceWindowSec ?? 5) * 1e3;
|
|
4453
|
+
const bcryptCost = authConfig?.bcryptCost ?? 12;
|
|
4454
|
+
const cookieSecure = env.AUTH_COOKIE_SECURE === "false" ? false : authConfig?.cookieSecure ?? true;
|
|
4455
|
+
return { accessTtlSec, refreshTtlSec, graceWindowMs, bcryptCost, cookieSecure };
|
|
4456
|
+
}
|
|
4365
4457
|
function isLoopbackHost(host) {
|
|
4366
4458
|
const h = host.trim().toLowerCase();
|
|
4367
4459
|
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.startsWith("127.");
|