@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 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 token = cookies?.[COOKIE_ACCESS];
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
- return sendUnauthorized(reply, "Invalid session");
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
- const [ipPeek, emailPeek] = await Promise.all([
541
- rateLimiter.peek(`rl:login:ip:${request.ip}`, { max: authRateLimitCfg.loginPerIpPerMinute, windowMs: 6e4 }),
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
- const [ipResult, emailResult] = await Promise.all([
571
- rateLimiter.check(`rl:login:ip:${request.ip}`, { max: perIpMax, windowMs: 6e4 }),
572
- rateLimiter.check(`rl:login:email:${emailNorm}`, { max: perEmailMax, windowMs: 6e4 })
573
- ]);
574
- if (!ipResult.allowed || !emailResult.allowed) {
575
- const retryAfter = !ipResult.allowed ? ipResult.retryAfterSec : !emailResult.allowed ? emailResult.retryAfterSec : 60;
576
- reply.header("Retry-After", String(retryAfter));
577
- return reply.code(429).send({ error: "Too Many Requests", message: "Rate limit exceeded", retryAfterSec: retryAfter });
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?.sessionAccessTtlSec ?? (process.env.AUTH_ACCESS_TTL_SEC ? parseInt(process.env.AUTH_ACCESS_TTL_SEC, 10) : 900);
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.");