@kb-labs/gateway-app 2.112.0 → 2.116.11

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
@@ -1,8 +1,7 @@
1
1
  import { logDiagnosticEvent } from '@kb-labs/core-platform';
2
2
  import { createInMemoryDocumentDatabase, createInMemoryKVStore } from '@kb-labs/core-platform/inmemory';
3
- import { createServiceBootstrap, platform, getPlatformRoot, getProjectRoot, getAdapterStatus } from '@kb-labs/core-runtime';
4
3
  import { makeAssemblyHook } from '@kb-labs/plugin-runtime';
5
- import { createCorrelatedLogger, registerOpenAPI, createServiceReadyResponse, OperationMetricsTracker, createServiceObservabilityDescribe, createServiceObservabilityHealth } from '@kb-labs/shared-http';
4
+ import { runService } from '@kb-labs/shared-daemon';
6
5
  import { HostStore, globalDispatcher, AdaptiveBuffer } from '@kb-labs/gateway-core';
7
6
  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';
8
7
  import { createRegistry, mergeOpenAPISpecs } from '@kb-labs/core-registry';
@@ -12,6 +11,8 @@ import Fastify from 'fastify';
12
11
  import fastifyCookie from '@fastify/cookie';
13
12
  import fastifyCors from '@fastify/cors';
14
13
  import fastifyHttpProxy from '@fastify/http-proxy';
14
+ import { platform, getAdapterStatus } from '@kb-labs/core-runtime';
15
+ import { createHttpLogger, registerOpenAPI, createServiceReadyResponse, OperationMetricsTracker, createServiceObservabilityDescribe, createServiceObservabilityHealth } from '@kb-labs/shared-http';
15
16
  import { PERMISSIONS, CANONICAL_OBSERVABILITY_METRICS, OBSERVABILITY_CONTRACT_VERSION, OBSERVABILITY_SCHEMA } from '@kb-labs/core-contracts';
16
17
  import { randomBytes, randomUUID, createHmac, timingSafeEqual } from 'crypto';
17
18
  import { classifyFailure } from '@kb-labs/core-retry';
@@ -3819,11 +3820,10 @@ function redactQueryToken(url) {
3819
3820
  return url.replace(/([?&]access_token=)[^&]*/gi, "$1[REDACTED]");
3820
3821
  }
