@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 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, llm } = options;
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
- if (!options.llm) {
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(options.llm).filter(
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
- if (!options.llm) {
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
- if (!options.llm) {
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(options.llm, model)) {
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(options.llm, {
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, { db: options.db, llm: options.llm });
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, llm: options.llm });
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(config, options) {
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: options.db,
7743
- throttleStore: options.throttleStore,
7744
- llm: config.llm
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, db, throttleStore) {
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 db.disconnect();
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
- async function runServer(config, options) {
7915
- const app = await createServer(config, {
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
- db: options.db,
7918
- throttleStore: options.throttleStore
8275
+ reloadConfig
7919
8276
  });
7920
- await options.db.connect();
7921
- await options.throttleStore.connect();
8277
+ await connectRuntimeContext(ctx);
7922
8278
  await app.listen({
7923
- host: config.host,
7924
- port: config.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 : config.port;
7928
- const host = typeof address === "object" && address ? address.address : config.host;
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:", 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, options.db, options.throttleStore);
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 db = createDatabase(config.db);
7939
- const throttleStore = createThrottleStore(config.redis);
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(