@kb-labs/gateway-app 2.119.1-canary.ee03bc25d → 2.120.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
@@ -4,7 +4,7 @@ import { createInMemoryDocumentDatabase, createInMemoryKVStore } from '@kb-labs/
4
4
  import { makeAssemblyHook } from '@kb-labs/plugin-runtime';
5
5
  import { runService } from '@kb-labs/shared-daemon';
6
6
  import { HostStore, globalDispatcher, AdaptiveBuffer } from '@kb-labs/gateway-core';
7
- import { UsersStore, CredentialsStore, MembershipsStore, SessionsStore, InvitesStore, loadIdentityProviders, createPasswordPolicy, createStubPDP, createTenantResolver, createUserAuthService, ensureBootstrapAdmin, createRateLimiter, OAuthStateStore, AuthService, ensureBootstrapCliCredentials, verifyUserAccessToken, AuthError, getClientByHandle, issueCsrfToken, verifyCsrfToken, getClientByHostId } from '@kb-labs/gateway-auth';
7
+ import { UsersStore, CredentialsStore, MembershipsStore, SessionsStore, InvitesStore, loadIdentityProviders, createPasswordPolicy, createStubPDP, createTenantResolver, createUserAuthService, ensureBootstrapAdmin, createRateLimiter, OAuthStateStore, AuthService, ensureBootstrapCliCredentials, verifyUserAccessToken, AuthError, getClientByHandle, issueCsrfToken, verifyCsrfToken, getClientByHostId, evaluateAuthReadiness } from '@kb-labs/gateway-auth';
8
8
  import { createRegistry, mergeOpenAPISpecs } from '@kb-labs/core-registry';
9
9
  import { loadEffectiveConfig } from '@kb-labs/core-config';
10
10
  import { GatewayConfigSchema, HostRegistrationSchema, RegisterRequestSchema, TokenRequestSchema, RefreshRequestSchema, ExecuteRequestSchema, ChatCompletionRequestSchema, TelemetryIngestRequestSchema, PlatformCallRequestSchema, HelloMessageSchema, SUPPORTED_PROTOCOL_VERSIONS, HostCapabilitySchema, AdapterCallMessageSchema, AdapterNameSchema, ClientHelloSchema, ClientCancelSchema, ClientUnsubscribeSchema, ClientSubscribeSchema, CLIENT_PROTOCOL_VERSION } from '@kb-labs/gateway-contracts';
@@ -30,6 +30,19 @@ async function loadGatewayConfig(repoRoot, platformRoot) {
30
30
  }
31
31
  return GatewayConfigSchema.parse({});
32
32
  }
33
+
34
+ // src/access.ts
35
+ var DEPLOYED_BIND_HOST = "0.0.0.0";
36
+ var LOCAL_BIND_HOST = "127.0.0.1";
37
+ var resolveAccess = (config) => {
38
+ const local = config.access?.mode === "local";
39
+ return {
40
+ authEnabled: config.auth?.enabled ?? !local,
41
+ bindHost: config.host ?? (local ? LOCAL_BIND_HOST : DEPLOYED_BIND_HOST)
42
+ };
43
+ };
44
+ var DEFAULT_BOOTSTRAP_TENANT = "kblabs-cloud";
45
+ var resolveBootstrapTenantId = (config, env) => config.auth?.bootstrap?.tenantId ?? env.GATEWAY_BOOTSTRAP_TENANT_ID ?? DEFAULT_BOOTSTRAP_TENANT;
33
46
  async function resolveToken(token, cache, jwtConfig) {
34
47
  const authService = new AuthService(cache, jwtConfig);
35
48
  const jwtContext = await authService.verify(token);
@@ -69,6 +82,8 @@ var LOCAL_ADMIN_CONTEXT = {
69
82
  var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
70
83
  "/health",
71
84
  "/health/adapters",
85
+ // Self-gated to local, non-proxied requests inside the handler (see health-route.ts).
86
+ "/health/auth",
72
87
  "/ready",
73
88
  // Studio SPA shell (index.html) — the login page itself. There is no
74
89
  // session yet when a browser first hits this, so it can't be gated.
@@ -138,6 +153,46 @@ function createAuthMiddleware(cache, jwtConfig, options = {}) {
138
153
  };
139
154
  }
140
155
 
156
+ // src/auth/health-route.ts
157
+ var FORWARDING_HEADERS = ["x-forwarded-for", "x-real-ip", "forwarded", "x-forwarded-host", "x-forwarded-proto"];
158
+ var LOOPBACK_IPV4 = /^127(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
159
+ var isLoopbackIp = (ip) => {
160
+ const a = ip.toLowerCase();
161
+ return a === "::1" || LOOPBACK_IPV4.test(a) || a.startsWith("::ffff:") && LOOPBACK_IPV4.test(a.slice(7));
162
+ };
163
+ var isLoopbackAddress = (address) => address !== void 0 && isLoopbackIp(address);
164
+ var hostnameOf = (hostHeader) => {
165
+ const h = hostHeader.trim().toLowerCase();
166
+ if (h.startsWith("[")) {
167
+ return h.slice(1, h.indexOf("]"));
168
+ }
169
+ const colon = h.indexOf(":");
170
+ return colon === -1 ? h : h.slice(0, colon);
171
+ };
172
+ var isLoopbackHostname = (hostname2) => hostname2 === "localhost" || isLoopbackIp(hostname2);
173
+ var isLocalOperatorRequest = (request) => {
174
+ if (!isLoopbackAddress(request.socket.remoteAddress)) {
175
+ return false;
176
+ }
177
+ if (FORWARDING_HEADERS.some((name) => request.headers[name] !== void 0)) {
178
+ return false;
179
+ }
180
+ const host = request.headers.host;
181
+ return typeof host === "string" && isLoopbackHostname(hostnameOf(host));
182
+ };
183
+ var registerAuthHealthRoute = (app, getReadiness) => {
184
+ app.get(
185
+ "/health/auth",
186
+ { schema: { tags: ["System"], summary: "Auth readiness (local operator only)" } },
187
+ async (request, reply) => {
188
+ if (!isLocalOperatorRequest(request)) {
189
+ return reply.code(404).send({ error: "Not Found" });
190
+ }
191
+ return getReadiness();
192
+ }
193
+ );
194
+ };
195
+
141
196
  // src/auth/user-cookies.ts
142
197
  var COOKIE_ACCESS = "kb_access";
143
198
  var COOKIE_REFRESH = "kb_refresh";
@@ -1870,12 +1925,17 @@ function registerAggregatedDocsRoutes(app, config, serviceTransport, cache) {
1870
1925
  (serviceId) => serviceTransport.call(serviceId, {
1871
1926
  path: "/openapi.json",
1872
1927
  signal: AbortSignal.timeout(3e3)
1873
- }).then((r) => r.payload)
1928
+ }).then((r) => {
1929
+ if (!r.ok) {
1930
+ throw new Error(`${serviceId} /openapi.json returned ${r.statusCode}`);
1931
+ }
1932
+ return r.payload;
1933
+ })
1874
1934
  )
