@harborclient/team-hub 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -431,7 +431,8 @@ function mapFirestoreCollection(id, data) {
431
431
  createdAt: data.createdAt,
432
432
  updatedAt: data.updatedAt ?? data.createdAt,
433
433
  createdByUserId: data.createdByUserId ?? null,
434
- updatedByUserId: data.updatedByUserId ?? null
434
+ updatedByUserId: data.updatedByUserId ?? null,
435
+ deletionLocked: data.deletionLocked ?? false
435
436
  };
436
437
  }
437
438
  function mapFirestoreEnvironment(id, data) {
@@ -442,7 +443,8 @@ function mapFirestoreEnvironment(id, data) {
442
443
  createdAt: data.createdAt,
443
444
  updatedAt: data.updatedAt ?? data.createdAt,
444
445
  createdByUserId: data.createdByUserId ?? null,
445
- updatedByUserId: data.updatedByUserId ?? null
446
+ updatedByUserId: data.updatedByUserId ?? null,
447
+ deletionLocked: data.deletionLocked ?? false
446
448
  };
447
449
  }
448
450
  function mapFirestoreFolder(id, data) {
@@ -988,7 +990,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
988
990
  createdAt: now,
989
991
  updatedAt: now,
990
992
  createdByUserId: actingUserId,
991
- updatedByUserId: actingUserId
993
+ updatedByUserId: actingUserId,
994
+ deletionLocked: false
992
995
  };
993
996
  await this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id).set(data);
994
997
  await this.recordAuditEntry(actingUserId, "create", "collection", id);
@@ -1050,6 +1053,46 @@ var FirestoreDatabase = class _FirestoreDatabase {
1050
1053
  ];
1051
1054
  await this.commitBatchedDeletes(refs);
1052
1055
  }
1056
+ /**
1057
+ * Finds a collection by stable identifier.
1058
+ *
1059
+ * @param id - Collection ID to look up.
1060
+ */
1061
+ async findCollectionById(id) {
1062
+ const snapshot = await this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id).get();
1063
+ if (!snapshot.exists) {
1064
+ return null;
1065
+ }
1066
+ return mapFirestoreCollection(id, snapshot.data());
1067
+ }
1068
+ /**
1069
+ * Updates whether non-admin users may delete a collection.
1070
+ *
1071
+ * @param id - Collection ID to update.
1072
+ * @param deletionLocked - When true, user-role tokens cannot delete the collection.
1073
+ * @param actingUserId - Admin user performing the update.
1074
+ */
1075
+ async setCollectionDeletionLocked(id, deletionLocked, actingUserId) {
1076
+ const docRef = this.requireClient().collection(COLLECTIONS_COLLECTION).doc(id);
1077
+ const snapshot = await docRef.get();
1078
+ if (!snapshot.exists) {
1079
+ throw new Error("Collection not found");
1080
+ }
1081
+ const updatedAt = /* @__PURE__ */ new Date();
1082
+ await docRef.update({
1083
+ deletionLocked,
1084
+ updatedAt,
1085
+ updatedByUserId: actingUserId
1086
+ });
1087
+ await this.recordAuditEntry(actingUserId, "update", "collection", id);
1088
+ const existing = snapshot.data();
1089
+ return mapFirestoreCollection(id, {
1090
+ ...existing,
1091
+ deletionLocked,
1092
+ updatedAt,
1093
+ updatedByUserId: actingUserId
1094
+ });
1095
+ }
1053
1096
  /**
1054
1097
  * Lists all environments ordered by name.
1055
1098
  */
@@ -1075,7 +1118,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
1075
1118
  createdAt: now,
1076
1119
  updatedAt: now,
1077
1120
  createdByUserId: actingUserId,
1078
- updatedByUserId: actingUserId
1121
+ updatedByUserId: actingUserId,
1122
+ deletionLocked: false
1079
1123
  };
1080
1124
  await this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id).set(data);
1081
1125
  await this.recordAuditEntry(actingUserId, "create", "environment", id);
@@ -1121,6 +1165,46 @@ var FirestoreDatabase = class _FirestoreDatabase {
1121
1165
  await this.recordAuditEntry(actingUserId, "delete", "environment", id);
1122
1166
  await this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id).delete();
1123
1167
  }
