@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 +392 -207
- package/dist/index.js.map +1 -1
- package/package.json +35 -43
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 {
|
|
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 =
|
|
3823
|
+
const gatewayLogger = createHttpLogger(logger, {
|
|
3823
3824
|
serviceId: "gateway",
|
|
3824
|
-
|
|
3825
|
-
|
|
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 =
|
|
3858
|
+
request.kbLogger = createHttpLogger(logger, {
|
|
3859
3859
|
serviceId: "gateway",
|
|
3860
|
-
|
|
3861
|
-
|
|
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.
|
|
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
|
-
|
|
3877
|
-
|
|
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, {
|
|
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(
|
|
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({
|
|
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(
|
|
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 = [
|
|
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(
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
const
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
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(
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
const
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
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(
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
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
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
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
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
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(
|
|
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(
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
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
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
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
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
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
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
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
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
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({
|
|
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(
|
|
4304
|
+
const hostId = globalDispatcher.firstHostWithCapability(
|
|
4305
|
+
namespaceId,
|
|
4306
|
+
"execution"
|
|
4307
|
+
);
|
|
4174
4308
|
if (!hostId) {
|
|
4175
4309
|
return;
|
|
4176
4310
|
}
|
|
4177
|
-
return globalDispatcher.call(
|
|
4178
|
-
|
|
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(
|
|
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
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
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
|
-
|
|
4221
|
-
|
|
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
|
|
4226
|
-
platformRoot
|
|
4382
|
+
projectRoot,
|
|
4383
|
+
platformRoot,
|
|
4227
4384
|
configProviderIds: config.auth?.providers ? Object.keys(config.auth.providers) : []
|
|
4228
4385
|
});
|
|
4229
4386
|
let hostStore;
|
|
4230
|
-
const configuredDocs =
|
|
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 {
|
|
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: {
|
|
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 =
|
|
4460
|
+
const kv = platform5.getAdapter("kvStore") ?? createInMemoryKVStore();
|
|
4296
4461
|
const rateLimiter = createRateLimiter(kv);
|
|
4297
|
-
if (!
|
|
4298
|
-
logger.warn(
|
|
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 && !
|
|
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 =
|
|
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(
|
|
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", {
|
|
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(
|
|
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 (
|
|
4406
|
-
registerPressureLimits(
|
|
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 =
|
|
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(
|
|
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", {
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
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
|
|
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 {
|
|
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((
|
|
4452
|
-
|
|
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
|