1875
1935
  );
1876
1936
  const specs = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
1877
1937
  const merged = mergeOpenAPISpecs(specs);
1878
- if (cache) {
1938
+ if (cache && specs.length > 0) {
1879
1939
  try {
1880
1940
  await cache.set(MERGED_CACHE_KEY, merged, MERGED_CACHE_TTL);
1881
1941
  } catch {
@@ -3941,7 +4001,7 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
3941
4001
  app.addHook(
3942
4002
  "onRequest",
3943
4003
  createAuthMiddleware(cache, jwtConfig, {
3944
- authEnabled: config.auth?.enabled !== false
4004
+ authEnabled: resolveAccess(config).authEnabled
3945
4005
  })
3946
4006
  );
3947
4007
  const socketWsUpstreams = [];
@@ -4151,6 +4211,9 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
4151
4211
  return getAdapterStatus();
4152
4212
  }
4153
4213
  );
4214
+ if (userAuth?.authReadiness) {
4215
+ registerAuthHealthRoute(scope, userAuth.authReadiness);
4216
+ }
4154
4217
  scope.get(
4155
4218
  "/ready",
4156
4219
  { schema: { tags: ["System"], summary: "Gateway readiness check" } },
@@ -4428,6 +4491,7 @@ async function startGateway({
4428
4491
  const logger = serviceLogger.forComponent("gateway-bootstrap").forOperation("gateway.bootstrap");
4429
4492
  logger.info("Platform initialized", { projectRoot, platformRoot });
4430
4493
  const config = await loadGatewayConfig(projectRoot, platformRoot);
4494
+ const access = resolveAccess(config);
4431
4495
  logger.info("Gateway config loaded", {
4432
4496
  port: config.port,
4433
4497
  upstreams: Object.keys(config.upstreams),
@@ -4456,7 +4520,7 @@ async function startGateway({
4456
4520
  cookieSecure
4457
4521
  } = resolveAuthRuntimeConfig(config.auth, process.env);
4458
4522
  const tenantPattern = config.tenants?.pattern ?? "{tenant}.kblabs.ru";
4459
- const bootstrapTenantId = config.auth?.bootstrap?.tenantId ?? process.env.GATEWAY_BOOTSTRAP_TENANT_ID ?? "kblabs-cloud";
4523
+ const bootstrapTenantId = resolveBootstrapTenantId(config, process.env);
4460
4524
  const users = new UsersStore(docs);
4461
4525
  const credentials = new CredentialsStore(docs);
4462
4526
  const memberships = new MembershipsStore(docs);
@@ -4496,7 +4560,7 @@ async function startGateway({
4496
4560
  });
4497
4561
  const adminEmail = config.auth?.bootstrap?.adminEmail ?? process.env.GATEWAY_BOOTSTRAP_ADMIN_EMAIL;
4498
4562
  const adminPassword = process.env.GATEWAY_BOOTSTRAP_ADMIN_PASSWORD;
4499
- await ensureBootstrapAdmin({
4563
+ const bootstrapStatus = await ensureBootstrapAdmin({
4500
4564
  bootstrap: adminEmail && adminPassword ? { adminEmail, adminPassword, tenantId: bootstrapTenantId } : void 0,
4501
4565
  users,
4502
4566
  credentials,
@@ -4504,9 +4568,12 @@ async function startGateway({
4504
4568
  bcryptCost,
4505
4569
  logger
4506
4570
  }).catch((err) => {
4507
- logger.warn("Bootstrap admin seed failed (non-fatal)", {
4508
- error: err instanceof Error ? err.message : String(err)
4509
- });
4571
+ logger.error(
4572
+ "Bootstrap admin seed failed (non-fatal)",
4573
+ err instanceof Error ? err : new Error(String(err)),
4574
+ { tenantId: bootstrapTenantId }
4575
+ );
4576
+ return "failed";
4510
4577
  });
4511
4578
  const inviteTtlMs = process.env.AUTH_INVITE_TTL_MS ? parseInt(process.env.AUTH_INVITE_TTL_MS, 10) : config.auth?.inviteTtlMs ?? 7 * 24 * 60 * 60 * 1e3;
4512
4579
  const kv = platform5.getAdapter("kvStore") ?? createInMemoryKVStore();
@@ -4525,6 +4592,16 @@ async function startGateway({
4525
4592
  "OAuth requires a shared kvStore (Redis) in multi-process/HA; in-memory state is per-process and callbacks may land on a different worker"
4526
4593
  );
4527
4594
  }
4595
+ const authReadiness = () => evaluateAuthReadiness({
4596
+ authEnabled: access.authEnabled,
4597
+ loopbackOnly: isLoopbackHost(access.bindHost),
4598
+ tenantId: bootstrapTenantId,
4599
+ jwtSecretIsDefault: !process.env.GATEWAY_JWT_SECRET,
4600
+ bootstrap: { status: bootstrapStatus, email: adminEmail },
4601
+ users,
4602
+ memberships
4603
+ });
4604
+ await logAuthReadiness(authReadiness, logger);
4528
4605
  userAuth = {
4529
4606
  userAuthService,
4530
4607
  users,
@@ -4540,7 +4617,8 @@ async function startGateway({
4540
4617
  inviteTtlMs,
4541
4618
  rateLimiter,
4542
4619
  authRateLimit: { loginPerIpPerMinute, loginPerEmailPerMinute },
4543
- oauthState
4620
+ oauthState,
4621
+ authReadiness
4544
4622
  };
4545
4623
  logger.info("User auth infrastructure initialised", {
4546
4624
  tenantPattern,
@@ -4648,8 +4726,8 @@ async function startGateway({
4648
4726
  userAuth,
4649
4727
  webhookManifests
4650
4728
  );
4651
- const bindHost = config.host ?? "0.0.0.0";
4652
- if (config.auth?.enabled === false && !isLoopbackHost(bindHost)) {
4729
+ const { authEnabled, bindHost } = access;
4730
+ if (!authEnabled && !isLoopbackHost(bindHost)) {
4653
4731
  throw new Error(
4654
4732
  `Refusing to start: auth is disabled but the gateway binds to "${bindHost}" (not loopback). A no-auth platform reachable on the network grants full access to anyone. Either set gateway.auth.enabled = true, or bind to 127.0.0.1 (gateway.host) for solo/local use.`
4655
4733
  );
@@ -4659,7 +4737,7 @@ async function startGateway({
4659
4737
  const address = await server.listen({ port: listenPort, host: bindHost });
4660
4738
  logger.info("Gateway listening", {
4661
4739
  address,
4662
- authEnabled: config.auth?.enabled !== false
4740
+ authEnabled
4663
4741
  });
4664
4742
  return async () => {
4665
4743
  await server.close();
@@ -4684,6 +4762,29 @@ function isLoopbackHost(host) {
4684
4762
  const h = host.trim().toLowerCase();
4685
4763
  return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.startsWith("127.");
4686
4764
  }
4765
+ async function logAuthReadiness(evaluate, logger) {
4766
+ try {
4767
+ const readiness = await evaluate();
4768
+ for (const issue of readiness.issues) {
4769
+ const meta = { code: issue.code, hint: issue.hint };
4770
+ if (issue.severity === "error") {
4771
+ logger.error(`auth-readiness: ${issue.message}`, void 0, meta);
4772
+ } else {
4773
+ logger.warn(`auth-readiness: ${issue.message}`, meta);
4774
+ }
4775
+ }
4776
+ if (readiness.ok && readiness.issues.length === 0) {
4777
+ logger.info("auth-readiness: ok", {
4778
+ authEnabled: readiness.authEnabled,
4779
+ activeAdmins: readiness.activeAdmins
4780
+ });
4781
+ }
4782
+ } catch (err) {
4783
+ logger.warn("auth-readiness: check failed", {
4784
+ error: err instanceof Error ? err.message : String(err)
4785
+ });
4786
+ }
4787
+ }
4687
4788
 
4688
4789
  // src/index.ts
4689
4790
  bootstrap(process.cwd()).catch(() => {