1168
+ /**
1169
+ * Finds an environment by stable identifier.
1170
+ *
1171
+ * @param id - Environment ID to look up.
1172
+ */
1173
+ async findEnvironmentById(id) {
1174
+ const snapshot = await this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id).get();
1175
+ if (!snapshot.exists) {
1176
+ return null;
1177
+ }
1178
+ return mapFirestoreEnvironment(id, snapshot.data());
1179
+ }
1180
+ /**
1181
+ * Updates whether non-admin users may delete an environment.
1182
+ *
1183
+ * @param id - Environment ID to update.
1184
+ * @param deletionLocked - When true, user-role tokens cannot delete the environment.
1185
+ * @param actingUserId - Admin user performing the update.
1186
+ */
1187
+ async setEnvironmentDeletionLocked(id, deletionLocked, actingUserId) {
1188
+ const docRef = this.requireClient().collection(ENVIRONMENTS_COLLECTION).doc(id);
1189
+ const snapshot = await docRef.get();
1190
+ if (!snapshot.exists) {
1191
+ throw new Error("Environment not found");
1192
+ }
1193
+ const updatedAt = /* @__PURE__ */ new Date();
1194
+ await docRef.update({
1195
+ deletionLocked,
1196
+ updatedAt,
1197
+ updatedByUserId: actingUserId
1198
+ });
1199
+ await this.recordAuditEntry(actingUserId, "update", "environment", id);
1200
+ const existing = snapshot.data();
1201
+ return mapFirestoreEnvironment(id, {
1202
+ ...existing,
1203
+ deletionLocked,
1204
+ updatedAt,
1205
+ updatedByUserId: actingUserId
1206
+ });
1207
+ }
1124
1208
  /**
1125
1209
  * Lists all saved requests in a collection.
1126
1210
  *
@@ -1731,7 +1815,8 @@ function mapCollectionSqlRow(row) {
1731
1815
  createdAt: row.created_at,
1732
1816
  updatedAt: row.updated_at ?? row.created_at,
1733
1817
  createdByUserId: row.created_by_user_id ?? null,
1734
- updatedByUserId: row.updated_by_user_id ?? null
1818
+ updatedByUserId: row.updated_by_user_id ?? null,
1819
+ deletionLocked: Boolean(row.deletion_locked)
1735
1820
  };
1736
1821
  }
1737
1822
  function mapEnvironmentSqlRow(row) {
@@ -1742,7 +1827,8 @@ function mapEnvironmentSqlRow(row) {
1742
1827
  createdAt: row.created_at,
1743
1828
  updatedAt: row.updated_at ?? row.created_at,
1744
1829
  createdByUserId: row.created_by_user_id ?? null,
1745
- updatedByUserId: row.updated_by_user_id ?? null
1830
+ updatedByUserId: row.updated_by_user_id ?? null,
1831
+ deletionLocked: Boolean(row.deletion_locked)
1746
1832
  };
1747
1833
  }
1748
1834
  function mapFolderSqlRow(row) {
@@ -1985,6 +2071,14 @@ CREATE TABLE IF NOT EXISTS llm_usage_log (
1985
2071
  )
1986
2072
  `.trim();
1987
2073
  var MYSQL_DEFAULT_AUTH_JSON = DEFAULT_AUTH_JSON;
2074
+ var COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL = `
2075
+ ALTER TABLE collections
2076
+ ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
2077
+ `.trim();
2078
+ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL = `
2079
+ ALTER TABLE environments
2080
+ ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
2081
+ `.trim();
1988
2082
  var MYSQL_MIGRATIONS = [
1989
2083
  USERS_MIGRATION_SQL,
1990
2084
  API_TOKENS_MIGRATION_SQL,
@@ -2005,7 +2099,9 @@ var MYSQL_MIGRATIONS = [
2005
2099
  FOLDERS_BACKFILL_UPDATED_AT_SQL,
2006
2100
  USERS_LLM_MIGRATION_SQL,
2007
2101
  LLM_USAGE_MIGRATION_SQL,
2008
- LLM_USAGE_LOG_MIGRATION_SQL
2102
+ LLM_USAGE_LOG_MIGRATION_SQL,
2103
+ COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
2104
+ ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL
2009
2105
  ];
2010
2106
 
2011
2107
  // src/db/mysql/schemas.ts
@@ -2062,8 +2158,8 @@ function serializeAccessList(access) {
2062
2158
  return JSON.stringify(access);
2063
2159
  }
2064
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`;
2065
- 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`;
2066
- var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2161
+ var COLLECTION_SELECT_COLUMNS = `id, name, variables, headers, auth, pre_request_script, post_request_script, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
2162
+ var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
2067
2163
  var FOLDER_SELECT_COLUMNS = `id, collection_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2068
2164
  var REQUEST_SELECT_COLUMNS = `id, collection_id, folder_id, name, method, url, headers, params, auth, body, body_type, pre_request_script, post_request_script, comment, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2069
2165
  var API_TOKEN_SELECT_COLUMNS = `id, user_id, name, token_hash, token_prefix, created_at, last_used_at, revoked_at, created_by_user_id, updated_by_user_id`;
@@ -2655,6 +2751,50 @@ var MysqlDatabase = class _MysqlDatabase {
2655
2751
  await this.recordAuditEntry(actingUserId, "delete", "collection", id);
2656
2752
  await this.executeStatement("DELETE FROM collections WHERE id = ?", [id]);
2657
2753
  }
2754
+ /**
2755
+ * Finds a collection by stable identifier.
2756
+ *
2757
+ * @param id - Collection ID to look up.
2758
+ */
2759
+ async findCollectionById(id) {
2760
+ const rows = await this.queryRows(
2761
+ `${COLLECTION_SELECT} WHERE id = ?`,
2762
+ [id]
2763
+ );
2764
+ const row = rows[0];
2765
+ return row ? mapCollectionSqlRow(row) : null;
2766
+ }
2767
+ /**
2768
+ * Updates whether non-admin users may delete a collection.
2769
+ *
2770
+ * @param id - Collection ID to update.
2771
+ * @param deletionLocked - When true, user-role tokens cannot delete the collection.
2772
+ * @param actingUserId - Admin user performing the update.
2773
+ */
2774
+ async setCollectionDeletionLocked(id, deletionLocked, actingUserId) {
2775
+ const updatedAt = /* @__PURE__ */ new Date();
2776
+ const result = await this.executeStatement(
2777
+ `UPDATE collections
2778
+ SET deletion_locked = ?,
2779
+ updated_at = ?,
2780
+ updated_by_user_id = ?
2781
+ WHERE id = ?`,
2782
+ [deletionLocked ? 1 : 0, updatedAt, actingUserId, id]
2783
+ );
2784
+ if ((result.affectedRows ?? 0) === 0) {
2785
+ throw new Error("Collection not found");
2786
+ }
2787
+ await this.recordAuditEntry(actingUserId, "update", "collection", id);
2788
+ const rows = await this.queryRows(
2789
+ `${COLLECTION_SELECT} WHERE id = ?`,
2790
+ [id]
2791
+ );
2792
+ const row = rows[0];
2793
+ if (!row) {
2794
+ throw new Error("Collection not found");
2795
+ }
2796
+ return mapCollectionSqlRow(row);
2797
+ }
2658
2798
  /**
2659
2799
  * Lists all environments ordered by name.
2660
2800
  */
