@harborclient/team-hub 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1264 -54
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,35 @@ import path from "path";
|
|
|
25
25
|
import { parse as parseYaml } from "yaml";
|
|
26
26
|
|
|
27
27
|
// src/config/llmConfig.ts
|
|
28
|
+
function headerRowsFromSingleKeyObject(record) {
|
|
29
|
+
const rows = [];
|
|
30
|
+
for (const [key, value] of Object.entries(record)) {
|
|
31
|
+
const trimmedKey = key.trim();
|
|
32
|
+
if (trimmedKey.length > 0) {
|
|
33
|
+
rows.push({ key: trimmedKey, value: String(value) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
function normalizeHubMcpHeaders(headers) {
|
|
39
|
+
if (!headers) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(headers)) {
|
|
43
|
+
return headerRowsFromSingleKeyObject(headers);
|
|
44
|
+
}
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const item of headers) {
|
|
47
|
+
if (Array.isArray(item)) {
|
|
48
|
+
for (const nested of item) {
|
|
49
|
+
rows.push(...headerRowsFromSingleKeyObject(nested));
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
rows.push(...headerRowsFromSingleKeyObject(item));
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
28
57
|
function normalizeLlmConfig(section) {
|
|
29
58
|
const providers = {};
|
|
30
59
|
if (section.providers.openai?.apiKey) {
|
|
@@ -36,9 +65,15 @@ function normalizeLlmConfig(section) {
|
|
|
36
65
|
if (section.providers.gemini?.apiKey) {
|
|
37
66
|
providers.gemini = { apiKey: section.providers.gemini.apiKey };
|
|
38
67
|
}
|
|
68
|
+
const mcp = section.mcp && section.mcp.length > 0 ? section.mcp.map((entry) => ({
|
|
69
|
+
name: entry.name.trim(),
|
|
70
|
+
url: entry.url.trim().replace(/\/+$/, ""),
|
|
71
|
+
headers: normalizeHubMcpHeaders(entry.headers)
|
|
72
|
+
})) : void 0;
|
|
39
73
|
return {
|
|
40
74
|
providers,
|
|
41
|
-
...section.models && section.models.length > 0 ? { models: section.models } : {}
|
|
75
|
+
...section.models && section.models.length > 0 ? { models: section.models } : {},
|
|
76
|
+
...mcp && mcp.length > 0 ? { mcp } : {}
|
|
42
77
|
};
|
|
43
78
|
}
|
|
44
79
|
|
|
@@ -116,6 +151,15 @@ var redisSectionSchema = z.object({
|
|
|
116
151
|
var llmProviderEntrySchema = z.object({
|
|
117
152
|
apiKey: z.string().trim().min(1, { message: "LLM provider apiKey must not be empty." })
|
|
118
153
|
});
|
|
154
|
+
var hubMcpHeadersSchema = z.union([
|
|
155
|
+
z.record(z.string(), z.string()),
|
|
156
|
+
z.array(z.union([z.record(z.string(), z.string()), z.array(z.record(z.string(), z.string()))]))
|
|
157
|
+
]);
|
|
158
|
+
var hubMcpServerEntrySchema = z.object({
|
|
159
|
+
name: z.string().trim().min(1),
|
|
160
|
+
url: z.string().trim().min(1),
|
|
161
|
+
headers: hubMcpHeadersSchema.optional()
|
|
162
|
+
});
|
|
119
163
|
var llmSectionSchema = z.object({
|
|
120
164
|
providers: z.object({
|
|
121
165
|
openai: llmProviderEntrySchema.optional(),
|
|
@@ -125,7 +169,8 @@ var llmSectionSchema = z.object({
|
|
|
125
169
|
(providers) => Boolean(providers.openai?.apiKey || providers.claude?.apiKey || providers.gemini?.apiKey),
|
|
126
170
|
{ message: "llm.providers must include at least one provider with an apiKey." }
|
|
127
171
|
),
|
|
128
|
-
models: z.array(z.string().trim().min(1)).optional()
|
|
172
|
+
models: z.array(z.string().trim().min(1)).optional(),
|
|
173
|
+
mcp: z.array(hubMcpServerEntrySchema).optional()
|
|
129
174
|
});
|
|
130
175
|
var pluginsSectionSchema = z.object({
|
|
131
176
|
catalogs: z.array(z.string().trim().url()).optional(),
|
|
@@ -313,6 +358,7 @@ var USERS_COLLECTION = "users";
|
|
|
313
358
|
var API_TOKENS_COLLECTION = "apiTokens";
|
|
314
359
|
var COLLECTIONS_COLLECTION = "collections";
|
|
315
360
|
var ENVIRONMENTS_COLLECTION = "environments";
|
|
361
|
+
var SNIPPETS_COLLECTION = "snippets";
|
|
316
362
|
var FOLDERS_COLLECTION = "folders";
|
|
317
363
|
var REQUESTS_COLLECTION = "requests";
|
|
318
364
|
var AUDIT_LOG_COLLECTION = "auditLog";
|
|
@@ -339,7 +385,8 @@ function createSystemUserInput() {
|
|
|
339
385
|
name: SYSTEM_USER_NAME,
|
|
340
386
|
role: "admin",
|
|
341
387
|
collectionAccess: [],
|
|
342
|
-
environmentAccess: []
|
|
388
|
+
environmentAccess: [],
|
|
389
|
+
snippetAccess: []
|
|
343
390
|
};
|
|
344
391
|
}
|
|
345
392
|
|
|
@@ -353,7 +400,7 @@ var firestoreConfigSchema = z2.object({
|
|
|
353
400
|
|
|
354
401
|
// src/db/firestore/utils.ts
|
|
355
402
|
function parseAuditEntityType(value) {
|
|
356
|
-
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "folder" || value === "request") {
|
|
403
|
+
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
|
|
357
404
|
return value;
|
|
358
405
|
}
|
|
359
406
|
throw new Error(`Invalid audit entity type: ${value}`);
|
|
@@ -382,6 +429,7 @@ function mapFirestoreUser(id, data) {
|
|
|
382
429
|
role: data.role,
|
|
383
430
|
collectionAccess: data.collectionAccess,
|
|
384
431
|
environmentAccess: data.environmentAccess,
|
|
432
|
+
snippetAccess: data.snippetAccess ?? [],
|
|
385
433
|
llmAccess: data.llmAccess ?? false,
|
|
386
434
|
llmModels: data.llmModels ?? [],
|
|
387
435
|
llmMonthlyTokenLimit: data.llmMonthlyTokenLimit ?? null,
|
|
@@ -447,6 +495,20 @@ function mapFirestoreEnvironment(id, data) {
|
|
|
447
495
|
deletionLocked: data.deletionLocked ?? false
|
|
448
496
|
};
|
|
449
497
|
}
|
|
498
|
+
function mapFirestoreSnippet(id, data) {
|
|
499
|
+
return {
|
|
500
|
+
id,
|
|
501
|
+
name: data.name,
|
|
502
|
+
code: data.code,
|
|
503
|
+
scope: data.scope,
|
|
504
|
+
sortOrder: data.sortOrder,
|
|
505
|
+
createdAt: data.createdAt,
|
|
506
|
+
updatedAt: data.updatedAt ?? data.createdAt,
|
|
507
|
+
createdByUserId: data.createdByUserId ?? null,
|
|
508
|
+
updatedByUserId: data.updatedByUserId ?? null,
|
|
509
|
+
deletionLocked: data.deletionLocked ?? false
|
|
510
|
+
};
|
|
511
|
+
}
|
|
450
512
|
function mapFirestoreFolder(id, data) {
|
|
451
513
|
return {
|
|
452
514
|
id,
|
|
@@ -654,6 +716,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
654
716
|
async migrate() {
|
|
655
717
|
await this.ensureSystemUser();
|
|
656
718
|
await this.migrateOrphanTokensToBootstrapUser();
|
|
719
|
+
await this.migrateSnippetAccessBackfill();
|
|
657
720
|
}
|
|
658
721
|
/**
|
|
659
722
|
* Returns the stable identifier of the internal system user, when provisioned.
|
|
@@ -700,6 +763,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
700
763
|
role: input.role,
|
|
701
764
|
collectionAccess: input.collectionAccess,
|
|
702
765
|
environmentAccess: input.environmentAccess,
|
|
766
|
+
snippetAccess: input.snippetAccess,
|
|
703
767
|
llmAccess: input.llmAccess ?? false,
|
|
704
768
|
llmModels: input.llmModels ?? [],
|
|
705
769
|
llmMonthlyTokenLimit: input.llmMonthlyTokenLimit ?? null,
|
|
@@ -771,6 +835,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
771
835
|
const role = input.role ?? existing.role;
|
|
772
836
|
const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
|
|
773
837
|
const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
|
|
838
|
+
const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
|
|
774
839
|
const llmAccess = input.llmAccess ?? existing.llmAccess;
|
|
775
840
|
const llmModels = input.llmModels ?? existing.llmModels;
|
|
776
841
|
const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
|
|
@@ -780,6 +845,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
780
845
|
role,
|
|
781
846
|
collectionAccess,
|
|
782
847
|
environmentAccess,
|
|
848
|
+
snippetAccess,
|
|
783
849
|
llmAccess,
|
|
784
850
|
llmModels,
|
|
785
851
|
llmMonthlyTokenLimit,
|
|
@@ -834,7 +900,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
834
900
|
name: BOOTSTRAP_USER_NAME,
|
|
835
901
|
role: "user",
|
|
836
902
|
collectionAccess: ["*"],
|
|
837
|
-
environmentAccess: ["*"]
|
|
903
|
+
environmentAccess: ["*"],
|
|
904
|
+
snippetAccess: ["*"]
|
|
838
905
|
},
|
|
839
906
|
systemUserId
|
|
840
907
|
);
|
|
@@ -848,6 +915,37 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
848
915
|
await batch.commit();
|
|
849
916
|
}
|
|
850
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* Grants wildcard snippet access to user accounts that already have wildcard collection access.
|
|
920
|
+
*/
|
|
921
|
+
async migrateSnippetAccessBackfill() {
|
|
922
|
+
const client = this.requireClient();
|
|
923
|
+
const snapshot = await client.collection(USERS_COLLECTION).where("role", "==", "user").get();
|
|
924
|
+
if (snapshot.docs.length === 0) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
let batch = client.batch();
|
|
928
|
+
let batchSize = 0;
|
|
929
|
+
for (const doc of snapshot.docs) {
|
|
930
|
+
const data = doc.data();
|
|
931
|
+
if ((data.snippetAccess?.length ?? 0) > 0) {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (!data.collectionAccess?.includes("*")) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
batch.update(doc.ref, { snippetAccess: ["*"] });
|
|
938
|
+
batchSize += 1;
|
|
939
|
+
if (batchSize >= WRITE_BATCH_LIMIT) {
|
|
940
|
+
await batch.commit();
|
|
941
|
+
batch = client.batch();
|
|
942
|
+
batchSize = 0;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (batchSize > 0) {
|
|
946
|
+
await batch.commit();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
851
949
|
/**
|
|
852
950
|
* Inserts a new API token document.
|
|
853
951
|
*
|
|
@@ -1205,6 +1303,129 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1205
1303
|
updatedByUserId: actingUserId
|
|
1206
1304
|
});
|
|
1207
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Lists all snippets ordered by sort order then name.
|
|
1308
|
+
*/
|
|
1309
|
+
async listSnippets() {
|
|
1310
|
+
const snapshot = await this.requireClient().collection(SNIPPETS_COLLECTION).get();
|
|
1311
|
+
return snapshot.docs.map((doc) => mapFirestoreSnippet(doc.id, doc.data())).sort((left, right) => {
|
|
1312
|
+
if (left.sortOrder !== right.sortOrder) {
|
|
1313
|
+
return left.sortOrder - right.sortOrder;
|
|
1314
|
+
}
|
|
1315
|
+
return left.name.localeCompare(right.name);
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Creates a new snippet with the given fields.
|
|
1320
|
+
*
|
|
1321
|
+
* @param name - Display name for the snippet.
|
|
1322
|
+
* @param code - JavaScript source for the snippet.
|
|
1323
|
+
* @param scope - Execution scope for the snippet.
|
|
1324
|
+
* @param actingUserId - User performing the create action.
|
|
1325
|
+
*/
|
|
1326
|
+
async createSnippet(name, code, scope, actingUserId) {
|
|
1327
|
+
const trimmedName = trimRequiredName(name, "Snippet name");
|
|
1328
|
+
const id = randomUUID2();
|
|
1329
|
+
const now = /* @__PURE__ */ new Date();
|
|
1330
|
+
const existing = await this.listSnippets();
|
|
1331
|
+
const maxOrder = existing.reduce((max, snippet) => Math.max(max, snippet.sortOrder), -1);
|
|
1332
|
+
const data = {
|
|
1333
|
+
name: trimmedName,
|
|
1334
|
+
code,
|
|
1335
|
+
scope,
|
|
1336
|
+
sortOrder: maxOrder + 1,
|
|
1337
|
+
createdAt: now,
|
|
1338
|
+
updatedAt: now,
|
|
1339
|
+
createdByUserId: actingUserId,
|
|
1340
|
+
updatedByUserId: actingUserId,
|
|
1341
|
+
deletionLocked: false
|
|
1342
|
+
};
|
|
1343
|
+
await this.requireClient().collection(SNIPPETS_COLLECTION).doc(id).set(data);
|
|
1344
|
+
await this.recordAuditEntry(actingUserId, "create", "snippet", id);
|
|
1345
|
+
return mapFirestoreSnippet(id, data);
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Updates a snippet's name, code, and scope. Sort order is left unchanged.
|
|
1349
|
+
*
|
|
1350
|
+
* @param actingUserId - User performing the update action.
|
|
1351
|
+
*/
|
|
1352
|
+
async updateSnippet(id, name, code, scope, actingUserId) {
|
|
1353
|
+
const trimmedName = trimRequiredName(name, "Snippet name");
|
|
1354
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
1355
|
+
const docRef = this.requireClient().collection(SNIPPETS_COLLECTION).doc(id);
|
|
1356
|
+
const snapshot = await docRef.get();
|
|
1357
|
+
if (!snapshot.exists) {
|
|
1358
|
+
throw new Error("Snippet not found");
|
|
1359
|
+
}
|
|
1360
|
+
const existing = snapshot.data();
|
|
1361
|
+
const updated = {
|
|
1362
|
+
...existing,
|
|
1363
|
+
name: trimmedName,
|
|
1364
|
+
code,
|
|
1365
|
+
scope,
|
|
1366
|
+
updatedAt,
|
|
1367
|
+
updatedByUserId: actingUserId
|
|
1368
|
+
};
|
|
1369
|
+
await docRef.update({
|
|
1370
|
+
name: trimmedName,
|
|
1371
|
+
code,
|
|
1372
|
+
scope,
|
|
1373
|
+
updatedAt,
|
|
1374
|
+
updatedByUserId: actingUserId
|
|
1375
|
+
});
|
|
1376
|
+
await this.recordAuditEntry(actingUserId, "update", "snippet", id);
|
|
1377
|
+
return mapFirestoreSnippet(id, updated);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Deletes a snippet.
|
|
1381
|
+
*
|
|
1382
|
+
* @param id - Snippet ID to delete.
|
|
1383
|
+
* @param actingUserId - User performing the delete action.
|
|
1384
|
+
*/
|
|
1385
|
+
async deleteSnippet(id, actingUserId) {
|
|
1386
|
+
await this.recordAuditEntry(actingUserId, "delete", "snippet", id);
|
|
1387
|
+
await this.requireClient().collection(SNIPPETS_COLLECTION).doc(id).delete();
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Finds a snippet by stable identifier.
|
|
1391
|
+
*
|
|
1392
|
+
* @param id - Snippet ID to look up.
|
|
1393
|
+
*/
|
|
1394
|
+
async findSnippetById(id) {
|
|
1395
|
+
const snapshot = await this.requireClient().collection(SNIPPETS_COLLECTION).doc(id).get();
|
|
1396
|
+
if (!snapshot.exists) {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
return mapFirestoreSnippet(id, snapshot.data());
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Updates whether non-admin users may delete a snippet.
|
|
1403
|
+
*
|
|
1404
|
+
* @param id - Snippet ID to update.
|
|
1405
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the snippet.
|
|
1406
|
+
* @param actingUserId - Admin user performing the update.
|
|
1407
|
+
*/
|
|
1408
|
+
async setSnippetDeletionLocked(id, deletionLocked, actingUserId) {
|
|
1409
|
+
const docRef = this.requireClient().collection(SNIPPETS_COLLECTION).doc(id);
|
|
1410
|
+
const snapshot = await docRef.get();
|
|
1411
|
+
if (!snapshot.exists) {
|
|
1412
|
+
throw new Error("Snippet not found");
|
|
1413
|
+
}
|
|
1414
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
1415
|
+
await docRef.update({
|
|
1416
|
+
deletionLocked,
|
|
1417
|
+
updatedAt,
|
|
1418
|
+
updatedByUserId: actingUserId
|
|
1419
|
+
});
|
|
1420
|
+
await this.recordAuditEntry(actingUserId, "update", "snippet", id);
|
|
1421
|
+
const existing = snapshot.data();
|
|
1422
|
+
return mapFirestoreSnippet(id, {
|
|
1423
|
+
...existing,
|
|
1424
|
+
deletionLocked,
|
|
1425
|
+
updatedAt,
|
|
1426
|
+
updatedByUserId: actingUserId
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1208
1429
|
/**
|
|
1209
1430
|
* Lists all saved requests in a collection.
|
|
1210
1431
|
*
|
|
@@ -1671,6 +1892,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
|
|
|
1671
1892
|
role: input.role,
|
|
1672
1893
|
collectionAccess: input.collectionAccess,
|
|
1673
1894
|
environmentAccess: input.environmentAccess,
|
|
1895
|
+
snippetAccess: input.snippetAccess,
|
|
1674
1896
|
llmAccess: false,
|
|
1675
1897
|
llmModels: [],
|
|
1676
1898
|
llmMonthlyTokenLimit: null,
|
|
@@ -1754,7 +1976,7 @@ function parseAuditAction(value) {
|
|
|
1754
1976
|
throw new Error(`Invalid audit action: ${value}`);
|
|
1755
1977
|
}
|
|
1756
1978
|
function parseAuditEntityType2(value) {
|
|
1757
|
-
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "folder" || value === "request") {
|
|
1979
|
+
if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
|
|
1758
1980
|
return value;
|
|
1759
1981
|
}
|
|
1760
1982
|
throw new Error(`Invalid audit entity type: ${value}`);
|
|
@@ -1831,6 +2053,26 @@ function mapEnvironmentSqlRow(row) {
|
|
|
1831
2053
|
deletionLocked: Boolean(row.deletion_locked)
|
|
1832
2054
|
};
|
|
1833
2055
|
}
|
|
2056
|
+
function parseSnippetScope(value) {
|
|
2057
|
+
if (value === "pre-request" || value === "post-request" || value === "any") {
|
|
2058
|
+
return value;
|
|
2059
|
+
}
|
|
2060
|
+
throw new Error(`Invalid snippet scope: ${value}`);
|
|
2061
|
+
}
|
|
2062
|
+
function mapSnippetSqlRow(row) {
|
|
2063
|
+
return {
|
|
2064
|
+
id: row.id,
|
|
2065
|
+
name: row.name,
|
|
2066
|
+
code: row.code,
|
|
2067
|
+
scope: parseSnippetScope(row.scope),
|
|
2068
|
+
sortOrder: row.sort_order,
|
|
2069
|
+
createdAt: row.created_at,
|
|
2070
|
+
updatedAt: row.updated_at ?? row.created_at,
|
|
2071
|
+
createdByUserId: row.created_by_user_id ?? null,
|
|
2072
|
+
updatedByUserId: row.updated_by_user_id ?? null,
|
|
2073
|
+
deletionLocked: Boolean(row.deletion_locked)
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
1834
2076
|
function mapFolderSqlRow(row) {
|
|
1835
2077
|
return {
|
|
1836
2078
|
id: row.id,
|
|
@@ -1915,6 +2157,22 @@ CREATE TABLE IF NOT EXISTS environments (
|
|
|
1915
2157
|
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
1916
2158
|
)
|
|
1917
2159
|
`.trim();
|
|
2160
|
+
var SNIPPETS_MIGRATION_SQL = `
|
|
2161
|
+
CREATE TABLE IF NOT EXISTS snippets (
|
|
2162
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
2163
|
+
name VARCHAR(255) NOT NULL,
|
|
2164
|
+
code LONGTEXT NOT NULL,
|
|
2165
|
+
scope VARCHAR(32) NOT NULL DEFAULT 'any',
|
|
2166
|
+
sort_order INT NOT NULL DEFAULT 0,
|
|
2167
|
+
created_at DATETIME NOT NULL,
|
|
2168
|
+
updated_at DATETIME NOT NULL,
|
|
2169
|
+
created_by_user_id VARCHAR(36) NULL,
|
|
2170
|
+
updated_by_user_id VARCHAR(36) NULL,
|
|
2171
|
+
deletion_locked TINYINT(1) NOT NULL DEFAULT 0,
|
|
2172
|
+
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL,
|
|
2173
|
+
FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
2174
|
+
)
|
|
2175
|
+
`.trim();
|
|
1918
2176
|
var FOLDERS_MIGRATION_SQL = `
|
|
1919
2177
|
CREATE TABLE IF NOT EXISTS folders (
|
|
1920
2178
|
id VARCHAR(36) PRIMARY KEY,
|
|
@@ -1964,6 +2222,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
1964
2222
|
role VARCHAR(16) NOT NULL,
|
|
1965
2223
|
collection_access LONGTEXT NOT NULL,
|
|
1966
2224
|
environment_access LONGTEXT NOT NULL,
|
|
2225
|
+
snippet_access LONGTEXT NOT NULL,
|
|
1967
2226
|
created_at DATETIME NOT NULL,
|
|
1968
2227
|
updated_at DATETIME NOT NULL,
|
|
1969
2228
|
created_by_user_id VARCHAR(36) NULL,
|
|
@@ -2079,11 +2338,23 @@ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL = `
|
|
|
2079
2338
|
ALTER TABLE environments
|
|
2080
2339
|
ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
|
|
2081
2340
|
`.trim();
|
|
2341
|
+
var USERS_SNIPPET_ACCESS_MIGRATION_SQL = `
|
|
2342
|
+
ALTER TABLE users
|
|
2343
|
+
ADD COLUMN IF NOT EXISTS snippet_access LONGTEXT NOT NULL DEFAULT '[]';
|
|
2344
|
+
`.trim();
|
|
2345
|
+
var USERS_SNIPPET_ACCESS_BACKFILL_SQL = `
|
|
2346
|
+
UPDATE users
|
|
2347
|
+
SET snippet_access = '["*"]'
|
|
2348
|
+
WHERE role = 'user'
|
|
2349
|
+
AND snippet_access = '[]'
|
|
2350
|
+
AND collection_access LIKE '%"*"%';
|
|
2351
|
+
`.trim();
|
|
2082
2352
|
var MYSQL_MIGRATIONS = [
|
|
2083
2353
|
USERS_MIGRATION_SQL,
|
|
2084
2354
|
API_TOKENS_MIGRATION_SQL,
|
|
2085
2355
|
COLLECTIONS_MIGRATION_SQL,
|
|
2086
2356
|
ENVIRONMENTS_MIGRATION_SQL,
|
|
2357
|
+
SNIPPETS_MIGRATION_SQL,
|
|
2087
2358
|
FOLDERS_MIGRATION_SQL,
|
|
2088
2359
|
REQUESTS_MIGRATION_SQL,
|
|
2089
2360
|
AUDIT_LOG_MIGRATION_SQL,
|
|
@@ -2101,7 +2372,9 @@ var MYSQL_MIGRATIONS = [
|
|
|
2101
2372
|
LLM_USAGE_MIGRATION_SQL,
|
|
2102
2373
|
LLM_USAGE_LOG_MIGRATION_SQL,
|
|
2103
2374
|
COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
|
|
2104
|
-
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL
|
|
2375
|
+
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
|
|
2376
|
+
USERS_SNIPPET_ACCESS_MIGRATION_SQL,
|
|
2377
|
+
USERS_SNIPPET_ACCESS_BACKFILL_SQL
|
|
2105
2378
|
];
|
|
2106
2379
|
|
|
2107
2380
|
// src/db/mysql/schemas.ts
|
|
@@ -2145,6 +2418,7 @@ function mapUserSqlRow(row) {
|
|
|
2145
2418
|
role: parseUserRole(row.role),
|
|
2146
2419
|
collectionAccess: parseAccessList(row.collection_access),
|
|
2147
2420
|
environmentAccess: parseAccessList(row.environment_access),
|
|
2421
|
+
snippetAccess: parseAccessList(row.snippet_access),
|
|
2148
2422
|
llmAccess: Boolean(row.llm_access),
|
|
2149
2423
|
llmModels: parseAccessList(row.llm_models),
|
|
2150
2424
|
llmMonthlyTokenLimit: row.llm_monthly_token_limit,
|
|
@@ -2157,9 +2431,10 @@ function mapUserSqlRow(row) {
|
|
|
2157
2431
|
function serializeAccessList(access) {
|
|
2158
2432
|
return JSON.stringify(access);
|
|
2159
2433
|
}
|
|
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`;
|
|
2434
|
+
var USER_SELECT_COLUMNS = `id, name, role, collection_access, environment_access, snippet_access, llm_access, llm_models, llm_monthly_token_limit, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2161
2435
|
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
2436
|
var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
2437
|
+
var SNIPPET_SELECT_COLUMNS = `id, name, code, scope, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
|
|
2163
2438
|
var FOLDER_SELECT_COLUMNS = `id, collection_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
|
|
2164
2439
|
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`;
|
|
2165
2440
|
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`;
|
|
@@ -2202,6 +2477,7 @@ var LLM_USAGE_SELECT_COLUMNS = `id, user_id, period, prompt_tokens, completion_t
|
|
|
2202
2477
|
// src/db/mysql/MysqlDatabase.ts
|
|
2203
2478
|
var COLLECTION_SELECT = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
|
|
2204
2479
|
var ENVIRONMENT_SELECT = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
|
|
2480
|
+
var SNIPPET_SELECT = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
|
|
2205
2481
|
var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
|
|
2206
2482
|
var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
|
|
2207
2483
|
var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
|
|
@@ -2339,6 +2615,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2339
2615
|
role,
|
|
2340
2616
|
collection_access,
|
|
2341
2617
|
environment_access,
|
|
2618
|
+
snippet_access,
|
|
2342
2619
|
llm_access,
|
|
2343
2620
|
llm_models,
|
|
2344
2621
|
llm_monthly_token_limit,
|
|
@@ -2346,13 +2623,14 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2346
2623
|
updated_at,
|
|
2347
2624
|
created_by_user_id,
|
|
2348
2625
|
updated_by_user_id
|
|
2349
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2626
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2350
2627
|
[
|
|
2351
2628
|
id,
|
|
2352
2629
|
trimmedName,
|
|
2353
2630
|
input.role,
|
|
2354
2631
|
serializeAccessList(input.collectionAccess),
|
|
2355
2632
|
serializeAccessList(input.environmentAccess),
|
|
2633
|
+
serializeAccessList(input.snippetAccess),
|
|
2356
2634
|
input.llmAccess ? 1 : 0,
|
|
2357
2635
|
serializeAccessList(input.llmModels ?? []),
|
|
2358
2636
|
input.llmMonthlyTokenLimit ?? null,
|
|
@@ -2425,6 +2703,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2425
2703
|
const role = input.role ?? existing.role;
|
|
2426
2704
|
const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
|
|
2427
2705
|
const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
|
|
2706
|
+
const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
|
|
2428
2707
|
const llmAccess = input.llmAccess ?? existing.llmAccess;
|
|
2429
2708
|
const llmModels = input.llmModels ?? existing.llmModels;
|
|
2430
2709
|
const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
|
|
@@ -2435,6 +2714,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2435
2714
|
role = ?,
|
|
2436
2715
|
collection_access = ?,
|
|
2437
2716
|
environment_access = ?,
|
|
2717
|
+
snippet_access = ?,
|
|
2438
2718
|
llm_access = ?,
|
|
2439
2719
|
llm_models = ?,
|
|
2440
2720
|
llm_monthly_token_limit = ?,
|
|
@@ -2446,6 +2726,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2446
2726
|
role,
|
|
2447
2727
|
serializeAccessList(collectionAccess),
|
|
2448
2728
|
serializeAccessList(environmentAccess),
|
|
2729
|
+
serializeAccessList(snippetAccess),
|
|
2449
2730
|
llmAccess ? 1 : 0,
|
|
2450
2731
|
serializeAccessList(llmModels),
|
|
2451
2732
|
llmMonthlyTokenLimit,
|
|
@@ -2507,7 +2788,8 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2507
2788
|
name: BOOTSTRAP_USER_NAME,
|
|
2508
2789
|
role: "user",
|
|
2509
2790
|
collectionAccess: ["*"],
|
|
2510
|
-
environmentAccess: ["*"]
|
|
2791
|
+
environmentAccess: ["*"],
|
|
2792
|
+
snippetAccess: ["*"]
|
|
2511
2793
|
},
|
|
2512
2794
|
systemUserId
|
|
2513
2795
|
);
|
|
@@ -2922,6 +3204,143 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
2922
3204
|
}
|
|
2923
3205
|
return mapEnvironmentSqlRow(row);
|
|
2924
3206
|
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Lists all snippets ordered by sort order then name.
|
|
3209
|
+
*/
|
|
3210
|
+
async listSnippets() {
|
|
3211
|
+
const rows = await this.queryRows(
|
|
3212
|
+
`${SNIPPET_SELECT} ORDER BY sort_order ASC, name ASC`
|
|
3213
|
+
);
|
|
3214
|
+
return rows.map(mapSnippetSqlRow);
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Creates a new snippet with the given fields.
|
|
3218
|
+
*
|
|
3219
|
+
* @param name - Display name for the snippet.
|
|
3220
|
+
* @param code - JavaScript source for the snippet.
|
|
3221
|
+
* @param scope - Execution scope for the snippet.
|
|
3222
|
+
* @param actingUserId - User performing the create action.
|
|
3223
|
+
*/
|
|
3224
|
+
async createSnippet(name, code, scope, actingUserId) {
|
|
3225
|
+
const trimmedName = trimRequiredName(name, "Snippet name");
|
|
3226
|
+
const id = randomUUID3();
|
|
3227
|
+
const now = /* @__PURE__ */ new Date();
|
|
3228
|
+
const maxRows = await this.queryRows(
|
|
3229
|
+
"SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM snippets"
|
|
3230
|
+
);
|
|
3231
|
+
const maxOrder = maxRows[0]?.max_order ?? -1;
|
|
3232
|
+
await this.executeStatement(
|
|
3233
|
+
`INSERT INTO snippets (
|
|
3234
|
+
id,
|
|
3235
|
+
name,
|
|
3236
|
+
code,
|
|
3237
|
+
scope,
|
|
3238
|
+
sort_order,
|
|
3239
|
+
created_at,
|
|
3240
|
+
updated_at,
|
|
3241
|
+
created_by_user_id,
|
|
3242
|
+
updated_by_user_id,
|
|
3243
|
+
deletion_locked
|
|
3244
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3245
|
+
[id, trimmedName, code, scope, maxOrder + 1, now, now, actingUserId, actingUserId, 0]
|
|
3246
|
+
);
|
|
3247
|
+
await this.recordAuditEntry(actingUserId, "create", "snippet", id);
|
|
3248
|
+
const rows = await this.queryRows(
|
|
3249
|
+
`${SNIPPET_SELECT} WHERE id = ?`,
|
|
3250
|
+
[id]
|
|
3251
|
+
);
|
|
3252
|
+
const row = rows[0];
|
|
3253
|
+
if (!row) {
|
|
3254
|
+
throw new Error("Snippet not found after insert");
|
|
3255
|
+
}
|
|
3256
|
+
return mapSnippetSqlRow(row);
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Updates a snippet's name, code, and scope. Sort order is left unchanged.
|
|
3260
|
+
*
|
|
3261
|
+
* @param actingUserId - User performing the update action.
|
|
3262
|
+
*/
|
|
3263
|
+
async updateSnippet(id, name, code, scope, actingUserId) {
|
|
3264
|
+
const trimmedName = trimRequiredName(name, "Snippet name");
|
|
3265
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
3266
|
+
const result = await this.executeStatement(
|
|
3267
|
+
`UPDATE snippets
|
|
3268
|
+
SET name = ?,
|
|
3269
|
+
code = ?,
|
|
3270
|
+
scope = ?,
|
|
3271
|
+
updated_at = ?,
|
|
3272
|
+
updated_by_user_id = ?
|
|
3273
|
+
WHERE id = ?`,
|
|
3274
|
+
[trimmedName, code, scope, updatedAt, actingUserId, id]
|
|
3275
|
+
);
|
|
3276
|
+
if ((result.affectedRows ?? 0) === 0) {
|
|
3277
|
+
throw new Error("Snippet not found");
|
|
3278
|
+
}
|
|
3279
|
+
await this.recordAuditEntry(actingUserId, "update", "snippet", id);
|
|
3280
|
+
const rows = await this.queryRows(
|
|
3281
|
+
`${SNIPPET_SELECT} WHERE id = ?`,
|
|
3282
|
+
[id]
|
|
3283
|
+
);
|
|
3284
|
+
const row = rows[0];
|
|
3285
|
+
if (!row) {
|
|
3286
|
+
throw new Error("Snippet not found");
|
|
3287
|
+
}
|
|
3288
|
+
return mapSnippetSqlRow(row);
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* Deletes a snippet.
|
|
3292
|
+
*
|
|
3293
|
+
* @param id - Snippet ID to delete.
|
|
3294
|
+
* @param actingUserId - User performing the delete action.
|
|
3295
|
+
*/
|
|
3296
|
+
async deleteSnippet(id, actingUserId) {
|
|
3297
|
+
await this.recordAuditEntry(actingUserId, "delete", "snippet", id);
|
|
3298
|
+
await this.executeStatement("DELETE FROM snippets WHERE id = ?", [id]);
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Finds a snippet by stable identifier.
|
|
3302
|
+
*
|
|
3303
|
+
* @param id - Snippet ID to look up.
|
|
3304
|
+
*/
|
|
3305
|
+
async findSnippetById(id) {
|
|
3306
|
+
const rows = await this.queryRows(
|
|
3307
|
+
`${SNIPPET_SELECT} WHERE id = ?`,
|
|
3308
|
+
[id]
|
|
3309
|
+
);
|
|
3310
|
+
const row = rows[0];
|
|
3311
|
+
return row ? mapSnippetSqlRow(row) : null;
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Updates whether non-admin users may delete a snippet.
|
|
3315
|
+
*
|
|
3316
|
+
* @param id - Snippet ID to update.
|
|
3317
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the snippet.
|
|
3318
|
+
* @param actingUserId - Admin user performing the update.
|
|
3319
|
+
*/
|
|
3320
|
+
async setSnippetDeletionLocked(id, deletionLocked, actingUserId) {
|
|
3321
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
3322
|
+
const result = await this.executeStatement(
|
|
3323
|
+
`UPDATE snippets
|
|
3324
|
+
SET deletion_locked = ?,
|
|
3325
|
+
updated_at = ?,
|
|
3326
|
+
updated_by_user_id = ?
|
|
3327
|
+
WHERE id = ?`,
|
|
3328
|
+
[deletionLocked ? 1 : 0, updatedAt, actingUserId, id]
|
|
3329
|
+
);
|
|
3330
|
+
if ((result.affectedRows ?? 0) === 0) {
|
|
3331
|
+
throw new Error("Snippet not found");
|
|
3332
|
+
}
|
|
3333
|
+
await this.recordAuditEntry(actingUserId, "update", "snippet", id);
|
|
3334
|
+
const rows = await this.queryRows(
|
|
3335
|
+
`${SNIPPET_SELECT} WHERE id = ?`,
|
|
3336
|
+
[id]
|
|
3337
|
+
);
|
|
3338
|
+
const row = rows[0];
|
|
3339
|
+
if (!row) {
|
|
3340
|
+
throw new Error("Snippet not found");
|
|
3341
|
+
}
|
|
3342
|
+
return mapSnippetSqlRow(row);
|
|
3343
|
+
}
|
|
2925
3344
|
/**
|
|
2926
3345
|
* Lists all saved requests in a collection.
|
|
2927
3346
|
*
|
|
@@ -3484,6 +3903,7 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3484
3903
|
role,
|
|
3485
3904
|
collection_access,
|
|
3486
3905
|
environment_access,
|
|
3906
|
+
snippet_access,
|
|
3487
3907
|
llm_access,
|
|
3488
3908
|
llm_models,
|
|
3489
3909
|
llm_monthly_token_limit,
|
|
@@ -3491,13 +3911,14 @@ var MysqlDatabase = class _MysqlDatabase {
|
|
|
3491
3911
|
updated_at,
|
|
3492
3912
|
created_by_user_id,
|
|
3493
3913
|
updated_by_user_id
|
|
3494
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3914
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3495
3915
|
[
|
|
3496
3916
|
id,
|
|
3497
3917
|
trimmedName,
|
|
3498
3918
|
input.role,
|
|
3499
3919
|
serializeAccessList(input.collectionAccess),
|
|
3500
3920
|
serializeAccessList(input.environmentAccess),
|
|
3921
|
+
serializeAccessList(input.snippetAccess),
|
|
3501
3922
|
0,
|
|
3502
3923
|
serializeAccessList([]),
|
|
3503
3924
|
null,
|
|
@@ -3626,6 +4047,20 @@ CREATE TABLE IF NOT EXISTS environments (
|
|
|
3626
4047
|
updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
|
|
3627
4048
|
);
|
|
3628
4049
|
`.trim();
|
|
4050
|
+
var SNIPPETS_MIGRATION_SQL2 = `
|
|
4051
|
+
CREATE TABLE IF NOT EXISTS snippets (
|
|
4052
|
+
id TEXT PRIMARY KEY,
|
|
4053
|
+
name TEXT NOT NULL,
|
|
4054
|
+
code TEXT NOT NULL DEFAULT '',
|
|
4055
|
+
scope TEXT NOT NULL DEFAULT 'any' CHECK (scope IN ('pre-request', 'post-request', 'any')),
|
|
4056
|
+
sort_order INT NOT NULL DEFAULT 0,
|
|
4057
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
4058
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
4059
|
+
created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4060
|
+
updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
4061
|
+
deletion_locked BOOLEAN NOT NULL DEFAULT FALSE
|
|
4062
|
+
);
|
|
4063
|
+
`.trim();
|
|
3629
4064
|
var FOLDERS_MIGRATION_SQL2 = `
|
|
3630
4065
|
CREATE TABLE IF NOT EXISTS folders (
|
|
3631
4066
|
id TEXT PRIMARY KEY,
|
|
@@ -3671,6 +4106,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|
|
3671
4106
|
role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
|
|
3672
4107
|
collection_access TEXT NOT NULL DEFAULT '[]',
|
|
3673
4108
|
environment_access TEXT NOT NULL DEFAULT '[]',
|
|
4109
|
+
snippet_access TEXT NOT NULL DEFAULT '[]',
|
|
3674
4110
|
created_at TIMESTAMPTZ NOT NULL,
|
|
3675
4111
|
updated_at TIMESTAMPTZ NOT NULL,
|
|
3676
4112
|
created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
@@ -3782,11 +4218,23 @@ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2 = `
|
|
|
3782
4218
|
ALTER TABLE environments
|
|
3783
4219
|
ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
|
|
3784
4220
|
`.trim();
|
|
4221
|
+
var USERS_SNIPPET_ACCESS_MIGRATION_SQL2 = `
|
|
4222
|
+
ALTER TABLE users
|
|
4223
|
+
ADD COLUMN IF NOT EXISTS snippet_access TEXT NOT NULL DEFAULT '[]';
|
|
4224
|
+
`.trim();
|
|
4225
|
+
var USERS_SNIPPET_ACCESS_BACKFILL_SQL2 = `
|
|
4226
|
+
UPDATE users
|
|
4227
|
+
SET snippet_access = '["*"]'
|
|
4228
|
+
WHERE role = 'user'
|
|
4229
|
+
AND snippet_access = '[]'
|
|
4230
|
+
AND collection_access LIKE '%"*"%';
|
|
4231
|
+
`.trim();
|
|
3785
4232
|
var POSTGRES_MIGRATIONS = [
|
|
3786
4233
|
USERS_MIGRATION_SQL2,
|
|
3787
4234
|
API_TOKENS_MIGRATION_SQL2,
|
|
3788
4235
|
COLLECTIONS_MIGRATION_SQL2,
|
|
3789
4236
|
ENVIRONMENTS_MIGRATION_SQL2,
|
|
4237
|
+
SNIPPETS_MIGRATION_SQL2,
|
|
3790
4238
|
FOLDERS_MIGRATION_SQL2,
|
|
3791
4239
|
REQUESTS_MIGRATION_SQL2,
|
|
3792
4240
|
AUDIT_LOG_MIGRATION_SQL2,
|
|
@@ -3804,7 +4252,9 @@ var POSTGRES_MIGRATIONS = [
|
|
|
3804
4252
|
LLM_USAGE_MIGRATION_SQL2,
|
|
3805
4253
|
LLM_USAGE_LOG_MIGRATION_SQL2,
|
|
3806
4254
|
COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
|
|
3807
|
-
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2
|
|
4255
|
+
ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
|
|
4256
|
+
USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
|
|
4257
|
+
USERS_SNIPPET_ACCESS_BACKFILL_SQL2
|
|
3808
4258
|
];
|
|
3809
4259
|
|
|
3810
4260
|
// src/db/postgres/schemas.ts
|
|
@@ -3828,6 +4278,7 @@ var postgresConfigSchema = z4.object({
|
|
|
3828
4278
|
var { Pool } = pg;
|
|
3829
4279
|
var COLLECTION_SELECT2 = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
|
|
3830
4280
|
var ENVIRONMENT_SELECT2 = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
|
|
4281
|
+
var SNIPPET_SELECT2 = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
|
|
3831
4282
|
var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
|
|
3832
4283
|
var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
|
|
3833
4284
|
var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
|
|
@@ -3968,6 +4419,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
3968
4419
|
role,
|
|
3969
4420
|
collection_access,
|
|
3970
4421
|
environment_access,
|
|
4422
|
+
snippet_access,
|
|
3971
4423
|
llm_access,
|
|
3972
4424
|
llm_models,
|
|
3973
4425
|
llm_monthly_token_limit,
|
|
@@ -3975,7 +4427,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
3975
4427
|
updated_at,
|
|
3976
4428
|
created_by_user_id,
|
|
3977
4429
|
updated_by_user_id
|
|
3978
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
4430
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
3979
4431
|
RETURNING ${USER_SELECT_COLUMNS}`,
|
|
3980
4432
|
[
|
|
3981
4433
|
id,
|
|
@@ -3983,6 +4435,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
3983
4435
|
input.role,
|
|
3984
4436
|
serializeAccessList(input.collectionAccess),
|
|
3985
4437
|
serializeAccessList(input.environmentAccess),
|
|
4438
|
+
serializeAccessList(input.snippetAccess),
|
|
3986
4439
|
input.llmAccess ?? false,
|
|
3987
4440
|
serializeAccessList(input.llmModels ?? []),
|
|
3988
4441
|
input.llmMonthlyTokenLimit ?? null,
|
|
@@ -4047,6 +4500,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
4047
4500
|
const role = input.role ?? existing.role;
|
|
4048
4501
|
const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
|
|
4049
4502
|
const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
|
|
4503
|
+
const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
|
|
4050
4504
|
const llmAccess = input.llmAccess ?? existing.llmAccess;
|
|
4051
4505
|
const llmModels = input.llmModels ?? existing.llmModels;
|
|
4052
4506
|
const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
|
|
@@ -4057,17 +4511,19 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
4057
4511
|
role = $2,
|
|
4058
4512
|
collection_access = $3,
|
|
4059
4513
|
environment_access = $4,
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4514
|
+
snippet_access = $5,
|
|
4515
|
+
llm_access = $6,
|
|
4516
|
+
llm_models = $7,
|
|
4517
|
+
llm_monthly_token_limit = $8,
|
|
4518
|
+
updated_at = $9,
|
|
4519
|
+
updated_by_user_id = $10
|
|
4520
|
+
WHERE id = $11`,
|
|
4066
4521
|
[
|
|
4067
4522
|
name,
|
|
4068
4523
|
role,
|
|
4069
4524
|
serializeAccessList(collectionAccess),
|
|
4070
4525
|
serializeAccessList(environmentAccess),
|
|
4526
|
+
serializeAccessList(snippetAccess),
|
|
4071
4527
|
llmAccess,
|
|
4072
4528
|
serializeAccessList(llmModels),
|
|
4073
4529
|
llmMonthlyTokenLimit,
|
|
@@ -4129,7 +4585,8 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
4129
4585
|
name: BOOTSTRAP_USER_NAME,
|
|
4130
4586
|
role: "user",
|
|
4131
4587
|
collectionAccess: ["*"],
|
|
4132
|
-
environmentAccess: ["*"]
|
|
4588
|
+
environmentAccess: ["*"],
|
|
4589
|
+
snippetAccess: ["*"]
|
|
4133
4590
|
},
|
|
4134
4591
|
systemUserId
|
|
4135
4592
|
);
|
|
@@ -4526,27 +4983,151 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
4526
4983
|
return mapEnvironmentSqlRow(row);
|
|
4527
4984
|
}
|
|
4528
4985
|
/**
|
|
4529
|
-
* Lists all
|
|
4530
|
-
*
|
|
4531
|
-
* @param collectionId - Collection to query.
|
|
4986
|
+
* Lists all snippets ordered by sort order then name.
|
|
4532
4987
|
*/
|
|
4533
|
-
async
|
|
4988
|
+
async listSnippets() {
|
|
4534
4989
|
const result = await this.query(
|
|
4535
|
-
`${
|
|
4536
|
-
[collectionId]
|
|
4990
|
+
`${SNIPPET_SELECT2} ORDER BY sort_order ASC, name ASC`
|
|
4537
4991
|
);
|
|
4538
|
-
return result.rows.map(
|
|
4992
|
+
return result.rows.map(mapSnippetSqlRow);
|
|
4539
4993
|
}
|
|
4540
4994
|
/**
|
|
4541
|
-
*
|
|
4995
|
+
* Creates a new snippet with the given fields.
|
|
4542
4996
|
*
|
|
4543
|
-
* @param
|
|
4997
|
+
* @param name - Display name for the snippet.
|
|
4998
|
+
* @param code - JavaScript source for the snippet.
|
|
4999
|
+
* @param scope - Execution scope for the snippet.
|
|
5000
|
+
* @param actingUserId - User performing the create action.
|
|
4544
5001
|
*/
|
|
4545
|
-
async
|
|
4546
|
-
const
|
|
4547
|
-
const
|
|
4548
|
-
|
|
4549
|
-
|
|
5002
|
+
async createSnippet(name, code, scope, actingUserId) {
|
|
5003
|
+
const trimmedName = trimRequiredName(name, "Snippet name");
|
|
5004
|
+
const id = randomUUID4();
|
|
5005
|
+
const now = /* @__PURE__ */ new Date();
|
|
5006
|
+
const maxResult = await this.query(
|
|
5007
|
+
"SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM snippets"
|
|
5008
|
+
);
|
|
5009
|
+
const maxOrder = maxResult.rows[0]?.max_order ?? -1;
|
|
5010
|
+
const result = await this.query(
|
|
5011
|
+
`INSERT INTO snippets (
|
|
5012
|
+
id,
|
|
5013
|
+
name,
|
|
5014
|
+
code,
|
|
5015
|
+
scope,
|
|
5016
|
+
sort_order,
|
|
5017
|
+
created_at,
|
|
5018
|
+
updated_at,
|
|
5019
|
+
created_by_user_id,
|
|
5020
|
+
updated_by_user_id
|
|
5021
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
5022
|
+
RETURNING ${SNIPPET_SELECT_COLUMNS}`,
|
|
5023
|
+
[id, trimmedName, code, scope, maxOrder + 1, now, now, actingUserId, actingUserId]
|
|
5024
|
+
);
|
|
5025
|
+
const row = result.rows[0];
|
|
5026
|
+
if (!row) {
|
|
5027
|
+
throw new Error("Snippet not found after insert");
|
|
5028
|
+
}
|
|
5029
|
+
await this.recordAuditEntry(actingUserId, "create", "snippet", id);
|
|
5030
|
+
return mapSnippetSqlRow(row);
|
|
5031
|
+
}
|
|
5032
|
+
/**
|
|
5033
|
+
* Updates a snippet's name, code, and scope. Sort order is left unchanged.
|
|
5034
|
+
*
|
|
5035
|
+
* @param actingUserId - User performing the update action.
|
|
5036
|
+
*/
|
|
5037
|
+
async updateSnippet(id, name, code, scope, actingUserId) {
|
|
5038
|
+
const trimmedName = trimRequiredName(name, "Snippet name");
|
|
5039
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
5040
|
+
const result = await this.query(
|
|
5041
|
+
`UPDATE snippets
|
|
5042
|
+
SET name = $1,
|
|
5043
|
+
code = $2,
|
|
5044
|
+
scope = $3,
|
|
5045
|
+
updated_at = $4,
|
|
5046
|
+
updated_by_user_id = $5
|
|
5047
|
+
WHERE id = $6`,
|
|
5048
|
+
[trimmedName, code, scope, updatedAt, actingUserId, id]
|
|
5049
|
+
);
|
|
5050
|
+
if ((result.rowCount ?? 0) === 0) {
|
|
5051
|
+
throw new Error("Snippet not found");
|
|
5052
|
+
}
|
|
5053
|
+
await this.recordAuditEntry(actingUserId, "update", "snippet", id);
|
|
5054
|
+
const selectResult = await this.query(`${SNIPPET_SELECT2} WHERE id = $1`, [id]);
|
|
5055
|
+
const row = selectResult.rows[0];
|
|
5056
|
+
if (!row) {
|
|
5057
|
+
throw new Error("Snippet not found");
|
|
5058
|
+
}
|
|
5059
|
+
return mapSnippetSqlRow(row);
|
|
5060
|
+
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Deletes a snippet.
|
|
5063
|
+
*
|
|
5064
|
+
* @param id - Snippet ID to delete.
|
|
5065
|
+
* @param actingUserId - User performing the delete action.
|
|
5066
|
+
*/
|
|
5067
|
+
async deleteSnippet(id, actingUserId) {
|
|
5068
|
+
await this.recordAuditEntry(actingUserId, "delete", "snippet", id);
|
|
5069
|
+
await this.query("DELETE FROM snippets WHERE id = $1", [id]);
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* Finds a snippet by stable identifier.
|
|
5073
|
+
*
|
|
5074
|
+
* @param id - Snippet ID to look up.
|
|
5075
|
+
*/
|
|
5076
|
+
async findSnippetById(id) {
|
|
5077
|
+
const result = await this.query(`${SNIPPET_SELECT2} WHERE id = $1`, [id]);
|
|
5078
|
+
const row = result.rows[0];
|
|
5079
|
+
return row ? mapSnippetSqlRow(row) : null;
|
|
5080
|
+
}
|
|
5081
|
+
/**
|
|
5082
|
+
* Updates whether non-admin users may delete a snippet.
|
|
5083
|
+
*
|
|
5084
|
+
* @param id - Snippet ID to update.
|
|
5085
|
+
* @param deletionLocked - When true, user-role tokens cannot delete the snippet.
|
|
5086
|
+
* @param actingUserId - Admin user performing the update.
|
|
5087
|
+
*/
|
|
5088
|
+
async setSnippetDeletionLocked(id, deletionLocked, actingUserId) {
|
|
5089
|
+
const updatedAt = /* @__PURE__ */ new Date();
|
|
5090
|
+
const result = await this.query(
|
|
5091
|
+
`UPDATE snippets
|
|
5092
|
+
SET deletion_locked = $1,
|
|
5093
|
+
updated_at = $2,
|
|
5094
|
+
updated_by_user_id = $3
|
|
5095
|
+
WHERE id = $4`,
|
|
5096
|
+
[deletionLocked, updatedAt, actingUserId, id]
|
|
5097
|
+
);
|
|
5098
|
+
if ((result.rowCount ?? 0) === 0) {
|
|
5099
|
+
throw new Error("Snippet not found");
|
|
5100
|
+
}
|
|
5101
|
+
await this.recordAuditEntry(actingUserId, "update", "snippet", id);
|
|
5102
|
+
const selectResult = await this.query(`${SNIPPET_SELECT2} WHERE id = $1`, [id]);
|
|
5103
|
+
const row = selectResult.rows[0];
|
|
5104
|
+
if (!row) {
|
|
5105
|
+
throw new Error("Snippet not found");
|
|
5106
|
+
}
|
|
5107
|
+
return mapSnippetSqlRow(row);
|
|
5108
|
+
}
|
|
5109
|
+
/**
|
|
5110
|
+
* Lists all saved requests in a collection.
|
|
5111
|
+
*
|
|
5112
|
+
* @param collectionId - Collection to query.
|
|
5113
|
+
*/
|
|
5114
|
+
async listRequests(collectionId) {
|
|
5115
|
+
const result = await this.query(
|
|
5116
|
+
`${REQUEST_SELECT2} WHERE collection_id = $1 ORDER BY sort_order ASC, name ASC`,
|
|
5117
|
+
[collectionId]
|
|
5118
|
+
);
|
|
5119
|
+
return result.rows.map(mapRequestSqlRow);
|
|
5120
|
+
}
|
|
5121
|
+
/**
|
|
5122
|
+
* Finds a saved request by id.
|
|
5123
|
+
*
|
|
5124
|
+
* @param id - Request identifier to look up.
|
|
5125
|
+
*/
|
|
5126
|
+
async findRequestById(id) {
|
|
5127
|
+
const result = await this.query(`${REQUEST_SELECT2} WHERE id = $1 LIMIT 1`, [id]);
|
|
5128
|
+
const row = result.rows[0];
|
|
5129
|
+
return row ? mapRequestSqlRow(row) : null;
|
|
5130
|
+
}
|
|
4550
5131
|
/**
|
|
4551
5132
|
* Inserts a new request or updates an existing one.
|
|
4552
5133
|
*
|
|
@@ -5082,6 +5663,7 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
5082
5663
|
role,
|
|
5083
5664
|
collection_access,
|
|
5084
5665
|
environment_access,
|
|
5666
|
+
snippet_access,
|
|
5085
5667
|
llm_access,
|
|
5086
5668
|
llm_models,
|
|
5087
5669
|
llm_monthly_token_limit,
|
|
@@ -5089,13 +5671,14 @@ var PostgresDatabase = class _PostgresDatabase {
|
|
|
5089
5671
|
updated_at,
|
|
5090
5672
|
created_by_user_id,
|
|
5091
5673
|
updated_by_user_id
|
|
5092
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
|
5674
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
|
5093
5675
|
[
|
|
5094
5676
|
id,
|
|
5095
5677
|
SYSTEM_USER_NAME,
|
|
5096
5678
|
input.role,
|
|
5097
5679
|
serializeAccessList(input.collectionAccess),
|
|
5098
5680
|
serializeAccessList(input.environmentAccess),
|
|
5681
|
+
serializeAccessList(input.snippetAccess),
|
|
5099
5682
|
false,
|
|
5100
5683
|
serializeAccessList([]),
|
|
5101
5684
|
null,
|
|
@@ -5395,21 +5978,26 @@ function normalizeLlmForRole(role, llmAccess, llmModels) {
|
|
|
5395
5978
|
llmModels
|
|
5396
5979
|
};
|
|
5397
5980
|
}
|
|
5398
|
-
function normalizeAccessForRole(role, collectionAccess, environmentAccess) {
|
|
5981
|
+
function normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess) {
|
|
5399
5982
|
if (role === "admin") {
|
|
5400
|
-
if (collectionAccess.length > 0 || environmentAccess.length > 0) {
|
|
5401
|
-
throw new ValidationError(
|
|
5983
|
+
if (collectionAccess.length > 0 || environmentAccess.length > 0 || snippetAccess.length > 0) {
|
|
5984
|
+
throw new ValidationError(
|
|
5985
|
+
"Admin users cannot have collection, environment, or snippet access."
|
|
5986
|
+
);
|
|
5402
5987
|
}
|
|
5403
5988
|
return {
|
|
5404
5989
|
collectionAccess: [],
|
|
5405
|
-
environmentAccess: []
|
|
5990
|
+
environmentAccess: [],
|
|
5991
|
+
snippetAccess: []
|
|
5406
5992
|
};
|
|
5407
5993
|
}
|
|
5408
5994
|
validateAccessList(collectionAccess);
|
|
5409
5995
|
validateAccessList(environmentAccess);
|
|
5996
|
+
validateAccessList(snippetAccess);
|
|
5410
5997
|
return {
|
|
5411
5998
|
collectionAccess,
|
|
5412
|
-
environmentAccess
|
|
5999
|
+
environmentAccess,
|
|
6000
|
+
snippetAccess
|
|
5413
6001
|
};
|
|
5414
6002
|
}
|
|
5415
6003
|
function findUnknownAccessIds(access, knownIds) {
|
|
@@ -5435,6 +6023,9 @@ function validateSubmittedAccessLists(submitted, catalogs) {
|
|
|
5435
6023
|
"environment"
|
|
5436
6024
|
);
|
|
5437
6025
|
}
|
|
6026
|
+
if (submitted.snippetAccess !== void 0) {
|
|
6027
|
+
validateKnownAccessIds(submitted.snippetAccess, catalogs.knownSnippetIds, "snippet");
|
|
6028
|
+
}
|
|
5438
6029
|
}
|
|
5439
6030
|
if (submitted.llmModels !== void 0 && catalogs.knownLlmModelIds !== null) {
|
|
5440
6031
|
validateKnownAccessIds(submitted.llmModels, catalogs.knownLlmModelIds, "LLM model");
|
|
@@ -5448,6 +6039,9 @@ function buildAccessListWarnings(stored, catalogs) {
|
|
|
5448
6039
|
for (const id of findUnknownAccessIds(stored.environmentAccess, catalogs.knownEnvironmentIds)) {
|
|
5449
6040
|
warnings.push(`Unknown environment id "${id}".`);
|
|
5450
6041
|
}
|
|
6042
|
+
for (const id of findUnknownAccessIds(stored.snippetAccess, catalogs.knownSnippetIds)) {
|
|
6043
|
+
warnings.push(`Unknown snippet id "${id}".`);
|
|
6044
|
+
}
|
|
5451
6045
|
if (catalogs.knownLlmModelIds !== null) {
|
|
5452
6046
|
for (const id of findUnknownAccessIds(stored.llmModels, catalogs.knownLlmModelIds)) {
|
|
5453
6047
|
warnings.push(`Unknown LLM model id "${id}".`);
|
|
@@ -5455,10 +6049,11 @@ function buildAccessListWarnings(stored, catalogs) {
|
|
|
5455
6049
|
}
|
|
5456
6050
|
return warnings;
|
|
5457
6051
|
}
|
|
5458
|
-
function buildAccessCatalogIds(collections, environments, llmModelIds) {
|
|
6052
|
+
function buildAccessCatalogIds(collections, environments, snippets, llmModelIds) {
|
|
5459
6053
|
return {
|
|
5460
6054
|
knownCollectionIds: new Set(collections.map((collection) => collection.id)),
|
|
5461
6055
|
knownEnvironmentIds: new Set(environments.map((environment) => environment.id)),
|
|
6056
|
+
knownSnippetIds: new Set(snippets.map((snippet) => snippet.id)),
|
|
5462
6057
|
knownLlmModelIds: llmModelIds === null ? null : new Set(llmModelIds)
|
|
5463
6058
|
};
|
|
5464
6059
|
}
|
|
@@ -5466,7 +6061,8 @@ function buildAdminUserUpdateInput(existing, body) {
|
|
|
5466
6061
|
const role = body.role ?? existing.role;
|
|
5467
6062
|
const collectionAccess = role === "admin" ? [] : body.collectionAccess ?? existing.collectionAccess;
|
|
5468
6063
|
const environmentAccess = role === "admin" ? [] : body.environmentAccess ?? existing.environmentAccess;
|
|
5469
|
-
const
|
|
6064
|
+
const snippetAccess = role === "admin" ? [] : body.snippetAccess ?? existing.snippetAccess;
|
|
6065
|
+
const access = normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess);
|
|
5470
6066
|
const llmAccess = role === "admin" ? false : body.llmAccess ?? existing.llmAccess;
|
|
5471
6067
|
const llmModels = role === "admin" ? [] : body.llmModels ?? existing.llmModels;
|
|
5472
6068
|
const llm = normalizeLlmForRole(role, llmAccess, llmModels);
|
|
@@ -5475,6 +6071,7 @@ function buildAdminUserUpdateInput(existing, body) {
|
|
|
5475
6071
|
role: body.role,
|
|
5476
6072
|
collectionAccess: access.collectionAccess,
|
|
5477
6073
|
environmentAccess: access.environmentAccess,
|
|
6074
|
+
snippetAccess: access.snippetAccess,
|
|
5478
6075
|
llmAccess: llm.llmAccess,
|
|
5479
6076
|
llmModels: llm.llmModels,
|
|
5480
6077
|
llmMonthlyTokenLimit: body.llmMonthlyTokenLimit
|
|
@@ -5483,7 +6080,13 @@ function buildAdminUserUpdateInput(existing, body) {
|
|
|
5483
6080
|
function buildAdminUserCreateInput(body) {
|
|
5484
6081
|
const collectionAccess = body.collectionAccess ?? [];
|
|
5485
6082
|
const environmentAccess = body.environmentAccess ?? [];
|
|
5486
|
-
const
|
|
6083
|
+
const snippetAccess = body.snippetAccess ?? [];
|
|
6084
|
+
const access = normalizeAccessForRole(
|
|
6085
|
+
body.role,
|
|
6086
|
+
collectionAccess,
|
|
6087
|
+
environmentAccess,
|
|
6088
|
+
snippetAccess
|
|
6089
|
+
);
|
|
5487
6090
|
const llmAccess = body.role === "admin" ? false : body.llmAccess ?? false;
|
|
5488
6091
|
const llmModels = body.role === "admin" ? [] : body.llmModels ?? [];
|
|
5489
6092
|
const llm = normalizeLlmForRole(body.role, llmAccess, llmModels);
|
|
@@ -5492,6 +6095,7 @@ function buildAdminUserCreateInput(body) {
|
|
|
5492
6095
|
role: body.role,
|
|
5493
6096
|
collectionAccess: access.collectionAccess ?? [],
|
|
5494
6097
|
environmentAccess: access.environmentAccess ?? [],
|
|
6098
|
+
snippetAccess: access.snippetAccess ?? [],
|
|
5495
6099
|
llmAccess: llm.llmAccess ?? false,
|
|
5496
6100
|
llmModels: llm.llmModels ?? [],
|
|
5497
6101
|
llmMonthlyTokenLimit: body.llmMonthlyTokenLimit ?? null
|
|
@@ -5590,6 +6194,7 @@ function printUser(user, usage) {
|
|
|
5590
6194
|
console.log(` role: ${user.role}`);
|
|
5591
6195
|
console.log(` collection access: ${formatAccessList(user.collectionAccess)}`);
|
|
5592
6196
|
console.log(` environment access: ${formatAccessList(user.environmentAccess)}`);
|
|
6197
|
+
console.log(` snippet access: ${formatAccessList(user.snippetAccess)}`);
|
|
5593
6198
|
console.log(` llm access: ${user.llmAccess ? "enabled" : "disabled"}`);
|
|
5594
6199
|
console.log(` llm models: ${formatAccessList(user.llmModels)}`);
|
|
5595
6200
|
console.log(
|
|
@@ -5609,13 +6214,15 @@ function printCreatedApiToken(user, record, secret) {
|
|
|
5609
6214
|
console.log(secret);
|
|
5610
6215
|
}
|
|
5611
6216
|
async function loadAccessCatalogs(db, llm) {
|
|
5612
|
-
const [collections, environments] = await Promise.all([
|
|
6217
|
+
const [collections, environments, snippets] = await Promise.all([
|
|
5613
6218
|
db.listCollections(),
|
|
5614
|
-
db.listEnvironments()
|
|
6219
|
+
db.listEnvironments(),
|
|
6220
|
+
db.listSnippets()
|
|
5615
6221
|
]);
|
|
5616
6222
|
return buildAccessCatalogIds(
|
|
5617
6223
|
collections,
|
|
5618
6224
|
environments,
|
|
6225
|
+
snippets,
|
|
5619
6226
|
llm ? listHubOfferedModels(llm).map((model) => model.id) : null
|
|
5620
6227
|
);
|
|
5621
6228
|
}
|
|
@@ -5641,7 +6248,12 @@ async function userCreateCommand(options) {
|
|
|
5641
6248
|
const config = loadServerConfig(options.config);
|
|
5642
6249
|
const db = createDatabase(config.db);
|
|
5643
6250
|
const access = mapValidationError(
|
|
5644
|
-
() => normalizeAccessForRole(
|
|
6251
|
+
() => normalizeAccessForRole(
|
|
6252
|
+
options.role,
|
|
6253
|
+
options.collectionAccess,
|
|
6254
|
+
options.environmentAccess,
|
|
6255
|
+
options.snippetAccess
|
|
6256
|
+
)
|
|
5645
6257
|
);
|
|
5646
6258
|
await db.connect();
|
|
5647
6259
|
const actingUserId = await requireSystemUserId(db);
|
|
@@ -5655,6 +6267,7 @@ async function userCreateCommand(options) {
|
|
|
5655
6267
|
role: options.role,
|
|
5656
6268
|
collectionAccess: access.collectionAccess,
|
|
5657
6269
|
environmentAccess: access.environmentAccess,
|
|
6270
|
+
snippetAccess: access.snippetAccess,
|
|
5658
6271
|
llmModels
|
|
5659
6272
|
},
|
|
5660
6273
|
catalogs
|
|
@@ -5665,6 +6278,7 @@ async function userCreateCommand(options) {
|
|
|
5665
6278
|
role: options.role,
|
|
5666
6279
|
collectionAccess: access.collectionAccess ?? [],
|
|
5667
6280
|
environmentAccess: access.environmentAccess ?? [],
|
|
6281
|
+
snippetAccess: access.snippetAccess ?? [],
|
|
5668
6282
|
llmAccess: llm.llmAccess,
|
|
5669
6283
|
llmModels: llm.llmModels,
|
|
5670
6284
|
llmMonthlyTokenLimit: options.llmMonthlyTokens ?? null
|
|
@@ -5702,6 +6316,7 @@ async function userListCommand(options) {
|
|
|
5702
6316
|
{
|
|
5703
6317
|
collectionAccess: user.collectionAccess,
|
|
5704
6318
|
environmentAccess: user.environmentAccess,
|
|
6319
|
+
snippetAccess: user.snippetAccess,
|
|
5705
6320
|
llmModels: user.llmModels
|
|
5706
6321
|
},
|
|
5707
6322
|
catalogs
|
|
@@ -5731,6 +6346,7 @@ async function userShowCommand(options) {
|
|
|
5731
6346
|
{
|
|
5732
6347
|
collectionAccess: user.collectionAccess,
|
|
5733
6348
|
environmentAccess: user.environmentAccess,
|
|
6349
|
+
snippetAccess: user.snippetAccess,
|
|
5734
6350
|
llmModels: user.llmModels
|
|
5735
6351
|
},
|
|
5736
6352
|
catalogs
|
|
@@ -5752,8 +6368,9 @@ async function userUpdateCommand(options) {
|
|
|
5752
6368
|
const role = options.role ?? existing.role;
|
|
5753
6369
|
const collectionAccess = options.collectionAccess ?? (options.role === "admin" ? [] : existing.collectionAccess);
|
|
5754
6370
|
const environmentAccess = options.environmentAccess ?? (options.role === "admin" ? [] : existing.environmentAccess);
|
|
6371
|
+
const snippetAccess = options.snippetAccess ?? (options.role === "admin" ? [] : existing.snippetAccess);
|
|
5755
6372
|
const access = mapValidationError(
|
|
5756
|
-
() => normalizeAccessForRole(role, collectionAccess, environmentAccess)
|
|
6373
|
+
() => normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess)
|
|
5757
6374
|
);
|
|
5758
6375
|
const llmAccess = role === "admin" ? false : options.llmAccess ?? existing.llmAccess;
|
|
5759
6376
|
const llmModels = role === "admin" ? [] : options.llmModels ?? existing.llmModels;
|
|
@@ -5764,6 +6381,7 @@ async function userUpdateCommand(options) {
|
|
|
5764
6381
|
role,
|
|
5765
6382
|
collectionAccess: options.collectionAccess,
|
|
5766
6383
|
environmentAccess: options.environmentAccess,
|
|
6384
|
+
snippetAccess: options.snippetAccess,
|
|
5767
6385
|
llmModels: options.llmModels
|
|
5768
6386
|
},
|
|
5769
6387
|
catalogs
|
|
@@ -5773,6 +6391,7 @@ async function userUpdateCommand(options) {
|
|
|
5773
6391
|
role: options.role,
|
|
5774
6392
|
collectionAccess: access.collectionAccess,
|
|
5775
6393
|
environmentAccess: access.environmentAccess,
|
|
6394
|
+
snippetAccess: access.snippetAccess,
|
|
5776
6395
|
llmAccess: llm.llmAccess,
|
|
5777
6396
|
llmModels: llm.llmModels,
|
|
5778
6397
|
llmMonthlyTokenLimit: options.llmMonthlyTokens !== void 0 ? options.llmMonthlyTokens : void 0
|
|
@@ -5856,6 +6475,11 @@ function registerUserCommand(program, handlers = {}) {
|
|
|
5856
6475
|
"Environment id or * (repeatable)",
|
|
5857
6476
|
parseAccessFlag,
|
|
5858
6477
|
[]
|
|
6478
|
+
).option(
|
|
6479
|
+
"--snippet-access <id>",
|
|
6480
|
+
"Snippet id or * (repeatable)",
|
|
6481
|
+
parseAccessFlag,
|
|
6482
|
+
[]
|
|
5859
6483
|
).option("--llm-access", "Enable hub-proxied LLM access for the user").option("--llm-model <id>", "LLM model id or * (repeatable)", parseAccessFlag, []).option("--llm-monthly-tokens <count>", "Monthly LLM token limit", parseMonthlyTokenLimit).action(
|
|
5860
6484
|
/**
|
|
5861
6485
|
* Runs the user create subcommand after merging global CLI options.
|
|
@@ -5890,6 +6514,11 @@ function registerUserCommand(program, handlers = {}) {
|
|
|
5890
6514
|
"Replacement environment id or * (repeatable)",
|
|
5891
6515
|
parseAccessFlag,
|
|
5892
6516
|
[]
|
|
6517
|
+
).option(
|
|
6518
|
+
"--snippet-access <id>",
|
|
6519
|
+
"Replacement snippet id or * (repeatable)",
|
|
6520
|
+
parseAccessFlag,
|
|
6521
|
+
[]
|
|
5893
6522
|
).option("--llm-access", "Enable hub-proxied LLM access for the user").option("--no-llm-access", "Disable hub-proxied LLM access for the user").option(
|
|
5894
6523
|
"--llm-model <id>",
|
|
5895
6524
|
"Replacement LLM model id or * (repeatable)",
|
|
@@ -5905,6 +6534,7 @@ function registerUserCommand(program, handlers = {}) {
|
|
|
5905
6534
|
...merged,
|
|
5906
6535
|
collectionAccess: (options.collectionAccess ?? []).length > 0 ? options.collectionAccess : void 0,
|
|
5907
6536
|
environmentAccess: (options.environmentAccess ?? []).length > 0 ? options.environmentAccess : void 0,
|
|
6537
|
+
snippetAccess: (options.snippetAccess ?? []).length > 0 ? options.snippetAccess : void 0,
|
|
5908
6538
|
llmModels: (() => {
|
|
5909
6539
|
const llmModels = readLlmModelsOption(options);
|
|
5910
6540
|
return llmModels.length > 0 ? llmModels : void 0;
|
|
@@ -6022,6 +6652,9 @@ function canListCollections(user) {
|
|
|
6022
6652
|
function canListEnvironments(user) {
|
|
6023
6653
|
return canUseDataApi(user) || canUseManagementApi(user);
|
|
6024
6654
|
}
|
|
6655
|
+
function canListSnippets(user) {
|
|
6656
|
+
return canUseDataApi(user) || canUseManagementApi(user);
|
|
6657
|
+
}
|
|
6025
6658
|
function hasWildcardAccess(access) {
|
|
6026
6659
|
return access.includes("*");
|
|
6027
6660
|
}
|
|
@@ -6043,6 +6676,15 @@ function canAccessEnvironment(user, environmentId) {
|
|
|
6043
6676
|
}
|
|
6044
6677
|
return user.environmentAccess.includes(environmentId);
|
|
6045
6678
|
}
|
|
6679
|
+
function canAccessSnippet(user, snippetId) {
|
|
6680
|
+
if (user.role === "admin") {
|
|
6681
|
+
return false;
|
|
6682
|
+
}
|
|
6683
|
+
if (hasWildcardAccess(user.snippetAccess)) {
|
|
6684
|
+
return true;
|
|
6685
|
+
}
|
|
6686
|
+
return user.snippetAccess.includes(snippetId);
|
|
6687
|
+
}
|
|
6046
6688
|
function canDeleteCollection(user, collection) {
|
|
6047
6689
|
return canUseDataApi(user) && canAccessCollection(user, collection.id) && collection.createdByUserId === user.id && !collection.deletionLocked;
|
|
6048
6690
|
}
|
|
@@ -6055,6 +6697,9 @@ function canCreateCollection(user) {
|
|
|
6055
6697
|
function canCreateEnvironment(user) {
|
|
6056
6698
|
return user.role === "user" && hasWildcardAccess(user.environmentAccess);
|
|
6057
6699
|
}
|
|
6700
|
+
function canCreateSnippet(user) {
|
|
6701
|
+
return user.role === "user" && hasWildcardAccess(user.snippetAccess);
|
|
6702
|
+
}
|
|
6058
6703
|
function filterAccessibleCollections(user, collections) {
|
|
6059
6704
|
if (user.role === "admin" || hasWildcardAccess(user.collectionAccess)) {
|
|
6060
6705
|
return collections;
|
|
@@ -6069,6 +6714,13 @@ function filterAccessibleEnvironments(user, environments) {
|
|
|
6069
6714
|
const allowed = new Set(user.environmentAccess);
|
|
6070
6715
|
return environments.filter((environment) => allowed.has(environment.id));
|
|
6071
6716
|
}
|
|
6717
|
+
function filterAccessibleSnippets(user, snippets) {
|
|
6718
|
+
if (user.role === "admin" || hasWildcardAccess(user.snippetAccess)) {
|
|
6719
|
+
return snippets;
|
|
6720
|
+
}
|
|
6721
|
+
const allowed = new Set(user.snippetAccess);
|
|
6722
|
+
return snippets.filter((snippet) => allowed.has(snippet.id));
|
|
6723
|
+
}
|
|
6072
6724
|
function canUseLlm(user) {
|
|
6073
6725
|
return user.role !== "admin" && user.llmAccess;
|
|
6074
6726
|
}
|
|
@@ -6297,12 +6949,18 @@ var updateAdminCollectionBodySchema = z8.object({
|
|
|
6297
6949
|
var updateAdminEnvironmentBodySchema = z8.object({
|
|
6298
6950
|
deletionLocked: z8.boolean()
|
|
6299
6951
|
});
|
|
6952
|
+
var updateAdminSnippetBodySchema = z8.object({
|
|
6953
|
+
deletionLocked: z8.boolean()
|
|
6954
|
+
});
|
|
6300
6955
|
var listAdminCollectionsResponseSchema = z8.object({
|
|
6301
6956
|
collections: z8.array(adminResourceOptionSchema)
|
|
6302
6957
|
});
|
|
6303
6958
|
var listAdminEnvironmentsResponseSchema = z8.object({
|
|
6304
6959
|
environments: z8.array(adminResourceOptionSchema)
|
|
6305
6960
|
});
|
|
6961
|
+
var listAdminSnippetsResponseSchema = z8.object({
|
|
6962
|
+
snippets: z8.array(adminResourceOptionSchema)
|
|
6963
|
+
});
|
|
6306
6964
|
var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
|
|
6307
6965
|
var hubUserRecordSchema = z8.object({
|
|
6308
6966
|
id: z8.string(),
|
|
@@ -6310,6 +6968,7 @@ var hubUserRecordSchema = z8.object({
|
|
|
6310
6968
|
role: userRoleSchema,
|
|
6311
6969
|
collectionAccess: z8.array(z8.string()),
|
|
6312
6970
|
environmentAccess: z8.array(z8.string()),
|
|
6971
|
+
snippetAccess: z8.array(z8.string()),
|
|
6313
6972
|
llmAccess: z8.boolean(),
|
|
6314
6973
|
llmModels: z8.array(z8.string()),
|
|
6315
6974
|
llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable(),
|
|
@@ -6327,6 +6986,7 @@ var updateAdminUserBodySchema = z8.object({
|
|
|
6327
6986
|
role: userRoleSchema.optional(),
|
|
6328
6987
|
collectionAccess: z8.array(z8.string()).optional(),
|
|
6329
6988
|
environmentAccess: z8.array(z8.string()).optional(),
|
|
6989
|
+
snippetAccess: z8.array(z8.string()).optional(),
|
|
6330
6990
|
llmAccess: z8.boolean().optional(),
|
|
6331
6991
|
llmModels: z8.array(z8.string()).optional(),
|
|
6332
6992
|
llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable().optional()
|
|
@@ -6336,6 +6996,7 @@ var createAdminUserBodySchema = z8.object({
|
|
|
6336
6996
|
role: userRoleSchema,
|
|
6337
6997
|
collectionAccess: z8.array(z8.string()).optional(),
|
|
6338
6998
|
environmentAccess: z8.array(z8.string()).optional(),
|
|
6999
|
+
snippetAccess: z8.array(z8.string()).optional(),
|
|
6339
7000
|
llmAccess: z8.boolean().optional(),
|
|
6340
7001
|
llmModels: z8.array(z8.string()).optional(),
|
|
6341
7002
|
llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable().optional()
|
|
@@ -6380,6 +7041,7 @@ function serializeHubUser(user) {
|
|
|
6380
7041
|
role: user.role,
|
|
6381
7042
|
collectionAccess: user.collectionAccess,
|
|
6382
7043
|
environmentAccess: user.environmentAccess,
|
|
7044
|
+
snippetAccess: user.snippetAccess,
|
|
6383
7045
|
llmAccess: user.llmAccess,
|
|
6384
7046
|
llmModels: user.llmModels,
|
|
6385
7047
|
llmMonthlyTokenLimit: user.llmMonthlyTokenLimit,
|
|
@@ -6425,6 +7087,19 @@ var environmentRecordSchema = z9.object({
|
|
|
6425
7087
|
updatedByUserId: z9.string().nullable(),
|
|
6426
7088
|
deletionLocked: z9.boolean()
|
|
6427
7089
|
});
|
|
7090
|
+
var snippetScopeSchema = z9.enum(["pre-request", "post-request", "any"]);
|
|
7091
|
+
var snippetRecordSchema = z9.object({
|
|
7092
|
+
id: z9.string(),
|
|
7093
|
+
name: z9.string(),
|
|
7094
|
+
code: z9.string(),
|
|
7095
|
+
scope: snippetScopeSchema,
|
|
7096
|
+
sortOrder: z9.number().int(),
|
|
7097
|
+
createdAt: timestampSchema,
|
|
7098
|
+
updatedAt: timestampSchema,
|
|
7099
|
+
createdByUserId: z9.string().nullable(),
|
|
7100
|
+
updatedByUserId: z9.string().nullable(),
|
|
7101
|
+
deletionLocked: z9.boolean()
|
|
7102
|
+
});
|
|
6428
7103
|
var folderRecordSchema = z9.object({
|
|
6429
7104
|
id: z9.string(),
|
|
6430
7105
|
collectionId: z9.string(),
|
|
@@ -6474,6 +7149,16 @@ var updateEnvironmentBodySchema = z9.object({
|
|
|
6474
7149
|
name: z9.string().trim().min(1),
|
|
6475
7150
|
variables: z9.array(variableSchema)
|
|
6476
7151
|
});
|
|
7152
|
+
var createSnippetBodySchema = z9.object({
|
|
7153
|
+
name: z9.string().trim().min(1),
|
|
7154
|
+
code: z9.string().default(""),
|
|
7155
|
+
scope: snippetScopeSchema.default("any")
|
|
7156
|
+
});
|
|
7157
|
+
var updateSnippetBodySchema = z9.object({
|
|
7158
|
+
name: z9.string().trim().min(1),
|
|
7159
|
+
code: z9.string(),
|
|
7160
|
+
scope: snippetScopeSchema
|
|
7161
|
+
});
|
|
6477
7162
|
var createFolderBodySchema = z9.object({
|
|
6478
7163
|
name: z9.string().trim().min(1)
|
|
6479
7164
|
});
|
|
@@ -6514,6 +7199,9 @@ var listCollectionsResponseSchema = z9.object({
|
|
|
6514
7199
|
var listEnvironmentsResponseSchema = z9.object({
|
|
6515
7200
|
environments: z9.array(environmentRecordSchema)
|
|
6516
7201
|
});
|
|
7202
|
+
var listSnippetsResponseSchema = z9.object({
|
|
7203
|
+
snippets: z9.array(snippetRecordSchema)
|
|
7204
|
+
});
|
|
6517
7205
|
var listFoldersResponseSchema = z9.object({
|
|
6518
7206
|
folders: z9.array(folderRecordSchema)
|
|
6519
7207
|
});
|
|
@@ -6535,6 +7223,13 @@ function serializeEnvironment(record) {
|
|
|
6535
7223
|
updatedAt: record.updatedAt.toISOString()
|
|
6536
7224
|
};
|
|
6537
7225
|
}
|
|
7226
|
+
function serializeSnippet(record) {
|
|
7227
|
+
return {
|
|
7228
|
+
...record,
|
|
7229
|
+
createdAt: record.createdAt.toISOString(),
|
|
7230
|
+
updatedAt: record.updatedAt.toISOString()
|
|
7231
|
+
};
|
|
7232
|
+
}
|
|
6538
7233
|
function serializeFolder(record) {
|
|
6539
7234
|
return {
|
|
6540
7235
|
...record,
|
|
@@ -6603,14 +7298,16 @@ async function registerAdminRoutes(app, options) {
|
|
|
6603
7298
|
}
|
|
6604
7299
|
const systemUserId = db.getSystemUserId();
|
|
6605
7300
|
const llm = getLlm();
|
|
6606
|
-
const [users, collections, environments] = await Promise.all([
|
|
7301
|
+
const [users, collections, environments, snippets] = await Promise.all([
|
|
6607
7302
|
db.listUsers(),
|
|
6608
7303
|
db.listCollections(),
|
|
6609
|
-
db.listEnvironments()
|
|
7304
|
+
db.listEnvironments(),
|
|
7305
|
+
db.listSnippets()
|
|
6610
7306
|
]);
|
|
6611
7307
|
const catalogs = buildAccessCatalogIds(
|
|
6612
7308
|
collections,
|
|
6613
7309
|
environments,
|
|
7310
|
+
snippets,
|
|
6614
7311
|
llm ? listHubOfferedModels(llm).map((model) => model.id) : null
|
|
6615
7312
|
);
|
|
6616
7313
|
return reply.send({
|
|
@@ -6620,6 +7317,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6620
7317
|
{
|
|
6621
7318
|
collectionAccess: record.collectionAccess,
|
|
6622
7319
|
environmentAccess: record.environmentAccess,
|
|
7320
|
+
snippetAccess: record.snippetAccess,
|
|
6623
7321
|
llmModels: record.llmModels
|
|
6624
7322
|
},
|
|
6625
7323
|
catalogs
|
|
@@ -6656,13 +7354,15 @@ async function registerAdminRoutes(app, options) {
|
|
|
6656
7354
|
}
|
|
6657
7355
|
const input = buildAdminUserCreateInput(request.body);
|
|
6658
7356
|
const llm = getLlm();
|
|
6659
|
-
const [collections, environments] = await Promise.all([
|
|
7357
|
+
const [collections, environments, snippets] = await Promise.all([
|
|
6660
7358
|
db.listCollections(),
|
|
6661
|
-
db.listEnvironments()
|
|
7359
|
+
db.listEnvironments(),
|
|
7360
|
+
db.listSnippets()
|
|
6662
7361
|
]);
|
|
6663
7362
|
const catalogs = buildAccessCatalogIds(
|
|
6664
7363
|
collections,
|
|
6665
7364
|
environments,
|
|
7365
|
+
snippets,
|
|
6666
7366
|
llm ? listHubOfferedModels(llm).map((model) => model.id) : null
|
|
6667
7367
|
);
|
|
6668
7368
|
validateSubmittedAccessLists(
|
|
@@ -6670,6 +7370,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
6670
7370
|
role: request.body.role,
|
|
6671
7371
|
collectionAccess: request.body.collectionAccess,
|
|
6672
7372
|
environmentAccess: request.body.environmentAccess,
|
|
7373
|
+
snippetAccess: request.body.snippetAccess,
|
|
6673
7374
|
llmModels: request.body.llmModels
|
|
6674
7375
|
},
|
|
6675
7376
|
catalogs
|
|
@@ -6832,6 +7533,40 @@ async function registerAdminRoutes(app, options) {
|
|
|
6832
7533
|
}
|
|
6833
7534
|
}
|
|
6834
7535
|
});
|
|
7536
|
+
routes.route({
|
|
7537
|
+
method: "GET",
|
|
7538
|
+
url: "/admin/snippets",
|
|
7539
|
+
schema: {
|
|
7540
|
+
response: {
|
|
7541
|
+
200: listAdminSnippetsResponseSchema,
|
|
7542
|
+
403: errorResponseSchema
|
|
7543
|
+
}
|
|
7544
|
+
},
|
|
7545
|
+
/**
|
|
7546
|
+
* Lists all snippets for operator user management.
|
|
7547
|
+
*/
|
|
7548
|
+
handler: async (request, reply) => {
|
|
7549
|
+
try {
|
|
7550
|
+
const user = requireAuthenticatedUser(request);
|
|
7551
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
7552
|
+
return;
|
|
7553
|
+
}
|
|
7554
|
+
const snippets = await db.listSnippets();
|
|
7555
|
+
return reply.send({
|
|
7556
|
+
snippets: snippets.map((snippet) => ({
|
|
7557
|
+
id: snippet.id,
|
|
7558
|
+
name: snippet.name,
|
|
7559
|
+
deletionLocked: snippet.deletionLocked
|
|
7560
|
+
}))
|
|
7561
|
+
});
|
|
7562
|
+
} catch (error) {
|
|
7563
|
+
if (handleDbError(reply, error)) {
|
|
7564
|
+
return;
|
|
7565
|
+
}
|
|
7566
|
+
throw error;
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
});
|
|
6835
7570
|
routes.route({
|
|
6836
7571
|
method: "DELETE",
|
|
6837
7572
|
url: "/admin/collections/:id",
|
|
@@ -6902,6 +7637,80 @@ async function registerAdminRoutes(app, options) {
|
|
|
6902
7637
|
}
|
|
6903
7638
|
}
|
|
6904
7639
|
});
|
|
7640
|
+
routes.route({
|
|
7641
|
+
method: "DELETE",
|
|
7642
|
+
url: "/admin/snippets/:id",
|
|
7643
|
+
schema: {
|
|
7644
|
+
params: idParamSchema,
|
|
7645
|
+
response: {
|
|
7646
|
+
204: emptyResponseSchema,
|
|
7647
|
+
403: errorResponseSchema,
|
|
7648
|
+
404: errorResponseSchema
|
|
7649
|
+
}
|
|
7650
|
+
},
|
|
7651
|
+
/**
|
|
7652
|
+
* Deletes a snippet regardless of deletion lock state.
|
|
7653
|
+
*/
|
|
7654
|
+
handler: async (request, reply) => {
|
|
7655
|
+
try {
|
|
7656
|
+
const user = requireAuthenticatedUser(request);
|
|
7657
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
7658
|
+
return;
|
|
7659
|
+
}
|
|
7660
|
+
const snippet = await db.findSnippetById(request.params.id);
|
|
7661
|
+
if (!snippet) {
|
|
7662
|
+
void reply.code(404).send({ error: "Snippet not found" });
|
|
7663
|
+
return;
|
|
7664
|
+
}
|
|
7665
|
+
await db.deleteSnippet(request.params.id, user.id);
|
|
7666
|
+
return reply.code(204).send(null);
|
|
7667
|
+
} catch (error) {
|
|
7668
|
+
if (handleDbError(reply, error)) {
|
|
7669
|
+
return;
|
|
7670
|
+
}
|
|
7671
|
+
throw error;
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
});
|
|
7675
|
+
routes.route({
|
|
7676
|
+
method: "PUT",
|
|
7677
|
+
url: "/admin/snippets/:id",
|
|
7678
|
+
schema: {
|
|
7679
|
+
params: idParamSchema,
|
|
7680
|
+
body: updateAdminSnippetBodySchema,
|
|
7681
|
+
response: {
|
|
7682
|
+
200: adminEntityConfigSchema,
|
|
7683
|
+
403: errorResponseSchema,
|
|
7684
|
+
404: errorResponseSchema
|
|
7685
|
+
}
|
|
7686
|
+
},
|
|
7687
|
+
/**
|
|
7688
|
+
* Updates admin configuration for a snippet.
|
|
7689
|
+
*/
|
|
7690
|
+
handler: async (request, reply) => {
|
|
7691
|
+
try {
|
|
7692
|
+
const user = requireAuthenticatedUser(request);
|
|
7693
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
const snippet = await db.setSnippetDeletionLocked(
|
|
7697
|
+
request.params.id,
|
|
7698
|
+
request.body.deletionLocked,
|
|
7699
|
+
user.id
|
|
7700
|
+
);
|
|
7701
|
+
return reply.send({
|
|
7702
|
+
id: snippet.id,
|
|
7703
|
+
name: snippet.name,
|
|
7704
|
+
deletionLocked: snippet.deletionLocked
|
|
7705
|
+
});
|
|
7706
|
+
} catch (error) {
|
|
7707
|
+
if (handleDbError(reply, error)) {
|
|
7708
|
+
return;
|
|
7709
|
+
}
|
|
7710
|
+
throw error;
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
});
|
|
6905
7714
|
routes.route({
|
|
6906
7715
|
method: "PUT",
|
|
6907
7716
|
url: "/admin/collections/:id",
|
|
@@ -7082,13 +7891,15 @@ async function registerAdminRoutes(app, options) {
|
|
|
7082
7891
|
const input = buildAdminUserUpdateInput(existing, request.body);
|
|
7083
7892
|
const role = request.body.role ?? existing.role;
|
|
7084
7893
|
const llm = getLlm();
|
|
7085
|
-
const [collections, environments] = await Promise.all([
|
|
7894
|
+
const [collections, environments, snippets] = await Promise.all([
|
|
7086
7895
|
db.listCollections(),
|
|
7087
|
-
db.listEnvironments()
|
|
7896
|
+
db.listEnvironments(),
|
|
7897
|
+
db.listSnippets()
|
|
7088
7898
|
]);
|
|
7089
7899
|
const catalogs = buildAccessCatalogIds(
|
|
7090
7900
|
collections,
|
|
7091
7901
|
environments,
|
|
7902
|
+
snippets,
|
|
7092
7903
|
llm ? listHubOfferedModels(llm).map((model) => model.id) : null
|
|
7093
7904
|
);
|
|
7094
7905
|
validateSubmittedAccessLists(
|
|
@@ -7096,6 +7907,7 @@ async function registerAdminRoutes(app, options) {
|
|
|
7096
7907
|
role,
|
|
7097
7908
|
collectionAccess: request.body.collectionAccess,
|
|
7098
7909
|
environmentAccess: request.body.environmentAccess,
|
|
7910
|
+
snippetAccess: request.body.snippetAccess,
|
|
7099
7911
|
llmModels: request.body.llmModels
|
|
7100
7912
|
},
|
|
7101
7913
|
catalogs
|
|
@@ -7632,6 +8444,150 @@ async function registerEnvironmentRoutes(app, db) {
|
|
|
7632
8444
|
});
|
|
7633
8445
|
}
|
|
7634
8446
|
|
|
8447
|
+
// src/server/routes/snippets.ts
|
|
8448
|
+
async function registerSnippetRoutes(app, db) {
|
|
8449
|
+
const routes = app.withTypeProvider();
|
|
8450
|
+
routes.route({
|
|
8451
|
+
method: "GET",
|
|
8452
|
+
url: "/snippets",
|
|
8453
|
+
schema: {
|
|
8454
|
+
response: {
|
|
8455
|
+
200: listSnippetsResponseSchema
|
|
8456
|
+
}
|
|
8457
|
+
},
|
|
8458
|
+
/**
|
|
8459
|
+
* Lists all snippets ordered by sort order then name.
|
|
8460
|
+
*/
|
|
8461
|
+
handler: async (request, reply) => {
|
|
8462
|
+
try {
|
|
8463
|
+
const user = requireAuthenticatedUser(request);
|
|
8464
|
+
if (denyUnlessAllowed(reply, canListSnippets(user))) {
|
|
8465
|
+
return;
|
|
8466
|
+
}
|
|
8467
|
+
const snippets = await db.listSnippets();
|
|
8468
|
+
return reply.send({
|
|
8469
|
+
snippets: filterAccessibleSnippets(user, snippets).map(
|
|
8470
|
+
(snippet) => serializeSnippet(snippet)
|
|
8471
|
+
)
|
|
8472
|
+
});
|
|
8473
|
+
} catch (error) {
|
|
8474
|
+
if (handleDbError(reply, error)) {
|
|
8475
|
+
return;
|
|
8476
|
+
}
|
|
8477
|
+
throw error;
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
});
|
|
8481
|
+
routes.route({
|
|
8482
|
+
method: "POST",
|
|
8483
|
+
url: "/snippets",
|
|
8484
|
+
schema: {
|
|
8485
|
+
body: createSnippetBodySchema,
|
|
8486
|
+
response: {
|
|
8487
|
+
200: snippetRecordSchema,
|
|
8488
|
+
400: errorResponseSchema
|
|
8489
|
+
}
|
|
8490
|
+
},
|
|
8491
|
+
/**
|
|
8492
|
+
* Creates a new snippet with the given display name and content.
|
|
8493
|
+
*/
|
|
8494
|
+
handler: async (request, reply) => {
|
|
8495
|
+
try {
|
|
8496
|
+
const user = requireAuthenticatedUser(request);
|
|
8497
|
+
if (denyUnlessAllowed(reply, canUseDataApi(user) && canCreateSnippet(user))) {
|
|
8498
|
+
return;
|
|
8499
|
+
}
|
|
8500
|
+
const snippet = await db.createSnippet(
|
|
8501
|
+
request.body.name,
|
|
8502
|
+
request.body.code,
|
|
8503
|
+
request.body.scope,
|
|
8504
|
+
user.id
|
|
8505
|
+
);
|
|
8506
|
+
return reply.send(serializeSnippet(snippet));
|
|
8507
|
+
} catch (error) {
|
|
8508
|
+
if (handleDbError(reply, error)) {
|
|
8509
|
+
return;
|
|
8510
|
+
}
|
|
8511
|
+
throw error;
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
});
|
|
8515
|
+
routes.route({
|
|
8516
|
+
method: "PUT",
|
|
8517
|
+
url: "/snippets/:id",
|
|
8518
|
+
schema: {
|
|
8519
|
+
params: idParamSchema,
|
|
8520
|
+
body: updateSnippetBodySchema,
|
|
8521
|
+
response: {
|
|
8522
|
+
200: snippetRecordSchema,
|
|
8523
|
+
400: errorResponseSchema,
|
|
8524
|
+
404: errorResponseSchema
|
|
8525
|
+
}
|
|
8526
|
+
},
|
|
8527
|
+
/**
|
|
8528
|
+
* Updates a snippet's name, code, and scope. Sort order is preserved.
|
|
8529
|
+
*/
|
|
8530
|
+
handler: async (request, reply) => {
|
|
8531
|
+
try {
|
|
8532
|
+
const user = requireAuthenticatedUser(request);
|
|
8533
|
+
if (denyUnlessAllowed(reply, canUseDataApi(user) && canAccessSnippet(user, request.params.id))) {
|
|
8534
|
+
return;
|
|
8535
|
+
}
|
|
8536
|
+
const snippet = await db.updateSnippet(
|
|
8537
|
+
request.params.id,
|
|
8538
|
+
request.body.name,
|
|
8539
|
+
request.body.code,
|
|
8540
|
+
request.body.scope,
|
|
8541
|
+
user.id
|
|
8542
|
+
);
|
|
8543
|
+
return reply.send(serializeSnippet(snippet));
|
|
8544
|
+
} catch (error) {
|
|
8545
|
+
if (handleDbError(reply, error)) {
|
|
8546
|
+
return;
|
|
8547
|
+
}
|
|
8548
|
+
throw error;
|
|
8549
|
+
}
|
|
8550
|
+
}
|
|
8551
|
+
});
|
|
8552
|
+
routes.route({
|
|
8553
|
+
method: "DELETE",
|
|
8554
|
+
url: "/snippets/:id",
|
|
8555
|
+
schema: {
|
|
8556
|
+
params: idParamSchema,
|
|
8557
|
+
response: {
|
|
8558
|
+
204: emptyResponseSchema,
|
|
8559
|
+
404: errorResponseSchema
|
|
8560
|
+
}
|
|
8561
|
+
},
|
|
8562
|
+
/**
|
|
8563
|
+
* Deletes a snippet by id.
|
|
8564
|
+
*/
|
|
8565
|
+
handler: async (request, reply) => {
|
|
8566
|
+
try {
|
|
8567
|
+
const user = requireAuthenticatedUser(request);
|
|
8568
|
+
if (denyUnlessAllowed(reply, canUseDataApi(user) && canAccessSnippet(user, request.params.id))) {
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
8571
|
+
const snippet = await db.findSnippetById(request.params.id);
|
|
8572
|
+
if (!snippet) {
|
|
8573
|
+
void reply.code(404).send({ error: "Snippet not found" });
|
|
8574
|
+
return;
|
|
8575
|
+
}
|
|
8576
|
+
if (snippet.deletionLocked) {
|
|
8577
|
+
throw new DeletionLockedError("snippet");
|
|
8578
|
+
}
|
|
8579
|
+
await db.deleteSnippet(request.params.id, user.id);
|
|
8580
|
+
return reply.code(204).send(null);
|
|
8581
|
+
} catch (error) {
|
|
8582
|
+
if (handleDbError(reply, error)) {
|
|
8583
|
+
return;
|
|
8584
|
+
}
|
|
8585
|
+
throw error;
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
});
|
|
8589
|
+
}
|
|
8590
|
+
|
|
7635
8591
|
// src/server/routes/folders.ts
|
|
7636
8592
|
async function registerFolderRoutes(app, db) {
|
|
7637
8593
|
const routes = app.withTypeProvider();
|
|
@@ -8216,6 +9172,258 @@ async function runLlmCompletion(config, input) {
|
|
|
8216
9172
|
return parseCompletionResponse(json);
|
|
8217
9173
|
}
|
|
8218
9174
|
|
|
9175
|
+
// src/server/llm/hubMcpToolNames.ts
|
|
9176
|
+
var HUB_MCP_TOOL_PREFIX = "hubmcp__";
|
|
9177
|
+
function encodeHubMcpToolName(serverIndex, toolName) {
|
|
9178
|
+
return `${HUB_MCP_TOOL_PREFIX}${serverIndex}__${toolName}`;
|
|
9179
|
+
}
|
|
9180
|
+
function decodeHubMcpToolName(prefixed) {
|
|
9181
|
+
if (!prefixed.startsWith(HUB_MCP_TOOL_PREFIX)) {
|
|
9182
|
+
return null;
|
|
9183
|
+
}
|
|
9184
|
+
const rest = prefixed.slice(HUB_MCP_TOOL_PREFIX.length);
|
|
9185
|
+
const separatorIndex = rest.indexOf("__");
|
|
9186
|
+
if (separatorIndex <= 0) {
|
|
9187
|
+
return null;
|
|
9188
|
+
}
|
|
9189
|
+
const serverIndex = Number.parseInt(rest.slice(0, separatorIndex), 10);
|
|
9190
|
+
if (!Number.isInteger(serverIndex) || serverIndex < 0) {
|
|
9191
|
+
return null;
|
|
9192
|
+
}
|
|
9193
|
+
const toolName = rest.slice(separatorIndex + 2);
|
|
9194
|
+
if (!toolName) {
|
|
9195
|
+
return null;
|
|
9196
|
+
}
|
|
9197
|
+
return { serverIndex, toolName };
|
|
9198
|
+
}
|
|
9199
|
+
function isHubMcpToolName(name) {
|
|
9200
|
+
return decodeHubMcpToolName(name) !== null;
|
|
9201
|
+
}
|
|
9202
|
+
|
|
9203
|
+
// src/server/llm/mcpClient.ts
|
|
9204
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9205
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9206
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9207
|
+
var connectedClients = /* @__PURE__ */ new Map();
|
|
9208
|
+
var activeConfigSignature = "";
|
|
9209
|
+
function buildRequestHeaders(headers) {
|
|
9210
|
+
const result = {};
|
|
9211
|
+
for (const row of headers) {
|
|
9212
|
+
if (row.key.trim()) {
|
|
9213
|
+
result[row.key.trim()] = row.value;
|
|
9214
|
+
}
|
|
9215
|
+
}
|
|
9216
|
+
return result;
|
|
9217
|
+
}
|
|
9218
|
+
function buildMcpConfigSignature(config) {
|
|
9219
|
+
return JSON.stringify(config.mcp ?? []);
|
|
9220
|
+
}
|
|
9221
|
+
function flattenMcpToolContent(content) {
|
|
9222
|
+
if (!Array.isArray(content)) {
|
|
9223
|
+
return JSON.stringify(content ?? null);
|
|
9224
|
+
}
|
|
9225
|
+
const parts = content.map((item) => {
|
|
9226
|
+
if (!item || typeof item !== "object") {
|
|
9227
|
+
return String(item);
|
|
9228
|
+
}
|
|
9229
|
+
const record = item;
|
|
9230
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
9231
|
+
return record.text;
|
|
9232
|
+
}
|
|
9233
|
+
if (record.type === "resource" && typeof record.uri === "string") {
|
|
9234
|
+
return record.uri;
|
|
9235
|
+
}
|
|
9236
|
+
return JSON.stringify(record);
|
|
9237
|
+
});
|
|
9238
|
+
return parts.join("\n");
|
|
9239
|
+
}
|
|
9240
|
+
function toOpenAiTool(serverIndex, tool) {
|
|
9241
|
+
return {
|
|
9242
|
+
type: "function",
|
|
9243
|
+
function: {
|
|
9244
|
+
name: encodeHubMcpToolName(serverIndex, tool.name),
|
|
9245
|
+
description: tool.description ?? `MCP tool ${tool.name}`,
|
|
9246
|
+
parameters: tool.inputSchema ?? {
|
|
9247
|
+
type: "object",
|
|
9248
|
+
properties: {},
|
|
9249
|
+
additionalProperties: true
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
};
|
|
9253
|
+
}
|
|
9254
|
+
async function connectHubMcpServer(serverIndex, server) {
|
|
9255
|
+
const headers = buildRequestHeaders(server.headers);
|
|
9256
|
+
const url = new URL(server.url);
|
|
9257
|
+
const client = new Client(
|
|
9258
|
+
{
|
|
9259
|
+
name: "team-hub",
|
|
9260
|
+
version: "1.0.0"
|
|
9261
|
+
},
|
|
9262
|
+
{}
|
|
9263
|
+
);
|
|
9264
|
+
let transport;
|
|
9265
|
+
try {
|
|
9266
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
9267
|
+
requestInit: { headers }
|
|
9268
|
+
});
|
|
9269
|
+
await client.connect(transport);
|
|
9270
|
+
} catch {
|
|
9271
|
+
transport = new SSEClientTransport(url, {
|
|
9272
|
+
requestInit: { headers }
|
|
9273
|
+
});
|
|
9274
|
+
await client.connect(transport);
|
|
9275
|
+
}
|
|
9276
|
+
const { tools } = await client.listTools();
|
|
9277
|
+
return {
|
|
9278
|
+
serverIndex,
|
|
9279
|
+
client,
|
|
9280
|
+
remoteTools: tools
|
|
9281
|
+
};
|
|
9282
|
+
}
|
|
9283
|
+
async function ensureHubMcpConnections(config) {
|
|
9284
|
+
const servers = config.mcp ?? [];
|
|
9285
|
+
const signature = buildMcpConfigSignature(config);
|
|
9286
|
+
if (signature !== activeConfigSignature) {
|
|
9287
|
+
await disposeHubMcpConnections();
|
|
9288
|
+
activeConfigSignature = signature;
|
|
9289
|
+
}
|
|
9290
|
+
if (servers.length === 0) {
|
|
9291
|
+
return;
|
|
9292
|
+
}
|
|
9293
|
+
for (const [index, server] of servers.entries()) {
|
|
9294
|
+
if (connectedClients.has(index)) {
|
|
9295
|
+
continue;
|
|
9296
|
+
}
|
|
9297
|
+
try {
|
|
9298
|
+
const connected = await connectHubMcpServer(index, server);
|
|
9299
|
+
connectedClients.set(index, connected);
|
|
9300
|
+
} catch (error) {
|
|
9301
|
+
const message = error instanceof Error ? error.message : "Connection failed";
|
|
9302
|
+
console.warn(`Hub MCP server "${server.name}" failed to connect: ${message}`);
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
}
|
|
9306
|
+
function listHubMcpTools() {
|
|
9307
|
+
const tools = [];
|
|
9308
|
+
for (const entry of connectedClients.values()) {
|
|
9309
|
+
for (const tool of entry.remoteTools) {
|
|
9310
|
+
tools.push(toOpenAiTool(entry.serverIndex, tool));
|
|
9311
|
+
}
|
|
9312
|
+
}
|
|
9313
|
+
return tools;
|
|
9314
|
+
}
|
|
9315
|
+
async function callHubMcpTool(prefixedName, args) {
|
|
9316
|
+
const decoded = decodeHubMcpToolName(prefixedName);
|
|
9317
|
+
if (!decoded) {
|
|
9318
|
+
return JSON.stringify({ error: `Unknown hub MCP tool: ${prefixedName}` });
|
|
9319
|
+
}
|
|
9320
|
+
const entry = connectedClients.get(decoded.serverIndex);
|
|
9321
|
+
if (!entry) {
|
|
9322
|
+
return JSON.stringify({ error: `Hub MCP server ${decoded.serverIndex} is not connected.` });
|
|
9323
|
+
}
|
|
9324
|
+
try {
|
|
9325
|
+
const result = await entry.client.callTool({
|
|
9326
|
+
name: decoded.toolName,
|
|
9327
|
+
arguments: args ?? {}
|
|
9328
|
+
});
|
|
9329
|
+
if (result.isError) {
|
|
9330
|
+
return JSON.stringify({
|
|
9331
|
+
error: flattenMcpToolContent(result.content)
|
|
9332
|
+
});
|
|
9333
|
+
}
|
|
9334
|
+
return flattenMcpToolContent(result.content);
|
|
9335
|
+
} catch (error) {
|
|
9336
|
+
const message = error instanceof Error ? error.message : "MCP tool execution failed.";
|
|
9337
|
+
return JSON.stringify({ error: message });
|
|
9338
|
+
}
|
|
9339
|
+
}
|
|
9340
|
+
async function disposeHubMcpConnections() {
|
|
9341
|
+
const closePromises = [...connectedClients.values()].map(async (entry) => {
|
|
9342
|
+
try {
|
|
9343
|
+
await entry.client.close();
|
|
9344
|
+
} catch {
|
|
9345
|
+
}
|
|
9346
|
+
});
|
|
9347
|
+
connectedClients.clear();
|
|
9348
|
+
activeConfigSignature = "";
|
|
9349
|
+
await Promise.allSettled(closePromises);
|
|
9350
|
+
}
|
|
9351
|
+
|
|
9352
|
+
// src/server/llm/agent.ts
|
|
9353
|
+
var HUB_CHAT_STEP_MAX_ITERATIONS = 8;
|
|
9354
|
+
function addUsage(current, next) {
|
|
9355
|
+
return {
|
|
9356
|
+
promptTokens: current.promptTokens + next.promptTokens,
|
|
9357
|
+
completionTokens: current.completionTokens + next.completionTokens,
|
|
9358
|
+
totalTokens: current.totalTokens + next.totalTokens
|
|
9359
|
+
};
|
|
9360
|
+
}
|
|
9361
|
+
function parseToolArguments(raw) {
|
|
9362
|
+
try {
|
|
9363
|
+
return JSON.parse(raw);
|
|
9364
|
+
} catch {
|
|
9365
|
+
return {};
|
|
9366
|
+
}
|
|
9367
|
+
}
|
|
9368
|
+
async function runHubChatStep(config, input, deps = {}) {
|
|
9369
|
+
const runCompletion = deps.runCompletion ?? runLlmCompletion;
|
|
9370
|
+
const ensureConnections = deps.ensureConnections ?? ensureHubMcpConnections;
|
|
9371
|
+
const listTools = deps.listTools ?? listHubMcpTools;
|
|
9372
|
+
const callTool = deps.callTool ?? callHubMcpTool;
|
|
9373
|
+
await ensureConnections(config);
|
|
9374
|
+
const hubTools = listTools();
|
|
9375
|
+
const mergedTools = hubTools.length > 0 || (input.tools?.length ?? 0) > 0 ? [...hubTools, ...input.tools ?? []] : void 0;
|
|
9376
|
+
let messages = [...input.messages];
|
|
9377
|
+
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
9378
|
+
let lastContent = null;
|
|
9379
|
+
for (let iteration = 0; iteration < HUB_CHAT_STEP_MAX_ITERATIONS; iteration += 1) {
|
|
9380
|
+
const result = await runCompletion(config, {
|
|
9381
|
+
model: input.model,
|
|
9382
|
+
messages,
|
|
9383
|
+
systemPrompt: input.systemPrompt,
|
|
9384
|
+
tools: mergedTools
|
|
9385
|
+
});
|
|
9386
|
+
usage = addUsage(usage, result.usage);
|
|
9387
|
+
lastContent = result.content;
|
|
9388
|
+
const toolCalls = result.toolCalls ?? [];
|
|
9389
|
+
if (toolCalls.length === 0) {
|
|
9390
|
+
return {
|
|
9391
|
+
content: result.content,
|
|
9392
|
+
usage
|
|
9393
|
+
};
|
|
9394
|
+
}
|
|
9395
|
+
const hubCalls = toolCalls.filter((call) => isHubMcpToolName(call.name));
|
|
9396
|
+
const passthroughCalls = toolCalls.filter((call) => !isHubMcpToolName(call.name));
|
|
9397
|
+
if (passthroughCalls.length > 0) {
|
|
9398
|
+
return {
|
|
9399
|
+
content: result.content,
|
|
9400
|
+
toolCalls: passthroughCalls,
|
|
9401
|
+
usage
|
|
9402
|
+
};
|
|
9403
|
+
}
|
|
9404
|
+
messages = [
|
|
9405
|
+
...messages,
|
|
9406
|
+
{
|
|
9407
|
+
role: "assistant",
|
|
9408
|
+
content: result.content,
|
|
9409
|
+
tool_calls: hubCalls
|
|
9410
|
+
}
|
|
9411
|
+
];
|
|
9412
|
+
for (const call of hubCalls) {
|
|
9413
|
+
const toolResult = await callTool(call.name, parseToolArguments(call.arguments));
|
|
9414
|
+
messages.push({
|
|
9415
|
+
role: "tool",
|
|
9416
|
+
tool_call_id: call.id,
|
|
9417
|
+
content: toolResult
|
|
9418
|
+
});
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
return {
|
|
9422
|
+
content: lastContent,
|
|
9423
|
+
usage
|
|
9424
|
+
};
|
|
9425
|
+
}
|
|
9426
|
+
|
|
8219
9427
|
// src/server/routes/llm.ts
|
|
8220
9428
|
function sendMonthlyLimitExceeded(reply) {
|
|
8221
9429
|
return reply.code(402).send({
|
|
@@ -8333,7 +9541,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
8333
9541
|
if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
|
|
8334
9542
|
return sendMonthlyLimitExceeded(reply);
|
|
8335
9543
|
}
|
|
8336
|
-
const result = await
|
|
9544
|
+
const result = await runHubChatStep(llm, {
|
|
8337
9545
|
model,
|
|
8338
9546
|
messages,
|
|
8339
9547
|
tools,
|
|
@@ -8481,6 +9689,7 @@ async function registerProtectedRoutes(app, options) {
|
|
|
8481
9689
|
});
|
|
8482
9690
|
await registerCollectionRoutes(app, options.db);
|
|
8483
9691
|
await registerEnvironmentRoutes(app, options.db);
|
|
9692
|
+
await registerSnippetRoutes(app, options.db);
|
|
8484
9693
|
await registerFolderRoutes(app, options.db);
|
|
8485
9694
|
await registerRequestRoutes(app, options.db);
|
|
8486
9695
|
await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
|
|
@@ -8825,6 +10034,7 @@ function registerGracefulShutdown(app, ctx) {
|
|
|
8825
10034
|
const shutdown = async (signal) => {
|
|
8826
10035
|
app.log.info(`Received ${signal}, shutting down.`);
|
|
8827
10036
|
await app.close();
|
|
10037
|
+
await disposeHubMcpConnections();
|
|
8828
10038
|
await disconnectAll(ctx);
|
|
8829
10039
|
process.exit(0);
|
|
8830
10040
|
};
|