3821
3822
  async function createServer(config, cache, logger, jwtConfig, registry, serviceTransport, userAuth, webhookManifests) {
3822
- const gatewayLogger = createCorrelatedLogger(logger, {
3823
+ const gatewayLogger = createHttpLogger(logger, {
3823
3824
  serviceId: "gateway",
3824
- logsSource: "gateway",
3825
- layer: "gateway",
3826
- service: "server",
3825
+ layer: "service",
3826
+ component: "gateway-server",
3827
3827
  operation: "gateway.http"
3828
3828
  });
3829
3829
  const app = Fastify({
@@ -3855,27 +3855,25 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
3855
3855
  reply.header("X-Request-Id", requestId);
3856
3856
  reply.header("X-Trace-Id", traceId);
3857
3857
  const safeUrl = redactQueryToken(request.url);
3858
- request.kbLogger = createCorrelatedLogger(logger, {
3858
+ request.kbLogger = createHttpLogger(logger, {
3859
3859
  serviceId: "gateway",
3860
- logsSource: "gateway",
3861
- layer: "gateway",
3862
- service: "request",
3860
+ layer: "service",
3861
+ component: "http-request",
3863
3862
  requestId,
3864
3863
  traceId,
3865
3864
  method: request.method,
3866
3865
  url: safeUrl,
3867
3866
  operation: "http.request"
3868
3867
  });
3869
- request.kbLogger.info(`\u2192 ${request.method.toUpperCase()} ${safeUrl}`);
3868
+ request.kbLogger.debug("HTTP request started");
3870
3869
  });
3871
3870
  app.addHook("onResponse", async (request, reply) => {
3872
3871
  const requestLogger = request.kbLogger;
3873
3872
  if (!requestLogger) {
3874
3873
  return;
3875
3874
  }
3876
- const safeUrl = redactQueryToken(request.url);
3877
- requestLogger.info(`\u2713 ${request.method.toUpperCase()} ${safeUrl} ${reply.statusCode}`, {
3878
- statusCode: reply.statusCode
3875
+ requestLogger.info("HTTP request completed", {
3876
+ "http.status_code": reply.statusCode
3879
3877
  });
3880
3878
  });
3881
3879
  if (userAuth) {
@@ -3890,7 +3888,9 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
3890
3888
  }
3891
3889
  app.addHook(
3892
3890
  "onRequest",
3893
- createAuthMiddleware(cache, jwtConfig, { authEnabled: config.auth?.enabled !== false })
3891
+ createAuthMiddleware(cache, jwtConfig, {
3892
+ authEnabled: config.auth?.enabled !== false
3893
+ })
3894
3894
  );
3895
3895
  const socketWsUpstreams = [];
3896
3896
  for (const [name, upstream] of Object.entries(config.upstreams)) {
@@ -3924,13 +3924,19 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
3924
3924
  });
3925
3925
  const connDesc = conn.socketPath ? `${conn.baseUrl} (unix:${conn.socketPath})` : conn.baseUrl;
3926
3926
  const wsDesc = upstream.websocket ? wsOverSocket ? ", ws\u2192unix" : ", ws" : "";
3927
- gatewayLogger.info(`Upstream registered: ${name} \u2192 ${connDesc} (${upstream.prefix}${wsDesc})`);
3927
+ gatewayLogger.info(
3928
+ `Upstream registered: ${name} \u2192 ${connDesc} (${upstream.prefix}${wsDesc})`
3929
+ );
3928
3930
  }
3929
3931
  await app.register(async function gatewayRoutes(scope) {
3930
3932
  if (config.pressure?.perTenant?.enabled === true && platform.hasResourceBroker) {
3931
3933
  scope.addHook(
3932
3934
  "preHandler",
3933
- createPressurePreHandler({ broker: platform.resourceBroker, logger, config })
3935
+ createPressurePreHandler({
3936
+ broker: platform.resourceBroker,
3937
+ logger,
3938
+ config
3939
+ })
3934
3940
  );
3935
3941
  }
3936
3942
  const authService = new AuthService(cache, jwtConfig);
@@ -3941,7 +3947,11 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
3941
3947
  }),
3942
3948
  pdp: userAuth.pdp
3943
3949
  } : void 0;
3944
- registerAuthRoutes(scope, authService, userExt);
3950
+ registerAuthRoutes(
3951
+ scope,
3952
+ authService,
3953
+ userExt
3954
+ );
3945
3955
  if (userAuth) {
3946
3956
  registerUserAuthRoutes(
3947
3957
  scope,
@@ -3973,32 +3983,69 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
3973
3983
  if (cached) {
3974
3984
  return cached;
3975
3985
  }
3976
- const adapterNames = ["llm", "cache", "analytics", "vectorStore", "embeddings"];
3986
+ const adapterNames = [
3987
+ "llm",
3988
+ "cache",
3989
+ "analytics",
3990
+ "vectorStore",
3991
+ "embeddings"
3992
+ ];
3977
3993
  const adapters = {};
3978
3994
  for (const name of adapterNames) {
3979
- await observability.observeOperation(`gateway.adapter.${name}`, async () => {
3980
- const probeStart = Date.now();
3981
- try {
3982
- const pf = platform;
3983
- const adapter = pf[name] ?? pf.getAdapter?.(name);
3984
- adapters[name] = { available: !!adapter, latencyMs: Date.now() - probeStart };
3985
- } catch {
3986
- adapters[name] = { available: false, latencyMs: Date.now() - probeStart };
3995
+ await observability.observeOperation(
3996
+ `gateway.adapter.${name}`,
3997
+ async () => {
3998
+ const probeStart = Date.now();
3999
+ try {
4000
+ const pf = platform;
4001
+ const adapter = pf[name] ?? pf.getAdapter?.(name);
4002
+ adapters[name] = {
4003
+ available: !!adapter,
4004
+ latencyMs: Date.now() - probeStart
4005
+ };
4006
+ } catch {
4007
+ adapters[name] = {
4008
+ available: false,
4009
+ latencyMs: Date.now() - probeStart
4010
+ };
4011
+ }
3987
4012
  }
3988
- });
4013
+ );
3989
4014
  }
3990
4015
  const upstreams = {};
3991
4016
  for (const [name, upstream] of Object.entries(config.upstreams)) {
3992
- await observability.observeOperation(`gateway.upstream.${name}.health`, async () => {
3993
- const probeStart = Date.now();
3994
- try {
3995
- const res = await serviceTransport.call(upstream.serviceId, {
3996
- path: "/health",
3997
- signal: AbortSignal.timeout(2e3)
3998
- });
3999
- const latencyMs = Date.now() - probeStart;
4000
- upstreams[name] = { status: res.ok ? "up" : "down", latencyMs };
4001
- if (!res.ok) {
4017
+ await observability.observeOperation(
4018
+ `gateway.upstream.${name}.health`,
4019
+ async () => {
4020
+ const probeStart = Date.now();
4021
+ try {
4022
+ const res = await serviceTransport.call(upstream.serviceId, {
4023
+ path: "/health",
4024
+ signal: AbortSignal.timeout(2e3)
4025
+ });
4026
+ const latencyMs = Date.now() - probeStart;
4027
+ upstreams[name] = { status: res.ok ? "up" : "down", latencyMs };
4028
+ if (!res.ok) {
4029
+ logDiagnosticEvent(logger, {
4030
+ domain: "service",
4031
+ event: "gateway.upstream.health",
4032
+ level: "warn",
4033
+ reasonCode: "upstream_unavailable",
4034
+ message: "Gateway upstream health probe failed",
4035
+ outcome: "failed",
4036
+ serviceId: "gateway",
4037
+ route: `${upstream.prefix}/health`,
4038
+ evidence: {
4039
+ upstreamId: name,
4040
+ serviceId: upstream.serviceId,
4041
+ statusCode: res.statusCode,
4042
+ latencyMs
4043
+ }
4044
+ });
4045
+ }
4046
+ } catch (error) {
4047
+ const latencyMs = Date.now() - probeStart;
4048
+ upstreams[name] = { status: "down", latencyMs };
4002
4049
  logDiagnosticEvent(logger, {
4003
4050
  domain: "service",
4004
4051
  event: "gateway.upstream.health",
@@ -4006,37 +4053,18 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
4006
4053
  reasonCode: "upstream_unavailable",
4007
4054
  message: "Gateway upstream health probe failed",
4008
4055
  outcome: "failed",
4056
+ error: error instanceof Error ? error : new Error(String(error)),
4009
4057
  serviceId: "gateway",
4010
4058
  route: `${upstream.prefix}/health`,
4011
4059
  evidence: {
4012
4060
  upstreamId: name,
4013
4061
  serviceId: upstream.serviceId,
4014
- statusCode: res.statusCode,
4015
4062
  latencyMs
4016
4063
  }
4017
4064
  });
4018
4065
  }
4019
- } catch (error) {
4020
- const latencyMs = Date.now() - probeStart;
4021
- upstreams[name] = { status: "down", latencyMs };
4022
- logDiagnosticEvent(logger, {
4023
- domain: "service",
4024
- event: "gateway.upstream.health",
4025
- level: "warn",
4026
- reasonCode: "upstream_unavailable",
4027
- message: "Gateway upstream health probe failed",
4028
- outcome: "failed",
4029
- error: error instanceof Error ? error : new Error(String(error)),
4030
- serviceId: "gateway",
4031
- route: `${upstream.prefix}/health`,
4032
- evidence: {
4033
- upstreamId: name,
4034
- serviceId: upstream.serviceId,
4035
- latencyMs
4036
- }
4037
- });
4038
4066
  }
4039
- });
4067
+ );
4040
4068
  }
4041
4069
  const llmOk = adapters.llm?.available ?? false;
4042
4070
  const allOk = Object.values(adapters).every((a) => a.available);
@@ -4052,131 +4080,241 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
4052
4080
  });
4053
4081
  return snapshot;
4054
4082
  };
4055
- scope.get("/health", { schema: { tags: ["System"], summary: "Gateway health check" } }, async () => {
4056
- return collectHealthSnapshot();
4057
- });
4058
- scope.get("/health/adapters", {
4059
- schema: {
4060
- tags: ["System"],
4061
- summary: "Platform adapter status \u2014 mode (real | inmemory | noop) per slot"
4083
+ scope.get(
4084
+ "/health",
4085
+ { schema: { tags: ["System"], summary: "Gateway health check" } },
4086
+ async () => {
4087
+ return collectHealthSnapshot();
4062
4088
  }
4063
- }, async () => {
4064
- return getAdapterStatus();
4065
- });
4066
- scope.get("/ready", { schema: { tags: ["System"], summary: "Gateway readiness check" } }, async (_request, reply) => {
4067
- const health = await collectHealthSnapshot();
4068
- const upstreams = health.upstreams ?? {};
4069
- const missingRequiredUpstreams = ["rest"].filter((id) => (upstreams[id]?.status ?? "down") !== "up");
4070
- const ready = missingRequiredUpstreams.length === 0;
4071
- return reply.code(ready ? 200 : 503).send(createServiceReadyResponse({
4072
- ready,
4073
- status: ready ? "ready" : "degraded",
4074
- reason: ready ? "ready" : `upstream_unavailable:${missingRequiredUpstreams.join(",")}`,
4075
- components: {
4076
- gatewayAdapters: {
4077
- ready: true
4078
- },
4079
- restUpstream: {
4080
- ready: (upstreams.rest?.status ?? "down") === "up",
4081
- status: upstreams.rest?.status ?? "down"
4082
- },
4083
- workflowUpstream: {
4084
- ready: (upstreams.workflow?.status ?? "down") === "up",
4085
- status: upstreams.workflow?.status ?? "down"
4086
- },
4087
- marketplaceUpstream: {
4088
- ready: (upstreams.marketplace?.status ?? "down") === "up",
4089
- status: upstreams.marketplace?.status ?? "down"
4090
- }
4089
+ );
4090
+ scope.get(
4091
+ "/health/adapters",
4092
+ {
4093
+ schema: {
4094
+ tags: ["System"],
4095
+ summary: "Platform adapter status \u2014 mode (real | inmemory | noop) per slot"
4091
4096
  }
4092
- }));
4093
- });
4094
- scope.get("/metrics", { schema: { tags: ["Observability"], summary: "Gateway metrics in Prometheus format" } }, async (_request, reply) => {
4095
- const health = await collectHealthSnapshot();
4096
- const status = health.status ?? "healthy";
4097
- reply.header("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
4098
- return observability.renderPrometheusMetrics(status);
4099
- });
4100
- scope.get("/observability/describe", {
4101
- schema: { tags: ["Observability"], summary: "Gateway observability contract descriptor" }
4102
- }, async () => observability.buildDescribe());
4103
- scope.get("/observability/health", {
4104
- schema: { tags: ["Observability"], summary: "Gateway observability health snapshot" }
4105
- }, async () => {
4106
- const health = await collectHealthSnapshot();
4107
- const adapterChecks = Object.entries(health.adapters ?? {}).map(([id, value]) => ({ id, available: !!value?.available, latencyMs: value?.latencyMs }));
4108
- const upstreamChecks = Object.entries(health.upstreams ?? {}).map(([id, value]) => ({ id, status: value?.status ?? "unknown", latencyMs: value?.latencyMs }));
4109
- const status = health.status ?? "healthy";
4110
- return observability.buildHealth({ status, adapterChecks, upstreamChecks });
4111
- });
4097
+ },
4098
+ async () => {
4099
+ return getAdapterStatus();
4100
+ }
4101
+ );
4102
+ scope.get(
4103
+ "/ready",
4104
+ { schema: { tags: ["System"], summary: "Gateway readiness check" } },
4105
+ async (_request, reply) => {
4106
+ const health = await collectHealthSnapshot();
4107
+ const upstreams = health.upstreams ?? {};
4108
+ const missingRequiredUpstreams = ["rest"].filter(
4109
+ (id) => (upstreams[id]?.status ?? "down") !== "up"
4110
+ );
4111
+ const ready = missingRequiredUpstreams.length === 0;
4112
+ return reply.code(ready ? 200 : 503).send(
4113
+ createServiceReadyResponse({
4114
+ ready,
4115
+ status: ready ? "ready" : "degraded",
4116
+ reason: ready ? "ready" : `upstream_unavailable:${missingRequiredUpstreams.join(",")}`,
4117
+ components: {
4118
+ gatewayAdapters: {
4119
+ ready: true
4120
+ },
4121
+ restUpstream: {
4122
+ ready: (upstreams.rest?.status ?? "down") === "up",
4123
+ status: upstreams.rest?.status ?? "down"
4124
+ },
4125
+ workflowUpstream: {
4126
+ ready: (upstreams.workflow?.status ?? "down") === "up",
4127
+ status: upstreams.workflow?.status ?? "down"
4128
+ },
4129
+ marketplaceUpstream: {
4130
+ ready: (upstreams.marketplace?.status ?? "down") === "up",
4131
+ status: upstreams.marketplace?.status ?? "down"
4132
+ }
4133
+ }
4134
+ })
4135
+ );
4136
+ }
4137
+ );
4138
+ scope.get(
4139
+ "/metrics",
4140
+ {
4141
+ schema: {
4142
+ tags: ["Observability"],
4143
+ summary: "Gateway metrics in Prometheus format"
4144
+ }
4145
+ },
4146
+ async (_request, reply) => {
4147
+ const health = await collectHealthSnapshot();
4148
+ const status = health.status ?? "healthy";
4149
+ reply.header(
4150
+ "Content-Type",
4151
+ "text/plain; version=0.0.4; charset=utf-8"
4152
+ );
4153
+ return observability.renderPrometheusMetrics(status);
4154
+ }
4155
+ );
4156
+ scope.get(
4157
+ "/observability/describe",
4158
+ {
4159
+ schema: {
4160
+ tags: ["Observability"],
4161
+ summary: "Gateway observability contract descriptor"
4162
+ }
4163
+ },
4164
+ async () => observability.buildDescribe()
4165
+ );
4166
+ scope.get(
4167
+ "/observability/health",
4168
+ {
4169
+ schema: {
4170
+ tags: ["Observability"],
4171
+ summary: "Gateway observability health snapshot"
4172
+ }
4173
+ },
4174
+ async () => {
4175
+ const health = await collectHealthSnapshot();
4176
+ const adapterChecks = Object.entries(
4177
+ health.adapters ?? {}
4178
+ ).map(([id, value]) => ({
4179
+ id,
4180
+ available: !!value?.available,
4181
+ latencyMs: value?.latencyMs
4182
+ }));
4183
+ const upstreamChecks = Object.entries(
4184
+ health.upstreams ?? {}
4185
+ ).map(([id, value]) => ({
4186
+ id,
4187
+ status: value?.status ?? "unknown",
4188
+ latencyMs: value?.latencyMs
4189
+ }));
4190
+ const status = health.status ?? "healthy";
4191
+ return observability.buildHealth({
4192
+ status,
4193
+ adapterChecks,
4194
+ upstreamChecks
4195
+ });
4196
+ }
4197
+ );
4112
4198
  if (!registry) {
4113
- gatewayLogger.warn("No persistent HostRegistry injected \u2014 hosts will be lost on restart");
4199
+ gatewayLogger.warn(
4200
+ "No persistent HostRegistry injected \u2014 hosts will be lost on restart"
4201
+ );
4114
4202
  }
4115
4203
  const hostRegistry = registry ?? new HostRegistry(cache);
4116
- scope.post("/hosts/register", { schema: { tags: ["Hosts"], summary: "Register a host" } }, async (request, reply) => {
4117
- const parsed = HostRegistrationSchema.safeParse(request.body);
4118
- if (!parsed.success) {
4119
- return reply.code(400).send({ error: "Bad Request", issues: parsed.error.issues });
4120
- }
4121
- const result = await hostRegistry.register(parsed.data);
4122
- return reply.code(201).send({
4123
- hostId: result.descriptor.hostId,
4124
- machineToken: result.machineToken,
4125
- status: result.descriptor.status
4126
- });
4127
- });
4128
- scope.get("/hosts", { schema: { tags: ["Hosts"], summary: "List registered hosts" } }, async (request, reply) => {
4129
- const auth = request.authContext;
4130
- if (!auth) {
4131
- return reply.code(401).send({ error: "Unauthorized" });
4132
- }
4133
- const hosts = await hostRegistry.list(auth.namespaceId);
4134
- return { hosts };
4135
- });
4136
- scope.get("/hosts/:hostId", { schema: { tags: ["Hosts"], summary: "Get host by ID" } }, async (request, reply) => {
4137
- const auth = request.authContext;
4138
- if (!auth) {
4139
- return reply.code(401).send({ error: "Unauthorized" });
4204
+ scope.post(
4205
+ "/hosts/register",
4206
+ { schema: { tags: ["Hosts"], summary: "Register a host" } },
4207
+ async (request, reply) => {
4208
+ const parsed = HostRegistrationSchema.safeParse(request.body);
4209
+ if (!parsed.success) {
4210
+ return reply.code(400).send({ error: "Bad Request", issues: parsed.error.issues });
4211
+ }
4212
+ const result = await hostRegistry.register(parsed.data);
4213
+ return reply.code(201).send({
4214
+ hostId: result.descriptor.hostId,
4215
+ machineToken: result.machineToken,
4216
+ status: result.descriptor.status
4217
+ });
4140
4218
  }
4141
- const { hostId } = request.params;
4142
- const host = await hostRegistry.get(hostId, auth.namespaceId);
4143
- if (!host) {
4144
- return reply.code(404).send({ error: "Host not found" });
4219
+ );
4220
+ scope.get(
4221
+ "/hosts",
4222
+ { schema: { tags: ["Hosts"], summary: "List registered hosts" } },
4223
+ async (request, reply) => {
4224
+ const auth = request.authContext;
4225
+ if (!auth) {
4226
+ return reply.code(401).send({ error: "Unauthorized" });
4227
+ }
4228
+ const hosts = await hostRegistry.list(auth.namespaceId);
4229
+ return { hosts };
4145
4230
  }
4146
- return host;
4147
- });
4148
- scope.delete("/hosts/:hostId", { schema: { tags: ["Hosts"], summary: "Deregister a host" } }, async (request, reply) => {
4149
- const auth = request.authContext;
4150
- if (!auth) {
4151
- return reply.code(401).send({ error: "Unauthorized" });
4231
+ );
4232
+ scope.get(
4233
+ "/hosts/:hostId",
4234
+ { schema: { tags: ["Hosts"], summary: "Get host by ID" } },
4235
+ async (request, reply) => {
4236
+ const auth = request.authContext;
4237
+ if (!auth) {
4238
+ return reply.code(401).send({ error: "Unauthorized" });
4239
+ }
4240
+ const { hostId } = request.params;
4241
+ const host = await hostRegistry.get(hostId, auth.namespaceId);
4242
+ if (!host) {
4243
+ return reply.code(404).send({ error: "Host not found" });
4244
+ }
4245
+ return host;
4152
4246
  }
4153
- const { hostId } = request.params;
4154
- const deleted = await hostRegistry.deregister(hostId, auth.namespaceId);
4155
- if (!deleted) {
4156
- return reply.code(404).send({ error: "Host not found" });
4247
+ );
4248
+ scope.delete(
4249
+ "/hosts/:hostId",
4250
+ { schema: { tags: ["Hosts"], summary: "Deregister a host" } },
4251
+ async (request, reply) => {
4252
+ const auth = request.authContext;
4253
+ if (!auth) {
4254
+ return reply.code(401).send({ error: "Unauthorized" });
4255
+ }
4256
+ const { hostId } = request.params;
4257
+ const deleted = await hostRegistry.deregister(hostId, auth.namespaceId);
4258
+ if (!deleted) {
4259
+ return reply.code(404).send({ error: "Host not found" });
4260
+ }
4261
+ return reply.code(204).send();
4157
4262
  }
4158
- return reply.code(204).send();
4159
- });
4160
- registerExecuteRoutes(scope, logger);
4161
- registerLLMGatewayRoutes(scope, logger);
4162
- registerTelemetryRoutes(scope, logger);
4163
- registerPlatformRoutes(scope, logger);
4164
- registerAggregatedDocsRoutes(scope, config, serviceTransport, cache);
4165
- registerInternalRoutes(scope, process.env.GATEWAY_INTERNAL_SECRET, hostRegistry, cache);
4263
+ );
4264
+ registerExecuteRoutes(
4265
+ scope,
4266
+ logger
4267
+ );
4268
+ registerLLMGatewayRoutes(
4269
+ scope,
4270
+ logger
4271
+ );
4272
+ registerTelemetryRoutes(
4273
+ scope,
4274
+ logger
4275
+ );
4276
+ registerPlatformRoutes(
4277
+ scope,
4278
+ logger
4279
+ );
4280
+ registerAggregatedDocsRoutes(
4281
+ scope,
4282
+ config,
4283
+ serviceTransport,
4284
+ cache
4285
+ );
4286
+ registerInternalRoutes(
4287
+ scope,
4288
+ process.env.GATEWAY_INTERNAL_SECRET,
4289
+ hostRegistry,
4290
+ cache
4291
+ );
4166
4292
  if (platform.hasResourceBroker) {
4167
4293
  const webhookBaseUrl = process.env.GATEWAY_PUBLIC_URL ?? `http://localhost:${config.port + (Number(process.env.KB_NET_OFFSET) || 0)}`;
4168
4294
  const provisionBackend = {
4169
- async execute({ handlerRef, pluginRoot, input, namespaceId }) {
4295
+ async execute({
4296
+ handlerRef,
4297
+ pluginRoot,
4298
+ input,
4299
+ namespaceId
4300
+ }) {
4170
4301
  if (!namespaceId) {
4171
4302
  return;
4172
4303
  }
4173
- const hostId = globalDispatcher.firstHostWithCapability(namespaceId, "execution");
4304
+ const hostId = globalDispatcher.firstHostWithCapability(
4305
+ namespaceId,
4306
+ "execution"
4307
+ );
4174
4308
  if (!hostId) {
4175
4309
  return;
4176
4310
  }
4177
- return globalDispatcher.call(namespaceId, hostId, "execution", "execute", [
4178
- { handlerRef, pluginRoot, input }
4179
- ]);
4311
+ return globalDispatcher.call(
4312
+ namespaceId,
4313
+ hostId,
4314
+ "execution",
4315
+ "execute",
4316
+ [{ handlerRef, pluginRoot, input }]
4317
+ );
4180
4318
  }
4181
4319
  };
4182
4320
  registerWebhookAdminRoutes(
@@ -4203,31 +4341,50 @@ async function createServer(config, cache, logger, jwtConfig, registry, serviceT
4203
4341
  });
4204
4342
  }
4205
4343
  await app.ready();
4206
- attachGatewayWs(app.server, cache, jwtConfig, logger, registry, socketWsUpstreams);
4344
+ attachGatewayWs(
4345
+ app.server,
4346
+ cache,
4347
+ jwtConfig,
4348
+ logger,
4349
+ registry,
4350
+ socketWsUpstreams
4351
+ );
4207
4352
  return app;
4208
4353
  }
4209
4354
 
4210
4355
  // src/bootstrap.ts
4211
4356
  async function bootstrap(repoRoot = process.cwd()) {
4212
- await createServiceBootstrap({ appId: "gateway", repoRoot, assemblyHook: makeAssemblyHook() });
4213
- const logger = createCorrelatedLogger(platform.logger, {
4214
- serviceId: "gateway",
4215
- logsSource: "gateway",
4216
- layer: "gateway",
4217
- service: "bootstrap",
4218
- operation: "gateway.bootstrap"
4357
+ await runService({
4358
+ appId: "gateway",
4359
+ startDir: repoRoot,
4360
+ defaultPort: 4e3,
4361
+ portEnvVar: "GATEWAY_PORT",
4362
+ defaultHost: "0.0.0.0",
4363
+ hostEnvVar: "GATEWAY_HOST",
4364
+ platform: {
4365
+ assemblyHook: makeAssemblyHook()
4366
+ },
4367
+ setup: startGateway
4219
4368
  });
4220
- logger.info("Platform initialized", { repoRoot });
4221
- const config = await loadGatewayConfig(getProjectRoot() ?? repoRoot, getPlatformRoot());
4369
+ }
4370
+ async function startGateway({
4371
+ platform: platform5,
4372
+ projectRoot,
4373
+ platformRoot,
4374
+ logger: serviceLogger
4375
+ }) {
4376
+ const logger = serviceLogger.forComponent("gateway-bootstrap").forOperation("gateway.bootstrap");
4377
+ logger.info("Platform initialized", { projectRoot, platformRoot });
4378
+ const config = await loadGatewayConfig(projectRoot, platformRoot);
4222
4379
  logger.info("Gateway config loaded", {
4223
4380
  port: config.port,
4224
4381
  upstreams: Object.keys(config.upstreams),
4225
- projectRoot: getProjectRoot() ?? repoRoot,
4226
- platformRoot: getPlatformRoot(),
4382
+ projectRoot,
4383
+ platformRoot,
4227
4384
  configProviderIds: config.auth?.providers ? Object.keys(config.auth.providers) : []
4228
4385
  });
4229
4386
  let hostStore;
4230
- const configuredDocs = platform.getAdapter("documentDatabase");
4387
+ const configuredDocs = platform5.getAdapter("documentDatabase");
4231
4388
  const docs = configuredDocs ?? createInMemoryDocumentDatabase();
4232
4389
  if (configuredDocs) {
4233
4390
  hostStore = new HostStore(docs);
@@ -4239,7 +4396,13 @@ async function bootstrap(repoRoot = process.cwd()) {
4239
4396
  }
4240
4397
  let userAuth;
4241
4398
  {
4242
- const { accessTtlSec, refreshTtlSec, graceWindowMs, bcryptCost, cookieSecure } = resolveAuthRuntimeConfig(config.auth, process.env);
4399
+ const {
4400
+ accessTtlSec,
4401
+ refreshTtlSec,
4402
+ graceWindowMs,
4403
+ bcryptCost,
4404
+ cookieSecure
4405
+ } = resolveAuthRuntimeConfig(config.auth, process.env);
4243
4406
  const tenantPattern = config.tenants?.pattern ?? "{tenant}.kblabs.ru";
4244
4407
  const bootstrapTenantId = config.auth?.bootstrap?.tenantId ?? process.env.GATEWAY_BOOTSTRAP_TENANT_ID ?? "kblabs-cloud";
4245
4408
  const users = new UsersStore(docs);
@@ -4272,7 +4435,9 @@ async function bootstrap(repoRoot = process.cwd()) {
4272
4435
  invites,
4273
4436
  providers,
4274
4437
  passwordPolicy,
4275
- jwtConfig: { secret: process.env.GATEWAY_JWT_SECRET ?? "dev-insecure-secret-change-me" },
4438
+ jwtConfig: {
4439
+ secret: process.env.GATEWAY_JWT_SECRET ?? "dev-insecure-secret-change-me"
4440
+ },
4276
4441
  accessTtlSec,
4277
4442
  refreshTtlSec,
4278
4443
  bcryptCost
@@ -4292,16 +4457,18 @@ async function bootstrap(repoRoot = process.cwd()) {
4292
4457
  });
4293
4458
  });
4294
4459
  const inviteTtlMs = process.env.AUTH_INVITE_TTL_MS ? parseInt(process.env.AUTH_INVITE_TTL_MS, 10) : config.auth?.inviteTtlMs ?? 7 * 24 * 60 * 60 * 1e3;
4295
- const kv = platform.getAdapter("kvStore") ?? createInMemoryKVStore();
4460
+ const kv = platform5.getAdapter("kvStore") ?? createInMemoryKVStore();
4296
4461
  const rateLimiter = createRateLimiter(kv);
4297
- if (!platform.getAdapter("kvStore")) {
4298
- logger.warn("kvStore adapter not configured \u2014 auth rate limiting using in-memory KV (counters reset on restart)");
4462
+ if (!platform5.getAdapter("kvStore")) {
4463
+ logger.warn(
4464
+ "kvStore adapter not configured \u2014 auth rate limiting using in-memory KV (counters reset on restart)"
4465
+ );
4299
4466
  }
4300
4467
  const loginPerIpPerMinute = process.env.AUTH_LOGIN_RATE_LIMIT_PER_IP ? parseInt(process.env.AUTH_LOGIN_RATE_LIMIT_PER_IP, 10) : config.auth?.rateLimit?.loginPerIpPerMinute ?? 10;
4301
4468
  const loginPerEmailPerMinute = process.env.AUTH_LOGIN_RATE_LIMIT_PER_EMAIL ? parseInt(process.env.AUTH_LOGIN_RATE_LIMIT_PER_EMAIL, 10) : config.auth?.rateLimit?.loginPerEmailPerMinute ?? 5;
4302
4469
  const oauthState = new OAuthStateStore(kv);
4303
4470
  const hasRedirectProvider = providers.list().some((p) => p.kind === "redirect");
4304
- if (hasRedirectProvider && !platform.getAdapter("kvStore")) {
4471
+ if (hasRedirectProvider && !platform5.getAdapter("kvStore")) {
4305
4472
  logger.warn(
4306
4473
  "OAuth requires a shared kvStore (Redis) in multi-process/HA; in-memory state is per-process and callbacks may land on a different worker"
4307
4474
  );
@@ -4330,13 +4497,13 @@ async function bootstrap(repoRoot = process.cwd()) {
4330
4497
  persistent: !!configuredDocs
4331
4498
  });
4332
4499
  }
4333
- const cache = platform.cache;
4500
+ const cache = platform5.cache;
4334
4501
  const registry = new HostRegistry(cache, hostStore);
4335
4502
  let restoredCount = 0;
4336
4503
  try {
4337
4504
  restoredCount = await registry.restore();
4338
4505
  } catch (error) {
4339
- logDiagnosticEvent(platform.logger, {
4506
+ logDiagnosticEvent(platform5.logger, {
4340
4507
  domain: "registry",
4341
4508
  event: "gateway.hosts.restore",
4342
4509
  level: "error",
@@ -4355,8 +4522,6 @@ async function bootstrap(repoRoot = process.cwd()) {
4355
4522
  }
4356
4523
  let webhookManifests = [];
4357
4524
  try {
4358
- const projectRoot = getProjectRoot() ?? repoRoot;
4359
- const platformRoot = getPlatformRoot();
4360
4525
  const pluginRegistry = await createRegistry({
4361
4526
  root: projectRoot,
4362
4527
  platformRoot: platformRoot !== projectRoot ? platformRoot : void 0,
@@ -4369,7 +4534,9 @@ async function bootstrap(repoRoot = process.cwd()) {
4369
4534
  pluginRoot: entry.pluginRoot
4370
4535
  }));
4371
4536
  if (webhookManifests.length > 0) {
4372
- logger.info("Webhook manifests discovered", { count: webhookManifests.length });
4537
+ logger.info("Webhook manifests discovered", {
4538
+ count: webhookManifests.length
4539
+ });
4373
4540
  }
4374
4541
  } catch (err) {
4375
4542
  logger.warn("Webhook manifest discovery failed \u2014 webhook routes disabled", {
@@ -4386,7 +4553,9 @@ async function bootstrap(repoRoot = process.cwd()) {
4386
4553
  );
4387
4554
  }
4388
4555
  if (!jwtSecret) {
4389
- logger.warn("GATEWAY_JWT_SECRET not set \u2014 using insecure default (dev only, never use in production!)");
4556
+ logger.warn(
4557
+ "GATEWAY_JWT_SECRET not set \u2014 using insecure default (dev only, never use in production!)"
4558
+ );
4390
4559
  }
4391
4560
  const jwtConfig = { secret: jwtSecret ?? DEV_JWT_SECRET };
4392
4561
  if (config.auth?.bootstrap?.provisionCliCredentials === true) {
@@ -4402,18 +4571,31 @@ async function bootstrap(repoRoot = process.cwd()) {
4402
4571
  });
4403
4572
  });
4404
4573
  }
4405
- if (platform.hasResourceBroker) {
4406
- registerPressureLimits(platform.resourceBroker, config.pressure, platform.logger);
4574
+ if (platform5.hasResourceBroker) {
4575
+ registerPressureLimits(
4576
+ platform5.resourceBroker,
4577
+ config.pressure,
4578
+ platform5.logger
4579
+ );
4407
4580
  } else {
4408
4581
  logger.warn("Resource broker unavailable \u2014 pressure control disabled");
4409
4582
  }
4410
- const serviceTransport = platform.getAdapter("serviceTransport");
4583
+ const serviceTransport = platform5.getAdapter("serviceTransport");
4411
4584
  if (!serviceTransport) {
4412
4585
  throw new Error(
4413
4586
  'Gateway requires the serviceTransport adapter. Configure it in kb.config.json:\n "adapters": { "serviceTransport": "@kb-labs/adapters-service-transport-http" },\n "adapterOptions": { "serviceTransport": { "services": { "rest": { "url": "http://127.0.0.1:5050" }, ... } } }'
4414
4587
  );
4415
4588
  }
4416
- const server = await createServer(config, cache, platform.logger, jwtConfig, registry, serviceTransport, userAuth, webhookManifests);
4589
+ const server = await createServer(
4590
+ config,
4591
+ cache,
4592
+ platform5.logger,
4593
+ jwtConfig,
4594
+ registry,
4595
+ serviceTransport,
4596
+ userAuth,
4597
+ webhookManifests
4598
+ );
4417
4599
  const bindHost = config.host ?? "0.0.0.0";
4418
4600
  if (config.auth?.enabled === false && !isLoopbackHost(bindHost)) {
4419
4601
  throw new Error(
@@ -4423,16 +4605,14 @@ async function bootstrap(repoRoot = process.cwd()) {
4423
4605
  const netOffset = Number(process.env.KB_NET_OFFSET) || 0;
4424
4606
  const listenPort = config.port + netOffset;
4425
4607
  const address = await server.listen({ port: listenPort, host: bindHost });
4426
- logger.info("Gateway listening", { address, authEnabled: config.auth?.enabled !== false });
4427
- const shutdown = async (signal) => {
4428
- logger.warn("Received shutdown signal", { signal });
4429
- await platform.shutdown();
4608
+ logger.info("Gateway listening", {
4609
+ address,
4610
+ authEnabled: config.auth?.enabled !== false
4611
+ });
4612
+ return async () => {
4430
4613
  await server.close();
4431
- logger.info("Gateway shutdown complete");
4432
- process.exit(0);
4614
+ logger.info("Gateway service resources closed");
4433
4615
  };
4434
- process.on("SIGTERM", () => shutdown("SIGTERM"));
4435
- process.on("SIGINT", () => shutdown("SIGINT"));
4436
4616
  }
4437
4617
  function resolveAuthRuntimeConfig(authConfig, env) {
4438
4618
  const accessTtlSec = env.AUTH_ACCESS_TTL_SEC ? parseInt(env.AUTH_ACCESS_TTL_SEC, 10) : authConfig?.sessionAccessTtlSec ?? 900;
@@ -4440,7 +4620,13 @@ function resolveAuthRuntimeConfig(authConfig, env) {
4440
4620
  const graceWindowMs = (authConfig?.refreshGraceWindowSec ?? 5) * 1e3;
4441
4621
  const bcryptCost = authConfig?.bcryptCost ?? 12;
4442
4622
  const cookieSecure = env.AUTH_COOKIE_SECURE === "false" ? false : authConfig?.cookieSecure ?? true;
4443
- return { accessTtlSec, refreshTtlSec, graceWindowMs, bcryptCost, cookieSecure };
4623
+ return {
4624
+ accessTtlSec,
4625
+ refreshTtlSec,
4626
+ graceWindowMs,
4627
+ bcryptCost,
4628
+ cookieSecure
4629
+ };
4444
4630
  }
4445
4631
  function isLoopbackHost(host) {
4446
4632
  const h = host.trim().toLowerCase();
@@ -4448,9 +4634,8 @@ function isLoopbackHost(host) {
4448
4634
  }
4449
4635
 
4450
4636
  // src/index.ts
4451
- bootstrap(process.cwd()).catch((error) => {
4452
- console.error("Failed to start gateway:", error);
4453
- process.exit(1);
4637
+ bootstrap(process.cwd()).catch(() => {
4638
+ process.exitCode = 1;
4454
4639
  });
4455
4640
  //# sourceMappingURL=index.js.map
4456
4641
  //# sourceMappingURL=index.js.map