@@ -2738,6 +2878,50 @@ var MysqlDatabase = class _MysqlDatabase {
2738
2878
  await this.recordAuditEntry(actingUserId, "delete", "environment", id);
2739
2879
  await this.executeStatement("DELETE FROM environments WHERE id = ?", [id]);
2740
2880
  }
2881
+ /**
2882
+ * Finds an environment by stable identifier.
2883
+ *
2884
+ * @param id - Environment ID to look up.
2885
+ */
2886
+ async findEnvironmentById(id) {
2887
+ const rows = await this.queryRows(
2888
+ `${ENVIRONMENT_SELECT} WHERE id = ?`,
2889
+ [id]
2890
+ );
2891
+ const row = rows[0];
2892
+ return row ? mapEnvironmentSqlRow(row) : null;
2893
+ }
2894
+ /**
2895
+ * Updates whether non-admin users may delete an environment.
2896
+ *
2897
+ * @param id - Environment ID to update.
2898
+ * @param deletionLocked - When true, user-role tokens cannot delete the environment.
2899
+ * @param actingUserId - Admin user performing the update.
2900
+ */
2901
+ async setEnvironmentDeletionLocked(id, deletionLocked, actingUserId) {
2902
+ const updatedAt = /* @__PURE__ */ new Date();
2903
+ const result = await this.executeStatement(
2904
+ `UPDATE environments
2905
+ SET deletion_locked = ?,
2906
+ updated_at = ?,
2907
+ updated_by_user_id = ?
2908
+ WHERE id = ?`,
2909
+ [deletionLocked ? 1 : 0, updatedAt, actingUserId, id]
2910
+ );
2911
+ if ((result.affectedRows ?? 0) === 0) {
2912
+ throw new Error("Environment not found");
2913
+ }
2914
+ await this.recordAuditEntry(actingUserId, "update", "environment", id);
2915
+ const rows = await this.queryRows(
2916
+ `${ENVIRONMENT_SELECT} WHERE id = ?`,
2917
+ [id]
2918
+ );
2919
+ const row = rows[0];
2920
+ if (!row) {
2921
+ throw new Error("Environment not found");
2922
+ }
2923
+ return mapEnvironmentSqlRow(row);
2924
+ }
2741
2925
  /**
2742
2926
  * Lists all saved requests in a collection.
2743
2927
  *
@@ -3590,6 +3774,14 @@ CREATE INDEX IF NOT EXISTS llm_usage_log_user_created_at_idx ON llm_usage_log (u
3590
3774
 
3591
3775
  CREATE INDEX IF NOT EXISTS llm_usage_log_period_idx ON llm_usage_log (period);
3592
3776
  `.trim();
3777
+ var COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2 = `
3778
+ ALTER TABLE collections
3779
+ ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
3780
+ `.trim();
3781
+ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2 = `
3782
+ ALTER TABLE environments
3783
+ ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
3784
+ `.trim();
3593
3785
  var POSTGRES_MIGRATIONS = [
3594
3786
  USERS_MIGRATION_SQL2,
3595
3787
  API_TOKENS_MIGRATION_SQL2,
@@ -3610,7 +3802,9 @@ var POSTGRES_MIGRATIONS = [
3610
3802
  FOLDERS_BACKFILL_UPDATED_AT_SQL2,
3611
3803
  USERS_LLM_MIGRATION_SQL2,
3612
3804
  LLM_USAGE_MIGRATION_SQL2,
3613
- LLM_USAGE_LOG_MIGRATION_SQL2
3805
+ LLM_USAGE_LOG_MIGRATION_SQL2,
3806
+ COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
3807
+ ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2
3614
3808
  ];
3615
3809
 
3616
3810
  // src/db/postgres/schemas.ts
@@ -4172,6 +4366,46 @@ var PostgresDatabase = class _PostgresDatabase {
4172
4366
  await this.recordAuditEntry(actingUserId, "delete", "collection", id);
4173
4367
  await this.query("DELETE FROM collections WHERE id = $1", [id]);
4174
4368
  }
4369
+ /**
4370
+ * Finds a collection by stable identifier.
4371
+ *
4372
+ * @param id - Collection ID to look up.
4373
+ */
4374
+ async findCollectionById(id) {
4375
+ const result = await this.query(`${COLLECTION_SELECT2} WHERE id = $1`, [id]);
4376
+ const row = result.rows[0];
4377
+ return row ? mapCollectionSqlRow(row) : null;
4378
+ }
4379
+ /**
4380
+ * Updates whether non-admin users may delete a collection.
4381
+ *
4382
+ * @param id - Collection ID to update.
4383
+ * @param deletionLocked - When true, user-role tokens cannot delete the collection.
4384
+ * @param actingUserId - Admin user performing the update.
4385
+ */
4386
+ async setCollectionDeletionLocked(id, deletionLocked, actingUserId) {
4387
+ const updatedAt = /* @__PURE__ */ new Date();
4388
+ const result = await this.query(
4389
+ `UPDATE collections
4390
+ SET deletion_locked = $1,
4391
+ updated_at = $2,
4392
+ updated_by_user_id = $3
4393
+ WHERE id = $4`,
4394
+ [deletionLocked, updatedAt, actingUserId, id]
4395
+ );
4396
+ if ((result.rowCount ?? 0) === 0) {
4397
+ throw new Error("Collection not found");
4398
+ }
4399
+ await this.recordAuditEntry(actingUserId, "update", "collection", id);
4400
+ const selectResult = await this.query(`${COLLECTION_SELECT2} WHERE id = $1`, [
4401
+ id
4402
+ ]);
4403
+ const row = selectResult.rows[0];
4404
+ if (!row) {
4405
+ throw new Error("Collection not found");
4406
+ }
4407
+ return mapCollectionSqlRow(row);
4408
+ }
4175
4409
  /**
4176
4410
  * Lists all environments ordered by name.
4177
4411
  */
