@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 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,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
- 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 ?? 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?.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);
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.");