@harborclient/team-hub 0.2.1 → 0.2.3
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 +882 -58
- 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) {
|
|
@@ -367,7 +431,8 @@ function mapFirestoreCollection(id, data) {
|
|
|
367
431
|
createdAt: data.createdAt,
|
|
368
432
|
updatedAt: data.updatedAt ?? data.createdAt,
|
|
369
433
|
createdByUserId: data.createdByUserId ?? null,
|
|
370
|
-
updatedByUserId: data.updatedByUserId ?? null
|
|
434
|
+
updatedByUserId: data.updatedByUserId ?? null,
|
|
435
|
+
deletionLocked: data.deletionLocked ?? false
|
|
371
436
|
};
|
|
372
437
|
}
|
|
373
438
|
function mapFirestoreEnvironment(id, data) {
|
|
@@ -378,7 +443,8 @@ function mapFirestoreEnvironment(id, data) {
|
|
|
378
443
|
createdAt: data.createdAt,
|
|
379
444
|
updatedAt: data.updatedAt ?? data.createdAt,
|
|
380
445
|
createdByUserId: data.createdByUserId ?? null,
|
|
381
|
-
updatedByUserId: data.updatedByUserId ?? null
|
|
446
|
+
updatedByUserId: data.updatedByUserId ?? null,
|
|
447
|
+
deletionLocked: data.deletionLocked ?? false
|
|
382
448
|
};
|
|
383
449
|
}
|
|
384
450
|
function mapFirestoreFolder(id, data) {
|
|
@@ -924,7 +990,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
924
990
|
createdAt: now,
|
|
925
991
|
updatedAt: now,
|
|
926
992
|
createdByUserId: actingUserId,
|
|
927
|
-
updatedByUserId: actingUserId
|
|
993
|
+
updatedByUserId: actingUserId,
|
|
994
|
+
deletionLocked: false
|
|
928
995
|
};
|
|
929
996
|
await this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id).set(data);
|
|
930
997
|
await this.recordAuditEntry(actingUserId, "create", "collection", id);
|
|
@@ -986,6 +1053,46 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
986
1053
|
];
|
|
987
1054
|
await this.commitBatchedDeletes(refs);
|
|
988
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Finds a collection by stable identifier.
|
|
1058
|
+
*
|
|
1059
|
+
* @param id - Collection ID to look up.
|
|
1060
|
+
*/
|
|
1061
|
+
async findCollectionById(id) {
|
|
1062
|
+
const snapshot = await this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id).get();
|
|
1063
|
+
if (!snapshot.exists) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
return mapFirestoreCollection(id, snapshot.data());
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Updates whether non-admin users may delete a collection.
|
|
1070
|
+
*
|
|
1071
|
+
* @param id - Collection ID to update.
|
|
1072
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the collection.
|
|
1073
|
+
* @param actingUserId - Admin user performing the update.
|
|
1074
|
+
*/
|
|
1075
|
+
async setCollectionDeletionLocked(id, deletionLocked, actingUserId) {
|
|
1076
|
+
const docRef = this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id);
|
|
1077
|
+
const snapshot = await docRef.get();
|
|
1078
|
+
if (!snapshot.exists) {
|
|
1079
|
+
throw new Error("Collection not found");
|
|
1080
|
+
}
|
|
1081
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
1082
|
+
await docRef.update({
|
|
1083
|
+
deletionLocked,
|
|
1084
|
+
updatedAt,
|
|
1085
|
+
updatedByUserId: actingUserId
|
|
1086
|
+
});
|
|
1087
|
+
await this.recordAuditEntry(actingUserId, "update", "collection", id);
|
|
1088
|
+
const existing = snapshot.data();
|
|
1089
|
+
return mapFirestoreCollection(id, {
|
|
1090
|
+
...existing,
|
|
1091
|
+
deletionLocked,
|
|
1092
|
+
updatedAt,
|
|
1093
|
+
updatedByUserId: actingUserId
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
989
1096
|
/**
|
|
990
1097
|
* Lists all environments ordered by name.
|
|
991
1098
|
*/
|
|
@@ -1011,7 +1118,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1011
1118
|
createdAt: now,
|
|
1012
1119
|
updatedAt: now,
|
|
1013
1120
|
createdByUserId: actingUserId,
|
|
1014
|
-
updatedByUserId: actingUserId
|
|
1121
|
+
updatedByUserId: actingUserId,
|
|
1122
|
+
deletionLocked: false
|
|
1015
1123
|
};
|
|
1016
1124
|
await this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id).set(data);
|
|
1017
1125
|
await this.recordAuditEntry(actingUserId, "create", "environment", id);
|
|
@@ -1057,6 +1165,46 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1057
1165
|
await this.recordAuditEntry(actingUserId, "delete", "environment", id);
|
|
1058
1166
|
await this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id).delete();
|
|
1059
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Finds an environment by stable identifier.
|
|
1170
|
+
*
|
|
1171
|
+
* @param id - Environment ID to look up.
|
|
1172
|
+
*/
|
|
1173
|
+
async findEnvironmentById(id) {
|
|
1174
|
+
const snapshot = await this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id).get();
|
|
1175
|
+
if (!snapshot.exists) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
return mapFirestoreEnvironment(id, snapshot.data());
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Updates whether non-admin users may delete an environment.
|
|
1182
|
+
*
|
|
1183
|
+
* @param id - Environment ID to update.
|
|
1184
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the environment.
|
|
1185
|
+
* @param actingUserId - Admin user performing the update.
|
|
1186
|
+
*/
|
|
1187
|
+
async setEnvironmentDeletionLocked(id, deletionLocked, actingUserId) {
|
|
1188
|
+
const docRef = this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id);
|
|
1189
|
+
const snapshot = await docRef.get();
|
|
1190
|
+
if (!snapshot.exists) {
|
|
1191
|
+
throw new Error("Environment not found");
|
|
1192
|
+
}
|
|
1193
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
1194
|
+
await docRef.update({
|
|
1195
|
+
deletionLocked,
|
|
1196
|
+
updatedAt,
|
|
1197
|
+
updatedByUserId: actingUserId
|
|
1198
|
+
});
|
|
1199
|
+
await this.recordAuditEntry(actingUserId, "update", "environment", id);
|
|
1200
|
+
const existing = snapshot.data();
|
|
1201
|
+
return mapFirestoreEnvironment(id, {
|
|
1202
|
+
...existing,
|
|
1203
|
+
deletionLocked,
|
|
1204
|
+
updatedAt,
|
|
1205
|
+
updatedByUserId: actingUserId
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1060
1208
|
/**
|
|
1061
1209
|
* Lists all saved requests in a collection.
|
|
1062
1210
|
*
|
|
@@ -1667,7 +1815,8 @@ function mapCollectionSqlRow(row) {
|
|
|
1667
1815
|
createdAt: row.created_at,
|
|
1668
1816
|
updatedAt: row.updated_at ?? row.created_at,
|
|
1669
1817
|
createdByUserId: row.created_by_user_id ?? null,
|
|
1670
|
-
updatedByUserId: row.updated_by_user_id ?? null
|
|
1818
|
+
updatedByUserId: row.updated_by_user_id ?? null,
|
|
1819
|
+
deletionLocked: Boolean(row.deletion_locked)
|
|
1671
1820
|
};
|
|
1672
1821
|
}
|
|
1673
1822
|
function mapEnvironmentSqlRow(row) {
|
|
@@ -1678,7 +1827,8 @@ function mapEnvironmentSqlRow(row) {
|
|
|
1678
1827
|
createdAt: row.created_at,
|
|
1679
1828
|
updatedAt: row.updated_at ?? row.created_at,
|
|
1680
1829
|
createdByUserId: row.created_by_user_id ?? null,
|
|
1681
|
-
updatedByUserId: row.updated_by_user_id ?? null
|
|
1830
|
+
updatedByUserId: row.updated_by_user_id ?? null,
|
|
1831
|
+
deletionLocked: Boolean(row.deletion_locked)
|
|
1682
1832
|
};
|
|
1683
1833
|
}
|
|
1684
1834
|
function mapFolderSqlRow(row) {
|
|
@@ -1921,6 +2071,14 @@ CREATE TABLE IF NOT EXISTS llm_usage_log (
|
|
|
1921
2071
|
)
|
|
1922
2072
|
`.trim();
|
|
1923
2073
|
var MYSQL_DEFAULT_AUTH_JSON = DEFAULT_AUTH_JSON;
|
|
2074
|
+
var COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL = `
|
|
2075
|
+
ALTER TABLE collections
|
|
2076
|
+
ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
|
|
2077
|
+
`.trim();
|
|
2078
|
+
var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL = `
|
|
2079
|
+
ALTER TABLE environments
|
|
2080
|
+
ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
|
|
2081
|
+
`.trim();
|
|
1924
2082
|
var MYSQL_MIGRATIONS = [
|
|
1925
2083
|
USERS_MIGRATION_SQL,
|
|
1926
2084
|
API_TOKENS_MIGRATION_SQL,
|
|
@@ -1941,7 +2099,9 @@ var MYSQL_MIGRATIONS = [
|
|
|
1941
2099
|
FOLDERS_BACKFILL_UPDATED_AT_SQL,
|
|
1942
2100
|
USERS_LLM_MIGRATION_SQL,
|
|
1943
2101
|
LLM_USAGE_MIGRATION_SQL,
|
|
1944
|
-
LLM_USAGE_LOG_MIGRATION_SQL
|
|
2102
|
+
LLM_USAGE_LOG_MIGRATION_SQL,
|
|
2103
|
+
COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
|
|
2104
|
+
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL
|
|
1945
2105
|
];
|
|
1946
2106
|
|
|
1947
2107
|
// src/db/mysql/schemas.ts
|
|
@@ -1998,8 +2158,8 @@ function serializeAccessList(access) {
|
|
|
1998
2158
|
return JSON.stringify(access);
|
|
1999
2159
|
}
|
|
2000
2160
|
var USER_SELECT_COLUMNS = `id, name, role, collection_access, environment_access, llm_access, llm_models, llm_monthly_token_limit, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2001
|
-
var COLLECTION_SELECT_COLUMNS = `id, name, variables, headers, auth, pre_request_script, post_request_script, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2002
|
-
var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2161
|
+
var COLLECTION_SELECT_COLUMNS = `id, name, variables, headers, auth, pre_request_script, post_request_script, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
2162
|
+
var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
2003
2163
|
var FOLDER_SELECT_COLUMNS = `id, collection_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2004
2164
|
var REQUEST_SELECT_COLUMNS = `id, collection_id, folder_id, name, method, url, headers, params, auth, body, body_type, pre_request_script, post_request_script, comment, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2005
2165
|
var API_TOKEN_SELECT_COLUMNS = `id, user_id, name, token_hash, token_prefix, created_at, last_used_at, revoked_at, created_by_user_id, updated_by_user_id`;
|
|
@@ -2591,6 +2751,50 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2591
2751
|
await this.recordAuditEntry(actingUserId, "delete", "collection", id);
|
|
2592
2752
|
await this.executeStatement("DELETE FROM collections WHERE id = ?", [id]);
|
|
2593
2753
|
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Finds a collection by stable identifier.
|
|
2756
|
+
*
|
|
2757
|
+
* @param id - Collection ID to look up.
|
|
2758
|
+
*/
|
|
2759
|
+
async findCollectionById(id) {
|
|
2760
|
+
const rows = await this.queryRows(
|
|
2761
|
+
`${COLLECTION_SELECT} WHERE id = ?`,
|
|
2762
|
+
[id]
|
|
2763
|
+
);
|
|
2764
|
+
const row = rows[0];
|
|
2765
|
+
return row ? mapCollectionSqlRow(row) : null;
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Updates whether non-admin users may delete a collection.
|
|
2769
|
+
*
|
|
2770
|
+
* @param id - Collection ID to update.
|
|
2771
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the collection.
|
|
2772
|
+
* @param actingUserId - Admin user performing the update.
|
|
2773
|
+
*/
|
|
2774
|
+
async setCollectionDeletionLocked(id, deletionLocked, actingUserId) {
|
|
2775
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
2776
|
+
const result = await this.executeStatement(
|
|
2777
|
+
`UPDATE collections
|
|
2778
|
+
SET deletion_locked = ?,
|
|
2779
|
+
updated_at = ?,
|
|
2780
|
+
updated_by_user_id = ?
|
|
2781
|
+
WHERE id = ?`,
|
|
2782
|
+
[deletionLocked ? 1 : 0, updatedAt, actingUserId, id]
|
|
2783
|
+
);
|
|
2784
|
+
if ((result.affectedRows ?? 0) === 0) {
|
|
2785
|
+
throw new Error("Collection not found");
|
|
2786
|
+
}
|
|
2787
|
+
await this.recordAuditEntry(actingUserId, "update", "collection", id);
|
|
2788
|
+
const rows = await this.queryRows(
|
|
2789
|
+
`${COLLECTION_SELECT} WHERE id = ?`,
|
|
2790
|
+
[id]
|
|
2791
|
+
);
|
|
2792
|
+
const row = rows[0];
|
|
2793
|
+
if (!row) {
|
|
2794
|
+
throw new Error("Collection not found");
|
|
2795
|
+
}
|
|
2796
|
+
return mapCollectionSqlRow(row);
|
|
2797
|
+
}
|
|
2594
2798
|
/**
|
|
2595
2799
|
* Lists all environments ordered by name.
|
|
2596
2800
|
*/
|
|
@@ -2674,6 +2878,50 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2674
2878
|
await this.recordAuditEntry(actingUserId, "delete", "environment", id);
|
|
2675
2879
|
await this.executeStatement("DELETE FROM environments WHERE id = ?", [id]);
|
|
2676
2880
|
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Finds an environment by stable identifier.
|
|
2883
|
+
*
|
|
2884
|
+
* @param id - Environment ID to look up.
|
|
2885
|
+
*/
|
|
2886
|
+
async findEnvironmentById(id) {
|
|
2887
|
+
const rows = await this.queryRows(
|
|
2888
|
+
`${ENVIRONMENT_SELECT} WHERE id = ?`,
|
|
2889
|
+
[id]
|
|
2890
|
+
);
|
|
2891
|
+
const row = rows[0];
|
|
2892
|
+
return row ? mapEnvironmentSqlRow(row) : null;
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Updates whether non-admin users may delete an environment.
|
|
2896
|
+
*
|
|
2897
|
+
* @param id - Environment ID to update.
|
|
2898
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the environment.
|
|
2899
|
+
* @param actingUserId - Admin user performing the update.
|
|
2900
|
+
*/
|
|
2901
|
+
async setEnvironmentDeletionLocked(id, deletionLocked, actingUserId) {
|
|
2902
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
2903
|
+
const result = await this.executeStatement(
|
|
2904
|
+
`UPDATE environments
|
|
2905
|
+
SET deletion_locked = ?,
|
|
2906
|
+
updated_at = ?,
|
|
2907
|
+
updated_by_user_id = ?
|
|
2908
|
+
WHERE id = ?`,
|
|
2909
|
+
[deletionLocked ? 1 : 0, updatedAt, actingUserId, id]
|
|
2910
|
+
);
|
|
2911
|
+
if ((result.affectedRows ?? 0) === 0) {
|
|
2912
|
+
throw new Error("Environment not found");
|
|
2913
|
+
}
|
|
2914
|
+
await this.recordAuditEntry(actingUserId, "update", "environment", id);
|
|
2915
|
+
const rows = await this.queryRows(
|
|
2916
|
+
`${ENVIRONMENT_SELECT} WHERE id = ?`,
|
|
2917
|
+
[id]
|
|
2918
|
+
);
|
|
2919
|
+
const row = rows[0];
|
|
2920
|
+
if (!row) {
|
|
2921
|
+
throw new Error("Environment not found");
|
|
2922
|
+
}
|
|
2923
|
+
return mapEnvironmentSqlRow(row);
|
|
2924
|
+
}
|
|
2677
2925
|
/**
|
|
2678
2926
|
* Lists all saved requests in a collection.
|
|
2679
2927
|
*
|
|
@@ -3526,6 +3774,14 @@ CREATE INDEX IF NOT EXISTS llm_usage_log_user_created_at_idx ON llm_usage_log (u
|
|
|
3526
3774
|
|
|
3527
3775
|
CREATE INDEX IF NOT EXISTS llm_usage_log_period_idx ON llm_usage_log (period);
|
|
3528
3776
|
`.trim();
|
|
3777
|
+
var COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2 = `
|
|
3778
|
+
ALTER TABLE collections
|
|
3779
|
+
ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
|
|
3780
|
+
`.trim();
|
|
3781
|
+
var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2 = `
|
|
3782
|
+
ALTER TABLE environments
|
|
3783
|
+
ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
|
|
3784
|
+
`.trim();
|
|
3529
3785
|
var POSTGRES_MIGRATIONS = [
|
|
3530
3786
|
USERS_MIGRATION_SQL2,
|
|
3531
3787
|
API_TOKENS_MIGRATION_SQL2,
|
|
@@ -3546,7 +3802,9 @@ var POSTGRES_MIGRATIONS = [
|
|
|
3546
3802
|
FOLDERS_BACKFILL_UPDATED_AT_SQL2,
|
|
3547
3803
|
USERS_LLM_MIGRATION_SQL2,
|
|
3548
3804
|
LLM_USAGE_MIGRATION_SQL2,
|
|
3549
|
-
LLM_USAGE_LOG_MIGRATION_SQL2
|
|
3805
|
+
LLM_USAGE_LOG_MIGRATION_SQL2,
|
|
3806
|
+
COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
|
|
3807
|
+
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2
|
|
3550
3808
|
];
|
|
3551
3809
|
|
|
3552
3810
|
// src/db/postgres/schemas.ts
|
|
@@ -4108,6 +4366,46 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
4108
4366
|
await this.recordAuditEntry(actingUserId, "delete", "collection", id);
|
|
4109
4367
|
await this.query("DELETE FROM collections WHERE id = $1", [id]);
|
|
4110
4368
|
}
|
|
4369
|
+
/**
|
|
4370
|
+
* Finds a collection by stable identifier.
|
|
4371
|
+
*
|
|
4372
|
+
* @param id - Collection ID to look up.
|
|
4373
|
+
*/
|
|
4374
|
+
async findCollectionById(id) {
|
|
4375
|
+
const result = await this.query(`${COLLECTION_SELECT2} WHERE id = $1`, [id]);
|
|
4376
|
+
const row = result.rows[0];
|
|
4377
|
+
return row ? mapCollectionSqlRow(row) : null;
|
|
4378
|
+
}
|
|
4379
|
+
/**
|
|
4380
|
+
* Updates whether non-admin users may delete a collection.
|
|
4381
|
+
*
|
|
4382
|
+
* @param id - Collection ID to update.
|
|
4383
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the collection.
|
|
4384
|
+
* @param actingUserId - Admin user performing the update.
|
|
4385
|
+
*/
|
|
4386
|
+
async setCollectionDeletionLocked(id, deletionLocked, actingUserId) {
|
|
4387
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
4388
|
+
const result = await this.query(
|
|
4389
|
+
`UPDATE collections
|
|
4390
|
+
SET deletion_locked = $1,
|
|
4391
|
+
updated_at = $2,
|
|
4392
|
+
updated_by_user_id = $3
|
|
4393
|
+
WHERE id = $4`,
|
|
4394
|
+
[deletionLocked, updatedAt, actingUserId, id]
|
|
4395
|
+
);
|
|
4396
|
+
if ((result.rowCount ?? 0) === 0) {
|
|
4397
|
+
throw new Error("Collection not found");
|
|
4398
|
+
}
|
|
4399
|
+
await this.recordAuditEntry(actingUserId, "update", "collection", id);
|
|
4400
|
+
const selectResult = await this.query(`${COLLECTION_SELECT2} WHERE id = $1`, [
|
|
4401
|
+
id
|
|
4402
|
+
]);
|
|
4403
|
+
const row = selectResult.rows[0];
|
|
4404
|
+
if (!row) {
|
|
4405
|
+
throw new Error("Collection not found");
|
|
4406
|
+
}
|
|
4407
|
+
return mapCollectionSqlRow(row);
|
|
4408
|
+
}
|
|
4111
4409
|
/**
|
|
4112
4410
|
* Lists all environments ordered by name.
|
|
4113
4411
|
*/
|
|
@@ -4186,6 +4484,47 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
4186
4484
|
await this.recordAuditEntry(actingUserId, "delete", "environment", id);
|
|
4187
4485
|
await this.query("DELETE FROM environments WHERE id = $1", [id]);
|
|
4188
4486
|
}
|
|
4487
|
+
/**
|
|
4488
|
+
* Finds an environment by stable identifier.
|
|
4489
|
+
*
|
|
4490
|
+
* @param id - Environment ID to look up.
|
|
4491
|
+
*/
|
|
4492
|
+
async findEnvironmentById(id) {
|
|
4493
|
+
const result = await this.query(`${ENVIRONMENT_SELECT2} WHERE id = $1`, [id]);
|
|
4494
|
+
const row = result.rows[0];
|
|
4495
|
+
return row ? mapEnvironmentSqlRow(row) : null;
|
|
4496
|
+
}
|
|
4497
|
+
/**
|
|
4498
|
+
* Updates whether non-admin users may delete an environment.
|
|
4499
|
+
*
|
|
4500
|
+
* @param id - Environment ID to update.
|
|
4501
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the environment.
|
|
4502
|
+
* @param actingUserId - Admin user performing the update.
|
|
4503
|
+
*/
|
|
4504
|
+
async setEnvironmentDeletionLocked(id, deletionLocked, actingUserId) {
|
|
4505
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
4506
|
+
const result = await this.query(
|
|
4507
|
+
`UPDATE environments
|
|
4508
|
+
SET deletion_locked = $1,
|
|
4509
|
+
updated_at = $2,
|
|
4510
|
+
updated_by_user_id = $3
|
|
4511
|
+
WHERE id = $4`,
|
|
4512
|
+
[deletionLocked, updatedAt, actingUserId, id]
|
|
4513
|
+
);
|
|
4514
|
+
if ((result.rowCount ?? 0) === 0) {
|
|
4515
|
+
throw new Error("Environment not found");
|
|
4516
|
+
}
|
|
4517
|
+
await this.recordAuditEntry(actingUserId, "update", "environment", id);
|
|
4518
|
+
const selectResult = await this.query(
|
|
4519
|
+
`${ENVIRONMENT_SELECT2} WHERE id = $1`,
|
|
4520
|
+
[id]
|
|
4521
|
+
);
|
|
4522
|
+
const row = selectResult.rows[0];
|
|
4523
|
+
if (!row) {
|
|
4524
|
+
throw new Error("Environment not found");
|
|
4525
|
+
}
|
|
4526
|
+
return mapEnvironmentSqlRow(row);
|
|
4527
|
+
}
|
|
4189
4528
|
/**
|
|
4190
4529
|
* Lists all saved requests in a collection.
|
|
4191
4530
|
*
|
|
@@ -5618,6 +5957,48 @@ import {
|
|
|
5618
5957
|
validatorCompiler
|
|
5619
5958
|
} from "fastify-type-provider-zod";
|
|
5620
5959
|
|
|
5960
|
+
// src/server/logging/httpLogging.ts
|
|
5961
|
+
function registerHttpLogging(app, logger) {
|
|
5962
|
+
app.addHook("onRequest", async (request) => {
|
|
5963
|
+
logger.debug("request", {
|
|
5964
|
+
reqId: request.id,
|
|
5965
|
+
method: request.method,
|
|
5966
|
+
url: request.url,
|
|
5967
|
+
ip: request.ip
|
|
5968
|
+
});
|
|
5969
|
+
});
|
|
5970
|
+
app.addHook("onError", async (request, reply, error) => {
|
|
5971
|
+
logger.error("request error", {
|
|
5972
|
+
reqId: request.id,
|
|
5973
|
+
method: request.method,
|
|
5974
|
+
url: request.url,
|
|
5975
|
+
statusCode: reply.statusCode,
|
|
5976
|
+
message: error.message,
|
|
5977
|
+
stack: error.stack
|
|
5978
|
+
});
|
|
5979
|
+
});
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
// src/server/logging/logger.ts
|
|
5983
|
+
import winston from "winston";
|
|
5984
|
+
function createLogger(config) {
|
|
5985
|
+
const transports = [];
|
|
5986
|
+
if (config.console) {
|
|
5987
|
+
transports.push(new winston.transports.Console());
|
|
5988
|
+
}
|
|
5989
|
+
if (config.file) {
|
|
5990
|
+
transports.push(new winston.transports.File({ filename: config.file }));
|
|
5991
|
+
}
|
|
5992
|
+
if (transports.length === 0) {
|
|
5993
|
+
transports.push(new winston.transports.Console({ silent: true }));
|
|
5994
|
+
}
|
|
5995
|
+
return winston.createLogger({
|
|
5996
|
+
level: config.level,
|
|
5997
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
|
5998
|
+
transports
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
|
|
5621
6002
|
// src/packageVersion.ts
|
|
5622
6003
|
import { readFileSync as readFileSync2 } from "fs";
|
|
5623
6004
|
function readPackageVersion() {
|
|
@@ -5638,6 +6019,9 @@ function canUseDataApi(user) {
|
|
|
5638
6019
|
function canListCollections(user) {
|
|
5639
6020
|
return canUseDataApi(user) || canUseManagementApi(user);
|
|
5640
6021
|
}
|
|
6022
|
+
function canListEnvironments(user) {
|
|
6023
|
+
return canUseDataApi(user) || canUseManagementApi(user);
|
|
6024
|
+
}
|
|
5641
6025
|
function hasWildcardAccess(access) {
|
|
5642
6026
|
return access.includes("*");
|
|
5643
6027
|
}
|
|
@@ -5666,20 +6050,14 @@ function canCreateEnvironment(user) {
|
|
|
5666
6050
|
return user.role === "user" && hasWildcardAccess(user.environmentAccess);
|
|
5667
6051
|
}
|
|
5668
6052
|
function filterAccessibleCollections(user, collections) {
|
|
5669
|
-
if (user.role === "admin") {
|
|
5670
|
-
return [];
|
|
5671
|
-
}
|
|
5672
|
-
if (hasWildcardAccess(user.collectionAccess)) {
|
|
6053
|
+
if (user.role === "admin" || hasWildcardAccess(user.collectionAccess)) {
|
|
5673
6054
|
return collections;
|
|
5674
6055
|
}
|
|
5675
6056
|
const allowed = new Set(user.collectionAccess);
|
|
5676
6057
|
return collections.filter((collection) => allowed.has(collection.id));
|
|
5677
6058
|
}
|
|
5678
6059
|
function filterAccessibleEnvironments(user, environments) {
|
|
5679
|
-
if (user.role === "admin") {
|
|
5680
|
-
return [];
|
|
5681
|
-
}
|
|
5682
|
-
if (hasWildcardAccess(user.environmentAccess)) {
|
|
6060
|
+
if (user.role === "admin" || hasWildcardAccess(user.environmentAccess)) {
|
|
5683
6061
|
return environments;
|
|
5684
6062
|
}
|
|
5685
6063
|
const allowed = new Set(user.environmentAccess);
|
|
@@ -5704,6 +6082,17 @@ function isOverMonthlyLimit(totalTokens, limit) {
|
|
|
5704
6082
|
return totalTokens >= limit;
|
|
5705
6083
|
}
|
|
5706
6084
|
|
|
6085
|
+
// src/db/deletionLockedError.ts
|
|
6086
|
+
var DeletionLockedError = class extends Error {
|
|
6087
|
+
/**
|
|
6088
|
+
* @param entityType - Human-readable entity kind shown in the error message.
|
|
6089
|
+
*/
|
|
6090
|
+
constructor(entityType) {
|
|
6091
|
+
super(`Deletion is locked for this ${entityType}.`);
|
|
6092
|
+
this.name = "DeletionLockedError";
|
|
6093
|
+
}
|
|
6094
|
+
};
|
|
6095
|
+
|
|
5707
6096
|
// src/server/routes/schemas/common.ts
|
|
5708
6097
|
import { z as z5 } from "zod/v4";
|
|
5709
6098
|
var errorResponseSchema = z5.object({
|
|
@@ -5780,6 +6169,10 @@ function handleDbError(reply, error) {
|
|
|
5780
6169
|
void reply.code(400).send(errorResponseSchema.parse({ error: error.message }));
|
|
5781
6170
|
return true;
|
|
5782
6171
|
}
|
|
6172
|
+
if (error instanceof DeletionLockedError) {
|
|
6173
|
+
void reply.code(403).send(errorResponseSchema.parse({ error: error.message }));
|
|
6174
|
+
return true;
|
|
6175
|
+
}
|
|
5783
6176
|
if (isDuplicateUserNameDbError(error)) {
|
|
5784
6177
|
void reply.code(400).send(errorResponseSchema.parse({ error: DUPLICATE_USER_NAME_MESSAGE }));
|
|
5785
6178
|
return true;
|
|
@@ -5884,7 +6277,19 @@ var llmUsageSummaryResponseSchema = z7.object({
|
|
|
5884
6277
|
// src/server/routes/schemas/admin.ts
|
|
5885
6278
|
var adminResourceOptionSchema = z8.object({
|
|
5886
6279
|
id: z8.string(),
|
|
5887
|
-
name: z8.string()
|
|
6280
|
+
name: z8.string(),
|
|
6281
|
+
deletionLocked: z8.boolean()
|
|
6282
|
+
});
|
|
6283
|
+
var adminEntityConfigSchema = z8.object({
|
|
6284
|
+
id: z8.string(),
|
|
6285
|
+
name: z8.string(),
|
|
6286
|
+
deletionLocked: z8.boolean()
|
|
6287
|
+
});
|
|
6288
|
+
var updateAdminCollectionBodySchema = z8.object({
|
|
6289
|
+
deletionLocked: z8.boolean()
|
|
6290
|
+
});
|
|
6291
|
+
var updateAdminEnvironmentBodySchema = z8.object({
|
|
6292
|
+
deletionLocked: z8.boolean()
|
|
5888
6293
|
});
|
|
5889
6294
|
var listAdminCollectionsResponseSchema = z8.object({
|
|
5890
6295
|
collections: z8.array(adminResourceOptionSchema)
|
|
@@ -5953,6 +6358,15 @@ var createdApiTokenResponseSchema = z8.object({
|
|
|
5953
6358
|
var listAdminTokensResponseSchema = z8.object({
|
|
5954
6359
|
tokens: z8.array(hubApiTokenRecordSchema)
|
|
5955
6360
|
});
|
|
6361
|
+
var reloadConfigSectionResultSchema = z8.object({
|
|
6362
|
+
section: z8.enum(["db", "redis", "llm", "plugins", "server"]),
|
|
6363
|
+
status: z8.enum(["reloaded", "unchanged", "failed", "restart-required"]),
|
|
6364
|
+
error: z8.string().optional()
|
|
6365
|
+
});
|
|
6366
|
+
var reloadConfigResponseSchema = z8.object({
|
|
6367
|
+
sections: z8.array(reloadConfigSectionResultSchema),
|
|
6368
|
+
fatalError: z8.string().optional()
|
|
6369
|
+
});
|
|
5956
6370
|
function serializeHubUser(user) {
|
|
5957
6371
|
return {
|
|
5958
6372
|
id: user.id,
|
|
@@ -5992,7 +6406,8 @@ var collectionRecordSchema = z9.object({
|
|
|
5992
6406
|
createdAt: timestampSchema,
|
|
5993
6407
|
updatedAt: timestampSchema,
|
|
5994
6408
|
createdByUserId: z9.string().nullable(),
|
|
5995
|
-
updatedByUserId: z9.string().nullable()
|
|
6409
|
+
updatedByUserId: z9.string().nullable(),
|
|
6410
|
+
deletionLocked: z9.boolean()
|
|
5996
6411
|
});
|
|
5997
6412
|
var environmentRecordSchema = z9.object({
|
|
5998
6413
|
id: z9.string(),
|
|
@@ -6001,7 +6416,8 @@ var environmentRecordSchema = z9.object({
|
|
|
6001
6416
|
createdAt: timestampSchema,
|
|
6002
6417
|
updatedAt: timestampSchema,
|
|
6003
6418
|
createdByUserId: z9.string().nullable(),
|
|
6004
|
-
updatedByUserId: z9.string().nullable()
|
|
6419
|
+
updatedByUserId: z9.string().nullable(),
|
|
6420
|
+
deletionLocked: z9.boolean()
|
|
6005
6421
|
});
|
|
6006
6422
|
var folderRecordSchema = z9.object({
|
|
6007
6423
|
id: z9.string(),
|
|
@@ -6159,7 +6575,7 @@ function denySelfRoleChange(reply, targetUserId, actorUserId, existingRole, requ
|
|
|
6159
6575
|
return true;
|
|
6160
6576
|
}
|
|
6161
6577
|
async function registerAdminRoutes(app, options) {
|
|
6162
|
-
const { db,
|
|
6578
|
+
const { db, getLlm, reloadConfig } = options;
|
|
6163
6579
|
const routes = app.withTypeProvider();
|
|
6164
6580
|
routes.route({
|
|
6165
6581
|
method: "GET",
|
|
@@ -6180,6 +6596,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6180
6596
|
return;
|
|
6181
6597
|
}
|
|
6182
6598
|
const systemUserId = db.getSystemUserId();
|
|
6599
|
+
const llm = getLlm();
|
|
6183
6600
|
const [users, collections, environments] = await Promise.all([
|
|
6184
6601
|
db.listUsers(),
|
|
6185
6602
|
db.listCollections(),
|
|
@@ -6232,6 +6649,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6232
6649
|
return;
|
|
6233
6650
|
}
|
|
6234
6651
|
const input = buildAdminUserCreateInput(request.body);
|
|
6652
|
+
const llm = getLlm();
|
|
6235
6653
|
const [collections, environments] = await Promise.all([
|
|
6236
6654
|
db.listCollections(),
|
|
6237
6655
|
db.listEnvironments()
|
|
@@ -6288,7 +6706,8 @@ async function registerAdminRoutes(app, options) {
|
|
|
6288
6706
|
return reply.send({
|
|
6289
6707
|
collections: collections.map((collection) => ({
|
|
6290
6708
|
id: collection.id,
|
|
6291
|
-
name: collection.name
|
|
6709
|
+
name: collection.name,
|
|
6710
|
+
deletionLocked: collection.deletionLocked
|
|
6292
6711
|
}))
|
|
6293
6712
|
});
|
|
6294
6713
|
} catch (error) {
|
|
@@ -6321,7 +6740,8 @@ async function registerAdminRoutes(app, options) {
|
|
|
6321
6740
|
return reply.send({
|
|
6322
6741
|
environments: environments.map((environment) => ({
|
|
6323
6742
|
id: environment.id,
|
|
6324
|
-
name: environment.name
|
|
6743
|
+
name: environment.name,
|
|
6744
|
+
deletionLocked: environment.deletionLocked
|
|
6325
6745
|
}))
|
|
6326
6746
|
});
|
|
6327
6747
|
} catch (error) {
|
|
@@ -6332,6 +6752,154 @@ async function registerAdminRoutes(app, options) {
|
|
|
6332
6752
|
}
|
|
6333
6753
|
}
|
|
6334
6754
|
});
|
|
6755
|
+
routes.route({
|
|
6756
|
+
method: "DELETE",
|
|
6757
|
+
url: "/admin/collections/:id",
|
|
6758
|
+
schema: {
|
|
6759
|
+
params: idParamSchema,
|
|
6760
|
+
response: {
|
|
6761
|
+
204: emptyResponseSchema,
|
|
6762
|
+
403: errorResponseSchema,
|
|
6763
|
+
404: errorResponseSchema
|
|
6764
|
+
}
|
|
6765
|
+
},
|
|
6766
|
+
/**
|
|
6767
|
+
* Deletes a collection regardless of deletion lock state.
|
|
6768
|
+
*/
|
|
6769
|
+
handler: async (request, reply) => {
|
|
6770
|
+
try {
|
|
6771
|
+
const user = requireAuthenticatedUser(request);
|
|
6772
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6773
|
+
return;
|
|
6774
|
+
}
|
|
6775
|
+
const collection = await db.findCollectionById(request.params.id);
|
|
6776
|
+
if (!collection) {
|
|
6777
|
+
void reply.code(404).send({ error: "Collection not found" });
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
await db.deleteCollection(request.params.id, user.id);
|
|
6781
|
+
return reply.code(204).send(null);
|
|
6782
|
+
} catch (error) {
|
|
6783
|
+
if (handleDbError(reply, error)) {
|
|
6784
|
+
return;
|
|
6785
|
+
}
|
|
6786
|
+
throw error;
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
});
|
|
6790
|
+
routes.route({
|
|
6791
|
+
method: "PUT",
|
|
6792
|
+
url: "/admin/collections/:id",
|
|
6793
|
+
schema: {
|
|
6794
|
+
params: idParamSchema,
|
|
6795
|
+
body: updateAdminCollectionBodySchema,
|
|
6796
|
+
response: {
|
|
6797
|
+
200: adminEntityConfigSchema,
|
|
6798
|
+
403: errorResponseSchema,
|
|
6799
|
+
404: errorResponseSchema
|
|
6800
|
+
}
|
|
6801
|
+
},
|
|
6802
|
+
/**
|
|
6803
|
+
* Updates admin configuration for a collection.
|
|
6804
|
+
*/
|
|
6805
|
+
handler: async (request, reply) => {
|
|
6806
|
+
try {
|
|
6807
|
+
const user = requireAuthenticatedUser(request);
|
|
6808
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6809
|
+
return;
|
|
6810
|
+
}
|
|
6811
|
+
const collection = await db.setCollectionDeletionLocked(
|
|
6812
|
+
request.params.id,
|
|
6813
|
+
request.body.deletionLocked,
|
|
6814
|
+
user.id
|
|
6815
|
+
);
|
|
6816
|
+
return reply.send({
|
|
6817
|
+
id: collection.id,
|
|
6818
|
+
name: collection.name,
|
|
6819
|
+
deletionLocked: collection.deletionLocked
|
|
6820
|
+
});
|
|
6821
|
+
} catch (error) {
|
|
6822
|
+
if (handleDbError(reply, error)) {
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
6825
|
+
throw error;
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
});
|
|
6829
|
+
routes.route({
|
|
6830
|
+
method: "DELETE",
|
|
6831
|
+
url: "/admin/environments/:id",
|
|
6832
|
+
schema: {
|
|
6833
|
+
params: idParamSchema,
|
|
6834
|
+
response: {
|
|
6835
|
+
204: emptyResponseSchema,
|
|
6836
|
+
403: errorResponseSchema,
|
|
6837
|
+
404: errorResponseSchema
|
|
6838
|
+
}
|
|
6839
|
+
},
|
|
6840
|
+
/**
|
|
6841
|
+
* Deletes an environment regardless of deletion lock state.
|
|
6842
|
+
*/
|
|
6843
|
+
handler: async (request, reply) => {
|
|
6844
|
+
try {
|
|
6845
|
+
const user = requireAuthenticatedUser(request);
|
|
6846
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
const environment = await db.findEnvironmentById(request.params.id);
|
|
6850
|
+
if (!environment) {
|
|
6851
|
+
void reply.code(404).send({ error: "Environment not found" });
|
|
6852
|
+
return;
|
|
6853
|
+
}
|
|
6854
|
+
await db.deleteEnvironment(request.params.id, user.id);
|
|
6855
|
+
return reply.code(204).send(null);
|
|
6856
|
+
} catch (error) {
|
|
6857
|
+
if (handleDbError(reply, error)) {
|
|
6858
|
+
return;
|
|
6859
|
+
}
|
|
6860
|
+
throw error;
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
});
|
|
6864
|
+
routes.route({
|
|
6865
|
+
method: "PUT",
|
|
6866
|
+
url: "/admin/environments/:id",
|
|
6867
|
+
schema: {
|
|
6868
|
+
params: idParamSchema,
|
|
6869
|
+
body: updateAdminEnvironmentBodySchema,
|
|
6870
|
+
response: {
|
|
6871
|
+
200: adminEntityConfigSchema,
|
|
6872
|
+
403: errorResponseSchema,
|
|
6873
|
+
404: errorResponseSchema
|
|
6874
|
+
}
|
|
6875
|
+
},
|
|
6876
|
+
/**
|
|
6877
|
+
* Updates admin configuration for an environment.
|
|
6878
|
+
*/
|
|
6879
|
+
handler: async (request, reply) => {
|
|
6880
|
+
try {
|
|
6881
|
+
const user = requireAuthenticatedUser(request);
|
|
6882
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6883
|
+
return;
|
|
6884
|
+
}
|
|
6885
|
+
const environment = await db.setEnvironmentDeletionLocked(
|
|
6886
|
+
request.params.id,
|
|
6887
|
+
request.body.deletionLocked,
|
|
6888
|
+
user.id
|
|
6889
|
+
);
|
|
6890
|
+
return reply.send({
|
|
6891
|
+
id: environment.id,
|
|
6892
|
+
name: environment.name,
|
|
6893
|
+
deletionLocked: environment.deletionLocked
|
|
6894
|
+
});
|
|
6895
|
+
} catch (error) {
|
|
6896
|
+
if (handleDbError(reply, error)) {
|
|
6897
|
+
return;
|
|
6898
|
+
}
|
|
6899
|
+
throw error;
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
});
|
|
6335
6903
|
routes.route({
|
|
6336
6904
|
method: "GET",
|
|
6337
6905
|
url: "/admin/llm/models",
|
|
@@ -6346,6 +6914,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6346
6914
|
* Lists all hub-offered LLM models for operator user management.
|
|
6347
6915
|
*/
|
|
6348
6916
|
handler: async (request, reply) => {
|
|
6917
|
+
const llm = getLlm();
|
|
6349
6918
|
if (!llm) {
|
|
6350
6919
|
return sendLlmUnavailable(reply);
|
|
6351
6920
|
}
|
|
@@ -6397,6 +6966,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6397
6966
|
}
|
|
6398
6967
|
const input = buildAdminUserUpdateInput(existing, request.body);
|
|
6399
6968
|
const role = request.body.role ?? existing.role;
|
|
6969
|
+
const llm = getLlm();
|
|
6400
6970
|
const [collections, environments] = await Promise.all([
|
|
6401
6971
|
db.listCollections(),
|
|
6402
6972
|
db.listEnvironments()
|
|
@@ -6586,6 +7156,31 @@ async function registerAdminRoutes(app, options) {
|
|
|
6586
7156
|
}
|
|
6587
7157
|
}
|
|
6588
7158
|
});
|
|
7159
|
+
routes.route({
|
|
7160
|
+
method: "POST",
|
|
7161
|
+
url: "/admin/config/reload",
|
|
7162
|
+
schema: {
|
|
7163
|
+
response: {
|
|
7164
|
+
200: reloadConfigResponseSchema,
|
|
7165
|
+
400: reloadConfigResponseSchema,
|
|
7166
|
+
403: errorResponseSchema
|
|
7167
|
+
}
|
|
7168
|
+
},
|
|
7169
|
+
/**
|
|
7170
|
+
* Re-reads server.yaml and applies reloadable config sections on a best-effort basis.
|
|
7171
|
+
*/
|
|
7172
|
+
handler: async (request, reply) => {
|
|
7173
|
+
const user = requireAuthenticatedUser(request);
|
|
7174
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
7177
|
+
const result = await reloadConfig();
|
|
7178
|
+
if (result.fatalError) {
|
|
7179
|
+
return reply.code(400).send(result);
|
|
7180
|
+
}
|
|
7181
|
+
return reply.send(result);
|
|
7182
|
+
}
|
|
7183
|
+
});
|
|
6589
7184
|
}
|
|
6590
7185
|
|
|
6591
7186
|
// src/server/auth/sessionCapabilities.ts
|
|
@@ -6761,6 +7356,14 @@ async function registerCollectionRoutes(app, db) {
|
|
|
6761
7356
|
)) {
|
|
6762
7357
|
return;
|
|
6763
7358
|
}
|
|
7359
|
+
const collection = await db.findCollectionById(request.params.id);
|
|
7360
|
+
if (!collection) {
|
|
7361
|
+
void reply.code(404).send({ error: "Collection not found" });
|
|
7362
|
+
return;
|
|
7363
|
+
}
|
|
7364
|
+
if (collection.deletionLocked) {
|
|
7365
|
+
throw new DeletionLockedError("collection");
|
|
7366
|
+
}
|
|
6764
7367
|
await db.deleteCollection(request.params.id, user.id);
|
|
6765
7368
|
return reply.code(204).send(null);
|
|
6766
7369
|
} catch (error) {
|
|
@@ -6790,7 +7393,7 @@ async function registerEnvironmentRoutes(app, db) {
|
|
|
6790
7393
|
handler: async (request, reply) => {
|
|
6791
7394
|
try {
|
|
6792
7395
|
const user = requireAuthenticatedUser(request);
|
|
6793
|
-
if (denyUnlessAllowed(reply,
|
|
7396
|
+
if (denyUnlessAllowed(reply, canListEnvironments(user))) {
|
|
6794
7397
|
return;
|
|
6795
7398
|
}
|
|
6796
7399
|
const environments = await db.listEnvironments();
|
|
@@ -6897,6 +7500,14 @@ async function registerEnvironmentRoutes(app, db) {
|
|
|
6897
7500
|
)) {
|
|
6898
7501
|
return;
|
|
6899
7502
|
}
|
|
7503
|
+
const environment = await db.findEnvironmentById(request.params.id);
|
|
7504
|
+
if (!environment) {
|
|
7505
|
+
void reply.code(404).send({ error: "Environment not found" });
|
|
7506
|
+
return;
|
|
7507
|
+
}
|
|
7508
|
+
if (environment.deletionLocked) {
|
|
7509
|
+
throw new DeletionLockedError("environment");
|
|
7510
|
+
}
|
|
6900
7511
|
await db.deleteEnvironment(request.params.id, user.id);
|
|
6901
7512
|
return reply.code(204).send(null);
|
|
6902
7513
|
} catch (error) {
|
|
@@ -7523,14 +8134,15 @@ async function registerLlmRoutes(app, options) {
|
|
|
7523
8134
|
* Lists hub-offered models the authenticated user may use.
|
|
7524
8135
|
*/
|
|
7525
8136
|
handler: async (request, reply) => {
|
|
7526
|
-
|
|
8137
|
+
const llm = options.getLlm();
|
|
8138
|
+
if (!llm) {
|
|
7527
8139
|
return sendLlmUnavailable2(reply);
|
|
7528
8140
|
}
|
|
7529
8141
|
const user = requireAuthenticatedUser(request);
|
|
7530
8142
|
if (denyUnlessAllowed(reply, canUseLlm(user))) {
|
|
7531
8143
|
return;
|
|
7532
8144
|
}
|
|
7533
|
-
const offered = listHubOfferedModels(
|
|
8145
|
+
const offered = listHubOfferedModels(llm).filter(
|
|
7534
8146
|
(model) => isLlmModelAllowed(user, model.id)
|
|
7535
8147
|
);
|
|
7536
8148
|
return reply.send({
|
|
@@ -7556,7 +8168,8 @@ async function registerLlmRoutes(app, options) {
|
|
|
7556
8168
|
* Returns the authenticated user's current monthly LLM usage summary.
|
|
7557
8169
|
*/
|
|
7558
8170
|
handler: async (request, reply) => {
|
|
7559
|
-
|
|
8171
|
+
const llm = options.getLlm();
|
|
8172
|
+
if (!llm) {
|
|
7560
8173
|
return sendLlmUnavailable2(reply);
|
|
7561
8174
|
}
|
|
7562
8175
|
const user = requireAuthenticatedUser(request);
|
|
@@ -7588,7 +8201,8 @@ async function registerLlmRoutes(app, options) {
|
|
|
7588
8201
|
* Runs one stateless LLM completion step using hub-configured provider keys.
|
|
7589
8202
|
*/
|
|
7590
8203
|
handler: async (request, reply) => {
|
|
7591
|
-
|
|
8204
|
+
const llm = options.getLlm();
|
|
8205
|
+
if (!llm) {
|
|
7592
8206
|
return sendLlmUnavailable2(reply);
|
|
7593
8207
|
}
|
|
7594
8208
|
const user = requireAuthenticatedUser(request);
|
|
@@ -7596,7 +8210,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
7596
8210
|
return;
|
|
7597
8211
|
}
|
|
7598
8212
|
const { model, messages, tools, systemPrompt } = request.body;
|
|
7599
|
-
if (!isHubModelOffered(
|
|
8213
|
+
if (!isHubModelOffered(llm, model)) {
|
|
7600
8214
|
return reply.code(403).send({ error: "Model is not offered by this Team Hub." });
|
|
7601
8215
|
}
|
|
7602
8216
|
if (!isLlmModelAllowed(user, model)) {
|
|
@@ -7610,7 +8224,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
7610
8224
|
if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
|
|
7611
8225
|
return sendMonthlyLimitExceeded(reply);
|
|
7612
8226
|
}
|
|
7613
|
-
const result = await runLlmCompletion(
|
|
8227
|
+
const result = await runLlmCompletion(llm, {
|
|
7614
8228
|
model,
|
|
7615
8229
|
messages,
|
|
7616
8230
|
tools,
|
|
@@ -7648,6 +8262,43 @@ async function registerLlmRoutes(app, options) {
|
|
|
7648
8262
|
});
|
|
7649
8263
|
}
|
|
7650
8264
|
|
|
8265
|
+
// src/server/routes/schemas/plugins.ts
|
|
8266
|
+
import { z as z11 } from "zod/v4";
|
|
8267
|
+
var pluginSourcesResponseSchema = z11.object({
|
|
8268
|
+
catalogs: z11.array(z11.string()),
|
|
8269
|
+
trusted: z11.array(z11.string())
|
|
8270
|
+
});
|
|
8271
|
+
|
|
8272
|
+
// src/server/routes/plugins.ts
|
|
8273
|
+
async function registerPluginsRoutes(app, options) {
|
|
8274
|
+
const routes = app.withTypeProvider();
|
|
8275
|
+
routes.route({
|
|
8276
|
+
method: "GET",
|
|
8277
|
+
url: "/plugins/sources",
|
|
8278
|
+
schema: {
|
|
8279
|
+
response: {
|
|
8280
|
+
200: pluginSourcesResponseSchema
|
|
8281
|
+
}
|
|
8282
|
+
},
|
|
8283
|
+
/**
|
|
8284
|
+
* Returns plugin catalog and trusted-publisher URLs configured on this Team Hub.
|
|
8285
|
+
*/
|
|
8286
|
+
handler: async (_request, reply) => {
|
|
8287
|
+
const plugins = options.getPlugins();
|
|
8288
|
+
if (!plugins) {
|
|
8289
|
+
return reply.send({
|
|
8290
|
+
catalogs: [],
|
|
8291
|
+
trusted: []
|
|
8292
|
+
});
|
|
8293
|
+
}
|
|
8294
|
+
return reply.send({
|
|
8295
|
+
catalogs: plugins.catalogs,
|
|
8296
|
+
trusted: plugins.trusted
|
|
8297
|
+
});
|
|
8298
|
+
}
|
|
8299
|
+
});
|
|
8300
|
+
}
|
|
8301
|
+
|
|
7651
8302
|
// src/server/auth/bearerAuthPlugin.ts
|
|
7652
8303
|
function registerBearerAuthDecorator(app) {
|
|
7653
8304
|
app.decorateRequest("apiToken", null);
|
|
@@ -7658,8 +8309,8 @@ function buildAuthThrottleKey(request, token) {
|
|
|
7658
8309
|
return `${request.ip}:${tokenPart}`;
|
|
7659
8310
|
}
|
|
7660
8311
|
function createBearerAuthHook(db, throttleStore) {
|
|
7661
|
-
const policy = throttleStore.getPolicy();
|
|
7662
8312
|
return async function bearerAuth(request, reply) {
|
|
8313
|
+
const policy = throttleStore.getPolicy();
|
|
7663
8314
|
const token = extractBearer(request.headers.authorization);
|
|
7664
8315
|
const throttleKey = buildAuthThrottleKey(request, token);
|
|
7665
8316
|
try {
|
|
@@ -7714,12 +8365,17 @@ async function registerProtectedRoutes(app, options) {
|
|
|
7714
8365
|
registerBearerAuthDecorator(app);
|
|
7715
8366
|
app.addHook("onRequest", createBearerAuthHook(options.db, options.throttleStore));
|
|
7716
8367
|
await registerAuthRoutes(app);
|
|
7717
|
-
await registerAdminRoutes(app, {
|
|
8368
|
+
await registerAdminRoutes(app, {
|
|
8369
|
+
db: options.db,
|
|
8370
|
+
getLlm: options.getLlm,
|
|
8371
|
+
reloadConfig: options.reloadConfig
|
|
8372
|
+
});
|
|
7718
8373
|
await registerCollectionRoutes(app, options.db);
|
|
7719
8374
|
await registerEnvironmentRoutes(app, options.db);
|
|
7720
8375
|
await registerFolderRoutes(app, options.db);
|
|
7721
8376
|
await registerRequestRoutes(app, options.db);
|
|
7722
|
-
await registerLlmRoutes(app, { db: options.db,
|
|
8377
|
+
await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
|
|
8378
|
+
await registerPluginsRoutes(app, { getPlugins: options.getPlugins });
|
|
7723
8379
|
}
|
|
7724
8380
|
async function registerRoutes(app, options) {
|
|
7725
8381
|
await app.register(async (publicApp) => {
|
|
@@ -7731,21 +8387,36 @@ async function registerRoutes(app, options) {
|
|
|
7731
8387
|
}
|
|
7732
8388
|
|
|
7733
8389
|
// src/server/createServer.ts
|
|
7734
|
-
async function createServer(
|
|
8390
|
+
async function createServer(ctxOrConfig, options = {}) {
|
|
8391
|
+
const isRuntimeContext = "getLlm" in ctxOrConfig && "configPath" in ctxOrConfig;
|
|
8392
|
+
const ctx = isRuntimeContext ? ctxOrConfig : null;
|
|
8393
|
+
const legacyConfig = isRuntimeContext ? null : ctxOrConfig;
|
|
8394
|
+
const db = options.db ?? ctx?.db;
|
|
8395
|
+
const throttleStore = options.throttleStore ?? ctx?.throttleStore;
|
|
8396
|
+
if (!db || !throttleStore) {
|
|
8397
|
+
throw new Error("createServer requires db and throttleStore.");
|
|
8398
|
+
}
|
|
8399
|
+
const logger = options.logger ?? ctx?.logger ?? createLogger(legacyConfig?.logging ?? DEFAULT_LOGGING_CONFIG);
|
|
7735
8400
|
const app = Fastify({
|
|
7736
8401
|
logger: options.verbose ?? false
|
|
7737
8402
|
}).withTypeProvider();
|
|
7738
8403
|
app.setValidatorCompiler(validatorCompiler);
|
|
7739
8404
|
app.setSerializerCompiler(serializerCompiler);
|
|
8405
|
+
registerHttpLogging(app, logger);
|
|
7740
8406
|
await registerRoutes(app, {
|
|
7741
8407
|
version: options.version ?? readPackageVersion(),
|
|
7742
|
-
db
|
|
7743
|
-
throttleStore
|
|
7744
|
-
|
|
8408
|
+
db,
|
|
8409
|
+
throttleStore,
|
|
8410
|
+
getLlm: ctx ? () => ctx.getLlm() : () => legacyConfig?.llm ?? null,
|
|
8411
|
+
getPlugins: ctx ? () => ctx.getPlugins() : () => legacyConfig?.plugins ?? null,
|
|
8412
|
+
reloadConfig: options.reloadConfig ?? (async () => ({ sections: [] }))
|
|
7745
8413
|
});
|
|
7746
8414
|
return app;
|
|
7747
8415
|
}
|
|
7748
8416
|
|
|
8417
|
+
// src/server/runtimeContext.ts
|
|
8418
|
+
import { isDeepStrictEqual } from "util";
|
|
8419
|
+
|
|
7749
8420
|
// src/server/auth/throttle/RedisThrottleStore.ts
|
|
7750
8421
|
import Redis from "ioredis";
|
|
7751
8422
|
|
|
@@ -7885,6 +8556,151 @@ function createThrottleStore(config) {
|
|
|
7885
8556
|
return RedisThrottleStore.fromConfig(config);
|
|
7886
8557
|
}
|
|
7887
8558
|
|
|
8559
|
+
// src/server/runtimeContext.ts
|
|
8560
|
+
var runtimeContextStates = /* @__PURE__ */ new WeakMap();
|
|
8561
|
+
function createSwappableProxy(holder) {
|
|
8562
|
+
return new Proxy({}, {
|
|
8563
|
+
/**
|
|
8564
|
+
* Forwards property reads to the current underlying instance.
|
|
8565
|
+
*/
|
|
8566
|
+
get(_target, prop) {
|
|
8567
|
+
const value = Reflect.get(holder.underlying, prop, holder.underlying);
|
|
8568
|
+
if (typeof value === "function") {
|
|
8569
|
+
return value.bind(holder.underlying);
|
|
8570
|
+
}
|
|
8571
|
+
return value;
|
|
8572
|
+
}
|
|
8573
|
+
});
|
|
8574
|
+
}
|
|
8575
|
+
function getState(ctx) {
|
|
8576
|
+
const state = runtimeContextStates.get(ctx);
|
|
8577
|
+
if (!state) {
|
|
8578
|
+
throw new Error("Invalid runtime context.");
|
|
8579
|
+
}
|
|
8580
|
+
return state;
|
|
8581
|
+
}
|
|
8582
|
+
function formatReloadError(error) {
|
|
8583
|
+
if (error instanceof Error) {
|
|
8584
|
+
return error.message;
|
|
8585
|
+
}
|
|
8586
|
+
return String(error);
|
|
8587
|
+
}
|
|
8588
|
+
function createRuntimeContext(config, configPath) {
|
|
8589
|
+
const state = {
|
|
8590
|
+
configPath,
|
|
8591
|
+
host: config.host,
|
|
8592
|
+
port: config.port,
|
|
8593
|
+
activeDbConfig: config.db,
|
|
8594
|
+
activeRedisConfig: config.redis,
|
|
8595
|
+
dbHolder: { underlying: createDatabase(config.db) },
|
|
8596
|
+
throttleHolder: { underlying: createThrottleStore(config.redis) },
|
|
8597
|
+
llm: config.llm,
|
|
8598
|
+
plugins: config.plugins
|
|
8599
|
+
};
|
|
8600
|
+
const ctx = {
|
|
8601
|
+
configPath: state.configPath,
|
|
8602
|
+
get host() {
|
|
8603
|
+
return state.host;
|
|
8604
|
+
},
|
|
8605
|
+
get port() {
|
|
8606
|
+
return state.port;
|
|
8607
|
+
},
|
|
8608
|
+
db: createSwappableProxy(state.dbHolder),
|
|
8609
|
+
throttleStore: createSwappableProxy(state.throttleHolder),
|
|
8610
|
+
getLlm: () => state.llm,
|
|
8611
|
+
getPlugins: () => state.plugins,
|
|
8612
|
+
logger: createLogger(config.logging)
|
|
8613
|
+
};
|
|
8614
|
+
runtimeContextStates.set(ctx, state);
|
|
8615
|
+
return ctx;
|
|
8616
|
+
}
|
|
8617
|
+
async function connectRuntimeContext(ctx) {
|
|
8618
|
+
const state = getState(ctx);
|
|
8619
|
+
await state.dbHolder.underlying.connect();
|
|
8620
|
+
await state.throttleHolder.underlying.connect();
|
|
8621
|
+
}
|
|
8622
|
+
async function disconnectAll(ctx) {
|
|
8623
|
+
const state = getState(ctx);
|
|
8624
|
+
await state.dbHolder.underlying.disconnect();
|
|
8625
|
+
await state.throttleHolder.underlying.disconnect();
|
|
8626
|
+
}
|
|
8627
|
+
async function reloadDbSection(state, nextDbConfig) {
|
|
8628
|
+
if (isDeepStrictEqual(state.activeDbConfig, nextDbConfig)) {
|
|
8629
|
+
return { section: "db", status: "unchanged" };
|
|
8630
|
+
}
|
|
8631
|
+
try {
|
|
8632
|
+
const nextDb = createDatabase(nextDbConfig);
|
|
8633
|
+
await nextDb.connect();
|
|
8634
|
+
const previousDb = state.dbHolder.underlying;
|
|
8635
|
+
state.dbHolder.underlying = nextDb;
|
|
8636
|
+
state.activeDbConfig = nextDbConfig;
|
|
8637
|
+
await previousDb.disconnect();
|
|
8638
|
+
return { section: "db", status: "reloaded" };
|
|
8639
|
+
} catch (error) {
|
|
8640
|
+
return { section: "db", status: "failed", error: formatReloadError(error) };
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
async function reloadRedisSection(state, nextRedisConfig) {
|
|
8644
|
+
if (isDeepStrictEqual(state.activeRedisConfig, nextRedisConfig)) {
|
|
8645
|
+
return { section: "redis", status: "unchanged" };
|
|
8646
|
+
}
|
|
8647
|
+
try {
|
|
8648
|
+
const nextStore = createThrottleStore(nextRedisConfig);
|
|
8649
|
+
await nextStore.connect();
|
|
8650
|
+
const previousStore = state.throttleHolder.underlying;
|
|
8651
|
+
state.throttleHolder.underlying = nextStore;
|
|
8652
|
+
state.activeRedisConfig = nextRedisConfig;
|
|
8653
|
+
await previousStore.disconnect();
|
|
8654
|
+
return { section: "redis", status: "reloaded" };
|
|
8655
|
+
} catch (error) {
|
|
8656
|
+
return { section: "redis", status: "failed", error: formatReloadError(error) };
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
function reloadServerSection(state, nextConfig) {
|
|
8660
|
+
if (state.host === nextConfig.host && state.port === nextConfig.port) {
|
|
8661
|
+
return { section: "server", status: "unchanged" };
|
|
8662
|
+
}
|
|
8663
|
+
return {
|
|
8664
|
+
section: "server",
|
|
8665
|
+
status: "restart-required",
|
|
8666
|
+
error: "Changes to server.host or server.port require a full process restart."
|
|
8667
|
+
};
|
|
8668
|
+
}
|
|
8669
|
+
async function reloadRuntimeConfig(ctx) {
|
|
8670
|
+
const state = getState(ctx);
|
|
8671
|
+
let nextConfig;
|
|
8672
|
+
try {
|
|
8673
|
+
nextConfig = loadServerConfig(state.configPath);
|
|
8674
|
+
} catch (error) {
|
|
8675
|
+
const message = error instanceof ConfigError ? error.message : formatReloadError(error);
|
|
8676
|
+
return { sections: [], fatalError: message };
|
|
8677
|
+
}
|
|
8678
|
+
const sections = [];
|
|
8679
|
+
sections.push(await reloadDbSection(state, nextConfig.db));
|
|
8680
|
+
sections.push(await reloadRedisSection(state, nextConfig.redis));
|
|
8681
|
+
state.llm = nextConfig.llm;
|
|
8682
|
+
sections.push({ section: "llm", status: "reloaded" });
|
|
8683
|
+
state.plugins = nextConfig.plugins;
|
|
8684
|
+
sections.push({ section: "plugins", status: "reloaded" });
|
|
8685
|
+
sections.push(reloadServerSection(state, nextConfig));
|
|
8686
|
+
return { sections };
|
|
8687
|
+
}
|
|
8688
|
+
function formatConfigReloadSummary(result) {
|
|
8689
|
+
return result.sections.map((section) => {
|
|
8690
|
+
if (section.error) {
|
|
8691
|
+
return `${section.section}: ${section.status} (${section.error})`;
|
|
8692
|
+
}
|
|
8693
|
+
return `${section.section}: ${section.status}`;
|
|
8694
|
+
}).join(", ");
|
|
8695
|
+
}
|
|
8696
|
+
function logConfigReloadResult(result) {
|
|
8697
|
+
if (result.fatalError) {
|
|
8698
|
+
console.error(`Team Hub config reload failed: ${result.fatalError}`);
|
|
8699
|
+
return;
|
|
8700
|
+
}
|
|
8701
|
+
console.log(`Team Hub config reloaded (${formatConfigReloadSummary(result)}).`);
|
|
8702
|
+
}
|
|
8703
|
+
|
|
7888
8704
|
// src/server.ts
|
|
7889
8705
|
function formatListenAddress(address, port) {
|
|
7890
8706
|
if (!address) {
|
|
@@ -7896,12 +8712,11 @@ function formatListenAddress(address, port) {
|
|
|
7896
8712
|
const host = address.includes(":") && !address.startsWith("[") ? `[${address}]` : address;
|
|
7897
8713
|
return `http://${host}:${port}`;
|
|
7898
8714
|
}
|
|
7899
|
-
function registerGracefulShutdown(app,
|
|
8715
|
+
function registerGracefulShutdown(app, ctx) {
|
|
7900
8716
|
const shutdown = async (signal) => {
|
|
7901
8717
|
app.log.info(`Received ${signal}, shutting down.`);
|
|
7902
8718
|
await app.close();
|
|
7903
|
-
await
|
|
7904
|
-
await throttleStore.disconnect();
|
|
8719
|
+
await disconnectAll(ctx);
|
|
7905
8720
|
process.exit(0);
|
|
7906
8721
|
};
|
|
7907
8722
|
process.once("SIGINT", () => {
|
|
@@ -7911,33 +8726,42 @@ function registerGracefulShutdown(app, db, throttleStore) {
|
|
|
7911
8726
|
void shutdown("SIGTERM");
|
|
7912
8727
|
});
|
|
7913
8728
|
}
|
|
7914
|
-
|
|
7915
|
-
|
|
8729
|
+
function registerConfigReloadHandler(reloadConfig) {
|
|
8730
|
+
process.on("SIGHUP", () => {
|
|
8731
|
+
void reloadConfig();
|
|
8732
|
+
});
|
|
8733
|
+
}
|
|
8734
|
+
async function runServer(ctx, options = {}) {
|
|
8735
|
+
const reloadConfig = async () => {
|
|
8736
|
+
const result = await reloadRuntimeConfig(ctx);
|
|
8737
|
+
logConfigReloadResult(result);
|
|
8738
|
+
return result;
|
|
8739
|
+
};
|
|
8740
|
+
const app = await createServer(ctx, {
|
|
7916
8741
|
verbose: options.verbose,
|
|
7917
|
-
|
|
7918
|
-
throttleStore: options.throttleStore
|
|
8742
|
+
reloadConfig
|
|
7919
8743
|
});
|
|
7920
|
-
await
|
|
7921
|
-
await options.throttleStore.connect();
|
|
8744
|
+
await connectRuntimeContext(ctx);
|
|
7922
8745
|
await app.listen({
|
|
7923
|
-
host:
|
|
7924
|
-
port:
|
|
8746
|
+
host: ctx.host,
|
|
8747
|
+
port: ctx.port
|
|
7925
8748
|
});
|
|
7926
8749
|
const address = app.server.address();
|
|
7927
|
-
const port = typeof address === "object" && address ? address.port :
|
|
7928
|
-
const host = typeof address === "object" && address ? address.address :
|
|
8750
|
+
const port = typeof address === "object" && address ? address.port : ctx.port;
|
|
8751
|
+
const host = typeof address === "object" && address ? address.address : ctx.host;
|
|
7929
8752
|
if (options.verbose) {
|
|
7930
|
-
console.log("Starting server with config:",
|
|
8753
|
+
console.log("Starting server with config path:", ctx.configPath);
|
|
7931
8754
|
}
|
|
7932
8755
|
console.log(`Team Hub listening on ${formatListenAddress(host, port)}`);
|
|
7933
|
-
registerGracefulShutdown(app,
|
|
8756
|
+
registerGracefulShutdown(app, ctx);
|
|
8757
|
+
registerConfigReloadHandler(reloadConfig);
|
|
7934
8758
|
return app;
|
|
7935
8759
|
}
|
|
7936
8760
|
async function startCommand(options) {
|
|
8761
|
+
const configPath = resolveConfigPath(options.config);
|
|
7937
8762
|
const config = loadServerConfig(options.config);
|
|
7938
|
-
const
|
|
7939
|
-
|
|
7940
|
-
await runServer(config, { verbose: options.verbose, db, throttleStore });
|
|
8763
|
+
const ctx = createRuntimeContext(config, configPath);
|
|
8764
|
+
await runServer(ctx, { verbose: options.verbose });
|
|
7941
8765
|
}
|
|
7942
8766
|
function registerStartCommand(program, handler = startCommand) {
|
|
7943
8767
|
program.command("start").description("Start the Team Hub server").action(
|