@@ -4250,6 +4484,47 @@ var PostgresDatabase = class _PostgresDatabase {
4250
4484
  await this.recordAuditEntry(actingUserId, "delete", "environment", id);
4251
4485
  await this.query("DELETE FROM environments WHERE id = $1", [id]);
4252
4486
  }
4487
+ /**
4488
+ * Finds an environment by stable identifier.
4489
+ *
4490
+ * @param id - Environment ID to look up.
4491
+ */
4492
+ async findEnvironmentById(id) {
4493
+ const result = await this.query(`${ENVIRONMENT_SELECT2} WHERE id = $1`, [id]);
4494
+ const row = result.rows[0];
4495
+ return row ? mapEnvironmentSqlRow(row) : null;
4496
+ }
4497
+ /**
4498
+ * Updates whether non-admin users may delete an environment.
4499
+ *
4500
+ * @param id - Environment ID to update.
4501
+ * @param deletionLocked - When true, user-role tokens cannot delete the environment.
4502
+ * @param actingUserId - Admin user performing the update.
4503
+ */
4504
+ async setEnvironmentDeletionLocked(id, deletionLocked, actingUserId) {
4505
+ const updatedAt = /* @__PURE__ */ new Date();
4506
+ const result = await this.query(
4507
+ `UPDATE environments
4508
+ SET deletion_locked = $1,
4509
+ updated_at = $2,
4510
+ updated_by_user_id = $3
4511
+ WHERE id = $4`,
4512
+ [deletionLocked, updatedAt, actingUserId, id]
4513
+ );
4514
+ if ((result.rowCount ?? 0) === 0) {
4515
+ throw new Error("Environment not found");
4516
+ }
4517
+ await this.recordAuditEntry(actingUserId, "update", "environment", id);
4518
+ const selectResult = await this.query(
4519
+ `${ENVIRONMENT_SELECT2} WHERE id = $1`,
4520
+ [id]
4521
+ );
4522
+ const row = selectResult.rows[0];
4523
+ if (!row) {
4524
+ throw new Error("Environment not found");
4525
+ }
4526
+ return mapEnvironmentSqlRow(row);
4527
+ }
4253
4528
  /**
4254
4529
  * Lists all saved requests in a collection.
4255
4530
  *
@@ -5744,6 +6019,9 @@ function canUseDataApi(user) {
5744
6019
  function canListCollections(user) {
5745
6020
  return canUseDataApi(user) || canUseManagementApi(user);
5746
6021
  }
6022
+ function canListEnvironments(user) {
6023
+ return canUseDataApi(user) || canUseManagementApi(user);
6024
+ }
5747
6025
  function hasWildcardAccess(access) {
5748
6026
  return access.includes("*");
5749
6027
  }
@@ -5765,6 +6043,12 @@ function canAccessEnvironment(user, environmentId) {
5765
6043
  }
5766
6044
  return user.environmentAccess.includes(environmentId);
5767
6045
  }
6046
+ function canDeleteCollection(user, collection) {
6047
+ return canUseDataApi(user) && canAccessCollection(user, collection.id) && collection.createdByUserId === user.id && !collection.deletionLocked;
6048
+ }
6049
+ function canDeleteRequest(user, request) {
6050
+ return canUseDataApi(user) && canAccessCollection(user, request.collectionId) && request.createdByUserId === user.id;
6051
+ }
5768
6052
  function canCreateCollection(user) {
5769
6053
  return user.role === "user" && hasWildcardAccess(user.collectionAccess);
5770
6054
  }
@@ -5772,20 +6056,14 @@ function canCreateEnvironment(user) {
5772
6056
  return user.role === "user" && hasWildcardAccess(user.environmentAccess);
5773
6057
  }
5774
6058
  function filterAccessibleCollections(user, collections) {
5775
- if (user.role === "admin") {
5776
- return [];
5777
- }
5778
- if (hasWildcardAccess(user.collectionAccess)) {
6059
+ if (user.role === "admin" || hasWildcardAccess(user.collectionAccess)) {
5779
6060
  return collections;
5780
6061
  }
5781
6062
  const allowed = new Set(user.collectionAccess);
5782
6063
  return collections.filter((collection) => allowed.has(collection.id));
5783
6064
  }
5784
6065
  function filterAccessibleEnvironments(user, environments) {
5785
- if (user.role === "admin") {
5786
- return [];
5787
- }
5788
- if (hasWildcardAccess(user.environmentAccess)) {
6066
+ if (user.role === "admin" || hasWildcardAccess(user.environmentAccess)) {
5789
6067
  return environments;
5790
6068
  }
5791
6069
  const allowed = new Set(user.environmentAccess);
@@ -5810,6 +6088,17 @@ function isOverMonthlyLimit(totalTokens, limit) {
5810
6088
  return totalTokens >= limit;
5811
6089
  }
5812
6090
 
6091
+ // src/db/deletionLockedError.ts
6092
+ var DeletionLockedError = class extends Error {
6093
+ /**
6094
+ * @param entityType - Human-readable entity kind shown in the error message.
6095
+ */
6096
+ constructor(entityType) {
6097
+ super(`Deletion is locked for this ${entityType}.`);
6098
+ this.name = "DeletionLockedError";
6099
+ }
6100
+ };
6101
+
5813
6102
  // src/server/routes/schemas/common.ts
