@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 +114 -13
- package/dist/index.js.map +1 -1
- package/kb-create.requirements.json +39 -0
- package/package.json +22 -21
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) =>
|
|
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.
|
|
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
|
|
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.
|
|
4508
|
-
|
|
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 =
|
|
4652
|
-
if (
|
|
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
|
|
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(() => {
|