@harborclient/team-hub 0.3.0 → 0.4.1

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.
Files changed (3) hide show
  1. package/dist/cli.js +1239 -273
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -358,6 +358,7 @@ var USERS_COLLECTION = "users";
358
358
  var API_TOKENS_COLLECTION = "apiTokens";
359
359
  var COLLECTIONS_COLLECTION = "collections";
360
360
  var ENVIRONMENTS_COLLECTION = "environments";
361
+ var SNIPPETS_COLLECTION = "snippets";
361
362
  var FOLDERS_COLLECTION = "folders";
362
363
  var REQUESTS_COLLECTION = "requests";
363
364
  var AUDIT_LOG_COLLECTION = "auditLog";
@@ -384,7 +385,8 @@ function createSystemUserInput() {
384
385
  name: SYSTEM_USER_NAME,
385
386
  role: "admin",
386
387
  collectionAccess: [],
387
- environmentAccess: []
388
+ environmentAccess: [],
389
+ snippetAccess: []
388
390
  };
389
391
  }
390
392
 
@@ -398,7 +400,7 @@ var firestoreConfigSchema = z2.object({
398
400
 
399
401
  // src/db/firestore/utils.ts
400
402
  function parseAuditEntityType(value) {
401
- 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") {
402
404
  return value;
403
405
  }
404
406
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -427,6 +429,7 @@ function mapFirestoreUser(id, data) {
427
429
  role: data.role,
428
430
  collectionAccess: data.collectionAccess,
429
431
  environmentAccess: data.environmentAccess,
432
+ snippetAccess: data.snippetAccess ?? [],
430
433
  llmAccess: data.llmAccess ?? false,
431
434
  llmModels: data.llmModels ?? [],
432
435
  llmMonthlyTokenLimit: data.llmMonthlyTokenLimit ?? null,
@@ -492,6 +495,20 @@ function mapFirestoreEnvironment(id, data) {
492
495
  deletionLocked: data.deletionLocked ?? false
493
496
  };
494
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
+ }
495
512
  function mapFirestoreFolder(id, data) {
496
513
  return {
497
514
  id,
@@ -699,6 +716,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
699
716
  async migrate() {
700
717
  await this.ensureSystemUser();
701
718
  await this.migrateOrphanTokensToBootstrapUser();
719
+ await this.migrateSnippetAccessBackfill();
702
720
  }
703
721
  /**
704
722
  * Returns the stable identifier of the internal system user, when provisioned.
@@ -745,6 +763,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
745
763
  role: input.role,
746
764
  collectionAccess: input.collectionAccess,
747
765
  environmentAccess: input.environmentAccess,
766
+ snippetAccess: input.snippetAccess,
748
767
  llmAccess: input.llmAccess ?? false,
749
768
  llmModels: input.llmModels ?? [],
750
769
  llmMonthlyTokenLimit: input.llmMonthlyTokenLimit ?? null,
@@ -816,6 +835,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
816
835
  const role = input.role ?? existing.role;
817
836
  const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
818
837
  const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
838
+ const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
819
839
  const llmAccess = input.llmAccess ?? existing.llmAccess;
820
840
  const llmModels = input.llmModels ?? existing.llmModels;
821
841
  const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
@@ -825,6 +845,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
825
845
  role,
826
846
  collectionAccess,
827
847
  environmentAccess,
848
+ snippetAccess,
828
849
  llmAccess,
829
850
  llmModels,
830
851
  llmMonthlyTokenLimit,
@@ -879,7 +900,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
879
900
  name: BOOTSTRAP_USER_NAME,
880
901
  role: "user",
881
902
  collectionAccess: ["*"],
882
- environmentAccess: ["*"]
903
+ environmentAccess: ["*"],
904
+ snippetAccess: ["*"]
883
905
  },
884
906
  systemUserId
885
907
  );
@@ -893,6 +915,37 @@ var FirestoreDatabase = class _FirestoreDatabase {
893
915
  await batch.commit();
894
916
  }
895
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
+ }
896
949
  /**
897
950
  * Inserts a new API token document.
898
951
  *
@@ -1250,6 +1303,129 @@ var FirestoreDatabase = class _FirestoreDatabase {
1250
1303
  updatedByUserId: actingUserId
1251
1304
  });
1252
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
+ }
1253
1429
  /**
1254
1430
  * Lists all saved requests in a collection.
1255
1431
  *
@@ -1716,6 +1892,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1716
1892
  role: input.role,
1717
1893
  collectionAccess: input.collectionAccess,
1718
1894
  environmentAccess: input.environmentAccess,
1895
+ snippetAccess: input.snippetAccess,
1719
1896
  llmAccess: false,
1720
1897
  llmModels: [],
1721
1898
  llmMonthlyTokenLimit: null,
@@ -1799,7 +1976,7 @@ function parseAuditAction(value) {
1799
1976
  throw new Error(`Invalid audit action: ${value}`);
1800
1977
  }
1801
1978
  function parseAuditEntityType2(value) {
1802
- 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") {
1803
1980
  return value;
1804
1981
  }
1805
1982
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -1876,6 +2053,26 @@ function mapEnvironmentSqlRow(row) {
1876
2053
  deletionLocked: Boolean(row.deletion_locked)
1877
2054
  };
1878
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
+ }
1879
2076
  function mapFolderSqlRow(row) {
1880
2077
  return {
1881
2078
  id: row.id,
@@ -1960,6 +2157,22 @@ CREATE TABLE IF NOT EXISTS environments (
1960
2157
  FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
1961
2158
  )
1962
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();
1963
2176
  var FOLDERS_MIGRATION_SQL = `
1964
2177
  CREATE TABLE IF NOT EXISTS folders (
1965
2178
  id VARCHAR(36) PRIMARY KEY,
@@ -2009,6 +2222,7 @@ CREATE TABLE IF NOT EXISTS users (
2009
2222
  role VARCHAR(16) NOT NULL,
2010
2223
  collection_access LONGTEXT NOT NULL,
2011
2224
  environment_access LONGTEXT NOT NULL,
2225
+ snippet_access LONGTEXT NOT NULL,
2012
2226
  created_at DATETIME NOT NULL,
2013
2227
  updated_at DATETIME NOT NULL,
2014
2228
  created_by_user_id VARCHAR(36) NULL,
@@ -2124,11 +2338,23 @@ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL = `
2124
2338
  ALTER TABLE environments
2125
2339
  ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
2126
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();
2127
2352
  var MYSQL_MIGRATIONS = [
2128
2353
  USERS_MIGRATION_SQL,
2129
2354
  API_TOKENS_MIGRATION_SQL,
2130
2355
  COLLECTIONS_MIGRATION_SQL,
2131
2356
  ENVIRONMENTS_MIGRATION_SQL,
2357
+ SNIPPETS_MIGRATION_SQL,
2132
2358
  FOLDERS_MIGRATION_SQL,
2133
2359
  REQUESTS_MIGRATION_SQL,
2134
2360
  AUDIT_LOG_MIGRATION_SQL,
@@ -2146,7 +2372,9 @@ var MYSQL_MIGRATIONS = [
2146
2372
  LLM_USAGE_MIGRATION_SQL,
2147
2373
  LLM_USAGE_LOG_MIGRATION_SQL,
2148
2374
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
2149
- ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL
2375
+ ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
2376
+ USERS_SNIPPET_ACCESS_MIGRATION_SQL,
2377
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL
2150
2378
  ];
2151
2379
 
2152
2380
  // src/db/mysql/schemas.ts
@@ -2190,6 +2418,7 @@ function mapUserSqlRow(row) {
2190
2418
  role: parseUserRole(row.role),
2191
2419
  collectionAccess: parseAccessList(row.collection_access),
2192
2420
  environmentAccess: parseAccessList(row.environment_access),
2421
+ snippetAccess: parseAccessList(row.snippet_access),
2193
2422
  llmAccess: Boolean(row.llm_access),
2194
2423
  llmModels: parseAccessList(row.llm_models),
2195
2424
  llmMonthlyTokenLimit: row.llm_monthly_token_limit,
@@ -2202,9 +2431,10 @@ function mapUserSqlRow(row) {
2202
2431
  function serializeAccessList(access) {
2203
2432
  return JSON.stringify(access);
2204
2433
  }
2205
- 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`;
2206
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`;
2207
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`;
2208
2438
  var FOLDER_SELECT_COLUMNS = `id, collection_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2209
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`;
2210
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`;
@@ -2247,6 +2477,7 @@ var LLM_USAGE_SELECT_COLUMNS = `id, user_id, period, prompt_tokens, completion_t
2247
2477
  // src/db/mysql/MysqlDatabase.ts
2248
2478
  var COLLECTION_SELECT = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
2249
2479
  var ENVIRONMENT_SELECT = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
2480
+ var SNIPPET_SELECT = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
2250
2481
  var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
2251
2482
  var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
2252
2483
  var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -2384,6 +2615,7 @@ var MysqlDatabase = class _MysqlDatabase {
2384
2615
  role,
2385
2616
  collection_access,
2386
2617
  environment_access,
2618
+ snippet_access,
2387
2619
  llm_access,
2388
2620
  llm_models,
2389
2621
  llm_monthly_token_limit,
@@ -2391,13 +2623,14 @@ var MysqlDatabase = class _MysqlDatabase {
2391
2623
  updated_at,
2392
2624
  created_by_user_id,
2393
2625
  updated_by_user_id
2394
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2626
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2395
2627
  [
2396
2628
  id,
2397
2629
  trimmedName,
2398
2630
  input.role,
2399
2631
  serializeAccessList(input.collectionAccess),
2400
2632
  serializeAccessList(input.environmentAccess),
2633
+ serializeAccessList(input.snippetAccess),
2401
2634
  input.llmAccess ? 1 : 0,
2402
2635
  serializeAccessList(input.llmModels ?? []),
2403
2636
  input.llmMonthlyTokenLimit ?? null,
@@ -2470,6 +2703,7 @@ var MysqlDatabase = class _MysqlDatabase {
2470
2703
  const role = input.role ?? existing.role;
2471
2704
  const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
2472
2705
  const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
2706
+ const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
2473
2707
  const llmAccess = input.llmAccess ?? existing.llmAccess;
2474
2708
  const llmModels = input.llmModels ?? existing.llmModels;
2475
2709
  const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
@@ -2480,6 +2714,7 @@ var MysqlDatabase = class _MysqlDatabase {
2480
2714
  role = ?,
2481
2715
  collection_access = ?,
2482
2716
  environment_access = ?,
2717
+ snippet_access = ?,
2483
2718
  llm_access = ?,
2484
2719
  llm_models = ?,
2485
2720
  llm_monthly_token_limit = ?,
@@ -2491,6 +2726,7 @@ var MysqlDatabase = class _MysqlDatabase {
2491
2726
  role,
2492
2727
  serializeAccessList(collectionAccess),
2493
2728
  serializeAccessList(environmentAccess),
2729
+ serializeAccessList(snippetAccess),
2494
2730
  llmAccess ? 1 : 0,
2495
2731
  serializeAccessList(llmModels),
2496
2732
  llmMonthlyTokenLimit,
@@ -2552,7 +2788,8 @@ var MysqlDatabase = class _MysqlDatabase {
2552
2788
  name: BOOTSTRAP_USER_NAME,
2553
2789
  role: "user",
2554
2790
  collectionAccess: ["*"],
2555
- environmentAccess: ["*"]
2791
+ environmentAccess: ["*"],
2792
+ snippetAccess: ["*"]
2556
2793
  },
2557
2794
  systemUserId
2558
2795
  );
@@ -2967,6 +3204,143 @@ var MysqlDatabase = class _MysqlDatabase {
2967
3204
  }
2968
3205
  return mapEnvironmentSqlRow(row);
2969
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
+ }
2970
3344
  /**
2971
3345
  * Lists all saved requests in a collection.
2972
3346
  *
@@ -3529,6 +3903,7 @@ var MysqlDatabase = class _MysqlDatabase {
3529
3903
  role,
3530
3904
  collection_access,
3531
3905
  environment_access,
3906
+ snippet_access,
3532
3907
  llm_access,
3533
3908
  llm_models,
3534
3909
  llm_monthly_token_limit,
@@ -3536,13 +3911,14 @@ var MysqlDatabase = class _MysqlDatabase {
3536
3911
  updated_at,
3537
3912
  created_by_user_id,
3538
3913
  updated_by_user_id
3539
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3914
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3540
3915
  [
3541
3916
  id,
3542
3917
  trimmedName,
3543
3918
  input.role,
3544
3919
  serializeAccessList(input.collectionAccess),
3545
3920
  serializeAccessList(input.environmentAccess),
3921
+ serializeAccessList(input.snippetAccess),
3546
3922
  0,
3547
3923
  serializeAccessList([]),
3548
3924
  null,
@@ -3671,6 +4047,20 @@ CREATE TABLE IF NOT EXISTS environments (
3671
4047
  updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
3672
4048
  );
3673
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();
3674
4064
  var FOLDERS_MIGRATION_SQL2 = `
3675
4065
  CREATE TABLE IF NOT EXISTS folders (
3676
4066
  id TEXT PRIMARY KEY,
@@ -3716,6 +4106,7 @@ CREATE TABLE IF NOT EXISTS users (
3716
4106
  role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
3717
4107
  collection_access TEXT NOT NULL DEFAULT '[]',
3718
4108
  environment_access TEXT NOT NULL DEFAULT '[]',
4109
+ snippet_access TEXT NOT NULL DEFAULT '[]',
3719
4110
  created_at TIMESTAMPTZ NOT NULL,
3720
4111
  updated_at TIMESTAMPTZ NOT NULL,
3721
4112
  created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
@@ -3827,11 +4218,23 @@ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2 = `
3827
4218
  ALTER TABLE environments
3828
4219
  ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
3829
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();
3830
4232
  var POSTGRES_MIGRATIONS = [
3831
4233
  USERS_MIGRATION_SQL2,
3832
4234
  API_TOKENS_MIGRATION_SQL2,
3833
4235
  COLLECTIONS_MIGRATION_SQL2,
3834
4236
  ENVIRONMENTS_MIGRATION_SQL2,
4237
+ SNIPPETS_MIGRATION_SQL2,
3835
4238
  FOLDERS_MIGRATION_SQL2,
3836
4239
  REQUESTS_MIGRATION_SQL2,
3837
4240
  AUDIT_LOG_MIGRATION_SQL2,
@@ -3849,7 +4252,9 @@ var POSTGRES_MIGRATIONS = [
3849
4252
  LLM_USAGE_MIGRATION_SQL2,
3850
4253
  LLM_USAGE_LOG_MIGRATION_SQL2,
3851
4254
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
3852
- ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2
4255
+ ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
4256
+ USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
4257
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL2
3853
4258
  ];
3854
4259
 
3855
4260
  // src/db/postgres/schemas.ts
@@ -3873,6 +4278,7 @@ var postgresConfigSchema = z4.object({
3873
4278
  var { Pool } = pg;
3874
4279
  var COLLECTION_SELECT2 = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
3875
4280
  var ENVIRONMENT_SELECT2 = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
4281
+ var SNIPPET_SELECT2 = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
3876
4282
  var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
3877
4283
  var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
3878
4284
  var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -4013,6 +4419,7 @@ var PostgresDatabase = class _PostgresDatabase {
4013
4419
  role,
4014
4420
  collection_access,
4015
4421
  environment_access,
4422
+ snippet_access,
4016
4423
  llm_access,
4017
4424
  llm_models,
4018
4425
  llm_monthly_token_limit,
@@ -4020,7 +4427,7 @@ var PostgresDatabase = class _PostgresDatabase {
4020
4427
  updated_at,
4021
4428
  created_by_user_id,
4022
4429
  updated_by_user_id
4023
- ) 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)
4024
4431
  RETURNING ${USER_SELECT_COLUMNS}`,
4025
4432
  [
4026
4433
  id,
@@ -4028,6 +4435,7 @@ var PostgresDatabase = class _PostgresDatabase {
4028
4435
  input.role,
4029
4436
  serializeAccessList(input.collectionAccess),
4030
4437
  serializeAccessList(input.environmentAccess),
4438
+ serializeAccessList(input.snippetAccess),
4031
4439
  input.llmAccess ?? false,
4032
4440
  serializeAccessList(input.llmModels ?? []),
4033
4441
  input.llmMonthlyTokenLimit ?? null,
@@ -4092,6 +4500,7 @@ var PostgresDatabase = class _PostgresDatabase {
4092
4500
  const role = input.role ?? existing.role;
4093
4501
  const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
4094
4502
  const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
4503
+ const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
4095
4504
  const llmAccess = input.llmAccess ?? existing.llmAccess;
4096
4505
  const llmModels = input.llmModels ?? existing.llmModels;
4097
4506
  const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
@@ -4102,17 +4511,19 @@ var PostgresDatabase = class _PostgresDatabase {
4102
4511
  role = $2,
4103
4512
  collection_access = $3,
4104
4513
  environment_access = $4,
4105
- llm_access = $5,
4106
- llm_models = $6,
4107
- llm_monthly_token_limit = $7,
4108
- updated_at = $8,
4109
- updated_by_user_id = $9
4110
- WHERE id = $10`,
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`,
4111
4521
  [
4112
4522
  name,
4113
4523
  role,
4114
4524
  serializeAccessList(collectionAccess),
4115
4525
  serializeAccessList(environmentAccess),
4526
+ serializeAccessList(snippetAccess),
4116
4527
  llmAccess,
4117
4528
  serializeAccessList(llmModels),
4118
4529
  llmMonthlyTokenLimit,
@@ -4174,7 +4585,8 @@ var PostgresDatabase = class _PostgresDatabase {
4174
4585
  name: BOOTSTRAP_USER_NAME,
4175
4586
  role: "user",
4176
4587
  collectionAccess: ["*"],
4177
- environmentAccess: ["*"]
4588
+ environmentAccess: ["*"],
4589
+ snippetAccess: ["*"]
4178
4590
  },
4179
4591
  systemUserId
4180
4592
  );
@@ -4571,39 +4983,163 @@ var PostgresDatabase = class _PostgresDatabase {
4571
4983
  return mapEnvironmentSqlRow(row);
4572
4984
  }
4573
4985
  /**
4574
- * Lists all saved requests in a collection.
4575
- *
4576
- * @param collectionId - Collection to query.
4986
+ * Lists all snippets ordered by sort order then name.
4577
4987
  */
4578
- async listRequests(collectionId) {
4988
+ async listSnippets() {
4579
4989
  const result = await this.query(
4580
- `${REQUEST_SELECT2} WHERE collection_id = $1 ORDER BY sort_order ASC, name ASC`,
4581
- [collectionId]
4990
+ `${SNIPPET_SELECT2} ORDER BY sort_order ASC, name ASC`
4582
4991
  );
4583
- return result.rows.map(mapRequestSqlRow);
4584
- }
4585
- /**
4586
- * Finds a saved request by id.
4587
- *
4588
- * @param id - Request identifier to look up.
4589
- */
4590
- async findRequestById(id) {
4591
- const result = await this.query(`${REQUEST_SELECT2} WHERE id = $1 LIMIT 1`, [id]);
4592
- const row = result.rows[0];
4593
- return row ? mapRequestSqlRow(row) : null;
4992
+ return result.rows.map(mapSnippetSqlRow);
4594
4993
  }
4595
4994
  /**
4596
- * Inserts a new request or updates an existing one.
4995
+ * Creates a new snippet with the given fields.
4597
4996
  *
4598
- * @param input - Request fields to persist.
4599
- * @param actingUserId - User performing the save action.
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.
4600
5001
  */
4601
- async saveRequest(input, actingUserId) {
4602
- const trimmedName = trimRequiredName(input.name, "Request name");
4603
- const headers = JSON.stringify(input.headers);
4604
- const params = JSON.stringify(input.params);
4605
- const auth = JSON.stringify(input.auth);
4606
- const folderId = input.folderId ?? null;
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
+ }
5131
+ /**
5132
+ * Inserts a new request or updates an existing one.
5133
+ *
5134
+ * @param input - Request fields to persist.
5135
+ * @param actingUserId - User performing the save action.
5136
+ */
5137
+ async saveRequest(input, actingUserId) {
5138
+ const trimmedName = trimRequiredName(input.name, "Request name");
5139
+ const headers = JSON.stringify(input.headers);
5140
+ const params = JSON.stringify(input.params);
5141
+ const auth = JSON.stringify(input.auth);
5142
+ const folderId = input.folderId ?? null;
4607
5143
  const now = /* @__PURE__ */ new Date();
4608
5144
  if (folderId != null) {
4609
5145
  const folderResult = await this.query(
@@ -5127,6 +5663,7 @@ var PostgresDatabase = class _PostgresDatabase {
5127
5663
  role,
5128
5664
  collection_access,
5129
5665
  environment_access,
5666
+ snippet_access,
5130
5667
  llm_access,
5131
5668
  llm_models,
5132
5669
  llm_monthly_token_limit,
@@ -5134,13 +5671,14 @@ var PostgresDatabase = class _PostgresDatabase {
5134
5671
  updated_at,
5135
5672
  created_by_user_id,
5136
5673
  updated_by_user_id
5137
- ) 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)`,
5138
5675
  [
5139
5676
  id,
5140
5677
  SYSTEM_USER_NAME,
5141
5678
  input.role,
5142
5679
  serializeAccessList(input.collectionAccess),
5143
5680
  serializeAccessList(input.environmentAccess),
5681
+ serializeAccessList(input.snippetAccess),
5144
5682
  false,
5145
5683
  serializeAccessList([]),
5146
5684
  null,
@@ -5440,21 +5978,26 @@ function normalizeLlmForRole(role, llmAccess, llmModels) {
5440
5978
  llmModels
5441
5979
  };
5442
5980
  }
5443
- function normalizeAccessForRole(role, collectionAccess, environmentAccess) {
5981
+ function normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess) {
5444
5982
  if (role === "admin") {
5445
- if (collectionAccess.length > 0 || environmentAccess.length > 0) {
5446
- throw new ValidationError("Admin users cannot have collection or environment access.");
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
+ );
5447
5987
  }
5448
5988
  return {
5449
5989
  collectionAccess: [],
5450
- environmentAccess: []
5990
+ environmentAccess: [],
5991
+ snippetAccess: []
5451
5992
  };
5452
5993
  }
5453
5994
  validateAccessList(collectionAccess);
5454
5995
  validateAccessList(environmentAccess);
5996
+ validateAccessList(snippetAccess);
5455
5997
  return {
5456
5998
  collectionAccess,
5457
- environmentAccess
5999
+ environmentAccess,
6000
+ snippetAccess
5458
6001
  };
5459
6002
  }
5460
6003
  function findUnknownAccessIds(access, knownIds) {
@@ -5480,6 +6023,9 @@ function validateSubmittedAccessLists(submitted, catalogs) {
5480
6023
  "environment"
5481
6024
  );
5482
6025
  }
6026
+ if (submitted.snippetAccess !== void 0) {
6027
+ validateKnownAccessIds(submitted.snippetAccess, catalogs.knownSnippetIds, "snippet");
6028
+ }
5483
6029
  }
5484
6030
  if (submitted.llmModels !== void 0 && catalogs.knownLlmModelIds !== null) {
5485
6031
  validateKnownAccessIds(submitted.llmModels, catalogs.knownLlmModelIds, "LLM model");
@@ -5493,6 +6039,9 @@ function buildAccessListWarnings(stored, catalogs) {
5493
6039
  for (const id of findUnknownAccessIds(stored.environmentAccess, catalogs.knownEnvironmentIds)) {
5494
6040
  warnings.push(`Unknown environment id "${id}".`);
5495
6041
  }
6042
+ for (const id of findUnknownAccessIds(stored.snippetAccess, catalogs.knownSnippetIds)) {
6043
+ warnings.push(`Unknown snippet id "${id}".`);
6044
+ }
5496
6045
  if (catalogs.knownLlmModelIds !== null) {
5497
6046
  for (const id of findUnknownAccessIds(stored.llmModels, catalogs.knownLlmModelIds)) {
5498
6047
  warnings.push(`Unknown LLM model id "${id}".`);
@@ -5500,10 +6049,11 @@ function buildAccessListWarnings(stored, catalogs) {
5500
6049
  }
5501
6050
  return warnings;
5502
6051
  }
5503
- function buildAccessCatalogIds(collections, environments, llmModelIds) {
6052
+ function buildAccessCatalogIds(collections, environments, snippets, llmModelIds) {
5504
6053
  return {
5505
6054
  knownCollectionIds: new Set(collections.map((collection) => collection.id)),
5506
6055
  knownEnvironmentIds: new Set(environments.map((environment) => environment.id)),
6056
+ knownSnippetIds: new Set(snippets.map((snippet) => snippet.id)),
5507
6057
  knownLlmModelIds: llmModelIds === null ? null : new Set(llmModelIds)
5508
6058
  };
5509
6059
  }
@@ -5511,7 +6061,8 @@ function buildAdminUserUpdateInput(existing, body) {
5511
6061
  const role = body.role ?? existing.role;
5512
6062
  const collectionAccess = role === "admin" ? [] : body.collectionAccess ?? existing.collectionAccess;
5513
6063
  const environmentAccess = role === "admin" ? [] : body.environmentAccess ?? existing.environmentAccess;
5514
- const access = normalizeAccessForRole(role, collectionAccess, environmentAccess);
6064
+ const snippetAccess = role === "admin" ? [] : body.snippetAccess ?? existing.snippetAccess;
6065
+ const access = normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess);
5515
6066
  const llmAccess = role === "admin" ? false : body.llmAccess ?? existing.llmAccess;
5516
6067
  const llmModels = role === "admin" ? [] : body.llmModels ?? existing.llmModels;
5517
6068
  const llm = normalizeLlmForRole(role, llmAccess, llmModels);
@@ -5520,6 +6071,7 @@ function buildAdminUserUpdateInput(existing, body) {
5520
6071
  role: body.role,
5521
6072
  collectionAccess: access.collectionAccess,
5522
6073
  environmentAccess: access.environmentAccess,
6074
+ snippetAccess: access.snippetAccess,
5523
6075
  llmAccess: llm.llmAccess,
5524
6076
  llmModels: llm.llmModels,
5525
6077
  llmMonthlyTokenLimit: body.llmMonthlyTokenLimit
@@ -5528,7 +6080,13 @@ function buildAdminUserUpdateInput(existing, body) {
5528
6080
  function buildAdminUserCreateInput(body) {
5529
6081
  const collectionAccess = body.collectionAccess ?? [];
5530
6082
  const environmentAccess = body.environmentAccess ?? [];
5531
- const access = normalizeAccessForRole(body.role, collectionAccess, environmentAccess);
6083
+ const snippetAccess = body.snippetAccess ?? [];
6084
+ const access = normalizeAccessForRole(
6085
+ body.role,
6086
+ collectionAccess,
6087
+ environmentAccess,
6088
+ snippetAccess
6089
+ );
5532
6090
  const llmAccess = body.role === "admin" ? false : body.llmAccess ?? false;
5533
6091
  const llmModels = body.role === "admin" ? [] : body.llmModels ?? [];
5534
6092
  const llm = normalizeLlmForRole(body.role, llmAccess, llmModels);
@@ -5537,6 +6095,7 @@ function buildAdminUserCreateInput(body) {
5537
6095
  role: body.role,
5538
6096
  collectionAccess: access.collectionAccess ?? [],
5539
6097
  environmentAccess: access.environmentAccess ?? [],
6098
+ snippetAccess: access.snippetAccess ?? [],
5540
6099
  llmAccess: llm.llmAccess ?? false,
5541
6100
  llmModels: llm.llmModels ?? [],
5542
6101
  llmMonthlyTokenLimit: body.llmMonthlyTokenLimit ?? null
@@ -5635,6 +6194,7 @@ function printUser(user, usage) {
5635
6194
  console.log(` role: ${user.role}`);
5636
6195
  console.log(` collection access: ${formatAccessList(user.collectionAccess)}`);
5637
6196
  console.log(` environment access: ${formatAccessList(user.environmentAccess)}`);
6197
+ console.log(` snippet access: ${formatAccessList(user.snippetAccess)}`);
5638
6198
  console.log(` llm access: ${user.llmAccess ? "enabled" : "disabled"}`);
5639
6199
  console.log(` llm models: ${formatAccessList(user.llmModels)}`);
5640
6200
  console.log(
@@ -5654,13 +6214,15 @@ function printCreatedApiToken(user, record, secret) {
5654
6214
  console.log(secret);
5655
6215
  }
5656
6216
  async function loadAccessCatalogs(db, llm) {
5657
- const [collections, environments] = await Promise.all([
6217
+ const [collections, environments, snippets] = await Promise.all([
5658
6218
  db.listCollections(),
5659
- db.listEnvironments()
6219
+ db.listEnvironments(),
6220
+ db.listSnippets()
5660
6221
  ]);
5661
6222
  return buildAccessCatalogIds(
5662
6223
  collections,
5663
6224
  environments,
6225
+ snippets,
5664
6226
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
5665
6227
  );
5666
6228
  }
@@ -5686,7 +6248,12 @@ async function userCreateCommand(options) {
5686
6248
  const config = loadServerConfig(options.config);
5687
6249
  const db = createDatabase(config.db);
5688
6250
  const access = mapValidationError(
5689
- () => normalizeAccessForRole(options.role, options.collectionAccess, options.environmentAccess)
6251
+ () => normalizeAccessForRole(
6252
+ options.role,
6253
+ options.collectionAccess,
6254
+ options.environmentAccess,
6255
+ options.snippetAccess
6256
+ )
5690
6257
  );
5691
6258
  await db.connect();
5692
6259
  const actingUserId = await requireSystemUserId(db);
@@ -5700,6 +6267,7 @@ async function userCreateCommand(options) {
5700
6267
  role: options.role,
5701
6268
  collectionAccess: access.collectionAccess,
5702
6269
  environmentAccess: access.environmentAccess,
6270
+ snippetAccess: access.snippetAccess,
5703
6271
  llmModels
5704
6272
  },
5705
6273
  catalogs
@@ -5710,6 +6278,7 @@ async function userCreateCommand(options) {
5710
6278
  role: options.role,
5711
6279
  collectionAccess: access.collectionAccess ?? [],
5712
6280
  environmentAccess: access.environmentAccess ?? [],
6281
+ snippetAccess: access.snippetAccess ?? [],
5713
6282
  llmAccess: llm.llmAccess,
5714
6283
  llmModels: llm.llmModels,
5715
6284
  llmMonthlyTokenLimit: options.llmMonthlyTokens ?? null
@@ -5747,6 +6316,7 @@ async function userListCommand(options) {
5747
6316
  {
5748
6317
  collectionAccess: user.collectionAccess,
5749
6318
  environmentAccess: user.environmentAccess,
6319
+ snippetAccess: user.snippetAccess,
5750
6320
  llmModels: user.llmModels
5751
6321
  },
5752
6322
  catalogs
@@ -5776,6 +6346,7 @@ async function userShowCommand(options) {
5776
6346
  {
5777
6347
  collectionAccess: user.collectionAccess,
5778
6348
  environmentAccess: user.environmentAccess,
6349
+ snippetAccess: user.snippetAccess,
5779
6350
  llmModels: user.llmModels
5780
6351
  },
5781
6352
  catalogs
@@ -5797,8 +6368,9 @@ async function userUpdateCommand(options) {
5797
6368
  const role = options.role ?? existing.role;
5798
6369
  const collectionAccess = options.collectionAccess ?? (options.role === "admin" ? [] : existing.collectionAccess);
5799
6370
  const environmentAccess = options.environmentAccess ?? (options.role === "admin" ? [] : existing.environmentAccess);
6371
+ const snippetAccess = options.snippetAccess ?? (options.role === "admin" ? [] : existing.snippetAccess);
5800
6372
  const access = mapValidationError(
5801
- () => normalizeAccessForRole(role, collectionAccess, environmentAccess)
6373
+ () => normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess)
5802
6374
  );
5803
6375
  const llmAccess = role === "admin" ? false : options.llmAccess ?? existing.llmAccess;
5804
6376
  const llmModels = role === "admin" ? [] : options.llmModels ?? existing.llmModels;
@@ -5809,6 +6381,7 @@ async function userUpdateCommand(options) {
5809
6381
  role,
5810
6382
  collectionAccess: options.collectionAccess,
5811
6383
  environmentAccess: options.environmentAccess,
6384
+ snippetAccess: options.snippetAccess,
5812
6385
  llmModels: options.llmModels
5813
6386
  },
5814
6387
  catalogs
@@ -5818,6 +6391,7 @@ async function userUpdateCommand(options) {
5818
6391
  role: options.role,
5819
6392
  collectionAccess: access.collectionAccess,
5820
6393
  environmentAccess: access.environmentAccess,
6394
+ snippetAccess: access.snippetAccess,
5821
6395
  llmAccess: llm.llmAccess,
5822
6396
  llmModels: llm.llmModels,
5823
6397
  llmMonthlyTokenLimit: options.llmMonthlyTokens !== void 0 ? options.llmMonthlyTokens : void 0
@@ -5901,6 +6475,11 @@ function registerUserCommand(program, handlers = {}) {
5901
6475
  "Environment id or * (repeatable)",
5902
6476
  parseAccessFlag,
5903
6477
  []
6478
+ ).option(
6479
+ "--snippet-access <id>",
6480
+ "Snippet id or * (repeatable)",
6481
+ parseAccessFlag,
6482
+ []
5904
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(
5905
6484
  /**
5906
6485
  * Runs the user create subcommand after merging global CLI options.
@@ -5935,6 +6514,11 @@ function registerUserCommand(program, handlers = {}) {
5935
6514
  "Replacement environment id or * (repeatable)",
5936
6515
  parseAccessFlag,
5937
6516
  []
6517
+ ).option(
6518
+ "--snippet-access <id>",
6519
+ "Replacement snippet id or * (repeatable)",
6520
+ parseAccessFlag,
6521
+ []
5938
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(
5939
6523
  "--llm-model <id>",
5940
6524
  "Replacement LLM model id or * (repeatable)",
@@ -5950,6 +6534,7 @@ function registerUserCommand(program, handlers = {}) {
5950
6534
  ...merged,
5951
6535
  collectionAccess: (options.collectionAccess ?? []).length > 0 ? options.collectionAccess : void 0,
5952
6536
  environmentAccess: (options.environmentAccess ?? []).length > 0 ? options.environmentAccess : void 0,
6537
+ snippetAccess: (options.snippetAccess ?? []).length > 0 ? options.snippetAccess : void 0,
5953
6538
  llmModels: (() => {
5954
6539
  const llmModels = readLlmModelsOption(options);
5955
6540
  return llmModels.length > 0 ? llmModels : void 0;
@@ -6067,6 +6652,9 @@ function canListCollections(user) {
6067
6652
  function canListEnvironments(user) {
6068
6653
  return canUseDataApi(user) || canUseManagementApi(user);
6069
6654
  }
6655
+ function canListSnippets(user) {
6656
+ return canUseDataApi(user) || canUseManagementApi(user);
6657
+ }
6070
6658
  function hasWildcardAccess(access) {
6071
6659
  return access.includes("*");
6072
6660
  }
@@ -6088,6 +6676,15 @@ function canAccessEnvironment(user, environmentId) {
6088
6676
  }
6089
6677
  return user.environmentAccess.includes(environmentId);
6090
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
+ }
6091
6688
  function canDeleteCollection(user, collection) {
6092
6689
  return canUseDataApi(user) && canAccessCollection(user, collection.id) && collection.createdByUserId === user.id && !collection.deletionLocked;
6093
6690
  }
@@ -6100,6 +6697,9 @@ function canCreateCollection(user) {
6100
6697
  function canCreateEnvironment(user) {
6101
6698
  return user.role === "user" && hasWildcardAccess(user.environmentAccess);
6102
6699
  }
6700
+ function canCreateSnippet(user) {
6701
+ return user.role === "user" && hasWildcardAccess(user.snippetAccess);
6702
+ }
6103
6703
  function filterAccessibleCollections(user, collections) {
6104
6704
  if (user.role === "admin" || hasWildcardAccess(user.collectionAccess)) {
6105
6705
  return collections;
@@ -6114,6 +6714,13 @@ function filterAccessibleEnvironments(user, environments) {
6114
6714
  const allowed = new Set(user.environmentAccess);
6115
6715
  return environments.filter((environment) => allowed.has(environment.id));
6116
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
+ }
6117
6724
  function canUseLlm(user) {
6118
6725
  return user.role !== "admin" && user.llmAccess;
6119
6726
  }
@@ -6258,7 +6865,7 @@ function denyUnlessAllowed(reply, allowed) {
6258
6865
  }
6259
6866
 
6260
6867
  // src/server/routes/schemas/admin.ts
6261
- import { z as z8 } from "zod/v4";
6868
+ import { z as z9 } from "zod/v4";
6262
6869
 
6263
6870
  // src/server/routes/schemas/auth.ts
6264
6871
  import { z as z6 } from "zod/v4";
@@ -6325,247 +6932,154 @@ var llmUsageSummaryResponseSchema = z7.object({
6325
6932
  limit: z7.number().int().positive().nullable()
6326
6933
  });
6327
6934
 
6328
- // src/server/routes/schemas/admin.ts
6329
- var adminResourceOptionSchema = z8.object({
6935
+ // src/server/routes/schemas/entities.ts
6936
+ import { z as z8 } from "zod/v4";
6937
+ var collectionRecordSchema = z8.object({
6330
6938
  id: z8.string(),
6331
6939
  name: z8.string(),
6940
+ variables: z8.array(variableSchema),
6941
+ headers: z8.array(keyValueSchema),
6942
+ auth: authConfigSchema,
6943
+ preRequestScript: z8.string(),
6944
+ postRequestScript: z8.string(),
6945
+ createdAt: timestampSchema,
6946
+ updatedAt: timestampSchema,
6947
+ createdByUserId: z8.string().nullable(),
6948
+ updatedByUserId: z8.string().nullable(),
6332
6949
  deletionLocked: z8.boolean()
6333
6950
  });
6334
- var adminEntityConfigSchema = z8.object({
6951
+ var environmentRecordSchema = z8.object({
6335
6952
  id: z8.string(),
6336
6953
  name: z8.string(),
6954
+ variables: z8.array(variableSchema),
6955
+ createdAt: timestampSchema,
6956
+ updatedAt: timestampSchema,
6957
+ createdByUserId: z8.string().nullable(),
6958
+ updatedByUserId: z8.string().nullable(),
6337
6959
  deletionLocked: z8.boolean()
6338
6960
  });
6339
- var updateAdminCollectionBodySchema = z8.object({
6340
- deletionLocked: z8.boolean()
6341
- });
6342
- var updateAdminEnvironmentBodySchema = z8.object({
6343
- deletionLocked: z8.boolean()
6344
- });
6345
- var listAdminCollectionsResponseSchema = z8.object({
6346
- collections: z8.array(adminResourceOptionSchema)
6347
- });
6348
- var listAdminEnvironmentsResponseSchema = z8.object({
6349
- environments: z8.array(adminResourceOptionSchema)
6350
- });
6351
- var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
6352
- var hubUserRecordSchema = z8.object({
6961
+ var snippetScopeSchema = z8.enum(["pre-request", "post-request", "any"]);
6962
+ var snippetRecordSchema = z8.object({
6353
6963
  id: z8.string(),
6354
6964
  name: z8.string(),
6355
- role: userRoleSchema,
6356
- collectionAccess: z8.array(z8.string()),
6357
- environmentAccess: z8.array(z8.string()),
6358
- llmAccess: z8.boolean(),
6359
- llmModels: z8.array(z8.string()),
6360
- llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable(),
6965
+ code: z8.string(),
6966
+ scope: snippetScopeSchema,
6967
+ sortOrder: z8.number().int(),
6361
6968
  createdAt: timestampSchema,
6362
- updatedAt: timestampSchema
6363
- });
6364
- var adminUserListEntrySchema = hubUserRecordSchema.extend({
6365
- warnings: z8.array(z8.string())
6366
- });
6367
- var listAdminUsersResponseSchema = z8.object({
6368
- users: z8.array(adminUserListEntrySchema)
6369
- });
6370
- var updateAdminUserBodySchema = z8.object({
6371
- name: z8.string().trim().min(1).optional(),
6372
- role: userRoleSchema.optional(),
6373
- collectionAccess: z8.array(z8.string()).optional(),
6374
- environmentAccess: z8.array(z8.string()).optional(),
6375
- llmAccess: z8.boolean().optional(),
6376
- llmModels: z8.array(z8.string()).optional(),
6377
- llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable().optional()
6378
- });
6379
- var createAdminUserBodySchema = z8.object({
6380
- name: z8.string().trim().min(1),
6381
- role: userRoleSchema,
6382
- collectionAccess: z8.array(z8.string()).optional(),
6383
- environmentAccess: z8.array(z8.string()).optional(),
6384
- llmAccess: z8.boolean().optional(),
6385
- llmModels: z8.array(z8.string()).optional(),
6386
- llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable().optional()
6969
+ updatedAt: timestampSchema,
6970
+ createdByUserId: z8.string().nullable(),
6971
+ updatedByUserId: z8.string().nullable(),
6972
+ deletionLocked: z8.boolean()
6387
6973
  });
6388
- var hubApiTokenRecordSchema = z8.object({
6974
+ var folderRecordSchema = z8.object({
6389
6975
  id: z8.string(),
6390
- userId: z8.string(),
6976
+ collectionId: z8.string(),
6391
6977
  name: z8.string(),
6392
- tokenPrefix: z8.string(),
6393
- createdAt: timestampSchema,
6394
- lastUsedAt: timestampSchema.nullable(),
6395
- revokedAt: timestampSchema.nullable()
6396
- });
6397
- var createAdminUserResponseSchema = z8.object({
6398
- user: hubUserRecordSchema,
6399
- token: hubApiTokenRecordSchema,
6400
- secret: z8.string()
6401
- });
6402
- var createAdminTokenBodySchema = z8.object({
6403
- name: z8.string().trim().min(1)
6404
- });
6405
- var createdApiTokenResponseSchema = z8.object({
6406
- token: hubApiTokenRecordSchema,
6407
- secret: z8.string()
6408
- });
6409
- var listAdminTokensResponseSchema = z8.object({
6410
- tokens: z8.array(hubApiTokenRecordSchema)
6411
- });
6412
- var reloadConfigSectionResultSchema = z8.object({
6413
- section: z8.enum(["db", "redis", "llm", "plugins", "server"]),
6414
- status: z8.enum(["reloaded", "unchanged", "failed", "restart-required"]),
6415
- error: z8.string().optional()
6416
- });
6417
- var reloadConfigResponseSchema = z8.object({
6418
- sections: z8.array(reloadConfigSectionResultSchema),
6419
- fatalError: z8.string().optional()
6420
- });
6421
- function serializeHubUser(user) {
6422
- return {
6423
- id: user.id,
6424
- name: user.name,
6425
- role: user.role,
6426
- collectionAccess: user.collectionAccess,
6427
- environmentAccess: user.environmentAccess,
6428
- llmAccess: user.llmAccess,
6429
- llmModels: user.llmModels,
6430
- llmMonthlyTokenLimit: user.llmMonthlyTokenLimit,
6431
- createdAt: user.createdAt.toISOString(),
6432
- updatedAt: user.updatedAt.toISOString()
6433
- };
6434
- }
6435
- function serializeApiToken(token) {
6436
- return {
6437
- id: token.id,
6438
- userId: token.userId,
6439
- name: token.name,
6440
- tokenPrefix: token.tokenPrefix,
6441
- createdAt: token.createdAt.toISOString(),
6442
- lastUsedAt: token.lastUsedAt ? token.lastUsedAt.toISOString() : null,
6443
- revokedAt: token.revokedAt ? token.revokedAt.toISOString() : null
6444
- };
6445
- }
6446
-
6447
- // src/server/routes/schemas/entities.ts
6448
- import { z as z9 } from "zod/v4";
6449
- var collectionRecordSchema = z9.object({
6450
- id: z9.string(),
6451
- name: z9.string(),
6452
- variables: z9.array(variableSchema),
6453
- headers: z9.array(keyValueSchema),
6454
- auth: authConfigSchema,
6455
- preRequestScript: z9.string(),
6456
- postRequestScript: z9.string(),
6457
- createdAt: timestampSchema,
6458
- updatedAt: timestampSchema,
6459
- createdByUserId: z9.string().nullable(),
6460
- updatedByUserId: z9.string().nullable(),
6461
- deletionLocked: z9.boolean()
6462
- });
6463
- var environmentRecordSchema = z9.object({
6464
- id: z9.string(),
6465
- name: z9.string(),
6466
- variables: z9.array(variableSchema),
6467
- createdAt: timestampSchema,
6468
- updatedAt: timestampSchema,
6469
- createdByUserId: z9.string().nullable(),
6470
- updatedByUserId: z9.string().nullable(),
6471
- deletionLocked: z9.boolean()
6472
- });
6473
- var folderRecordSchema = z9.object({
6474
- id: z9.string(),
6475
- collectionId: z9.string(),
6476
- name: z9.string(),
6477
- sortOrder: z9.number().int(),
6978
+ sortOrder: z8.number().int(),
6478
6979
  createdAt: timestampSchema,
6479
6980
  updatedAt: timestampSchema,
6480
- createdByUserId: z9.string().nullable(),
6481
- updatedByUserId: z9.string().nullable()
6981
+ createdByUserId: z8.string().nullable(),
6982
+ updatedByUserId: z8.string().nullable()
6482
6983
  });
6483
- var savedRequestRecordSchema = z9.object({
6484
- id: z9.string(),
6485
- collectionId: z9.string(),
6486
- name: z9.string(),
6984
+ var savedRequestRecordSchema = z8.object({
6985
+ id: z8.string(),
6986
+ collectionId: z8.string(),
6987
+ name: z8.string(),
6487
6988
  method: httpMethodSchema,
6488
- url: z9.string(),
6489
- headers: z9.array(keyValueSchema),
6490
- params: z9.array(keyValueSchema),
6989
+ url: z8.string(),
6990
+ headers: z8.array(keyValueSchema),
6991
+ params: z8.array(keyValueSchema),
6491
6992
  auth: authConfigSchema,
6492
- body: z9.string(),
6993
+ body: z8.string(),
6493
6994
  bodyType: bodyTypeSchema,
6494
- preRequestScript: z9.string(),
6495
- postRequestScript: z9.string(),
6496
- comment: z9.string(),
6497
- folderId: z9.string().nullable(),
6498
- sortOrder: z9.number().int(),
6995
+ preRequestScript: z8.string(),
6996
+ postRequestScript: z8.string(),
6997
+ comment: z8.string(),
6998
+ folderId: z8.string().nullable(),
6999
+ sortOrder: z8.number().int(),
6499
7000
  createdAt: timestampSchema,
6500
7001
  updatedAt: timestampSchema,
6501
- createdByUserId: z9.string().nullable(),
6502
- updatedByUserId: z9.string().nullable()
7002
+ createdByUserId: z8.string().nullable(),
7003
+ updatedByUserId: z8.string().nullable()
6503
7004
  });
6504
- var createCollectionBodySchema = z9.object({
6505
- name: z9.string().trim().min(1)
7005
+ var createCollectionBodySchema = z8.object({
7006
+ name: z8.string().trim().min(1)
6506
7007
  });
6507
- var updateCollectionBodySchema = z9.object({
6508
- name: z9.string().trim().min(1),
6509
- variables: z9.array(variableSchema),
6510
- headers: z9.array(keyValueSchema),
6511
- preRequestScript: z9.string(),
6512
- postRequestScript: z9.string(),
7008
+ var updateCollectionBodySchema = z8.object({
7009
+ name: z8.string().trim().min(1),
7010
+ variables: z8.array(variableSchema),
7011
+ headers: z8.array(keyValueSchema),
7012
+ preRequestScript: z8.string(),
7013
+ postRequestScript: z8.string(),
6513
7014
  auth: authConfigSchema
6514
7015
  });
6515
- var createEnvironmentBodySchema = z9.object({
6516
- name: z9.string().trim().min(1)
7016
+ var createEnvironmentBodySchema = z8.object({
7017
+ name: z8.string().trim().min(1)
6517
7018
  });
6518
- var updateEnvironmentBodySchema = z9.object({
6519
- name: z9.string().trim().min(1),
6520
- variables: z9.array(variableSchema)
7019
+ var updateEnvironmentBodySchema = z8.object({
7020
+ name: z8.string().trim().min(1),
7021
+ variables: z8.array(variableSchema)
6521
7022
  });
6522
- var createFolderBodySchema = z9.object({
6523
- name: z9.string().trim().min(1)
7023
+ var createSnippetBodySchema = z8.object({
7024
+ name: z8.string().trim().min(1),
7025
+ code: z8.string().default(""),
7026
+ scope: snippetScopeSchema.default("any")
6524
7027
  });
6525
- var renameFolderBodySchema = z9.object({
6526
- name: z9.string().trim().min(1)
7028
+ var updateSnippetBodySchema = z8.object({
7029
+ name: z8.string().trim().min(1),
7030
+ code: z8.string(),
7031
+ scope: snippetScopeSchema
6527
7032
  });
6528
- var reorderFoldersBodySchema = z9.object({
6529
- orderedFolderIds: z9.array(z9.string().trim().min(1))
7033
+ var createFolderBodySchema = z8.object({
7034
+ name: z8.string().trim().min(1)
6530
7035
  });
6531
- var saveRequestBodySchema = z9.object({
6532
- name: z9.string().trim().min(1),
7036
+ var renameFolderBodySchema = z8.object({
7037
+ name: z8.string().trim().min(1)
7038
+ });
7039
+ var reorderFoldersBodySchema = z8.object({
7040
+ orderedFolderIds: z8.array(z8.string().trim().min(1))
7041
+ });
7042
+ var saveRequestBodySchema = z8.object({
7043
+ name: z8.string().trim().min(1),
6533
7044
  method: httpMethodSchema,
6534
- url: z9.string(),
6535
- headers: z9.array(keyValueSchema),
6536
- params: z9.array(keyValueSchema),
7045
+ url: z8.string(),
7046
+ headers: z8.array(keyValueSchema),
7047
+ params: z8.array(keyValueSchema),
6537
7048
  auth: authConfigSchema,
6538
- body: z9.string(),
7049
+ body: z8.string(),
6539
7050
  bodyType: bodyTypeSchema,
6540
- preRequestScript: z9.string(),
6541
- postRequestScript: z9.string(),
6542
- comment: z9.string(),
6543
- folderId: z9.string().nullable().optional()
7051
+ preRequestScript: z8.string(),
7052
+ postRequestScript: z8.string(),
7053
+ comment: z8.string(),
7054
+ folderId: z8.string().nullable().optional()
6544
7055
  });
6545
7056
  var updateSaveRequestBodySchema = saveRequestBodySchema.extend({
6546
- collectionId: z9.string().trim().min(1)
7057
+ collectionId: z8.string().trim().min(1)
6547
7058
  });
6548
- var reorderRequestsBodySchema = z9.object({
6549
- folderId: z9.string().nullable(),
6550
- orderedRequestIds: z9.array(z9.string().trim().min(1))
7059
+ var reorderRequestsBodySchema = z8.object({
7060
+ folderId: z8.string().nullable(),
7061
+ orderedRequestIds: z8.array(z8.string().trim().min(1))
6551
7062
  });
6552
- var moveRequestBodySchema = z9.object({
6553
- folderId: z9.string().nullable(),
6554
- index: z9.number().int().min(0)
7063
+ var moveRequestBodySchema = z8.object({
7064
+ folderId: z8.string().nullable(),
7065
+ index: z8.number().int().min(0)
6555
7066
  });
6556
- var listCollectionsResponseSchema = z9.object({
6557
- collections: z9.array(collectionRecordSchema)
7067
+ var listCollectionsResponseSchema = z8.object({
7068
+ collections: z8.array(collectionRecordSchema)
6558
7069
  });
6559
- var listEnvironmentsResponseSchema = z9.object({
6560
- environments: z9.array(environmentRecordSchema)
7070
+ var listEnvironmentsResponseSchema = z8.object({
7071
+ environments: z8.array(environmentRecordSchema)
6561
7072
  });
6562
- var listFoldersResponseSchema = z9.object({
6563
- folders: z9.array(folderRecordSchema)
7073
+ var listSnippetsResponseSchema = z8.object({
7074
+ snippets: z8.array(snippetRecordSchema)
6564
7075
  });
6565
- var listRequestsResponseSchema = z9.object({
6566
- requests: z9.array(savedRequestRecordSchema)
7076
+ var listFoldersResponseSchema = z8.object({
7077
+ folders: z8.array(folderRecordSchema)
6567
7078
  });
6568
- var emptyResponseSchema = z9.null();
7079
+ var listRequestsResponseSchema = z8.object({
7080
+ requests: z8.array(savedRequestRecordSchema)
7081
+ });
7082
+ var emptyResponseSchema = z8.null();
6569
7083
  function serializeCollection(record) {
6570
7084
  return {
6571
7085
  ...record,
@@ -6580,6 +7094,13 @@ function serializeEnvironment(record) {
6580
7094
  updatedAt: record.updatedAt.toISOString()
6581
7095
  };
6582
7096
  }
7097
+ function serializeSnippet(record) {
7098
+ return {
7099
+ ...record,
7100
+ createdAt: record.createdAt.toISOString(),
7101
+ updatedAt: record.updatedAt.toISOString()
7102
+ };
7103
+ }
6583
7104
  function serializeFolder(record) {
6584
7105
  return {
6585
7106
  ...record,
@@ -6595,18 +7116,167 @@ function serializeSavedRequest(record) {
6595
7116
  };
6596
7117
  }
6597
7118
 
6598
- // src/server/routes/admin.ts
6599
- function sendLlmUnavailable(reply) {
6600
- return reply.code(503).send({
6601
- error: "LLM support is not configured on this Team Hub."
6602
- });
6603
- }
6604
- function denySystemUserTarget(reply, existing, systemUserId) {
6605
- if (!isSystemUser(existing, systemUserId)) {
6606
- return false;
7119
+ // src/server/routes/schemas/admin.ts
7120
+ var adminResourceOptionSchema = z9.object({
7121
+ id: z9.string(),
7122
+ name: z9.string(),
7123
+ deletionLocked: z9.boolean()
7124
+ });
7125
+ var adminEntityConfigSchema = z9.object({
7126
+ id: z9.string(),
7127
+ name: z9.string(),
7128
+ deletionLocked: z9.boolean()
7129
+ });
7130
+ var updateAdminCollectionBodySchema = z9.object({
7131
+ deletionLocked: z9.boolean()
7132
+ });
7133
+ var updateAdminEnvironmentBodySchema = z9.object({
7134
+ deletionLocked: z9.boolean()
7135
+ });
7136
+ var updateAdminSnippetBodySchema = z9.object({
7137
+ name: z9.string().trim().min(1).optional(),
7138
+ code: z9.string().optional(),
7139
+ scope: snippetScopeSchema.optional(),
7140
+ deletionLocked: z9.boolean().optional()
7141
+ }).superRefine((body, ctx) => {
7142
+ const hasContentFields = body.name !== void 0 || body.code !== void 0 || body.scope !== void 0;
7143
+ const hasFullContent = body.name !== void 0 && body.code !== void 0 && body.scope !== void 0;
7144
+ if (hasContentFields && !hasFullContent) {
7145
+ ctx.addIssue({
7146
+ code: "custom",
7147
+ message: "name, code, and scope must be provided together"
7148
+ });
6607
7149
  }
6608
- void reply.code(403).send(errorResponseSchema.parse({ error: "Forbidden" }));
6609
- return true;
7150
+ if (!hasContentFields && body.deletionLocked === void 0) {
7151
+ ctx.addIssue({
7152
+ code: "custom",
7153
+ message: "Provide snippet content (name, code, scope) and/or deletionLocked"
7154
+ });
7155
+ }
7156
+ });
7157
+ var createAdminSnippetBodySchema = createSnippetBodySchema;
7158
+ var listAdminCollectionsResponseSchema = z9.object({
7159
+ collections: z9.array(adminResourceOptionSchema)
7160
+ });
7161
+ var listAdminEnvironmentsResponseSchema = z9.object({
7162
+ environments: z9.array(adminResourceOptionSchema)
7163
+ });
7164
+ var listAdminSnippetsResponseSchema = z9.object({
7165
+ snippets: z9.array(snippetRecordSchema)
7166
+ });
7167
+ var adminSnippetRecordSchema = snippetRecordSchema;
7168
+ var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
7169
+ var hubUserRecordSchema = z9.object({
7170
+ id: z9.string(),
7171
+ name: z9.string(),
7172
+ role: userRoleSchema,
7173
+ collectionAccess: z9.array(z9.string()),
7174
+ environmentAccess: z9.array(z9.string()),
7175
+ snippetAccess: z9.array(z9.string()),
7176
+ llmAccess: z9.boolean(),
7177
+ llmModels: z9.array(z9.string()),
7178
+ llmMonthlyTokenLimit: z9.number().int().nonnegative().nullable(),
7179
+ createdAt: timestampSchema,
7180
+ updatedAt: timestampSchema
7181
+ });
7182
+ var adminUserListEntrySchema = hubUserRecordSchema.extend({
7183
+ warnings: z9.array(z9.string())
7184
+ });
7185
+ var listAdminUsersResponseSchema = z9.object({
7186
+ users: z9.array(adminUserListEntrySchema)
7187
+ });
7188
+ var updateAdminUserBodySchema = z9.object({
7189
+ name: z9.string().trim().min(1).optional(),
7190
+ role: userRoleSchema.optional(),
7191
+ collectionAccess: z9.array(z9.string()).optional(),
7192
+ environmentAccess: z9.array(z9.string()).optional(),
7193
+ snippetAccess: z9.array(z9.string()).optional(),
7194
+ llmAccess: z9.boolean().optional(),
7195
+ llmModels: z9.array(z9.string()).optional(),
7196
+ llmMonthlyTokenLimit: z9.number().int().nonnegative().nullable().optional()
7197
+ });
7198
+ var createAdminUserBodySchema = z9.object({
7199
+ name: z9.string().trim().min(1),
7200
+ role: userRoleSchema,
7201
+ collectionAccess: z9.array(z9.string()).optional(),
7202
+ environmentAccess: z9.array(z9.string()).optional(),
7203
+ snippetAccess: z9.array(z9.string()).optional(),
7204
+ llmAccess: z9.boolean().optional(),
7205
+ llmModels: z9.array(z9.string()).optional(),
7206
+ llmMonthlyTokenLimit: z9.number().int().nonnegative().nullable().optional()
7207
+ });
7208
+ var hubApiTokenRecordSchema = z9.object({
7209
+ id: z9.string(),
7210
+ userId: z9.string(),
7211
+ name: z9.string(),
7212
+ tokenPrefix: z9.string(),
7213
+ createdAt: timestampSchema,
7214
+ lastUsedAt: timestampSchema.nullable(),
7215
+ revokedAt: timestampSchema.nullable()
7216
+ });
7217
+ var createAdminUserResponseSchema = z9.object({
7218
+ user: hubUserRecordSchema,
7219
+ token: hubApiTokenRecordSchema,
7220
+ secret: z9.string()
7221
+ });
7222
+ var createAdminTokenBodySchema = z9.object({
7223
+ name: z9.string().trim().min(1)
7224
+ });
7225
+ var createdApiTokenResponseSchema = z9.object({
7226
+ token: hubApiTokenRecordSchema,
7227
+ secret: z9.string()
7228
+ });
7229
+ var listAdminTokensResponseSchema = z9.object({
7230
+ tokens: z9.array(hubApiTokenRecordSchema)
7231
+ });
7232
+ var reloadConfigSectionResultSchema = z9.object({
7233
+ section: z9.enum(["db", "redis", "llm", "plugins", "server"]),
7234
+ status: z9.enum(["reloaded", "unchanged", "failed", "restart-required"]),
7235
+ error: z9.string().optional()
7236
+ });
7237
+ var reloadConfigResponseSchema = z9.object({
7238
+ sections: z9.array(reloadConfigSectionResultSchema),
7239
+ fatalError: z9.string().optional()
7240
+ });
7241
+ function serializeHubUser(user) {
7242
+ return {
7243
+ id: user.id,
7244
+ name: user.name,
7245
+ role: user.role,
7246
+ collectionAccess: user.collectionAccess,
7247
+ environmentAccess: user.environmentAccess,
7248
+ snippetAccess: user.snippetAccess,
7249
+ llmAccess: user.llmAccess,
7250
+ llmModels: user.llmModels,
7251
+ llmMonthlyTokenLimit: user.llmMonthlyTokenLimit,
7252
+ createdAt: user.createdAt.toISOString(),
7253
+ updatedAt: user.updatedAt.toISOString()
7254
+ };
7255
+ }
7256
+ function serializeApiToken(token) {
7257
+ return {
7258
+ id: token.id,
7259
+ userId: token.userId,
7260
+ name: token.name,
7261
+ tokenPrefix: token.tokenPrefix,
7262
+ createdAt: token.createdAt.toISOString(),
7263
+ lastUsedAt: token.lastUsedAt ? token.lastUsedAt.toISOString() : null,
7264
+ revokedAt: token.revokedAt ? token.revokedAt.toISOString() : null
7265
+ };
7266
+ }
7267
+
7268
+ // src/server/routes/admin.ts
7269
+ function sendLlmUnavailable(reply) {
7270
+ return reply.code(503).send({
7271
+ error: "LLM support is not configured on this Team Hub."
7272
+ });
7273
+ }
7274
+ function denySystemUserTarget(reply, existing, systemUserId) {
7275
+ if (!isSystemUser(existing, systemUserId)) {
7276
+ return false;
7277
+ }
7278
+ void reply.code(403).send(errorResponseSchema.parse({ error: "Forbidden" }));
7279
+ return true;
6610
7280
  }
6611
7281
  function denySelfUserTarget(reply, targetUserId, actorUserId) {
6612
7282
  if (targetUserId !== actorUserId) {
@@ -6648,14 +7318,16 @@ async function registerAdminRoutes(app, options) {
6648
7318
  }
6649
7319
  const systemUserId = db.getSystemUserId();
6650
7320
  const llm = getLlm();
6651
- const [users, collections, environments] = await Promise.all([
7321
+ const [users, collections, environments, snippets] = await Promise.all([
6652
7322
  db.listUsers(),
6653
7323
  db.listCollections(),
6654
- db.listEnvironments()
7324
+ db.listEnvironments(),
7325
+ db.listSnippets()
6655
7326
  ]);
6656
7327
  const catalogs = buildAccessCatalogIds(
6657
7328
  collections,
6658
7329
  environments,
7330
+ snippets,
6659
7331
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
6660
7332
  );
6661
7333
  return reply.send({
@@ -6665,6 +7337,7 @@ async function registerAdminRoutes(app, options) {
6665
7337
  {
6666
7338
  collectionAccess: record.collectionAccess,
6667
7339
  environmentAccess: record.environmentAccess,
7340
+ snippetAccess: record.snippetAccess,
6668
7341
  llmModels: record.llmModels
6669
7342
  },
6670
7343
  catalogs
@@ -6701,13 +7374,15 @@ async function registerAdminRoutes(app, options) {
6701
7374
  }
6702
7375
  const input = buildAdminUserCreateInput(request.body);
6703
7376
  const llm = getLlm();
6704
- const [collections, environments] = await Promise.all([
7377
+ const [collections, environments, snippets] = await Promise.all([
6705
7378
  db.listCollections(),
6706
- db.listEnvironments()
7379
+ db.listEnvironments(),
7380
+ db.listSnippets()
6707
7381
  ]);
6708
7382
  const catalogs = buildAccessCatalogIds(
6709
7383
  collections,
6710
7384
  environments,
7385
+ snippets,
6711
7386
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
6712
7387
  );
6713
7388
  validateSubmittedAccessLists(
@@ -6715,6 +7390,7 @@ async function registerAdminRoutes(app, options) {
6715
7390
  role: request.body.role,
6716
7391
  collectionAccess: request.body.collectionAccess,
6717
7392
  environmentAccess: request.body.environmentAccess,
7393
+ snippetAccess: request.body.snippetAccess,
6718
7394
  llmModels: request.body.llmModels
6719
7395
  },
6720
7396
  catalogs
@@ -6877,6 +7553,70 @@ async function registerAdminRoutes(app, options) {
6877
7553
  }
6878
7554
  }
6879
7555
  });
7556
+ routes.route({
7557
+ method: "GET",
7558
+ url: "/admin/snippets",
7559
+ schema: {
7560
+ response: {
7561
+ 200: listAdminSnippetsResponseSchema,
7562
+ 403: errorResponseSchema
7563
+ }
7564
+ },
7565
+ /**
7566
+ * Lists all snippets for operator user management.
7567
+ */
7568
+ handler: async (request, reply) => {
7569
+ try {
7570
+ const user = requireAuthenticatedUser(request);
7571
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7572
+ return;
7573
+ }
7574
+ const snippets = await db.listSnippets();
7575
+ return reply.send({
7576
+ snippets: snippets.map((snippet) => serializeSnippet(snippet))
7577
+ });
7578
+ } catch (error) {
7579
+ if (handleDbError(reply, error)) {
7580
+ return;
7581
+ }
7582
+ throw error;
7583
+ }
7584
+ }
7585
+ });
7586
+ routes.route({
7587
+ method: "POST",
7588
+ url: "/admin/snippets",
7589
+ schema: {
7590
+ body: createAdminSnippetBodySchema,
7591
+ response: {
7592
+ 201: adminSnippetRecordSchema,
7593
+ 403: errorResponseSchema
7594
+ }
7595
+ },
7596
+ /**
7597
+ * Creates a snippet through the management API.
7598
+ */
7599
+ handler: async (request, reply) => {
7600
+ try {
7601
+ const user = requireAuthenticatedUser(request);
7602
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7603
+ return;
7604
+ }
7605
+ const snippet = await db.createSnippet(
7606
+ request.body.name,
7607
+ request.body.code,
7608
+ request.body.scope,
7609
+ user.id
7610
+ );
7611
+ return reply.code(201).send(serializeSnippet(snippet));
7612
+ } catch (error) {
7613
+ if (handleDbError(reply, error)) {
7614
+ return;
7615
+ }
7616
+ throw error;
7617
+ }
7618
+ }
7619
+ });
6880
7620
  routes.route({
6881
7621
  method: "DELETE",
6882
7622
  url: "/admin/collections/:id",
@@ -6947,6 +7687,84 @@ async function registerAdminRoutes(app, options) {
6947
7687
  }
6948
7688
  }
6949
7689
  });
7690
+ routes.route({
7691
+ method: "DELETE",
7692
+ url: "/admin/snippets/:id",
7693
+ schema: {
7694
+ params: idParamSchema,
7695
+ response: {
7696
+ 204: emptyResponseSchema,
7697
+ 403: errorResponseSchema,
7698
+ 404: errorResponseSchema
7699
+ }
7700
+ },
7701
+ /**
7702
+ * Deletes a snippet regardless of deletion lock state.
7703
+ */
7704
+ handler: async (request, reply) => {
7705
+ try {
7706
+ const user = requireAuthenticatedUser(request);
7707
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7708
+ return;
7709
+ }
7710
+ const snippet = await db.findSnippetById(request.params.id);
7711
+ if (!snippet) {
7712
+ void reply.code(404).send({ error: "Snippet not found" });
7713
+ return;
7714
+ }
7715
+ await db.deleteSnippet(request.params.id, user.id);
7716
+ return reply.code(204).send(null);
7717
+ } catch (error) {
7718
+ if (handleDbError(reply, error)) {
7719
+ return;
7720
+ }
7721
+ throw error;
7722
+ }
7723
+ }
7724
+ });
7725
+ routes.route({
7726
+ method: "PUT",
7727
+ url: "/admin/snippets/:id",
7728
+ schema: {
7729
+ params: idParamSchema,
7730
+ body: updateAdminSnippetBodySchema,
7731
+ response: {
7732
+ 200: adminSnippetRecordSchema,
7733
+ 403: errorResponseSchema,
7734
+ 404: errorResponseSchema
7735
+ }
7736
+ },
7737
+ /**
7738
+ * Updates snippet content and/or admin configuration.
7739
+ */
7740
+ handler: async (request, reply) => {
7741
+ try {
7742
+ const user = requireAuthenticatedUser(request);
7743
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7744
+ return;
7745
+ }
7746
+ const existing = await db.findSnippetById(request.params.id);
7747
+ if (!existing) {
7748
+ void reply.code(404).send({ error: "Snippet not found" });
7749
+ return;
7750
+ }
7751
+ let snippet = existing;
7752
+ const { name, code, scope, deletionLocked } = request.body;
7753
+ if (name !== void 0 && code !== void 0 && scope !== void 0) {
7754
+ snippet = await db.updateSnippet(request.params.id, name, code, scope, user.id);
7755
+ }
7756
+ if (deletionLocked !== void 0) {
7757
+ snippet = await db.setSnippetDeletionLocked(request.params.id, deletionLocked, user.id);
7758
+ }
7759
+ return reply.send(serializeSnippet(snippet));
7760
+ } catch (error) {
7761
+ if (handleDbError(reply, error)) {
7762
+ return;
7763
+ }
7764
+ throw error;
7765
+ }
7766
+ }
7767
+ });
6950
7768
  routes.route({
6951
7769
  method: "PUT",
6952
7770
  url: "/admin/collections/:id",
@@ -7127,13 +7945,15 @@ async function registerAdminRoutes(app, options) {
7127
7945
  const input = buildAdminUserUpdateInput(existing, request.body);
7128
7946
  const role = request.body.role ?? existing.role;
7129
7947
  const llm = getLlm();
7130
- const [collections, environments] = await Promise.all([
7948
+ const [collections, environments, snippets] = await Promise.all([
7131
7949
  db.listCollections(),
7132
- db.listEnvironments()
7950
+ db.listEnvironments(),
7951
+ db.listSnippets()
7133
7952
  ]);
7134
7953
  const catalogs = buildAccessCatalogIds(
7135
7954
  collections,
7136
7955
  environments,
7956
+ snippets,
7137
7957
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
7138
7958
  );
7139
7959
  validateSubmittedAccessLists(
@@ -7141,6 +7961,7 @@ async function registerAdminRoutes(app, options) {
7141
7961
  role,
7142
7962
  collectionAccess: request.body.collectionAccess,
7143
7963
  environmentAccess: request.body.environmentAccess,
7964
+ snippetAccess: request.body.snippetAccess,
7144
7965
  llmModels: request.body.llmModels
7145
7966
  },
7146
7967
  catalogs
@@ -7677,6 +8498,150 @@ async function registerEnvironmentRoutes(app, db) {
7677
8498
  });
7678
8499
  }
7679
8500
 
8501
+ // src/server/routes/snippets.ts
8502
+ async function registerSnippetRoutes(app, db) {
8503
+ const routes = app.withTypeProvider();
8504
+ routes.route({
8505
+ method: "GET",
8506
+ url: "/snippets",
8507
+ schema: {
8508
+ response: {
8509
+ 200: listSnippetsResponseSchema
8510
+ }
8511
+ },
8512
+ /**
8513
+ * Lists all snippets ordered by sort order then name.
8514
+ */
8515
+ handler: async (request, reply) => {
8516
+ try {
8517
+ const user = requireAuthenticatedUser(request);
8518
+ if (denyUnlessAllowed(reply, canListSnippets(user))) {
8519
+ return;
8520
+ }
8521
+ const snippets = await db.listSnippets();
8522
+ return reply.send({
8523
+ snippets: filterAccessibleSnippets(user, snippets).map(
8524
+ (snippet) => serializeSnippet(snippet)
8525
+ )
8526
+ });
8527
+ } catch (error) {
8528
+ if (handleDbError(reply, error)) {
8529
+ return;
8530
+ }
8531
+ throw error;
8532
+ }
8533
+ }
8534
+ });
8535
+ routes.route({
8536
+ method: "POST",
8537
+ url: "/snippets",
8538
+ schema: {
8539
+ body: createSnippetBodySchema,
8540
+ response: {
8541
+ 200: snippetRecordSchema,
8542
+ 400: errorResponseSchema
8543
+ }
8544
+ },
8545
+ /**
8546
+ * Creates a new snippet with the given display name and content.
8547
+ */
8548
+ handler: async (request, reply) => {
8549
+ try {
8550
+ const user = requireAuthenticatedUser(request);
8551
+ if (denyUnlessAllowed(reply, canUseDataApi(user) && canCreateSnippet(user))) {
8552
+ return;
8553
+ }
8554
+ const snippet = await db.createSnippet(
8555
+ request.body.name,
8556
+ request.body.code,
8557
+ request.body.scope,
8558
+ user.id
8559
+ );
8560
+ return reply.send(serializeSnippet(snippet));
8561
+ } catch (error) {
8562
+ if (handleDbError(reply, error)) {
8563
+ return;
8564
+ }
8565
+ throw error;
8566
+ }
8567
+ }
8568
+ });
8569
+ routes.route({
8570
+ method: "PUT",
8571
+ url: "/snippets/:id",
8572
+ schema: {
8573
+ params: idParamSchema,
8574
+ body: updateSnippetBodySchema,
8575
+ response: {
8576
+ 200: snippetRecordSchema,
8577
+ 400: errorResponseSchema,
8578
+ 404: errorResponseSchema
8579
+ }
8580
+ },
8581
+ /**
8582
+ * Updates a snippet's name, code, and scope. Sort order is preserved.
8583
+ */
8584
+ handler: async (request, reply) => {
8585
+ try {
8586
+ const user = requireAuthenticatedUser(request);
8587
+ if (denyUnlessAllowed(reply, canUseDataApi(user) && canAccessSnippet(user, request.params.id))) {
8588
+ return;
8589
+ }
8590
+ const snippet = await db.updateSnippet(
8591
+ request.params.id,
8592
+ request.body.name,
8593
+ request.body.code,
8594
+ request.body.scope,
8595
+ user.id
8596
+ );
8597
+ return reply.send(serializeSnippet(snippet));
8598
+ } catch (error) {
8599
+ if (handleDbError(reply, error)) {
8600
+ return;
8601
+ }
8602
+ throw error;
8603
+ }
8604
+ }
8605
+ });
8606
+ routes.route({
8607
+ method: "DELETE",
8608
+ url: "/snippets/:id",
8609
+ schema: {
8610
+ params: idParamSchema,
8611
+ response: {
8612
+ 204: emptyResponseSchema,
8613
+ 404: errorResponseSchema
8614
+ }
8615
+ },
8616
+ /**
8617
+ * Deletes a snippet by id.
8618
+ */
8619
+ handler: async (request, reply) => {
8620
+ try {
8621
+ const user = requireAuthenticatedUser(request);
8622
+ if (denyUnlessAllowed(reply, canUseDataApi(user) && canAccessSnippet(user, request.params.id))) {
8623
+ return;
8624
+ }
8625
+ const snippet = await db.findSnippetById(request.params.id);
8626
+ if (!snippet) {
8627
+ void reply.code(404).send({ error: "Snippet not found" });
8628
+ return;
8629
+ }
8630
+ if (snippet.deletionLocked) {
8631
+ throw new DeletionLockedError("snippet");
8632
+ }
8633
+ await db.deleteSnippet(request.params.id, user.id);
8634
+ return reply.code(204).send(null);
8635
+ } catch (error) {
8636
+ if (handleDbError(reply, error)) {
8637
+ return;
8638
+ }
8639
+ throw error;
8640
+ }
8641
+ }
8642
+ });
8643
+ }
8644
+
7680
8645
  // src/server/routes/folders.ts
7681
8646
  async function registerFolderRoutes(app, db) {
7682
8647
  const routes = app.withTypeProvider();
@@ -8778,6 +9743,7 @@ async function registerProtectedRoutes(app, options) {
8778
9743
  });
8779
9744
  await registerCollectionRoutes(app, options.db);
8780
9745
  await registerEnvironmentRoutes(app, options.db);
9746
+ await registerSnippetRoutes(app, options.db);
8781
9747
  await registerFolderRoutes(app, options.db);
8782
9748
  await registerRequestRoutes(app, options.db);
8783
9749
  await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });