@harborclient/team-hub 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -364,6 +364,7 @@ var REQUESTS_COLLECTION = "requests";
364
364
  var AUDIT_LOG_COLLECTION = "auditLog";
365
365
  var LLM_USAGE_COLLECTION = "llmUsage";
366
366
  var LLM_USAGE_LOG_COLLECTION = "llmUsageLog";
367
+ var RUN_RESULTS_COLLECTION = "runResults";
367
368
  var WRITE_BATCH_LIMIT = 500;
368
369
 
369
370
  // src/db/systemUsers.ts
@@ -400,7 +401,7 @@ var firestoreConfigSchema = z2.object({
400
401
 
401
402
  // src/db/firestore/utils.ts
402
403
  function parseAuditEntityType(value) {
403
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
404
+ if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
404
405
  return value;
405
406
  }
406
407
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -544,6 +545,19 @@ function mapFirestoreRequest(id, data) {
544
545
  updatedByUserId: data.updatedByUserId ?? null
545
546
  };
546
547
  }
548
+ function mapFirestoreRunResult(id, data) {
549
+ return {
550
+ id,
551
+ kind: data.kind,
552
+ label: data.label,
553
+ collectionName: data.collectionName,
554
+ requestName: data.requestName,
555
+ summary: data.summary,
556
+ payload: data.payload,
557
+ createdAt: data.createdAt,
558
+ createdByUserId: data.createdByUserId ?? null
559
+ };
560
+ }
547
561
  function mapFirestoreAuditLog(id, data) {
548
562
  return {
549
563
  id,
@@ -557,6 +571,53 @@ function mapFirestoreAuditLog(id, data) {
557
571
  };
558
572
  }
559
573
 
574
+ // src/db/runResultPayload.ts
575
+ function isRecord(value) {
576
+ return value != null && typeof value === "object" && !Array.isArray(value);
577
+ }
578
+ function summarizeResultRows(results) {
579
+ let passed = 0;
580
+ let failed = 0;
581
+ let skipped = 0;
582
+ for (const row of results) {
583
+ if (!isRecord(row)) {
584
+ continue;
585
+ }
586
+ const status = row.status;
587
+ if (status === "passed") {
588
+ passed += 1;
589
+ } else if (status === "failed") {
590
+ failed += 1;
591
+ } else if (status === "skipped") {
592
+ skipped += 1;
593
+ }
594
+ }
595
+ return { passed, failed, skipped };
596
+ }
597
+ function buildDefaultRunResultLabel(metadata) {
598
+ const target = metadata.requestName ?? metadata.collectionName ?? "Run";
599
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
600
+ return `${target} \u2014 ${timestamp}`;
601
+ }
602
+ function parseRunResultPayload(payload) {
603
+ const kind = payload.harborclientExport;
604
+ if (kind !== "collection-run-results" && kind !== "request-run-results") {
605
+ throw new Error("Invalid run result payload: harborclientExport is required");
606
+ }
607
+ const results = payload.results;
608
+ if (!Array.isArray(results) || results.length === 0) {
609
+ throw new Error("Invalid run result payload: results are required");
610
+ }
611
+ const collection = isRecord(payload.collection) ? payload.collection : void 0;
612
+ const request = isRecord(payload.request) ? payload.request : void 0;
613
+ return {
614
+ kind,
615
+ collectionName: typeof collection?.name === "string" ? collection.name : null,
616
+ requestName: typeof request?.name === "string" ? request.name : null,
617
+ summary: summarizeResultRows(results)
618
+ };
619
+ }
620
+
560
621
  // src/db/trimRequiredName.ts
561
622
  function trimRequiredName(name, label) {
562
623
  const trimmed = name.trim();
@@ -1822,6 +1883,78 @@ var FirestoreDatabase = class _FirestoreDatabase {
1822
1883
  }
1823
1884
  return usage;
1824
1885
  }
1886
+ /**
1887
+ * Lists run results saved by the given user, newest first.
1888
+ *
1889
+ * @param userId - Owning user identifier.
1890
+ */
1891
+ async listRunResultsForUser(userId) {
1892
+ const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).where("createdByUserId", "==", userId).orderBy("createdAt", "desc").get();
1893
+ return snapshot.docs.map(
1894
+ (doc) => mapFirestoreRunResult(doc.id, doc.data())
1895
+ );
1896
+ }
1897
+ /**
1898
+ * Lists all run results for admin inspection, newest first.
1899
+ */
1900
+ async listAllRunResults() {
1901
+ const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).orderBy("createdAt", "desc").get();
1902
+ return snapshot.docs.map(
1903
+ (doc) => mapFirestoreRunResult(doc.id, doc.data())
1904
+ );
1905
+ }
1906
+ /**
1907
+ * Creates a standalone run result snapshot.
1908
+ *
1909
+ * @param input - HarborClient export payload and optional label.
1910
+ * @param actingUserId - User performing the create action.
1911
+ */
1912
+ async createRunResult(input, actingUserId) {
1913
+ const metadata = parseRunResultPayload(input.payload);
1914
+ const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
1915
+ const id = randomUUID2();
1916
+ const now = /* @__PURE__ */ new Date();
1917
+ const data = {
1918
+ kind: metadata.kind,
1919
+ label,
1920
+ collectionName: metadata.collectionName,
1921
+ requestName: metadata.requestName,
1922
+ summary: metadata.summary,
1923
+ payload: input.payload,
1924
+ createdAt: now,
1925
+ createdByUserId: actingUserId
1926
+ };
1927
+ await this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id).set(data);
1928
+ await this.recordAuditEntry(actingUserId, "create", "run_result", id);
1929
+ return mapFirestoreRunResult(id, data);
1930
+ }
1931
+ /**
1932
+ * Finds a run result by stable identifier.
1933
+ *
1934
+ * @param id - Run result ID to look up.
1935
+ */
1936
+ async findRunResultById(id) {
1937
+ const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id).get();
1938
+ if (!snapshot.exists) {
1939
+ return null;
1940
+ }
1941
+ return mapFirestoreRunResult(id, snapshot.data());
1942
+ }
1943
+ /**
1944
+ * Deletes a run result.
1945
+ *
1946
+ * @param id - Run result ID to delete.
1947
+ * @param actingUserId - User performing the delete action.
1948
+ */
1949
+ async deleteRunResult(id, actingUserId) {
1950
+ const docRef = this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id);
1951
+ const snapshot = await docRef.get();
1952
+ if (!snapshot.exists) {
1953
+ throw new Error("Run result not found");
1954
+ }
1955
+ await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
1956
+ await docRef.delete();
1957
+ }
1825
1958
  /**
1826
1959
  * Inserts a per-request LLM usage log entry.
1827
1960
  *
@@ -1976,7 +2109,7 @@ function parseAuditAction(value) {
1976
2109
  throw new Error(`Invalid audit action: ${value}`);
1977
2110
  }
1978
2111
  function parseAuditEntityType2(value) {
1979
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
2112
+ if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
1980
2113
  return value;
1981
2114
  }
1982
2115
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -2073,6 +2206,23 @@ function mapSnippetSqlRow(row) {
2073
2206
  deletionLocked: Boolean(row.deletion_locked)
2074
2207
  };
2075
2208
  }
2209
+ function mapRunResultSqlRow(row) {
2210
+ return {
2211
+ id: row.id,
2212
+ kind: row.kind,
2213
+ label: row.label,
2214
+ collectionName: row.collection_name,
2215
+ requestName: row.request_name,
2216
+ summary: {
2217
+ passed: row.summary_passed,
2218
+ failed: row.summary_failed,
2219
+ skipped: row.summary_skipped
2220
+ },
2221
+ payload: parseJson(row.payload, {}),
2222
+ createdAt: row.created_at,
2223
+ createdByUserId: row.created_by_user_id ?? null
2224
+ };
2225
+ }
2076
2226
  function mapFolderSqlRow(row) {
2077
2227
  return {
2078
2228
  id: row.id,
@@ -2349,6 +2499,22 @@ WHERE role = 'user'
2349
2499
  AND snippet_access = '[]'
2350
2500
  AND collection_access LIKE '%"*"%';
2351
2501
  `.trim();
2502
+ var RUN_RESULTS_MIGRATION_SQL = `
2503
+ CREATE TABLE IF NOT EXISTS run_results (
2504
+ id VARCHAR(36) PRIMARY KEY,
2505
+ kind VARCHAR(64) NOT NULL,
2506
+ label VARCHAR(512) NOT NULL,
2507
+ collection_name VARCHAR(512),
2508
+ request_name VARCHAR(512),
2509
+ summary_passed INT NOT NULL DEFAULT 0,
2510
+ summary_failed INT NOT NULL DEFAULT 0,
2511
+ summary_skipped INT NOT NULL DEFAULT 0,
2512
+ payload LONGTEXT NOT NULL,
2513
+ created_at DATETIME NOT NULL,
2514
+ created_by_user_id VARCHAR(36),
2515
+ INDEX run_results_created_idx (created_at)
2516
+ )
2517
+ `.trim();
2352
2518
  var MYSQL_MIGRATIONS = [
2353
2519
  USERS_MIGRATION_SQL,
2354
2520
  API_TOKENS_MIGRATION_SQL,
@@ -2374,7 +2540,8 @@ var MYSQL_MIGRATIONS = [
2374
2540
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
2375
2541
  ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
2376
2542
  USERS_SNIPPET_ACCESS_MIGRATION_SQL,
2377
- USERS_SNIPPET_ACCESS_BACKFILL_SQL
2543
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL,
2544
+ RUN_RESULTS_MIGRATION_SQL
2378
2545
  ];
2379
2546
 
2380
2547
  // src/db/mysql/schemas.ts
@@ -2478,6 +2645,8 @@ var LLM_USAGE_SELECT_COLUMNS = `id, user_id, period, prompt_tokens, completion_t
2478
2645
  var COLLECTION_SELECT = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
2479
2646
  var ENVIRONMENT_SELECT = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
2480
2647
  var SNIPPET_SELECT = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
2648
+ var RUN_RESULT_SELECT_COLUMNS = "id, kind, label, collection_name, request_name, summary_passed, summary_failed, summary_skipped, payload, created_at, created_by_user_id";
2649
+ var RUN_RESULT_SELECT = `SELECT ${RUN_RESULT_SELECT_COLUMNS} FROM run_results`;
2481
2650
  var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
2482
2651
  var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
2483
2652
  var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -3883,6 +4052,93 @@ var MysqlDatabase = class _MysqlDatabase {
3883
4052
  );
3884
4053
  return rows.map(mapLlmUsageLogSqlRow);
3885
4054
  }
4055
+ /**
4056
+ * Lists run results saved by the given user, newest first.
4057
+ */
4058
+ async listRunResultsForUser(userId) {
4059
+ const rows = await this.queryRows(
4060
+ `${RUN_RESULT_SELECT} WHERE created_by_user_id = ? ORDER BY created_at DESC`,
4061
+ [userId]
4062
+ );
4063
+ return rows.map(mapRunResultSqlRow);
4064
+ }
4065
+ /**
4066
+ * Lists all run results for admin inspection, newest first.
4067
+ */
4068
+ async listAllRunResults() {
4069
+ const rows = await this.queryRows(
4070
+ `${RUN_RESULT_SELECT} ORDER BY created_at DESC`
4071
+ );
4072
+ return rows.map(mapRunResultSqlRow);
4073
+ }
4074
+ /**
4075
+ * Creates a standalone run result snapshot.
4076
+ */
4077
+ async createRunResult(input, actingUserId) {
4078
+ const metadata = parseRunResultPayload(input.payload);
4079
+ const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
4080
+ const id = randomUUID3();
4081
+ const now = /* @__PURE__ */ new Date();
4082
+ await this.executeStatement(
4083
+ `INSERT INTO run_results (
4084
+ id,
4085
+ kind,
4086
+ label,
4087
+ collection_name,
4088
+ request_name,
4089
+ summary_passed,
4090
+ summary_failed,
4091
+ summary_skipped,
4092
+ payload,
4093
+ created_at,
4094
+ created_by_user_id
4095
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4096
+ [
4097
+ id,
4098
+ metadata.kind,
4099
+ label,
4100
+ metadata.collectionName,
4101
+ metadata.requestName,
4102
+ metadata.summary.passed,
4103
+ metadata.summary.failed,
4104
+ metadata.summary.skipped,
4105
+ JSON.stringify(input.payload),
4106
+ now,
4107
+ actingUserId
4108
+ ]
4109
+ );
4110
+ const rows = await this.queryRows(
4111
+ `${RUN_RESULT_SELECT} WHERE id = ?`,
4112
+ [id]
4113
+ );
4114
+ const row = rows[0];
4115
+ if (!row) {
4116
+ throw new Error("Run result not found after insert");
4117
+ }
4118
+ await this.recordAuditEntry(actingUserId, "create", "run_result", id);
4119
+ return mapRunResultSqlRow(row);
4120
+ }
4121
+ /**
4122
+ * Finds a run result by id.
4123
+ */
4124
+ async findRunResultById(id) {
4125
+ const rows = await this.queryRows(
4126
+ `${RUN_RESULT_SELECT} WHERE id = ?`,
4127
+ [id]
4128
+ );
4129
+ const row = rows[0];
4130
+ return row ? mapRunResultSqlRow(row) : null;
4131
+ }
4132
+ /**
4133
+ * Deletes a run result by id.
4134
+ */
4135
+ async deleteRunResult(id, actingUserId) {
4136
+ const result = await this.executeStatement("DELETE FROM run_results WHERE id = ?", [id]);
4137
+ if (result.affectedRows === 0) {
4138
+ throw new Error("Run result not found");
4139
+ }
4140
+ await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
4141
+ }
3886
4142
  /**
3887
4143
  * Ensures the internal system user exists and caches its identifier.
3888
4144
  */
@@ -4229,6 +4485,22 @@ WHERE role = 'user'
4229
4485
  AND snippet_access = '[]'
4230
4486
  AND collection_access LIKE '%"*"%';
4231
4487
  `.trim();
4488
+ var RUN_RESULTS_MIGRATION_SQL2 = `
4489
+ CREATE TABLE IF NOT EXISTS run_results (
4490
+ id TEXT PRIMARY KEY,
4491
+ kind TEXT NOT NULL CHECK (kind IN ('collection-run-results', 'request-run-results')),
4492
+ label TEXT NOT NULL,
4493
+ collection_name TEXT,
4494
+ request_name TEXT,
4495
+ summary_passed INT NOT NULL DEFAULT 0,
4496
+ summary_failed INT NOT NULL DEFAULT 0,
4497
+ summary_skipped INT NOT NULL DEFAULT 0,
4498
+ payload TEXT NOT NULL,
4499
+ created_at TIMESTAMPTZ NOT NULL,
4500
+ created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
4501
+ );
4502
+ CREATE INDEX IF NOT EXISTS run_results_created_idx ON run_results (created_at DESC);
4503
+ `.trim();
4232
4504
  var POSTGRES_MIGRATIONS = [
4233
4505
  USERS_MIGRATION_SQL2,
4234
4506
  API_TOKENS_MIGRATION_SQL2,
@@ -4254,7 +4526,8 @@ var POSTGRES_MIGRATIONS = [
4254
4526
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
4255
4527
  ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
4256
4528
  USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
4257
- USERS_SNIPPET_ACCESS_BACKFILL_SQL2
4529
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL2,
4530
+ RUN_RESULTS_MIGRATION_SQL2
4258
4531
  ];
4259
4532
 
4260
4533
  // src/db/postgres/schemas.ts
@@ -4279,6 +4552,8 @@ var { Pool } = pg;
4279
4552
  var COLLECTION_SELECT2 = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
4280
4553
  var ENVIRONMENT_SELECT2 = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
4281
4554
  var SNIPPET_SELECT2 = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
4555
+ var RUN_RESULT_SELECT_COLUMNS2 = "id, kind, label, collection_name, request_name, summary_passed, summary_failed, summary_skipped, payload, created_at, created_by_user_id";
4556
+ var RUN_RESULT_SELECT2 = `SELECT ${RUN_RESULT_SELECT_COLUMNS2} FROM run_results`;
4282
4557
  var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
4283
4558
  var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
4284
4559
  var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -5629,6 +5904,87 @@ var PostgresDatabase = class _PostgresDatabase {
5629
5904
  );
5630
5905
  return result.rows.map(mapLlmUsageLogSqlRow);
5631
5906
  }
5907
+ /**
5908
+ * Lists run results saved by the given user, newest first.
5909
+ */
5910
+ async listRunResultsForUser(userId) {
5911
+ const result = await this.query(
5912
+ `${RUN_RESULT_SELECT2} WHERE created_by_user_id = $1 ORDER BY created_at DESC`,
5913
+ [userId]
5914
+ );
5915
+ return result.rows.map(mapRunResultSqlRow);
5916
+ }
5917
+ /**
5918
+ * Lists all run results for admin inspection, newest first.
5919
+ */
5920
+ async listAllRunResults() {
5921
+ const result = await this.query(
5922
+ `${RUN_RESULT_SELECT2} ORDER BY created_at DESC`
5923
+ );
5924
+ return result.rows.map(mapRunResultSqlRow);
5925
+ }
5926
+ /**
5927
+ * Creates a standalone run result snapshot.
5928
+ */
5929
+ async createRunResult(input, actingUserId) {
5930
+ const metadata = parseRunResultPayload(input.payload);
5931
+ const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
5932
+ const id = randomUUID4();
5933
+ const now = /* @__PURE__ */ new Date();
5934
+ const result = await this.query(
5935
+ `INSERT INTO run_results (
5936
+ id,
5937
+ kind,
5938
+ label,
5939
+ collection_name,
5940
+ request_name,
5941
+ summary_passed,
5942
+ summary_failed,
5943
+ summary_skipped,
5944
+ payload,
5945
+ created_at,
5946
+ created_by_user_id
5947
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
5948
+ RETURNING ${RUN_RESULT_SELECT_COLUMNS2}`,
5949
+ [
5950
+ id,
5951
+ metadata.kind,
5952
+ label,
5953
+ metadata.collectionName,
5954
+ metadata.requestName,
5955
+ metadata.summary.passed,
5956
+ metadata.summary.failed,
5957
+ metadata.summary.skipped,
5958
+ JSON.stringify(input.payload),
5959
+ now,
5960
+ actingUserId
5961
+ ]
5962
+ );
5963
+ const row = result.rows[0];
5964
+ if (!row) {
5965
+ throw new Error("Run result not found after insert");
5966
+ }
5967
+ await this.recordAuditEntry(actingUserId, "create", "run_result", id);
5968
+ return mapRunResultSqlRow(row);
5969
+ }
5970
+ /**
5971
+ * Finds a run result by id.
5972
+ */
5973
+ async findRunResultById(id) {
5974
+ const result = await this.query(`${RUN_RESULT_SELECT2} WHERE id = $1`, [id]);
5975
+ const row = result.rows[0];
5976
+ return row ? mapRunResultSqlRow(row) : null;
5977
+ }
5978
+ /**
5979
+ * Deletes a run result by id.
5980
+ */
5981
+ async deleteRunResult(id, actingUserId) {
5982
+ const result = await this.query("DELETE FROM run_results WHERE id = $1", [id]);
5983
+ if ((result.rowCount ?? 0) === 0) {
5984
+ throw new Error("Run result not found");
5985
+ }
5986
+ await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
5987
+ }
5632
5988
  /**
5633
5989
  * Returns the active pool or throws when connect has not been called.
5634
5990
  *
@@ -6655,6 +7011,9 @@ function canListEnvironments(user) {
6655
7011
  function canListSnippets(user) {
6656
7012
  return canUseDataApi(user) || canUseManagementApi(user);
6657
7013
  }
7014
+ function canListRunResults(user) {
7015
+ return canUseDataApi(user) || canUseManagementApi(user);
7016
+ }
6658
7017
  function hasWildcardAccess(access) {
6659
7018
  return access.includes("*");
6660
7019
  }
@@ -6691,6 +7050,9 @@ function canDeleteCollection(user, collection) {
6691
7050
  function canDeleteRequest(user, request) {
6692
7051
  return canUseDataApi(user) && canAccessCollection(user, request.collectionId) && request.createdByUserId === user.id;
6693
7052
  }
7053
+ function canDeleteRunResult(user, runResult) {
7054
+ return canUseDataApi(user) && runResult.createdByUserId === user.id;
7055
+ }
6694
7056
  function canCreateCollection(user) {
6695
7057
  return user.role === "user" && hasWildcardAccess(user.collectionAccess);
6696
7058
  }
@@ -7115,6 +7477,48 @@ function serializeSavedRequest(record) {
7115
7477
  updatedAt: record.updatedAt.toISOString()
7116
7478
  };
7117
7479
  }
7480
+ var runResultRecordSchema = z8.object({
7481
+ id: z8.string(),
7482
+ kind: z8.enum(["collection-run-results", "request-run-results"]),
7483
+ label: z8.string(),
7484
+ collectionName: z8.string().nullable(),
7485
+ requestName: z8.string().nullable(),
7486
+ summary: z8.object({
7487
+ passed: z8.number().int().nonnegative(),
7488
+ failed: z8.number().int().nonnegative(),
7489
+ skipped: z8.number().int().nonnegative()
7490
+ }),
7491
+ createdAt: timestampSchema,
7492
+ createdByUserId: z8.string().nullable()
7493
+ });
7494
+ var runResultDetailSchema = runResultRecordSchema.extend({
7495
+ payload: z8.record(z8.string(), z8.unknown())
7496
+ });
7497
+ var createRunResultBodySchema = z8.object({
7498
+ label: z8.string().trim().min(1).optional(),
7499
+ payload: z8.record(z8.string(), z8.unknown())
7500
+ });
7501
+ var listRunResultsResponseSchema = z8.object({
7502
+ runResults: z8.array(runResultRecordSchema)
7503
+ });
7504
+ function serializeRunResult(record) {
7505
+ return {
7506
+ id: record.id,
7507
+ kind: record.kind,
7508
+ label: record.label,
7509
+ collectionName: record.collectionName,
7510
+ requestName: record.requestName,
7511
+ summary: record.summary,
7512
+ createdAt: record.createdAt.toISOString(),
7513
+ createdByUserId: record.createdByUserId
7514
+ };
7515
+ }
7516
+ function serializeRunResultDetail(record) {
7517
+ return {
7518
+ ...serializeRunResult(record),
7519
+ payload: record.payload
7520
+ };
7521
+ }
7118
7522
 
7119
7523
  // src/server/routes/schemas/admin.ts
7120
7524
  var adminResourceOptionSchema = z9.object({
@@ -7164,6 +7568,9 @@ var listAdminEnvironmentsResponseSchema = z9.object({
7164
7568
  var listAdminSnippetsResponseSchema = z9.object({
7165
7569
  snippets: z9.array(snippetRecordSchema)
7166
7570
  });
7571
+ var listAdminRunResultsResponseSchema = z9.object({
7572
+ runResults: z9.array(runResultRecordSchema)
7573
+ });
7167
7574
  var adminSnippetRecordSchema = snippetRecordSchema;
7168
7575
  var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
7169
7576
  var hubUserRecordSchema = z9.object({
@@ -7722,6 +8129,71 @@ async function registerAdminRoutes(app, options) {
7722
8129
  }
7723
8130
  }
7724
8131
  });
8132
+ routes.route({
8133
+ method: "GET",
8134
+ url: "/admin/run-results",
8135
+ schema: {
8136
+ response: {
8137
+ 200: listAdminRunResultsResponseSchema,
8138
+ 403: errorResponseSchema
8139
+ }
8140
+ },
8141
+ /**
8142
+ * Lists all run results for operator management.
8143
+ */
8144
+ handler: async (request, reply) => {
8145
+ try {
8146
+ const user = requireAuthenticatedUser(request);
8147
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
8148
+ return;
8149
+ }
8150
+ const runResults = await db.listAllRunResults();
8151
+ return reply.send({
8152
+ runResults: runResults.map((record) => serializeRunResult(record))
8153
+ });
8154
+ } catch (error) {
8155
+ if (handleDbError(reply, error)) {
8156
+ return;
8157
+ }
8158
+ throw error;
8159
+ }
8160
+ }
8161
+ });
8162
+ routes.route({
8163
+ method: "DELETE",
8164
+ url: "/admin/run-results/:id",
8165
+ schema: {
8166
+ params: idParamSchema,
8167
+ response: {
8168
+ 204: emptyResponseSchema,
8169
+ 403: errorResponseSchema,
8170
+ 404: errorResponseSchema
8171
+ }
8172
+ },
8173
+ /**
8174
+ * Deletes a run result regardless of creator ownership.
8175
+ */
8176
+ handler: async (request, reply) => {
8177
+ try {
8178
+ const user = requireAuthenticatedUser(request);
8179
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
8180
+ return;
8181
+ }
8182
+ const record = await db.findRunResultById(request.params.id);
8183
+ if (!record) {
8184
+ void reply.code(404).send({ error: "Run result not found" });
8185
+ return;
8186
+ }
8187
+ await db.deleteRunResult(request.params.id, user.id);
8188
+ return reply.code(204).send(null);
8189
+ } catch (error) {
8190
+ if (handleDbError(reply, error)) {
8191
+ return;
8192
+ }
8193
+ throw error;
8194
+ }
8195
+ }
8196
+ });
7725
8197
  routes.route({
7726
8198
  method: "PUT",
7727
8199
  url: "/admin/snippets/:id",
@@ -9104,6 +9576,140 @@ async function registerRequestRoutes(app, db) {
9104
9576
  });
9105
9577
  }
9106
9578
 
9579
+ // src/server/routes/runResults.ts
9580
+ async function registerRunResultRoutes(app, db) {
9581
+ const routes = app.withTypeProvider();
9582
+ routes.route({
9583
+ method: "GET",
9584
+ url: "/run-results",
9585
+ schema: {
9586
+ response: {
9587
+ 200: listRunResultsResponseSchema
9588
+ }
9589
+ },
9590
+ /**
9591
+ * Lists run results saved by the authenticated user token.
9592
+ */
9593
+ handler: async (request, reply) => {
9594
+ try {
9595
+ const user = requireAuthenticatedUser(request);
9596
+ if (denyUnlessAllowed(reply, canListRunResults(user))) {
9597
+ return;
9598
+ }
9599
+ const runResults = await db.listRunResultsForUser(user.id);
9600
+ return reply.send({
9601
+ runResults: runResults.map((record) => serializeRunResult(record))
9602
+ });
9603
+ } catch (error) {
9604
+ if (handleDbError(reply, error)) {
9605
+ return;
9606
+ }
9607
+ throw error;
9608
+ }
9609
+ }
9610
+ });
9611
+ routes.route({
9612
+ method: "POST",
9613
+ url: "/run-results",
9614
+ schema: {
9615
+ body: createRunResultBodySchema,
9616
+ response: {
9617
+ 200: runResultDetailSchema,
9618
+ 400: errorResponseSchema
9619
+ }
9620
+ },
9621
+ /**
9622
+ * Saves a standalone run result snapshot.
9623
+ */
9624
+ handler: async (request, reply) => {
9625
+ try {
9626
+ const user = requireAuthenticatedUser(request);
9627
+ if (denyUnlessAllowed(reply, canUseDataApi(user))) {
9628
+ return;
9629
+ }
9630
+ const record = await db.createRunResult(request.body, user.id);
9631
+ return reply.send(serializeRunResultDetail(record));
9632
+ } catch (error) {
9633
+ if (handleDbError(reply, error)) {
9634
+ return;
9635
+ }
9636
+ throw error;
9637
+ }
9638
+ }
9639
+ });
9640
+ routes.route({
9641
+ method: "GET",
9642
+ url: "/run-results/:id",
9643
+ schema: {
9644
+ params: idParamSchema,
9645
+ response: {
9646
+ 200: runResultDetailSchema,
9647
+ 404: errorResponseSchema
9648
+ }
9649
+ },
9650
+ /**
9651
+ * Loads a run result snapshot by id for shared deep links.
9652
+ */
9653
+ handler: async (request, reply) => {
9654
+ try {
9655
+ const user = requireAuthenticatedUser(request);
9656
+ if (denyUnlessAllowed(reply, canUseDataApi(user))) {
9657
+ return;
9658
+ }
9659
+ const record = await db.findRunResultById(request.params.id);
9660
+ if (!record) {
9661
+ void reply.code(404).send({ error: "Run result not found" });
9662
+ return;
9663
+ }
9664
+ return reply.send(serializeRunResultDetail(record));
9665
+ } catch (error) {
9666
+ if (handleDbError(reply, error)) {
9667
+ return;
9668
+ }
9669
+ throw error;
9670
+ }
9671
+ }
9672
+ });
9673
+ routes.route({
9674
+ method: "DELETE",
9675
+ url: "/run-results/:id",
9676
+ schema: {
9677
+ params: idParamSchema,
9678
+ response: {
9679
+ 204: emptyResponseSchema,
9680
+ 403: errorResponseSchema,
9681
+ 404: errorResponseSchema
9682
+ }
9683
+ },
9684
+ /**
9685
+ * Deletes a run result saved by the authenticated user when permitted.
9686
+ */
9687
+ handler: async (request, reply) => {
9688
+ try {
9689
+ const user = requireAuthenticatedUser(request);
9690
+ if (denyUnlessAllowed(reply, canUseDataApi(user))) {
9691
+ return;
9692
+ }
9693
+ const record = await db.findRunResultById(request.params.id);
9694
+ if (!record) {
9695
+ void reply.code(404).send({ error: "Run result not found" });
9696
+ return;
9697
+ }
9698
+ if (denyUnlessAllowed(reply, canDeleteRunResult(user, record))) {
9699
+ return;
9700
+ }
9701
+ await db.deleteRunResult(request.params.id, user.id);
9702
+ return reply.code(204).send(null);
9703
+ } catch (error) {
9704
+ if (handleDbError(reply, error)) {
9705
+ return;
9706
+ }
9707
+ throw error;
9708
+ }
9709
+ }
9710
+ });
9711
+ }
9712
+
9107
9713
  // src/server/llm/client.ts
9108
9714
  var OPENAI_BASE_URL = "https://api.openai.com/v1";
9109
9715
  var CLAUDE_BASE_URL = "https://api.anthropic.com/v1";
@@ -9746,6 +10352,7 @@ async function registerProtectedRoutes(app, options) {
9746
10352
  await registerSnippetRoutes(app, options.db);
9747
10353
  await registerFolderRoutes(app, options.db);
9748
10354
  await registerRequestRoutes(app, options.db);
10355
+ await registerRunResultRoutes(app, options.db);
9749
10356
  await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
9750
10357
  await registerPluginsRoutes(app, { getPlugins: options.getPlugins });
9751
10358
  }