@harborclient/team-hub 0.2.1 → 0.2.2
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/cli.js +391 -34
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -42,6 +42,40 @@ function normalizeLlmConfig(section) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// src/config/loggingConfig.ts
|
|
46
|
+
var DEFAULT_LOGGING_CONFIG = {
|
|
47
|
+
level: "info",
|
|
48
|
+
file: null,
|
|
49
|
+
console: true
|
|
50
|
+
};
|
|
51
|
+
function normalizeLoggingConfig(section) {
|
|
52
|
+
return {
|
|
53
|
+
level: section?.level ?? DEFAULT_LOGGING_CONFIG.level,
|
|
54
|
+
file: section?.file ?? DEFAULT_LOGGING_CONFIG.file,
|
|
55
|
+
console: section?.console ?? DEFAULT_LOGGING_CONFIG.console
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/config/pluginsConfig.ts
|
|
60
|
+
function dedupeUrls(urls) {
|
|
61
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62
|
+
const deduped = [];
|
|
63
|
+
for (const url of urls) {
|
|
64
|
+
if (seen.has(url)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
seen.add(url);
|
|
68
|
+
deduped.push(url);
|
|
69
|
+
}
|
|
70
|
+
return deduped;
|
|
71
|
+
}
|
|
72
|
+
function normalizePluginsConfig(section) {
|
|
73
|
+
return {
|
|
74
|
+
catalogs: dedupeUrls(section.catalogs ?? []),
|
|
75
|
+
trusted: dedupeUrls(section.trusted ?? [])
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
// src/config/serverConfig.schema.ts
|
|
46
80
|
import { z } from "zod/v4";
|
|
47
81
|
var portSchema = z.union([
|
|
@@ -93,11 +127,23 @@ var llmSectionSchema = z.object({
|
|
|
93
127
|
),
|
|
94
128
|
models: z.array(z.string().trim().min(1)).optional()
|
|
95
129
|
});
|
|
130
|
+
var pluginsSectionSchema = z.object({
|
|
131
|
+
catalogs: z.array(z.string().trim().url()).optional(),
|
|
132
|
+
trusted: z.array(z.string().trim().url()).optional()
|
|
133
|
+
});
|
|
134
|
+
var logLevelSchema = z.enum(["debug", "info", "warn", "error"]);
|
|
135
|
+
var loggingSectionSchema = z.object({
|
|
136
|
+
level: logLevelSchema.optional(),
|
|
137
|
+
file: z.string().trim().min(1).optional(),
|
|
138
|
+
console: z.boolean().optional()
|
|
139
|
+
});
|
|
96
140
|
var serverConfigDocumentSchema = z.object({
|
|
97
141
|
server: serverSectionSchema,
|
|
98
142
|
db: dbSectionSchema,
|
|
99
143
|
redis: redisSectionSchema,
|
|
100
|
-
llm: llmSectionSchema.optional()
|
|
144
|
+
llm: llmSectionSchema.optional(),
|
|
145
|
+
plugins: pluginsSectionSchema.optional(),
|
|
146
|
+
logging: loggingSectionSchema.optional()
|
|
101
147
|
});
|
|
102
148
|
|
|
103
149
|
// src/config/serverConfig.ts
|
|
@@ -189,12 +235,30 @@ function parseServerConfig(document) {
|
|
|
189
235
|
}
|
|
190
236
|
llm = normalizeLlmConfig(parsedLlmSection.data);
|
|
191
237
|
}
|
|
238
|
+
let plugins = null;
|
|
239
|
+
if (root.plugins !== void 0) {
|
|
240
|
+
const parsedPluginsSection = pluginsSectionSchema.safeParse(root.plugins);
|
|
241
|
+
if (!parsedPluginsSection.success) {
|
|
242
|
+
throw new ConfigError(formatZodError(parsedPluginsSection.error));
|
|
243
|
+
}
|
|
244
|
+
plugins = normalizePluginsConfig(parsedPluginsSection.data);
|
|
245
|
+
}
|
|
246
|
+
let logging = DEFAULT_LOGGING_CONFIG;
|
|
247
|
+
if (root.logging !== void 0) {
|
|
248
|
+
const parsedLoggingSection = loggingSectionSchema.safeParse(root.logging);
|
|
249
|
+
if (!parsedLoggingSection.success) {
|
|
250
|
+
throw new ConfigError(formatZodError(parsedLoggingSection.error));
|
|
251
|
+
}
|
|
252
|
+
logging = normalizeLoggingConfig(parsedLoggingSection.data);
|
|
253
|
+
}
|
|
192
254
|
return {
|
|
193
255
|
port: parsedDocument.data.server.port,
|
|
194
256
|
host: parsedDocument.data.server.host,
|
|
195
257
|
db: parsedDbSection.data,
|
|
196
258
|
redis: parsedRedisSection.data,
|
|
197
|
-
llm
|
|
259
|
+
llm,
|
|
260
|
+
plugins,
|
|
261
|
+
logging
|
|
198
262
|
};
|
|
199
263
|
}
|
|
200
264
|
function loadServerConfig(configPath) {
|
|
@@ -5618,6 +5682,48 @@ import {
|
|
|
5618
5682
|
validatorCompiler
|
|
5619
5683
|
} from "fastify-type-provider-zod";
|
|
5620
5684
|
|
|
5685
|
+
// src/server/logging/httpLogging.ts
|
|
5686
|
+
function registerHttpLogging(app, logger) {
|
|
5687
|
+
app.addHook("onRequest", async (request) => {
|
|
5688
|
+
logger.debug("request", {
|
|
5689
|
+
reqId: request.id,
|
|
5690
|
+
method: request.method,
|
|
5691
|
+
url: request.url,
|
|
5692
|
+
ip: request.ip
|
|
5693
|
+
});
|
|
5694
|
+
});
|
|
5695
|
+
app.addHook("onError", async (request, reply, error) => {
|
|
5696
|
+
logger.error("request error", {
|
|
5697
|
+
reqId: request.id,
|
|
5698
|
+
method: request.method,
|
|
5699
|
+
url: request.url,
|
|
5700
|
+
statusCode: reply.statusCode,
|
|
5701
|
+
message: error.message,
|
|
5702
|
+
stack: error.stack
|
|
5703
|
+
});
|
|
5704
|
+
});
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
// src/server/logging/logger.ts
|
|
5708
|
+
import winston from "winston";
|
|
5709
|
+
function createLogger(config) {
|
|
5710
|
+
const transports = [];
|
|
5711
|
+
if (config.console) {
|
|
5712
|
+
transports.push(new winston.transports.Console());
|
|
5713
|
+
}
|
|
5714
|
+
if (config.file) {
|
|
5715
|
+
transports.push(new winston.transports.File({ filename: config.file }));
|
|
5716
|
+
}
|
|
5717
|
+
if (transports.length === 0) {
|
|
5718
|
+
transports.push(new winston.transports.Console({ silent: true }));
|
|
5719
|
+
}
|
|
5720
|
+
return winston.createLogger({
|
|
5721
|
+
level: config.level,
|
|
5722
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
|
5723
|
+
transports
|
|
5724
|
+
});
|
|
5725
|
+
}
|
|
5726
|
+
|
|
5621
5727
|
// src/packageVersion.ts
|
|
5622
5728
|
import { readFileSync as readFileSync2 } from "fs";
|
|
5623
5729
|
function readPackageVersion() {
|
|
@@ -5953,6 +6059,15 @@ var createdApiTokenResponseSchema = z8.object({
|
|
|
5953
6059
|
var listAdminTokensResponseSchema = z8.object({
|
|
5954
6060
|
tokens: z8.array(hubApiTokenRecordSchema)
|
|
5955
6061
|
});
|
|
6062
|
+
var reloadConfigSectionResultSchema = z8.object({
|
|
6063
|
+
section: z8.enum(["db", "redis", "llm", "plugins", "server"]),
|
|
6064
|
+
status: z8.enum(["reloaded", "unchanged", "failed", "restart-required"]),
|
|
6065
|
+
error: z8.string().optional()
|
|
6066
|
+
});
|
|
6067
|
+
var reloadConfigResponseSchema = z8.object({
|
|
6068
|
+
sections: z8.array(reloadConfigSectionResultSchema),
|
|
6069
|
+
fatalError: z8.string().optional()
|
|
6070
|
+
});
|
|
5956
6071
|
function serializeHubUser(user) {
|
|
5957
6072
|
return {
|
|
5958
6073
|
id: user.id,
|
|
@@ -6159,7 +6274,7 @@ function denySelfRoleChange(reply, targetUserId, actorUserId, existingRole, requ
|
|
|
6159
6274
|
return true;
|
|
6160
6275
|
}
|
|
6161
6276
|
async function registerAdminRoutes(app, options) {
|
|
6162
|
-
const { db,
|
|
6277
|
+
const { db, getLlm, reloadConfig } = options;
|
|
6163
6278
|
const routes = app.withTypeProvider();
|
|
6164
6279
|
routes.route({
|
|
6165
6280
|
method: "GET",
|
|
@@ -6180,6 +6295,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6180
6295
|
return;
|
|
6181
6296
|
}
|
|
6182
6297
|
const systemUserId = db.getSystemUserId();
|
|
6298
|
+
const llm = getLlm();
|
|
6183
6299
|
const [users, collections, environments] = await Promise.all([
|
|
6184
6300
|
db.listUsers(),
|
|
6185
6301
|
db.listCollections(),
|
|
@@ -6232,6 +6348,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6232
6348
|
return;
|
|
6233
6349
|
}
|
|
6234
6350
|
const input = buildAdminUserCreateInput(request.body);
|
|
6351
|
+
const llm = getLlm();
|
|
6235
6352
|
const [collections, environments] = await Promise.all([
|
|
6236
6353
|
db.listCollections(),
|
|
6237
6354
|
db.listEnvironments()
|
|
@@ -6346,6 +6463,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6346
6463
|
* Lists all hub-offered LLM models for operator user management.
|
|
6347
6464
|
*/
|
|
6348
6465
|
handler: async (request, reply) => {
|
|
6466
|
+
const llm = getLlm();
|
|
6349
6467
|
if (!llm) {
|
|
6350
6468
|
return sendLlmUnavailable(reply);
|
|
6351
6469
|
}
|
|
@@ -6397,6 +6515,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6397
6515
|
}
|
|
6398
6516
|
const input = buildAdminUserUpdateInput(existing, request.body);
|
|
6399
6517
|
const role = request.body.role ?? existing.role;
|
|
6518
|
+
const llm = getLlm();
|
|
6400
6519
|
const [collections, environments] = await Promise.all([
|
|
6401
6520
|
db.listCollections(),
|
|
6402
6521
|
db.listEnvironments()
|
|
@@ -6586,6 +6705,31 @@ async function registerAdminRoutes(app, options) {
|
|
|
6586
6705
|
}
|
|
6587
6706
|
}
|
|
6588
6707
|
});
|
|
6708
|
+
routes.route({
|
|
6709
|
+
method: "POST",
|
|
6710
|
+
url: "/admin/config/reload",
|
|
6711
|
+
schema: {
|
|
6712
|
+
response: {
|
|
6713
|
+
200: reloadConfigResponseSchema,
|
|
6714
|
+
400: reloadConfigResponseSchema,
|
|
6715
|
+
403: errorResponseSchema
|
|
6716
|
+
}
|
|
6717
|
+
},
|
|
6718
|
+
/**
|
|
6719
|
+
* Re-reads server.yaml and applies reloadable config sections on a best-effort basis.
|
|
6720
|
+
*/
|
|
6721
|
+
handler: async (request, reply) => {
|
|
6722
|
+
const user = requireAuthenticatedUser(request);
|
|
6723
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6726
|
+
const result = await reloadConfig();
|
|
6727
|
+
if (result.fatalError) {
|
|
6728
|
+
return reply.code(400).send(result);
|
|
6729
|
+
}
|
|
6730
|
+
return reply.send(result);
|
|
6731
|
+
}
|
|
6732
|
+
});
|
|
6589
6733
|
}
|
|
6590
6734
|
|
|
6591
6735
|
// src/server/auth/sessionCapabilities.ts
|
|
@@ -7523,14 +7667,15 @@ async function registerLlmRoutes(app, options) {
|
|
|
7523
7667
|
* Lists hub-offered models the authenticated user may use.
|
|
7524
7668
|
*/
|
|
7525
7669
|
handler: async (request, reply) => {
|
|
7526
|
-
|
|
7670
|
+
const llm = options.getLlm();
|
|
7671
|
+
if (!llm) {
|
|
7527
7672
|
return sendLlmUnavailable2(reply);
|
|
7528
7673
|
}
|
|
7529
7674
|
const user = requireAuthenticatedUser(request);
|
|
7530
7675
|
if (denyUnlessAllowed(reply, canUseLlm(user))) {
|
|
7531
7676
|
return;
|
|
7532
7677
|
}
|
|
7533
|
-
const offered = listHubOfferedModels(
|
|
7678
|
+
const offered = listHubOfferedModels(llm).filter(
|
|
7534
7679
|
(model) => isLlmModelAllowed(user, model.id)
|
|
7535
7680
|
);
|
|
7536
7681
|
return reply.send({
|
|
@@ -7556,7 +7701,8 @@ async function registerLlmRoutes(app, options) {
|
|
|
7556
7701
|
* Returns the authenticated user's current monthly LLM usage summary.
|
|
7557
7702
|
*/
|
|
7558
7703
|
handler: async (request, reply) => {
|
|
7559
|
-
|
|
7704
|
+
const llm = options.getLlm();
|
|
7705
|
+
if (!llm) {
|
|
7560
7706
|
return sendLlmUnavailable2(reply);
|
|
7561
7707
|
}
|
|
7562
7708
|
const user = requireAuthenticatedUser(request);
|
|
@@ -7588,7 +7734,8 @@ async function registerLlmRoutes(app, options) {
|
|
|
7588
7734
|
* Runs one stateless LLM completion step using hub-configured provider keys.
|
|
7589
7735
|
*/
|
|
7590
7736
|
handler: async (request, reply) => {
|
|
7591
|
-
|
|
7737
|
+
const llm = options.getLlm();
|
|
7738
|
+
if (!llm) {
|
|
7592
7739
|
return sendLlmUnavailable2(reply);
|
|
7593
7740
|
}
|
|
7594
7741
|
const user = requireAuthenticatedUser(request);
|
|
@@ -7596,7 +7743,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
7596
7743
|
return;
|
|
7597
7744
|
}
|
|
7598
7745
|
const { model, messages, tools, systemPrompt } = request.body;
|
|
7599
|
-
if (!isHubModelOffered(
|
|
7746
|
+
if (!isHubModelOffered(llm, model)) {
|
|
7600
7747
|
return reply.code(403).send({ error: "Model is not offered by this Team Hub." });
|
|
7601
7748
|
}
|
|
7602
7749
|
if (!isLlmModelAllowed(user, model)) {
|
|
@@ -7610,7 +7757,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
7610
7757
|
if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
|
|
7611
7758
|
return sendMonthlyLimitExceeded(reply);
|
|
7612
7759
|
}
|
|
7613
|
-
const result = await runLlmCompletion(
|
|
7760
|
+
const result = await runLlmCompletion(llm, {
|
|
7614
7761
|
model,
|
|
7615
7762
|
messages,
|
|
7616
7763
|
tools,
|
|
@@ -7648,6 +7795,43 @@ async function registerLlmRoutes(app, options) {
|
|
|
7648
7795
|
});
|
|
7649
7796
|
}
|
|
7650
7797
|
|
|
7798
|
+
// src/server/routes/schemas/plugins.ts
|
|
7799
|
+
import { z as z11 } from "zod/v4";
|
|
7800
|
+
var pluginSourcesResponseSchema = z11.object({
|
|
7801
|
+
catalogs: z11.array(z11.string()),
|
|
7802
|
+
trusted: z11.array(z11.string())
|
|
7803
|
+
});
|
|
7804
|
+
|
|
7805
|
+
// src/server/routes/plugins.ts
|
|
7806
|
+
async function registerPluginsRoutes(app, options) {
|
|
7807
|
+
const routes = app.withTypeProvider();
|
|
7808
|
+
routes.route({
|
|
7809
|
+
method: "GET",
|
|
7810
|
+
url: "/plugins/sources",
|
|
7811
|
+
schema: {
|
|
7812
|
+
response: {
|
|
7813
|
+
200: pluginSourcesResponseSchema
|
|
7814
|
+
}
|
|
7815
|
+
},
|
|
7816
|
+
/**
|
|
7817
|
+
* Returns plugin catalog and trusted-publisher URLs configured on this Team Hub.
|
|
7818
|
+
*/
|
|
7819
|
+
handler: async (_request, reply) => {
|
|
7820
|
+
const plugins = options.getPlugins();
|
|
7821
|
+
if (!plugins) {
|
|
7822
|
+
return reply.send({
|
|
7823
|
+
catalogs: [],
|
|
7824
|
+
trusted: []
|
|
7825
|
+
});
|
|
7826
|
+
}
|
|
7827
|
+
return reply.send({
|
|
7828
|
+
catalogs: plugins.catalogs,
|
|
7829
|
+
trusted: plugins.trusted
|
|
7830
|
+
});
|
|
7831
|
+
}
|
|
7832
|
+
});
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7651
7835
|
// src/server/auth/bearerAuthPlugin.ts
|
|
7652
7836
|
function registerBearerAuthDecorator(app) {
|
|
7653
7837
|
app.decorateRequest("apiToken", null);
|
|
@@ -7658,8 +7842,8 @@ function buildAuthThrottleKey(request, token) {
|
|
|
7658
7842
|
return `${request.ip}:${tokenPart}`;
|
|
7659
7843
|
}
|
|
7660
7844
|
function createBearerAuthHook(db, throttleStore) {
|
|
7661
|
-
const policy = throttleStore.getPolicy();
|
|
7662
7845
|
return async function bearerAuth(request, reply) {
|
|
7846
|
+
const policy = throttleStore.getPolicy();
|
|
7663
7847
|
const token = extractBearer(request.headers.authorization);
|
|
7664
7848
|
const throttleKey = buildAuthThrottleKey(request, token);
|
|
7665
7849
|
try {
|
|
@@ -7714,12 +7898,17 @@ async function registerProtectedRoutes(app, options) {
|
|
|
7714
7898
|
registerBearerAuthDecorator(app);
|
|
7715
7899
|
app.addHook("onRequest", createBearerAuthHook(options.db, options.throttleStore));
|
|
7716
7900
|
await registerAuthRoutes(app);
|
|
7717
|
-
await registerAdminRoutes(app, {
|
|
7901
|
+
await registerAdminRoutes(app, {
|
|
7902
|
+
db: options.db,
|
|
7903
|
+
getLlm: options.getLlm,
|
|
7904
|
+
reloadConfig: options.reloadConfig
|
|
7905
|
+
});
|
|
7718
7906
|
await registerCollectionRoutes(app, options.db);
|
|
7719
7907
|
await registerEnvironmentRoutes(app, options.db);
|
|
7720
7908
|
await registerFolderRoutes(app, options.db);
|
|
7721
7909
|
await registerRequestRoutes(app, options.db);
|
|
7722
|
-
await registerLlmRoutes(app, { db: options.db,
|
|
7910
|
+
await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
|
|
7911
|
+
await registerPluginsRoutes(app, { getPlugins: options.getPlugins });
|
|
7723
7912
|
}
|
|
7724
7913
|
async function registerRoutes(app, options) {
|
|
7725
7914
|
await app.register(async (publicApp) => {
|
|
@@ -7731,21 +7920,36 @@ async function registerRoutes(app, options) {
|
|
|
7731
7920
|
}
|
|
7732
7921
|
|
|
7733
7922
|
// src/server/createServer.ts
|
|
7734
|
-
async function createServer(
|
|
7923
|
+
async function createServer(ctxOrConfig, options = {}) {
|
|
7924
|
+
const isRuntimeContext = "getLlm" in ctxOrConfig && "configPath" in ctxOrConfig;
|
|
7925
|
+
const ctx = isRuntimeContext ? ctxOrConfig : null;
|
|
7926
|
+
const legacyConfig = isRuntimeContext ? null : ctxOrConfig;
|
|
7927
|
+
const db = options.db ?? ctx?.db;
|
|
7928
|
+
const throttleStore = options.throttleStore ?? ctx?.throttleStore;
|
|
7929
|
+
if (!db || !throttleStore) {
|
|
7930
|
+
throw new Error("createServer requires db and throttleStore.");
|
|
7931
|
+
}
|
|
7932
|
+
const logger = options.logger ?? ctx?.logger ?? createLogger(legacyConfig?.logging ?? DEFAULT_LOGGING_CONFIG);
|
|
7735
7933
|
const app = Fastify({
|
|
7736
7934
|
logger: options.verbose ?? false
|
|
7737
7935
|
}).withTypeProvider();
|
|
7738
7936
|
app.setValidatorCompiler(validatorCompiler);
|
|
7739
7937
|
app.setSerializerCompiler(serializerCompiler);
|
|
7938
|
+
registerHttpLogging(app, logger);
|
|
7740
7939
|
await registerRoutes(app, {
|
|
7741
7940
|
version: options.version ?? readPackageVersion(),
|
|
7742
|
-
db
|
|
7743
|
-
throttleStore
|
|
7744
|
-
|
|
7941
|
+
db,
|
|
7942
|
+
throttleStore,
|
|
7943
|
+
getLlm: ctx ? () => ctx.getLlm() : () => legacyConfig?.llm ?? null,
|
|
7944
|
+
getPlugins: ctx ? () => ctx.getPlugins() : () => legacyConfig?.plugins ?? null,
|
|
7945
|
+
reloadConfig: options.reloadConfig ?? (async () => ({ sections: [] }))
|
|
7745
7946
|
});
|
|
7746
7947
|
return app;
|
|
7747
7948
|
}
|
|
7748
7949
|
|
|
7950
|
+
// src/server/runtimeContext.ts
|
|
7951
|
+
import { isDeepStrictEqual } from "util";
|
|
7952
|
+
|
|
7749
7953
|
// src/server/auth/throttle/RedisThrottleStore.ts
|
|
7750
7954
|
import Redis from "ioredis";
|
|
7751
7955
|
|
|
@@ -7885,6 +8089,151 @@ function createThrottleStore(config) {
|
|
|
7885
8089
|
return RedisThrottleStore.fromConfig(config);
|
|
7886
8090
|
}
|
|
7887
8091
|
|
|
8092
|
+
// src/server/runtimeContext.ts
|
|
8093
|
+
var runtimeContextStates = /* @__PURE__ */ new WeakMap();
|
|
8094
|
+
function createSwappableProxy(holder) {
|
|
8095
|
+
return new Proxy({}, {
|
|
8096
|
+
/**
|
|
8097
|
+
* Forwards property reads to the current underlying instance.
|
|
8098
|
+
*/
|
|
8099
|
+
get(_target, prop) {
|
|
8100
|
+
const value = Reflect.get(holder.underlying, prop, holder.underlying);
|
|
8101
|
+
if (typeof value === "function") {
|
|
8102
|
+
return value.bind(holder.underlying);
|
|
8103
|
+
}
|
|
8104
|
+
return value;
|
|
8105
|
+
}
|
|
8106
|
+
});
|
|
8107
|
+
}
|
|
8108
|
+
function getState(ctx) {
|
|
8109
|
+
const state = runtimeContextStates.get(ctx);
|
|
8110
|
+
if (!state) {
|
|
8111
|
+
throw new Error("Invalid runtime context.");
|
|
8112
|
+
}
|
|
8113
|
+
return state;
|
|
8114
|
+
}
|
|
8115
|
+
function formatReloadError(error) {
|
|
8116
|
+
if (error instanceof Error) {
|
|
8117
|
+
return error.message;
|
|
8118
|
+
}
|
|
8119
|
+
return String(error);
|
|
8120
|
+
}
|
|
8121
|
+
function createRuntimeContext(config, configPath) {
|
|
8122
|
+
const state = {
|
|
8123
|
+
configPath,
|
|
8124
|
+
host: config.host,
|
|
8125
|
+
port: config.port,
|
|
8126
|
+
activeDbConfig: config.db,
|
|
8127
|
+
activeRedisConfig: config.redis,
|
|
8128
|
+
dbHolder: { underlying: createDatabase(config.db) },
|
|
8129
|
+
throttleHolder: { underlying: createThrottleStore(config.redis) },
|
|
8130
|
+
llm: config.llm,
|
|
8131
|
+
plugins: config.plugins
|
|
8132
|
+
};
|
|
8133
|
+
const ctx = {
|
|
8134
|
+
configPath: state.configPath,
|
|
8135
|
+
get host() {
|
|
8136
|
+
return state.host;
|
|
8137
|
+
},
|
|
8138
|
+
get port() {
|
|
8139
|
+
return state.port;
|
|
8140
|
+
},
|
|
8141
|
+
db: createSwappableProxy(state.dbHolder),
|
|
8142
|
+
throttleStore: createSwappableProxy(state.throttleHolder),
|
|
8143
|
+
getLlm: () => state.llm,
|
|
8144
|
+
getPlugins: () => state.plugins,
|
|
8145
|
+
logger: createLogger(config.logging)
|
|
8146
|
+
};
|
|
8147
|
+
runtimeContextStates.set(ctx, state);
|
|
8148
|
+
return ctx;
|
|
8149
|
+
}
|
|
8150
|
+
async function connectRuntimeContext(ctx) {
|
|
8151
|
+
const state = getState(ctx);
|
|
8152
|
+
await state.dbHolder.underlying.connect();
|
|
8153
|
+
await state.throttleHolder.underlying.connect();
|
|
8154
|
+
}
|
|
8155
|
+
async function disconnectAll(ctx) {
|
|
8156
|
+
const state = getState(ctx);
|
|
8157
|
+
await state.dbHolder.underlying.disconnect();
|
|
8158
|
+
await state.throttleHolder.underlying.disconnect();
|
|
8159
|
+
}
|
|
8160
|
+
async function reloadDbSection(state, nextDbConfig) {
|
|
8161
|
+
if (isDeepStrictEqual(state.activeDbConfig, nextDbConfig)) {
|
|
8162
|
+
return { section: "db", status: "unchanged" };
|
|
8163
|
+
}
|
|
8164
|
+
try {
|
|
8165
|
+
const nextDb = createDatabase(nextDbConfig);
|
|
8166
|
+
await nextDb.connect();
|
|
8167
|
+
const previousDb = state.dbHolder.underlying;
|
|
8168
|
+
state.dbHolder.underlying = nextDb;
|
|
8169
|
+
state.activeDbConfig = nextDbConfig;
|
|
8170
|
+
await previousDb.disconnect();
|
|
8171
|
+
return { section: "db", status: "reloaded" };
|
|
8172
|
+
} catch (error) {
|
|
8173
|
+
return { section: "db", status: "failed", error: formatReloadError(error) };
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8176
|
+
async function reloadRedisSection(state, nextRedisConfig) {
|
|
8177
|
+
if (isDeepStrictEqual(state.activeRedisConfig, nextRedisConfig)) {
|
|
8178
|
+
return { section: "redis", status: "unchanged" };
|
|
8179
|
+
}
|
|
8180
|
+
try {
|
|
8181
|
+
const nextStore = createThrottleStore(nextRedisConfig);
|
|
8182
|
+
await nextStore.connect();
|
|
8183
|
+
const previousStore = state.throttleHolder.underlying;
|
|
8184
|
+
state.throttleHolder.underlying = nextStore;
|
|
8185
|
+
state.activeRedisConfig = nextRedisConfig;
|
|
8186
|
+
await previousStore.disconnect();
|
|
8187
|
+
return { section: "redis", status: "reloaded" };
|
|
8188
|
+
} catch (error) {
|
|
8189
|
+
return { section: "redis", status: "failed", error: formatReloadError(error) };
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
function reloadServerSection(state, nextConfig) {
|
|
8193
|
+
if (state.host === nextConfig.host && state.port === nextConfig.port) {
|
|
8194
|
+
return { section: "server", status: "unchanged" };
|
|
8195
|
+
}
|
|
8196
|
+
return {
|
|
8197
|
+
section: "server",
|
|
8198
|
+
status: "restart-required",
|
|
8199
|
+
error: "Changes to server.host or server.port require a full process restart."
|
|
8200
|
+
};
|
|
8201
|
+
}
|
|
8202
|
+
async function reloadRuntimeConfig(ctx) {
|
|
8203
|
+
const state = getState(ctx);
|
|
8204
|
+
let nextConfig;
|
|
8205
|
+
try {
|
|
8206
|
+
nextConfig = loadServerConfig(state.configPath);
|
|
8207
|
+
} catch (error) {
|
|
8208
|
+
const message = error instanceof ConfigError ? error.message : formatReloadError(error);
|
|
8209
|
+
return { sections: [], fatalError: message };
|
|
8210
|
+
}
|
|
8211
|
+
const sections = [];
|
|
8212
|
+
sections.push(await reloadDbSection(state, nextConfig.db));
|
|
8213
|
+
sections.push(await reloadRedisSection(state, nextConfig.redis));
|
|
8214
|
+
state.llm = nextConfig.llm;
|
|
8215
|
+
sections.push({ section: "llm", status: "reloaded" });
|
|
8216
|
+
state.plugins = nextConfig.plugins;
|
|
8217
|
+
sections.push({ section: "plugins", status: "reloaded" });
|
|
8218
|
+
sections.push(reloadServerSection(state, nextConfig));
|
|
8219
|
+
return { sections };
|
|
8220
|
+
}
|
|
8221
|
+
function formatConfigReloadSummary(result) {
|
|
8222
|
+
return result.sections.map((section) => {
|
|
8223
|
+
if (section.error) {
|
|
8224
|
+
return `${section.section}: ${section.status} (${section.error})`;
|
|
8225
|
+
}
|
|
8226
|
+
return `${section.section}: ${section.status}`;
|
|
8227
|
+
}).join(", ");
|
|
8228
|
+
}
|
|
8229
|
+
function logConfigReloadResult(result) {
|
|
8230
|
+
if (result.fatalError) {
|
|
8231
|
+
console.error(`Team Hub config reload failed: ${result.fatalError}`);
|
|
8232
|
+
return;
|
|
8233
|
+
}
|
|
8234
|
+
console.log(`Team Hub config reloaded (${formatConfigReloadSummary(result)}).`);
|
|
8235
|
+
}
|
|
8236
|
+
|
|
7888
8237
|
// src/server.ts
|
|
7889
8238
|
function formatListenAddress(address, port) {
|
|
7890
8239
|
if (!address) {
|
|
@@ -7896,12 +8245,11 @@ function formatListenAddress(address, port) {
|
|
|
7896
8245
|
const host = address.includes(":") && !address.startsWith("[") ? `[${address}]` : address;
|
|
7897
8246
|
return `http://${host}:${port}`;
|
|
7898
8247
|
}
|
|
7899
|
-
function registerGracefulShutdown(app,
|
|
8248
|
+
function registerGracefulShutdown(app, ctx) {
|
|
7900
8249
|
const shutdown = async (signal) => {
|
|
7901
8250
|
app.log.info(`Received ${signal}, shutting down.`);
|
|
7902
8251
|
await app.close();
|
|
7903
|
-
await
|
|
7904
|
-
await throttleStore.disconnect();
|
|
8252
|
+
await disconnectAll(ctx);
|
|
7905
8253
|
process.exit(0);
|
|
7906
8254
|
};
|
|
7907
8255
|
process.once("SIGINT", () => {
|
|
@@ -7911,33 +8259,42 @@ function registerGracefulShutdown(app, db, throttleStore) {
|
|
|
7911
8259
|
void shutdown("SIGTERM");
|
|
7912
8260
|
});
|
|
7913
8261
|
}
|
|
7914
|
-
|
|
7915
|
-
|
|
8262
|
+
function registerConfigReloadHandler(reloadConfig) {
|
|
8263
|
+
process.on("SIGHUP", () => {
|
|
8264
|
+
void reloadConfig();
|
|
8265
|
+
});
|
|
8266
|
+
}
|
|
8267
|
+
async function runServer(ctx, options = {}) {
|
|
8268
|
+
const reloadConfig = async () => {
|
|
8269
|
+
const result = await reloadRuntimeConfig(ctx);
|
|
8270
|
+
logConfigReloadResult(result);
|
|
8271
|
+
return result;
|
|
8272
|
+
};
|
|
8273
|
+
const app = await createServer(ctx, {
|
|
7916
8274
|
verbose: options.verbose,
|
|
7917
|
-
|
|
7918
|
-
throttleStore: options.throttleStore
|
|
8275
|
+
reloadConfig
|
|
7919
8276
|
});
|
|
7920
|
-
await
|
|
7921
|
-
await options.throttleStore.connect();
|
|
8277
|
+
await connectRuntimeContext(ctx);
|
|
7922
8278
|
await app.listen({
|
|
7923
|
-
host:
|
|
7924
|
-
port:
|
|
8279
|
+
host: ctx.host,
|
|
8280
|
+
port: ctx.port
|
|
7925
8281
|
});
|
|
7926
8282
|
const address = app.server.address();
|
|
7927
|
-
const port = typeof address === "object" && address ? address.port :
|
|
7928
|
-
const host = typeof address === "object" && address ? address.address :
|
|
8283
|
+
const port = typeof address === "object" && address ? address.port : ctx.port;
|
|
8284
|
+
const host = typeof address === "object" && address ? address.address : ctx.host;
|
|
7929
8285
|
if (options.verbose) {
|
|
7930
|
-
console.log("Starting server with config:",
|
|
8286
|
+
console.log("Starting server with config path:", ctx.configPath);
|
|
7931
8287
|
}
|
|
7932
8288
|
console.log(`Team Hub listening on ${formatListenAddress(host, port)}`);
|
|
7933
|
-
registerGracefulShutdown(app,
|
|
8289
|
+
registerGracefulShutdown(app, ctx);
|
|
8290
|
+
registerConfigReloadHandler(reloadConfig);
|
|
7934
8291
|
return app;
|
|
7935
8292
|
}
|
|
7936
8293
|
async function startCommand(options) {
|
|
8294
|
+
const configPath = resolveConfigPath(options.config);
|
|
7937
8295
|
const config = loadServerConfig(options.config);
|
|
7938
|
-
const
|
|
7939
|
-
|
|
7940
|
-
await runServer(config, { verbose: options.verbose, db, throttleStore });
|
|
8296
|
+
const ctx = createRuntimeContext(config, configPath);
|
|
8297
|
+
await runServer(ctx, { verbose: options.verbose });
|
|
7941
8298
|
}
|
|
7942
8299
|
function registerStartCommand(program, handler = startCommand) {
|
|
7943
8300
|
program.command("start").description("Start the Team Hub server").action(
|