5814
6103
  import { z as z5 } from "zod/v4";
5815
6104
  var errorResponseSchema = z5.object({
@@ -5886,6 +6175,10 @@ function handleDbError(reply, error) {
5886
6175
  void reply.code(400).send(errorResponseSchema.parse({ error: error.message }));
5887
6176
  return true;
5888
6177
  }
6178
+ if (error instanceof DeletionLockedError) {
6179
+ void reply.code(403).send(errorResponseSchema.parse({ error: error.message }));
6180
+ return true;
6181
+ }
5889
6182
  if (isDuplicateUserNameDbError(error)) {
5890
6183
  void reply.code(400).send(errorResponseSchema.parse({ error: DUPLICATE_USER_NAME_MESSAGE }));
5891
6184
  return true;
@@ -5990,7 +6283,19 @@ var llmUsageSummaryResponseSchema = z7.object({
5990
6283
  // src/server/routes/schemas/admin.ts
5991
6284
  var adminResourceOptionSchema = z8.object({
5992
6285
  id: z8.string(),
5993
- name: z8.string()
6286
+ name: z8.string(),
6287
+ deletionLocked: z8.boolean()
6288
+ });
6289
+ var adminEntityConfigSchema = z8.object({
6290
+ id: z8.string(),
6291
+ name: z8.string(),
6292
+ deletionLocked: z8.boolean()
6293
+ });
6294
+ var updateAdminCollectionBodySchema = z8.object({
6295
+ deletionLocked: z8.boolean()
6296
+ });
6297
+ var updateAdminEnvironmentBodySchema = z8.object({
6298
+ deletionLocked: z8.boolean()
5994
6299
  });
5995
6300
  var listAdminCollectionsResponseSchema = z8.object({
5996
6301
  collections: z8.array(adminResourceOptionSchema)
@@ -6107,7 +6412,8 @@ var collectionRecordSchema = z9.object({
6107
6412
  createdAt: timestampSchema,
6108
6413
  updatedAt: timestampSchema,
6109
6414
  createdByUserId: z9.string().nullable(),
6110
- updatedByUserId: z9.string().nullable()
6415
+ updatedByUserId: z9.string().nullable(),
6416
+ deletionLocked: z9.boolean()
6111
6417
  });
6112
6418
  var environmentRecordSchema = z9.object({
6113
6419
  id: z9.string(),
@@ -6116,7 +6422,8 @@ var environmentRecordSchema = z9.object({
6116
6422
  createdAt: timestampSchema,
6117
6423
  updatedAt: timestampSchema,
6118
6424
  createdByUserId: z9.string().nullable(),
6119
- updatedByUserId: z9.string().nullable()
6425
+ updatedByUserId: z9.string().nullable(),
6426
+ deletionLocked: z9.boolean()
6120
6427
  });
6121
6428
  var folderRecordSchema = z9.object({
6122
6429
  id: z9.string(),
@@ -6405,7 +6712,8 @@ async function registerAdminRoutes(app, options) {
6405
6712
  return reply.send({
6406
6713
  collections: collections.map((collection) => ({
6407
6714
  id: collection.id,
6408
- name: collection.name
6715
+ name: collection.name,
6716
+ deletionLocked: collection.deletionLocked
6409
6717
  }))
6410
6718
  });
6411
6719
  } catch (error) {
@@ -6416,6 +6724,80 @@ async function registerAdminRoutes(app, options) {
6416
6724
  }
6417
6725
  }
6418
6726
  });
6727
+ routes.route({
6728
+ method: "GET",
6729
+ url: "/admin/collections/:collectionId/folders",
6730
+ schema: {
6731
+ params: collectionIdParamSchema,
6732
+ response: {
6733
+ 200: listFoldersResponseSchema,
6734
+ 403: errorResponseSchema,
6735
+ 404: errorResponseSchema
6736
+ }
6737
+ },
6738
+ /**
6739
+ * Lists folders in a collection for operator inspection.
6740
+ */
6741
+ handler: async (request, reply) => {
6742
+ try {
6743
+ const user = requireAuthenticatedUser(request);
6744
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6745
+ return;
6746
+ }
6747
+ const collection = await db.findCollectionById(request.params.collectionId);
6748
+ if (!collection) {
6749
+ void reply.code(404).send({ error: "Collection not found" });
6750
+ return;
6751
+ }
6752
+ const folders = await db.listFolders(request.params.collectionId);
6753
+ return reply.send({
6754
+ folders: folders.map((folder) => serializeFolder(folder))
6755
+ });
6756
+ } catch (error) {
6757
+ if (handleDbError(reply, error)) {
6758
+ return;
6759
+ }
6760
+ throw error;
6761
+ }
6762
+ }
6763
+ });
6764
+ routes.route({
6765
+ method: "GET",
6766
+ url: "/admin/collections/:collectionId/requests",
6767
+ schema: {
6768
+ params: collectionIdParamSchema,
6769
+ response: {
6770
+ 200: listRequestsResponseSchema,
6771
+ 403: errorResponseSchema,
6772
+ 404: errorResponseSchema
6773
+ }
6774
+ },
6775
+ /**
6776
+ * Lists saved requests in a collection for operator inspection.
6777
+ */
6778
+ handler: async (request, reply) => {
6779
+ try {
6780
+ const user = requireAuthenticatedUser(request);
6781
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6782
+ return;
6783
+ }
6784
+ const collection = await db.findCollectionById(request.params.collectionId);
6785
+ if (!collection) {
6786
+ void reply.code(404).send({ error: "Collection not found" });
6787
+ return;
6788
+ }
6789
+ const requests = await db.listRequests(request.params.collectionId);
6790
+ return reply.send({
6791
+ requests: requests.map((savedRequest) => serializeSavedRequest(savedRequest))
6792
+ });
6793
+ } catch (error) {
6794
+ if (handleDbError(reply, error)) {
6795
+ return;
6796
+ }
6797
+ throw error;
6798
+ }
6799
+ }
6800
+ });
6419
6801
  routes.route({
6420
6802
  method: "GET",
6421
6803
  url: "/admin/environments",
@@ -6438,7 +6820,8 @@ async function registerAdminRoutes(app, options) {
6438
6820
  return reply.send({
6439
6821
  environments: environments.map((environment) => ({
6440
6822
  id: environment.id,
6441
- name: environment.name
6823
+ name: environment.name,
6824
+ deletionLocked: environment.deletionLocked
6442
6825
  }))
6443
6826
  });
6444
6827
  } catch (error) {
@@ -6449,6 +6832,189 @@ async function registerAdminRoutes(app, options) {
6449
6832
  }
6450
6833
  }
6451
6834
  });
6835
+ routes.route({
6836
+ method: "DELETE",
6837
+ url: "/admin/collections/:id",
6838
+ schema: {
6839
+ params: idParamSchema,
6840
+ response: {
6841
+ 204: emptyResponseSchema,
6842
+ 403: errorResponseSchema,
6843
+ 404: errorResponseSchema
6844
+ }
6845
+ },
6846
+ /**
6847
+ * Deletes a collection regardless of deletion lock state.
6848
+ */
6849
+ handler: async (request, reply) => {
6850
+ try {
6851
+ const user = requireAuthenticatedUser(request);
6852
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6853
+ return;
6854
+ }
6855
+ const collection = await db.findCollectionById(request.params.id);
6856
+ if (!collection) {
6857
+ void reply.code(404).send({ error: "Collection not found" });
6858
+ return;
6859
+ }
6860
+ await db.deleteCollection(request.params.id, user.id);
6861
+ return reply.code(204).send(null);
6862
+ } catch (error) {
6863
+ if (handleDbError(reply, error)) {
6864
+ return;
6865
+ }
6866
+ throw error;
6867
+ }
6868
+ }
6869
+ });
6870
+ routes.route({
6871
+ method: "DELETE",
6872
+ url: "/admin/requests/:id",
6873
+ schema: {
6874
+ params: idParamSchema,
6875
+ response: {
6876
+ 204: emptyResponseSchema,
6877
+ 403: errorResponseSchema,
6878
+ 404: errorResponseSchema
6879
+ }
6880
+ },
6881
+ /**
6882
+ * Deletes a saved request regardless of collection access lists.
6883
+ */
6884
+ handler: async (request, reply) => {
6885
+ try {
6886
+ const user = requireAuthenticatedUser(request);
6887
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6888
+ return;
6889
+ }
6890
+ const existingRequest = await db.findRequestById(request.params.id);
6891
+ if (!existingRequest) {
6892
+ void reply.code(404).send({ error: "Request not found" });
6893
+ return;
6894
+ }
6895
+ await db.deleteRequest(request.params.id, user.id);
6896
+ return reply.code(204).send(null);
6897
+ } catch (error) {
6898
+ if (handleDbError(reply, error)) {
6899
+ return;
6900
+ }
6901
+ throw error;
6902
+ }
6903
+ }
6904
+ });
6905
+ routes.route({
6906
+ method: "PUT",
6907
+ url: "/admin/collections/:id",
6908
+ schema: {
6909
+ params: idParamSchema,
6910
+ body: updateAdminCollectionBodySchema,
6911
+ response: {
6912
+ 200: adminEntityConfigSchema,
6913
+ 403: errorResponseSchema,
6914
+ 404: errorResponseSchema
6915
+ }
6916
+ },
6917
+ /**
6918
+ * Updates admin configuration for a collection.
6919
+ */
6920
+ handler: async (request, reply) => {
6921
+ try {
6922
+ const user = requireAuthenticatedUser(request);
6923
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6924
+ return;
6925
+ }
6926
+ const collection = await db.setCollectionDeletionLocked(
6927
+ request.params.id,
6928
+ request.body.deletionLocked,
6929
+ user.id
6930
+ );
6931
+ return reply.send({
6932
+ id: collection.id,
6933
+ name: collection.name,
6934
+ deletionLocked: collection.deletionLocked
6935
+ });
6936
+ } catch (error) {
6937
+ if (handleDbError(reply, error)) {
6938
+ return;
6939
+ }
6940
+ throw error;
6941
+ }
6942
+ }
6943
+ });
6944
+ routes.route({
6945
+ method: "DELETE",
6946
+ url: "/admin/environments/:id",
6947
+ schema: {
6948
+ params: idParamSchema,
6949
+ response: {
6950
+ 204: emptyResponseSchema,
6951
+ 403: errorResponseSchema,
6952
+ 404: errorResponseSchema
6953
+ }
6954
+ },
6955
+ /**
6956
+ * Deletes an environment regardless of deletion lock state.
6957
+ */
6958
+ handler: async (request, reply) => {
6959
+ try {
6960
+ const user = requireAuthenticatedUser(request);
6961
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6962
+ return;
6963
+ }
6964
+ const environment = await db.findEnvironmentById(request.params.id);
6965
+ if (!environment) {
6966
+ void reply.code(404).send({ error: "Environment not found" });
6967
+ return;
6968
+ }
6969
+ await db.deleteEnvironment(request.params.id, user.id);
6970
+ return reply.code(204).send(null);
6971
+ } catch (error) {
6972
+ if (handleDbError(reply, error)) {
6973
+ return;
6974
+ }
6975
+ throw error;
6976
+ }
6977
+ }
6978
+ });
6979
+ routes.route({
6980
+ method: "PUT",
6981
+ url: "/admin/environments/:id",
6982
+ schema: {
6983
+ params: idParamSchema,
6984
+ body: updateAdminEnvironmentBodySchema,
6985
+ response: {
6986
+ 200: adminEntityConfigSchema,
6987
+ 403: errorResponseSchema,
6988
+ 404: errorResponseSchema
6989
+ }
6990
+ },
6991
+ /**
6992
+ * Updates admin configuration for an environment.
6993
+ */
6994
+ handler: async (request, reply) => {
6995
+ try {
6996
+ const user = requireAuthenticatedUser(request);
6997
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
6998
+ return;
6999
+ }
7000
+ const environment = await db.setEnvironmentDeletionLocked(
7001
+ request.params.id,
7002
+ request.body.deletionLocked,
7003
+ user.id
7004
+ );
7005
+ return reply.send({
7006
+ id: environment.id,
7007
+ name: environment.name,
7008
+ deletionLocked: environment.deletionLocked
7009
+ });
7010
+ } catch (error) {
7011
+ if (handleDbError(reply, error)) {
7012
+ return;
7013
+ }
7014
+ throw error;
7015
+ }
7016
+ }
7017
+ });
6452
7018
  routes.route({
6453
7019
  method: "GET",
6454
7020
  url: "/admin/llm/models",
@@ -6899,10 +7465,15 @@ async function registerCollectionRoutes(app, db) {
6899
7465
  handler: async (request, reply) => {
6900
7466
  try {
6901
7467
  const user = requireAuthenticatedUser(request);
6902
- if (denyUnlessAllowed(
6903
- reply,
6904
- canUseDataApi(user) && canAccessCollection(user, request.params.id)
6905
- )) {
7468
+ const collection = await db.findCollectionById(request.params.id);
7469
+ if (!collection) {
7470
+ void reply.code(404).send({ error: "Collection not found" });
7471
+ return;
7472
+ }
7473
+ if (collection.deletionLocked) {
7474
+ throw new DeletionLockedError("collection");
7475
+ }
7476
+ if (denyUnlessAllowed(reply, canDeleteCollection(user, collection))) {
6906
7477
  return;
6907
7478
  }
6908
7479
  await db.deleteCollection(request.params.id, user.id);
@@ -6934,7 +7505,7 @@ async function registerEnvironmentRoutes(app, db) {
6934
7505
  handler: async (request, reply) => {
6935
7506
  try {
6936
7507
  const user = requireAuthenticatedUser(request);
6937
- if (denyUnlessAllowed(reply, canUseDataApi(user))) {
7508
+ if (denyUnlessAllowed(reply, canListEnvironments(user))) {
6938
7509
  return;
6939
7510
  }
6940
7511
  const environments = await db.listEnvironments();
@@ -7041,6 +7612,14 @@ async function registerEnvironmentRoutes(app, db) {
7041
7612
  )) {
7042
7613
  return;
7043
7614
  }
7615
+ const environment = await db.findEnvironmentById(request.params.id);
7616
+ if (!environment) {
7617
+ void reply.code(404).send({ error: "Environment not found" });
7618
+ return;
7619
+ }
7620
+ if (environment.deletionLocked) {
7621
+ throw new DeletionLockedError("environment");
7622
+ }
7044
7623
  await db.deleteEnvironment(request.params.id, user.id);
7045
7624
  return reply.code(204).send(null);
7046
7625
  } catch (error) {
@@ -7425,10 +8004,7 @@ async function registerRequestRoutes(app, db) {
7425
8004
  if (!existingRequest) {
7426
8005
  return reply.code(404).send({ error: "Request not found" });
7427
8006
  }
7428
- if (denyUnlessAllowed(
7429
- reply,
7430
- canUseDataApi(user) && canAccessCollection(user, existingRequest.collectionId)
7431
- )) {
8007
+ if (denyUnlessAllowed(reply, canDeleteRequest(user, existingRequest))) {
7432
8008
  return;
7433
8009
  }
7434
8010
  await db.deleteRequest(request.params.id, user.id);