@harborclient/team-hub 0.4.3 → 0.4.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.
Files changed (3) hide show
  1. package/dist/cli.js +1628 -130
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -366,7 +366,7 @@ function mergeGlobalOptions(command, options) {
366
366
  }
367
367
 
368
368
  // src/db/firestore/FirestoreDatabase.ts
369
- import { randomUUID as randomUUID2 } from "crypto";
369
+ import { randomUUID as randomUUID3 } from "crypto";
370
370
  import { Firestore } from "@google-cloud/firestore";
371
371
 
372
372
  // src/db/attribution.ts
@@ -379,9 +379,26 @@ async function resolveActingUserName(findUserById, actingUserId) {
379
379
  import { randomUUID } from "crypto";
380
380
  var BOOTSTRAP_USER_NAME = "bootstrap";
381
381
 
382
+ // src/db/invitationErrors.ts
383
+ var InvitationUnavailableError = class extends Error {
384
+ /**
385
+ * Specific failure reason used to choose a safe HTTP status code.
386
+ */
387
+ reason;
388
+ /**
389
+ * @param reason - Why the invitation cannot be used.
390
+ */
391
+ constructor(reason) {
392
+ super("Invalid or expired invitation.");
393
+ this.name = "InvitationUnavailableError";
394
+ this.reason = reason;
395
+ }
396
+ };
397
+
382
398
  // src/db/firestore/const.ts
383
399
  var USERS_COLLECTION = "users";
384
400
  var API_TOKENS_COLLECTION = "apiTokens";
401
+ var INVITATIONS_COLLECTION = "invitations";
385
402
  var COLLECTIONS_COLLECTION = "collections";
386
403
  var ENVIRONMENTS_COLLECTION = "environments";
387
404
  var SNIPPETS_COLLECTION = "snippets";
@@ -427,11 +444,25 @@ var firestoreConfigSchema = z2.object({
427
444
 
428
445
  // src/db/firestore/utils.ts
429
446
  function parseAuditEntityType(value) {
430
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
447
+ if (value === "user" || value === "api_token" || value === "invitation" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
431
448
  return value;
432
449
  }
433
450
  throw new Error(`Invalid audit entity type: ${value}`);
434
451
  }
452
+ function mapFirestoreInvitation(id, data) {
453
+ return {
454
+ id,
455
+ userId: data.userId,
456
+ codeHash: data.codeHash,
457
+ codePrefix: data.codePrefix,
458
+ expiresAt: data.expiresAt,
459
+ redeemedAt: data.redeemedAt,
460
+ revokedAt: data.revokedAt,
461
+ createdAt: data.createdAt,
462
+ createdByUserId: data.createdByUserId ?? null,
463
+ updatedByUserId: data.updatedByUserId ?? null
464
+ };
465
+ }
435
466
  function mapFirestoreApiToken(id, data) {
436
467
  if (!data.userId) {
437
468
  throw new Error(`API token ${id} is missing a userId`);
@@ -644,6 +675,39 @@ function parseRunResultPayload(payload) {
644
675
  };
645
676
  }
646
677
 
678
+ // src/server/auth/apiTokens.ts
679
+ import { createHash, randomBytes, randomUUID as randomUUID2 } from "crypto";
680
+ var TOKEN_PREFIX = "hbk_";
681
+ function hashToken(token) {
682
+ return createHash("sha256").update(token).digest("hex");
683
+ }
684
+ function generateApiToken(userId, name) {
685
+ const secretSuffix = randomBytes(32).toString("base64url");
686
+ const secret = `${TOKEN_PREFIX}${secretSuffix}`;
687
+ const tokenPrefix = `${TOKEN_PREFIX}${secretSuffix.slice(0, 8)}`;
688
+ const createdAt = /* @__PURE__ */ new Date();
689
+ const record = {
690
+ id: randomUUID2(),
691
+ userId,
692
+ name,
693
+ tokenHash: hashToken(secret),
694
+ tokenPrefix,
695
+ createdAt,
696
+ lastUsedAt: null,
697
+ revokedAt: null,
698
+ createdByUserId: null,
699
+ updatedByUserId: null
700
+ };
701
+ return { record, secret };
702
+ }
703
+ function extractBearer(headerValue) {
704
+ if (!headerValue) {
705
+ return null;
706
+ }
707
+ const match = /^Bearer\s+(\S+)$/i.exec(headerValue.trim());
708
+ return match?.[1] ?? null;
709
+ }
710
+
647
711
  // src/db/trimRequiredName.ts
648
712
  function trimRequiredName(name, label) {
649
713
  const trimmed = name.trim();
@@ -842,7 +906,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
842
906
  async createUser(input, actingUserId) {
843
907
  const trimmedName = trimRequiredName(input.name, "User name");
844
908
  assertUserNameNotReserved(trimmedName);
845
- const id = randomUUID2();
909
+ const id = randomUUID3();
846
910
  const now = /* @__PURE__ */ new Date();
847
911
  const attributionUserId = trimmedName === SYSTEM_USER_NAME ? id : actingUserId;
848
912
  const data = {
@@ -1146,6 +1210,205 @@ var FirestoreDatabase = class _FirestoreDatabase {
1146
1210
  async touchApiTokenLastUsed(id, when) {
1147
1211
  await this.requireClient().collection(API_TOKENS_COLLECTION).doc(id).update({ lastUsedAt: when });
1148
1212
  }
1213
+ /**
1214
+ * Creates a user account and its initial onboarding invitation in one transaction.
1215
+ *
1216
+ * @param userId - Pre-generated stable identifier for the new user.
1217
+ * @param input - User fields to persist.
1218
+ * @param invitation - Invitation metadata including the stored code hash.
1219
+ * @param actingUserId - User performing the create action.
1220
+ */
1221
+ async createInvitedUser(userId, input, invitation, actingUserId) {
1222
+ const trimmedName = trimRequiredName(input.name, "User name");
1223
+ assertUserNameNotReserved(trimmedName);
1224
+ const now = /* @__PURE__ */ new Date();
1225
+ const client = this.requireClient();
1226
+ const userRef = client.collection(USERS_COLLECTION).doc(userId);
1227
+ const invitationRef = client.collection(INVITATIONS_COLLECTION).doc(invitation.id);
1228
+ const userData = {
1229
+ name: trimmedName,
1230
+ role: input.role,
1231
+ collectionAccess: input.collectionAccess,
1232
+ environmentAccess: input.environmentAccess,
1233
+ snippetAccess: input.snippetAccess,
1234
+ llmAccess: input.llmAccess ?? false,
1235
+ llmModels: input.llmModels ?? [],
1236
+ llmMonthlyTokenLimit: input.llmMonthlyTokenLimit ?? null,
1237
+ createdAt: now,
1238
+ updatedAt: now,
1239
+ createdByUserId: actingUserId,
1240
+ updatedByUserId: actingUserId
1241
+ };
1242
+ const invitationData = {
1243
+ userId: invitation.userId,
1244
+ codeHash: invitation.codeHash,
1245
+ codePrefix: invitation.codePrefix,
1246
+ expiresAt: invitation.expiresAt,
1247
+ redeemedAt: invitation.redeemedAt,
1248
+ revokedAt: invitation.revokedAt,
1249
+ createdAt: invitation.createdAt,
1250
+ createdByUserId: actingUserId,
1251
+ updatedByUserId: actingUserId
1252
+ };
1253
+ await client.runTransaction(async (transaction) => {
1254
+ transaction.set(userRef, userData);
1255
+ transaction.set(invitationRef, invitationData);
1256
+ });
1257
+ await this.recordAuditEntry(actingUserId, "create", "user", userId);
1258
+ await this.recordAuditEntry(actingUserId, "create", "invitation", invitation.id);
1259
+ return {
1260
+ user: mapFirestoreUser(userId, userData),
1261
+ invitation
1262
+ };
1263
+ }
1264
+ /**
1265
+ * Persists a new onboarding invitation for an existing user account.
1266
+ *
1267
+ * @param invitation - Invitation metadata including the stored code hash.
1268
+ * @param actingUserId - User performing the create action.
1269
+ */
1270
+ async createInvitation(invitation, actingUserId) {
1271
+ const user = await this.findUserById(invitation.userId);
1272
+ if (!user) {
1273
+ throw new Error("User not found");
1274
+ }
1275
+ const data = {
1276
+ userId: invitation.userId,
1277
+ codeHash: invitation.codeHash,
1278
+ codePrefix: invitation.codePrefix,
1279
+ expiresAt: invitation.expiresAt,
1280
+ redeemedAt: invitation.redeemedAt,
1281
+ revokedAt: invitation.revokedAt,
1282
+ createdAt: invitation.createdAt,
1283
+ createdByUserId: actingUserId,
1284
+ updatedByUserId: actingUserId
1285
+ };
1286
+ await this.requireClient().collection(INVITATIONS_COLLECTION).doc(invitation.id).set(data);
1287
+ await this.recordAuditEntry(actingUserId, "create", "invitation", invitation.id);
1288
+ return invitation;
1289
+ }
1290
+ /**
1291
+ * Finds an invitation by stable identifier.
1292
+ *
1293
+ * @param id - Invitation identifier to look up.
1294
+ */
1295
+ async findInvitationById(id) {
1296
+ const snapshot = await this.requireClient().collection(INVITATIONS_COLLECTION).doc(id).get();
1297
+ if (!snapshot.exists) {
1298
+ return null;
1299
+ }
1300
+ return mapFirestoreInvitation(snapshot.id, snapshot.data());
1301
+ }
1302
+ /**
1303
+ * Finds an invitation by the sha256 hash of its secret.
1304
+ *
1305
+ * @param codeHash - sha256 hex digest of the invitation secret.
1306
+ */
1307
+ async findInvitationByCodeHash(codeHash) {
1308
+ const snapshot = await this.requireClient().collection(INVITATIONS_COLLECTION).where("codeHash", "==", codeHash).limit(1).get();
1309
+ const doc = snapshot.docs[0];
1310
+ if (!doc) {
1311
+ return null;
1312
+ }
1313
+ return mapFirestoreInvitation(doc.id, doc.data());
1314
+ }
1315
+ /**
1316
+ * Lists all invitations ordered by creation time descending.
1317
+ */
1318
+ async listInvitations() {
1319
+ const snapshot = await this.requireClient().collection(INVITATIONS_COLLECTION).orderBy("createdAt", "desc").get();
1320
+ return snapshot.docs.map(
1321
+ (doc) => mapFirestoreInvitation(doc.id, doc.data())
1322
+ );
1323
+ }
1324
+ /**
1325
+ * Revokes a pending invitation by id.
1326
+ *
1327
+ * @param id - Invitation identifier to revoke.
1328
+ * @param actingUserId - User performing the revoke action.
1329
+ */
1330
+ async revokeInvitation(id, actingUserId) {
1331
+ const docRef = this.requireClient().collection(INVITATIONS_COLLECTION).doc(id);
1332
+ const snapshot = await docRef.get();
1333
+ if (!snapshot.exists) {
1334
+ return false;
1335
+ }
1336
+ const data = snapshot.data();
1337
+ const now = /* @__PURE__ */ new Date();
1338
+ if (data.redeemedAt !== null || data.revokedAt !== null || data.expiresAt.getTime() <= now.getTime()) {
1339
+ return false;
1340
+ }
1341
+ await docRef.update({ revokedAt: now, updatedByUserId: actingUserId });
1342
+ await this.recordAuditEntry(actingUserId, "update", "invitation", id);
1343
+ return true;
1344
+ }
1345
+ /**
1346
+ * Atomically consumes a pending invitation and issues a permanent API token.
1347
+ *
1348
+ * @param codeHash - sha256 hex digest of the invitation secret.
1349
+ * @param tokenName - Label stored on the newly created API token.
1350
+ * @param actingUserId - Internal user attributed with the redemption action.
1351
+ */
1352
+ async redeemInvitation(codeHash, tokenName, actingUserId) {
1353
+ const now = /* @__PURE__ */ new Date();
1354
+ const client = this.requireClient();
1355
+ const invitationQuery = client.collection(INVITATIONS_COLLECTION).where("codeHash", "==", codeHash).limit(1);
1356
+ let user;
1357
+ let invitation;
1358
+ let token;
1359
+ let secret;
1360
+ await client.runTransaction(async (transaction) => {
1361
+ const invitationSnapshot = await transaction.get(invitationQuery);
1362
+ const invitationDoc = invitationSnapshot.docs[0];
1363
+ if (!invitationDoc) {
1364
+ throw new InvitationUnavailableError("not_found");
1365
+ }
1366
+ const invitationData = invitationDoc.data();
1367
+ if (invitationData.redeemedAt) {
1368
+ throw new InvitationUnavailableError("redeemed");
1369
+ }
1370
+ if (invitationData.revokedAt) {
1371
+ throw new InvitationUnavailableError("revoked");
1372
+ }
1373
+ if (invitationData.expiresAt.getTime() <= now.getTime()) {
1374
+ throw new InvitationUnavailableError("expired");
1375
+ }
1376
+ transaction.update(invitationDoc.ref, {
1377
+ redeemedAt: now,
1378
+ updatedByUserId: actingUserId
1379
+ });
1380
+ const userRef = client.collection(USERS_COLLECTION).doc(invitationData.userId);
1381
+ const userSnapshot = await transaction.get(userRef);
1382
+ if (!userSnapshot.exists) {
1383
+ throw new Error("User not found");
1384
+ }
1385
+ user = mapFirestoreUser(userSnapshot.id, userSnapshot.data());
1386
+ invitation = mapFirestoreInvitation(invitationDoc.id, {
1387
+ ...invitationData,
1388
+ redeemedAt: now,
1389
+ updatedByUserId: actingUserId
1390
+ });
1391
+ const generated = generateApiToken(user.id, tokenName);
1392
+ token = generated.record;
1393
+ secret = generated.secret;
1394
+ const tokenRef = client.collection(API_TOKENS_COLLECTION).doc(token.id);
1395
+ const tokenData = {
1396
+ userId: token.userId,
1397
+ name: token.name,
1398
+ tokenHash: token.tokenHash,
1399
+ tokenPrefix: token.tokenPrefix,
1400
+ createdAt: token.createdAt,
1401
+ lastUsedAt: token.lastUsedAt,
1402
+ revokedAt: token.revokedAt,
1403
+ createdByUserId: actingUserId,
1404
+ updatedByUserId: actingUserId
1405
+ };
1406
+ transaction.set(tokenRef, tokenData);
1407
+ });
1408
+ await this.recordAuditEntry(actingUserId, "update", "invitation", invitation.id);
1409
+ await this.recordAuditEntry(actingUserId, "create", "api_token", token.id);
1410
+ return { user, token, secret };
1411
+ }
1149
1412
  /**
1150
1413
  * Lists all collections ordered by name.
1151
1414
  */
@@ -1163,7 +1426,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1163
1426
  */
1164
1427
  async createCollection(name, actingUserId) {
1165
1428
  const trimmedName = trimRequiredName(name, "Collection name");
1166
- const id = randomUUID2();
1429
+ const id = randomUUID3();
1167
1430
  const now = /* @__PURE__ */ new Date();
1168
1431
  const data = {
1169
1432
  name: trimmedName,
@@ -1295,7 +1558,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1295
1558
  */
1296
1559
  async createEnvironment(name, actingUserId) {
1297
1560
  const trimmedName = trimRequiredName(name, "Environment name");
1298
- const id = randomUUID2();
1561
+ const id = randomUUID3();
1299
1562
  const now = /* @__PURE__ */ new Date();
1300
1563
  const data = {
1301
1564
  name: trimmedName,
@@ -1412,7 +1675,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1412
1675
  */
1413
1676
  async createSnippet(name, code, scope, actingUserId) {
1414
1677
  const trimmedName = trimRequiredName(name, "Snippet name");
1415
- const id = randomUUID2();
1678
+ const id = randomUUID3();
1416
1679
  const now = /* @__PURE__ */ new Date();
1417
1680
  const existing = await this.listSnippets();
1418
1681
  const maxOrder = existing.reduce((max, snippet) => Math.max(max, snippet.sortOrder), -1);
@@ -1606,7 +1869,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1606
1869
  }
1607
1870
  const existingRequests = await this.listRequests(input.collectionId);
1608
1871
  const maxOrder = existingRequests.filter((request) => request.folderId === folderId).reduce((max, request) => Math.max(max, request.sortOrder), -1);
1609
- const id = randomUUID2();
1872
+ const id = randomUUID3();
1610
1873
  const data = {
1611
1874
  collectionId: input.collectionId,
1612
1875
  folderId,
@@ -1676,7 +1939,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1676
1939
  */
1677
1940
  async createFolder(collectionId, name, actingUserId) {
1678
1941
  const trimmedName = trimRequiredName(name, "Folder name");
1679
- const id = randomUUID2();
1942
+ const id = randomUUID3();
1680
1943
  const now = /* @__PURE__ */ new Date();
1681
1944
  const existingFolders = await this.listFolders(collectionId);
1682
1945
  const maxOrder = existingFolders.reduce((max, folder) => Math.max(max, folder.sortOrder), -1);
@@ -1938,7 +2201,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1938
2201
  async createRunResult(input, actingUserId) {
1939
2202
  const metadata = parseRunResultPayload(input.payload);
1940
2203
  const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
1941
- const id = randomUUID2();
2204
+ const id = randomUUID3();
1942
2205
  const now = /* @__PURE__ */ new Date();
1943
2206
  const data = {
1944
2207
  kind: metadata.kind,
@@ -1987,7 +2250,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1987
2250
  * @param input - Usage details for one successful completion step.
1988
2251
  */
1989
2252
  async createLlmUsageLog(input) {
1990
- const id = randomUUID2();
2253
+ const id = randomUUID3();
1991
2254
  const now = /* @__PURE__ */ new Date();
1992
2255
  const data = {
1993
2256
  userId: input.userId,
@@ -2043,7 +2306,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
2043
2306
  return;
2044
2307
  }
2045
2308
  const input = createSystemUserInput();
2046
- const id = randomUUID2();
2309
+ const id = randomUUID3();
2047
2310
  const now = /* @__PURE__ */ new Date();
2048
2311
  const trimmedName = trimRequiredName(input.name, "User name");
2049
2312
  const data = {
@@ -2077,7 +2340,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
2077
2340
  (userId) => this.findUserById(userId),
2078
2341
  actingUserId
2079
2342
  );
2080
- const id = randomUUID2();
2343
+ const id = randomUUID3();
2081
2344
  const now = /* @__PURE__ */ new Date();
2082
2345
  const data = {
2083
2346
  userId: actingUserId,
@@ -2105,7 +2368,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
2105
2368
  };
2106
2369
 
2107
2370
  // src/db/mysql/MysqlDatabase.ts
2108
- import { randomUUID as randomUUID3 } from "crypto";
2371
+ import { randomUUID as randomUUID4 } from "crypto";
2109
2372
  import mysql from "mysql2/promise";
2110
2373
 
2111
2374
  // src/db/apiTokenRows.ts
@@ -2127,6 +2390,49 @@ function mapApiTokenSqlRow(row) {
2127
2390
  };
2128
2391
  }
2129
2392
 
2393
+ // src/db/invitationRows.ts
2394
+ function mapInvitationSqlRow(row) {
2395
+ return {
2396
+ id: row.id,
2397
+ userId: row.user_id,
2398
+ codeHash: row.code_hash,
2399
+ codePrefix: row.code_prefix,
2400
+ expiresAt: row.expires_at,
2401
+ redeemedAt: row.redeemed_at,
2402
+ revokedAt: row.revoked_at,
2403
+ createdAt: row.created_at,
2404
+ createdByUserId: row.created_by_user_id ?? null,
2405
+ updatedByUserId: row.updated_by_user_id ?? null
2406
+ };
2407
+ }
2408
+ function getInvitationStatus(invitation, now = /* @__PURE__ */ new Date()) {
2409
+ if (invitation.redeemedAt) {
2410
+ return "redeemed";
2411
+ }
2412
+ if (invitation.revokedAt) {
2413
+ return "revoked";
2414
+ }
2415
+ if (invitation.expiresAt.getTime() <= now.getTime()) {
2416
+ return "expired";
2417
+ }
2418
+ return "pending";
2419
+ }
2420
+
2421
+ // src/db/mysql/invitationSql.ts
2422
+ var INVITATION_SELECT_COLUMNS = `
2423
+ id,
2424
+ user_id,
2425
+ code_hash,
2426
+ code_prefix,
2427
+ expires_at,
2428
+ redeemed_at,
2429
+ revoked_at,
2430
+ created_at,
2431
+ created_by_user_id,
2432
+ updated_by_user_id
2433
+ `.trim();
2434
+ var INVITATION_SELECT = `SELECT ${INVITATION_SELECT_COLUMNS} FROM user_invitations`;
2435
+
2130
2436
  // src/db/auditLogRows.ts
2131
2437
  function parseAuditAction(value) {
2132
2438
  if (value === "create" || value === "update" || value === "delete" || value === "reorder" || value === "move") {
@@ -2135,7 +2441,7 @@ function parseAuditAction(value) {
2135
2441
  throw new Error(`Invalid audit action: ${value}`);
2136
2442
  }
2137
2443
  function parseAuditEntityType2(value) {
2138
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
2444
+ if (value === "user" || value === "api_token" || value === "invitation" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
2139
2445
  return value;
2140
2446
  }
2141
2447
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -2541,6 +2847,25 @@ CREATE TABLE IF NOT EXISTS run_results (
2541
2847
  INDEX run_results_created_idx (created_at)
2542
2848
  )
2543
2849
  `.trim();
2850
+ var USER_INVITATIONS_MIGRATION_SQL = `
2851
+ CREATE TABLE IF NOT EXISTS user_invitations (
2852
+ id VARCHAR(36) PRIMARY KEY,
2853
+ user_id VARCHAR(36) NOT NULL,
2854
+ code_hash CHAR(64) NOT NULL UNIQUE,
2855
+ code_prefix VARCHAR(32) NOT NULL,
2856
+ expires_at DATETIME NOT NULL,
2857
+ redeemed_at DATETIME,
2858
+ revoked_at DATETIME,
2859
+ created_at DATETIME NOT NULL,
2860
+ created_by_user_id VARCHAR(36),
2861
+ updated_by_user_id VARCHAR(36),
2862
+ INDEX user_invitations_user_id_idx (user_id),
2863
+ INDEX user_invitations_expires_at_idx (expires_at),
2864
+ CONSTRAINT user_invitations_user_id_fk FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
2865
+ CONSTRAINT user_invitations_created_by_fk FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL,
2866
+ CONSTRAINT user_invitations_updated_by_fk FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
2867
+ )
2868
+ `.trim();
2544
2869
  var MYSQL_MIGRATIONS = [
2545
2870
  USERS_MIGRATION_SQL,
2546
2871
  API_TOKENS_MIGRATION_SQL,
@@ -2567,7 +2892,8 @@ var MYSQL_MIGRATIONS = [
2567
2892
  ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
2568
2893
  USERS_SNIPPET_ACCESS_MIGRATION_SQL,
2569
2894
  USERS_SNIPPET_ACCESS_BACKFILL_SQL,
2570
- RUN_RESULTS_MIGRATION_SQL
2895
+ RUN_RESULTS_MIGRATION_SQL,
2896
+ USER_INVITATIONS_MIGRATION_SQL
2571
2897
  ];
2572
2898
 
2573
2899
  // src/db/mysql/schemas.ts
@@ -2800,7 +3126,7 @@ var MysqlDatabase = class _MysqlDatabase {
2800
3126
  async createUser(input, actingUserId) {
2801
3127
  const trimmedName = trimRequiredName(input.name, "User name");
2802
3128
  assertUserNameNotReserved(trimmedName);
2803
- const id = randomUUID3();
3129
+ const id = randomUUID4();
2804
3130
  const now = /* @__PURE__ */ new Date();
2805
3131
  const attributionUserId = trimmedName === SYSTEM_USER_NAME ? id : actingUserId;
2806
3132
  await this.executeStatement(
@@ -3128,65 +3454,355 @@ var MysqlDatabase = class _MysqlDatabase {
3128
3454
  await this.executeStatement(`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`, [when, id]);
3129
3455
  }
3130
3456
  /**
3131
- * Lists all collections ordered by name.
3457
+ * Creates a user account and its initial onboarding invitation in one transaction.
3458
+ *
3459
+ * @param userId - Pre-generated stable identifier for the new user.
3460
+ * @param input - User fields to persist.
3461
+ * @param invitation - Invitation metadata including the stored code hash.
3462
+ * @param actingUserId - User performing the create action.
3132
3463
  */
3133
- async listCollections() {
3134
- const rows = await this.queryRows(
3135
- `${COLLECTION_SELECT} ORDER BY name ASC`
3136
- );
3137
- return rows.map(mapCollectionSqlRow);
3464
+ async createInvitedUser(userId, input, invitation, actingUserId) {
3465
+ const trimmedName = trimRequiredName(input.name, "User name");
3466
+ assertUserNameNotReserved(trimmedName);
3467
+ const now = /* @__PURE__ */ new Date();
3468
+ const connection = await this.requirePool().getConnection();
3469
+ try {
3470
+ await connection.beginTransaction();
3471
+ await connection.execute(
3472
+ `INSERT INTO users (
3473
+ id,
3474
+ name,
3475
+ role,
3476
+ collection_access,
3477
+ environment_access,
3478
+ snippet_access,
3479
+ llm_access,
3480
+ llm_models,
3481
+ llm_monthly_token_limit,
3482
+ created_at,
3483
+ updated_at,
3484
+ created_by_user_id,
3485
+ updated_by_user_id
3486
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3487
+ [
3488
+ userId,
3489
+ trimmedName,
3490
+ input.role,
3491
+ serializeAccessList(input.collectionAccess),
3492
+ serializeAccessList(input.environmentAccess),
3493
+ serializeAccessList(input.snippetAccess),
3494
+ input.llmAccess ? 1 : 0,
3495
+ serializeAccessList(input.llmModels ?? []),
3496
+ input.llmMonthlyTokenLimit ?? null,
3497
+ now,
3498
+ now,
3499
+ actingUserId,
3500
+ actingUserId
3501
+ ]
3502
+ );
3503
+ const [userRows] = await connection.execute(
3504
+ `${USER_SELECT} WHERE id = ? LIMIT 1`,
3505
+ [userId]
3506
+ );
3507
+ const userRow = userRows[0];
3508
+ if (!userRow) {
3509
+ throw new Error("User not found after insert");
3510
+ }
3511
+ await connection.execute(
3512
+ `INSERT INTO user_invitations (
3513
+ id,
3514
+ user_id,
3515
+ code_hash,
3516
+ code_prefix,
3517
+ expires_at,
3518
+ redeemed_at,
3519
+ revoked_at,
3520
+ created_at,
3521
+ created_by_user_id,
3522
+ updated_by_user_id
3523
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3524
+ [
3525
+ invitation.id,
3526
+ invitation.userId,
3527
+ invitation.codeHash,
3528
+ invitation.codePrefix,
3529
+ invitation.expiresAt,
3530
+ invitation.redeemedAt,
3531
+ invitation.revokedAt,
3532
+ invitation.createdAt,
3533
+ actingUserId,
3534
+ actingUserId
3535
+ ]
3536
+ );
3537
+ await connection.commit();
3538
+ await this.recordAuditEntry(actingUserId, "create", "user", userId);
3539
+ await this.recordAuditEntry(actingUserId, "create", "invitation", invitation.id);
3540
+ return {
3541
+ user: mapUserSqlRow(userRow),
3542
+ invitation
3543
+ };
3544
+ } catch (error) {
3545
+ await connection.rollback();
3546
+ throw error;
3547
+ } finally {
3548
+ connection.release();
3549
+ }
3138
3550
  }
3139
3551
  /**
3140
- * Creates a new collection with the given name.
3552
+ * Persists a new onboarding invitation for an existing user account.
3141
3553
  *
3142
- * @param name - Display name for the collection.
3554
+ * @param invitation - Invitation metadata including the stored code hash.
3143
3555
  * @param actingUserId - User performing the create action.
3144
3556
  */
3145
- async createCollection(name, actingUserId) {
3146
- const trimmedName = trimRequiredName(name, "Collection name");
3147
- const id = randomUUID3();
3148
- const now = /* @__PURE__ */ new Date();
3557
+ async createInvitation(invitation, actingUserId) {
3558
+ const user = await this.findUserById(invitation.userId);
3559
+ if (!user) {
3560
+ throw new Error("User not found");
3561
+ }
3149
3562
  await this.executeStatement(
3150
- `INSERT INTO collections (
3563
+ `INSERT INTO user_invitations (
3151
3564
  id,
3152
- name,
3153
- variables,
3154
- headers,
3155
- auth,
3156
- pre_request_script,
3157
- post_request_script,
3565
+ user_id,
3566
+ code_hash,
3567
+ code_prefix,
3568
+ expires_at,
3569
+ redeemed_at,
3570
+ revoked_at,
3158
3571
  created_at,
3159
- updated_at,
3160
3572
  created_by_user_id,
3161
3573
  updated_by_user_id
3162
- ) VALUES (?, ?, '[]', '[]', ?, '', '', ?, ?, ?, ?)`,
3163
- [id, trimmedName, MYSQL_DEFAULT_AUTH_JSON, now, now, actingUserId, actingUserId]
3574
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3575
+ [
3576
+ invitation.id,
3577
+ invitation.userId,
3578
+ invitation.codeHash,
3579
+ invitation.codePrefix,
3580
+ invitation.expiresAt,
3581
+ invitation.redeemedAt,
3582
+ invitation.revokedAt,
3583
+ invitation.createdAt,
3584
+ actingUserId,
3585
+ actingUserId
3586
+ ]
3164
3587
  );
3165
- await this.recordAuditEntry(actingUserId, "create", "collection", id);
3588
+ await this.recordAuditEntry(actingUserId, "create", "invitation", invitation.id);
3589
+ return invitation;
3590
+ }
3591
+ /**
3592
+ * Finds an invitation by stable identifier.
3593
+ *
3594
+ * @param id - Invitation identifier to look up.
3595
+ */
3596
+ async findInvitationById(id) {
3166
3597
  const rows = await this.queryRows(
3167
- `${COLLECTION_SELECT} WHERE id = ?`,
3598
+ `${INVITATION_SELECT} WHERE id = ? LIMIT 1`,
3168
3599
  [id]
3169
3600
  );
3170
3601
  const row = rows[0];
3171
- if (!row) {
3172
- throw new Error("Collection not found after insert");
3173
- }
3174
- return mapCollectionSqlRow(row);
3602
+ return row ? mapInvitationSqlRow(row) : null;
3175
3603
  }
3176
3604
  /**
3177
- * Updates a collection's name, variables, headers, and scripts.
3605
+ * Finds an invitation by the sha256 hash of its secret.
3178
3606
  *
3179
- * @param actingUserId - User performing the update action.
3607
+ * @param codeHash - sha256 hex digest of the invitation secret.
3180
3608
  */
3181
- async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId) {
3182
- const trimmedName = trimRequiredName(name, "Collection name");
3183
- const updatedAt = /* @__PURE__ */ new Date();
3184
- const result = await this.executeStatement(
3185
- `UPDATE collections
3186
- SET name = ?,
3187
- variables = ?,
3188
- headers = ?,
3189
- auth = ?,
3609
+ async findInvitationByCodeHash(codeHash) {
3610
+ const rows = await this.queryRows(
3611
+ `${INVITATION_SELECT} WHERE code_hash = ? LIMIT 1`,
3612
+ [codeHash]
3613
+ );
3614
+ const row = rows[0];
3615
+ return row ? mapInvitationSqlRow(row) : null;
3616
+ }
3617
+ /**
3618
+ * Lists all invitations ordered by creation time descending.
3619
+ */
3620
+ async listInvitations() {
3621
+ const rows = await this.queryRows(
3622
+ `${INVITATION_SELECT} ORDER BY created_at DESC`
3623
+ );
3624
+ return rows.map(mapInvitationSqlRow);
3625
+ }
3626
+ /**
3627
+ * Revokes a pending invitation by id.
3628
+ *
3629
+ * @param id - Invitation identifier to revoke.
3630
+ * @param actingUserId - User performing the revoke action.
3631
+ */
3632
+ async revokeInvitation(id, actingUserId) {
3633
+ const now = /* @__PURE__ */ new Date();
3634
+ const result = await this.executeStatement(
3635
+ `UPDATE user_invitations
3636
+ SET revoked_at = ?,
3637
+ updated_by_user_id = ?
3638
+ WHERE id = ?
3639
+ AND redeemed_at IS NULL
3640
+ AND revoked_at IS NULL
3641
+ AND expires_at > ?`,
3642
+ [now, actingUserId, id, now]
3643
+ );
3644
+ const revoked = (result.affectedRows ?? 0) > 0;
3645
+ if (revoked) {
3646
+ await this.recordAuditEntry(actingUserId, "update", "invitation", id);
3647
+ }
3648
+ return revoked;
3649
+ }
3650
+ /**
3651
+ * Atomically consumes a pending invitation and issues a permanent API token.
3652
+ *
3653
+ * @param codeHash - sha256 hex digest of the invitation secret.
3654
+ * @param tokenName - Label stored on the newly created API token.
3655
+ * @param actingUserId - Internal user attributed with the redemption action.
3656
+ */
3657
+ async redeemInvitation(codeHash, tokenName, actingUserId) {
3658
+ const now = /* @__PURE__ */ new Date();
3659
+ const connection = await this.requirePool().getConnection();
3660
+ try {
3661
+ await connection.beginTransaction();
3662
+ const [updateResult] = await connection.execute(
3663
+ `UPDATE user_invitations
3664
+ SET redeemed_at = ?,
3665
+ updated_by_user_id = ?
3666
+ WHERE code_hash = ?
3667
+ AND redeemed_at IS NULL
3668
+ AND revoked_at IS NULL
3669
+ AND expires_at > ?`,
3670
+ [now, actingUserId, codeHash, now]
3671
+ );
3672
+ if ((updateResult.affectedRows ?? 0) === 0) {
3673
+ const [existingRows] = await connection.execute(
3674
+ `${INVITATION_SELECT} WHERE code_hash = ? LIMIT 1`,
3675
+ [codeHash]
3676
+ );
3677
+ const existingRow = existingRows[0];
3678
+ if (!existingRow) {
3679
+ throw new InvitationUnavailableError("not_found");
3680
+ }
3681
+ const existing = mapInvitationSqlRow(existingRow);
3682
+ if (existing.redeemedAt) {
3683
+ throw new InvitationUnavailableError("redeemed");
3684
+ }
3685
+ if (existing.revokedAt) {
3686
+ throw new InvitationUnavailableError("revoked");
3687
+ }
3688
+ throw new InvitationUnavailableError("expired");
3689
+ }
3690
+ const [rows] = await connection.execute(
3691
+ `${INVITATION_SELECT} WHERE code_hash = ? LIMIT 1`,
3692
+ [codeHash]
3693
+ );
3694
+ const invitationRow = rows[0];
3695
+ if (!invitationRow) {
3696
+ throw new Error("Invitation not found after claim");
3697
+ }
3698
+ const invitation = mapInvitationSqlRow(invitationRow);
3699
+ const [userRows] = await connection.execute(
3700
+ `${USER_SELECT} WHERE id = ? LIMIT 1`,
3701
+ [invitation.userId]
3702
+ );
3703
+ const userRow = userRows[0];
3704
+ if (!userRow) {
3705
+ throw new Error("User not found");
3706
+ }
3707
+ const user = mapUserSqlRow(userRow);
3708
+ const { record, secret } = generateApiToken(user.id, tokenName);
3709
+ await connection.execute(
3710
+ `INSERT INTO api_tokens (
3711
+ id,
3712
+ user_id,
3713
+ name,
3714
+ token_hash,
3715
+ token_prefix,
3716
+ created_at,
3717
+ last_used_at,
3718
+ revoked_at,
3719
+ created_by_user_id,
3720
+ updated_by_user_id
3721
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3722
+ [
3723
+ record.id,
3724
+ record.userId,
3725
+ record.name,
3726
+ record.tokenHash,
3727
+ record.tokenPrefix,
3728
+ record.createdAt,
3729
+ record.lastUsedAt,
3730
+ record.revokedAt,
3731
+ actingUserId,
3732
+ actingUserId
3733
+ ]
3734
+ );
3735
+ await connection.commit();
3736
+ await this.recordAuditEntry(actingUserId, "update", "invitation", invitation.id);
3737
+ await this.recordAuditEntry(actingUserId, "create", "api_token", record.id);
3738
+ return { user, token: record, secret };
3739
+ } catch (error) {
3740
+ await connection.rollback();
3741
+ throw error;
3742
+ } finally {
3743
+ connection.release();
3744
+ }
3745
+ }
3746
+ /**
3747
+ * Lists all collections ordered by name.
3748
+ */
3749
+ async listCollections() {
3750
+ const rows = await this.queryRows(
3751
+ `${COLLECTION_SELECT} ORDER BY name ASC`
3752
+ );
3753
+ return rows.map(mapCollectionSqlRow);
3754
+ }
3755
+ /**
3756
+ * Creates a new collection with the given name.
3757
+ *
3758
+ * @param name - Display name for the collection.
3759
+ * @param actingUserId - User performing the create action.
3760
+ */
3761
+ async createCollection(name, actingUserId) {
3762
+ const trimmedName = trimRequiredName(name, "Collection name");
3763
+ const id = randomUUID4();
3764
+ const now = /* @__PURE__ */ new Date();
3765
+ await this.executeStatement(
3766
+ `INSERT INTO collections (
3767
+ id,
3768
+ name,
3769
+ variables,
3770
+ headers,
3771
+ auth,
3772
+ pre_request_script,
3773
+ post_request_script,
3774
+ created_at,
3775
+ updated_at,
3776
+ created_by_user_id,
3777
+ updated_by_user_id
3778
+ ) VALUES (?, ?, '[]', '[]', ?, '', '', ?, ?, ?, ?)`,
3779
+ [id, trimmedName, MYSQL_DEFAULT_AUTH_JSON, now, now, actingUserId, actingUserId]
3780
+ );
3781
+ await this.recordAuditEntry(actingUserId, "create", "collection", id);
3782
+ const rows = await this.queryRows(
3783
+ `${COLLECTION_SELECT} WHERE id = ?`,
3784
+ [id]
3785
+ );
3786
+ const row = rows[0];
3787
+ if (!row) {
3788
+ throw new Error("Collection not found after insert");
3789
+ }
3790
+ return mapCollectionSqlRow(row);
3791
+ }
3792
+ /**
3793
+ * Updates a collection's name, variables, headers, and scripts.
3794
+ *
3795
+ * @param actingUserId - User performing the update action.
3796
+ */
3797
+ async updateCollection(id, name, variables, headers, preRequestScript, postRequestScript, auth, actingUserId) {
3798
+ const trimmedName = trimRequiredName(name, "Collection name");
3799
+ const updatedAt = /* @__PURE__ */ new Date();
3800
+ const result = await this.executeStatement(
3801
+ `UPDATE collections
3802
+ SET name = ?,
3803
+ variables = ?,
3804
+ headers = ?,
3805
+ auth = ?,
3190
3806
  pre_request_script = ?,
3191
3807
  post_request_script = ?,
3192
3808
  updated_at = ?,
@@ -3289,7 +3905,7 @@ var MysqlDatabase = class _MysqlDatabase {
3289
3905
  */
3290
3906
  async createEnvironment(name, actingUserId) {
3291
3907
  const trimmedName = trimRequiredName(name, "Environment name");
3292
- const id = randomUUID3();
3908
+ const id = randomUUID4();
3293
3909
  const now = /* @__PURE__ */ new Date();
3294
3910
  await this.executeStatement(
3295
3911
  `INSERT INTO environments (
@@ -3418,7 +4034,7 @@ var MysqlDatabase = class _MysqlDatabase {
3418
4034
  */
3419
4035
  async createSnippet(name, code, scope, actingUserId) {
3420
4036
  const trimmedName = trimRequiredName(name, "Snippet name");
3421
- const id = randomUUID3();
4037
+ const id = randomUUID4();
3422
4038
  const now = /* @__PURE__ */ new Date();
3423
4039
  const maxRows = await this.queryRows(
3424
4040
  "SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM snippets"
@@ -3641,7 +4257,7 @@ var MysqlDatabase = class _MysqlDatabase {
3641
4257
  [input.collectionId, folderId, folderId]
3642
4258
  );
3643
4259
  const maxOrder = maxRows[0]?.max_order ?? -1;
3644
- const id = randomUUID3();
4260
+ const id = randomUUID4();
3645
4261
  await this.executeStatement(
3646
4262
  `INSERT INTO requests (
3647
4263
  id,
@@ -3741,7 +4357,7 @@ var MysqlDatabase = class _MysqlDatabase {
3741
4357
  */
3742
4358
  async createFolder(collectionId, name, actingUserId) {
3743
4359
  const trimmedName = trimRequiredName(name, "Folder name");
3744
- const id = randomUUID3();
4360
+ const id = randomUUID4();
3745
4361
  const now = /* @__PURE__ */ new Date();
3746
4362
  const maxRows = await this.queryRows(
3747
4363
  "SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM folders WHERE collection_id = ?",
@@ -3995,7 +4611,7 @@ var MysqlDatabase = class _MysqlDatabase {
3995
4611
  async addLlmUsage(userId, period, promptTokens, completionTokens) {
3996
4612
  const totalDelta = promptTokens + completionTokens;
3997
4613
  const now = /* @__PURE__ */ new Date();
3998
- const id = randomUUID3();
4614
+ const id = randomUUID4();
3999
4615
  await this.executeStatement(
4000
4616
  `INSERT INTO llm_usage (
4001
4617
  id,
@@ -4025,7 +4641,7 @@ var MysqlDatabase = class _MysqlDatabase {
4025
4641
  * @param input - Usage details for one successful completion step.
4026
4642
  */
4027
4643
  async createLlmUsageLog(input) {
4028
- const id = randomUUID3();
4644
+ const id = randomUUID4();
4029
4645
  const now = /* @__PURE__ */ new Date();
4030
4646
  await this.executeStatement(
4031
4647
  `INSERT INTO llm_usage_log (
@@ -4103,7 +4719,7 @@ var MysqlDatabase = class _MysqlDatabase {
4103
4719
  async createRunResult(input, actingUserId) {
4104
4720
  const metadata = parseRunResultPayload(input.payload);
4105
4721
  const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
4106
- const id = randomUUID3();
4722
+ const id = randomUUID4();
4107
4723
  const now = /* @__PURE__ */ new Date();
4108
4724
  await this.executeStatement(
4109
4725
  `INSERT INTO run_results (
@@ -4175,7 +4791,7 @@ var MysqlDatabase = class _MysqlDatabase {
4175
4791
  return;
4176
4792
  }
4177
4793
  const input = createSystemUserInput();
4178
- const id = randomUUID3();
4794
+ const id = randomUUID4();
4179
4795
  const now = /* @__PURE__ */ new Date();
4180
4796
  const trimmedName = trimRequiredName(input.name, "User name");
4181
4797
  await this.executeStatement(
@@ -4223,7 +4839,7 @@ var MysqlDatabase = class _MysqlDatabase {
4223
4839
  */
4224
4840
  async recordAuditEntry(actingUserId, action, entityType, entityId, metadata) {
4225
4841
  const userName = await resolveActingUserName(this.findUserById.bind(this), actingUserId);
4226
- const id = randomUUID3();
4842
+ const id = randomUUID4();
4227
4843
  const now = /* @__PURE__ */ new Date();
4228
4844
  await this.executeStatement(
4229
4845
  `INSERT INTO audit_log (
@@ -4285,9 +4901,24 @@ var MysqlDatabase = class _MysqlDatabase {
4285
4901
  };
4286
4902
 
4287
4903
  // src/db/postgres/PostgresDatabase.ts
4288
- import { randomUUID as randomUUID4 } from "crypto";
4904
+ import { randomUUID as randomUUID5 } from "crypto";
4289
4905
  import pg from "pg";
4290
4906
 
4907
+ // src/db/postgres/invitationSql.ts
4908
+ var INVITATION_SELECT_COLUMNS2 = `
4909
+ id,
4910
+ user_id,
4911
+ code_hash,
4912
+ code_prefix,
4913
+ expires_at,
4914
+ redeemed_at,
4915
+ revoked_at,
4916
+ created_at,
4917
+ created_by_user_id,
4918
+ updated_by_user_id
4919
+ `.trim();
4920
+ var INVITATION_SELECT2 = `SELECT ${INVITATION_SELECT_COLUMNS2} FROM user_invitations`;
4921
+
4291
4922
  // src/db/postgres/migrations.ts
4292
4923
  var API_TOKENS_MIGRATION_SQL2 = `
4293
4924
  CREATE TABLE IF NOT EXISTS api_tokens (
@@ -4527,6 +5158,22 @@ CREATE TABLE IF NOT EXISTS run_results (
4527
5158
  );
4528
5159
  CREATE INDEX IF NOT EXISTS run_results_created_idx ON run_results (created_at DESC);
4529
5160
  `.trim();
5161
+ var USER_INVITATIONS_MIGRATION_SQL2 = `
5162
+ CREATE TABLE IF NOT EXISTS user_invitations (
5163
+ id TEXT PRIMARY KEY,
5164
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5165
+ code_hash CHAR(64) NOT NULL UNIQUE,
5166
+ code_prefix TEXT NOT NULL,
5167
+ expires_at TIMESTAMPTZ NOT NULL,
5168
+ redeemed_at TIMESTAMPTZ,
5169
+ revoked_at TIMESTAMPTZ,
5170
+ created_at TIMESTAMPTZ NOT NULL,
5171
+ created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
5172
+ updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
5173
+ );
5174
+ CREATE INDEX IF NOT EXISTS user_invitations_user_id_idx ON user_invitations (user_id);
5175
+ CREATE INDEX IF NOT EXISTS user_invitations_expires_at_idx ON user_invitations (expires_at);
5176
+ `.trim();
4530
5177
  var POSTGRES_MIGRATIONS = [
4531
5178
  USERS_MIGRATION_SQL2,
4532
5179
  API_TOKENS_MIGRATION_SQL2,
@@ -4553,7 +5200,8 @@ var POSTGRES_MIGRATIONS = [
4553
5200
  ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
4554
5201
  USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
4555
5202
  USERS_SNIPPET_ACCESS_BACKFILL_SQL2,
4556
- RUN_RESULTS_MIGRATION_SQL2
5203
+ RUN_RESULTS_MIGRATION_SQL2,
5204
+ USER_INVITATIONS_MIGRATION_SQL2
4557
5205
  ];
4558
5206
 
4559
5207
  // src/db/postgres/schemas.ts
@@ -4711,7 +5359,7 @@ var PostgresDatabase = class _PostgresDatabase {
4711
5359
  async createUser(input, actingUserId) {
4712
5360
  const trimmedName = trimRequiredName(input.name, "User name");
4713
5361
  assertUserNameNotReserved(trimmedName);
4714
- const id = randomUUID4();
5362
+ const id = randomUUID5();
4715
5363
  const now = /* @__PURE__ */ new Date();
4716
5364
  const result = await this.query(
4717
5365
  `INSERT INTO users (
@@ -5029,6 +5677,286 @@ var PostgresDatabase = class _PostgresDatabase {
5029
5677
  async touchApiTokenLastUsed(id, when) {
5030
5678
  await this.query(`UPDATE api_tokens SET last_used_at = $2 WHERE id = $1`, [id, when]);
5031
5679
  }
5680
+ /**
5681
+ * Creates a user account and its initial onboarding invitation in one transaction.
5682
+ *
5683
+ * @param userId - Pre-generated stable identifier for the new user.
5684
+ * @param input - User fields to persist.
5685
+ * @param invitation - Invitation metadata including the stored code hash.
5686
+ * @param actingUserId - User performing the create action.
5687
+ */
5688
+ async createInvitedUser(userId, input, invitation, actingUserId) {
5689
+ const trimmedName = trimRequiredName(input.name, "User name");
5690
+ assertUserNameNotReserved(trimmedName);
5691
+ const now = /* @__PURE__ */ new Date();
5692
+ const client = await this.requirePool().connect();
5693
+ try {
5694
+ await client.query("BEGIN");
5695
+ const userResult = await client.query(
5696
+ `INSERT INTO users (
5697
+ id,
5698
+ name,
5699
+ role,
5700
+ collection_access,
5701
+ environment_access,
5702
+ snippet_access,
5703
+ llm_access,
5704
+ llm_models,
5705
+ llm_monthly_token_limit,
5706
+ created_at,
5707
+ updated_at,
5708
+ created_by_user_id,
5709
+ updated_by_user_id
5710
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
5711
+ RETURNING ${USER_SELECT_COLUMNS}`,
5712
+ [
5713
+ userId,
5714
+ trimmedName,
5715
+ input.role,
5716
+ serializeAccessList(input.collectionAccess),
5717
+ serializeAccessList(input.environmentAccess),
5718
+ serializeAccessList(input.snippetAccess),
5719
+ input.llmAccess ?? false,
5720
+ serializeAccessList(input.llmModels ?? []),
5721
+ input.llmMonthlyTokenLimit ?? null,
5722
+ now,
5723
+ now,
5724
+ actingUserId,
5725
+ actingUserId
5726
+ ]
5727
+ );
5728
+ const userRow = userResult.rows[0];
5729
+ if (!userRow) {
5730
+ throw new Error("User not found after insert");
5731
+ }
5732
+ await client.query(
5733
+ `INSERT INTO user_invitations (
5734
+ id,
5735
+ user_id,
5736
+ code_hash,
5737
+ code_prefix,
5738
+ expires_at,
5739
+ redeemed_at,
5740
+ revoked_at,
5741
+ created_at,
5742
+ created_by_user_id,
5743
+ updated_by_user_id
5744
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
5745
+ [
5746
+ invitation.id,
5747
+ invitation.userId,
5748
+ invitation.codeHash,
5749
+ invitation.codePrefix,
5750
+ invitation.expiresAt,
5751
+ invitation.redeemedAt,
5752
+ invitation.revokedAt,
5753
+ invitation.createdAt,
5754
+ actingUserId,
5755
+ actingUserId
5756
+ ]
5757
+ );
5758
+ await client.query("COMMIT");
5759
+ await this.recordAuditEntry(actingUserId, "create", "user", userId);
5760
+ await this.recordAuditEntry(actingUserId, "create", "invitation", invitation.id);
5761
+ return {
5762
+ user: mapUserSqlRow(userRow),
5763
+ invitation
5764
+ };
5765
+ } catch (error) {
5766
+ await client.query("ROLLBACK");
5767
+ throw error;
5768
+ } finally {
5769
+ client.release();
5770
+ }
5771
+ }
5772
+ /**
5773
+ * Persists a new onboarding invitation for an existing user account.
5774
+ *
5775
+ * @param invitation - Invitation metadata including the stored code hash.
5776
+ * @param actingUserId - User performing the create action.
5777
+ */
5778
+ async createInvitation(invitation, actingUserId) {
5779
+ const user = await this.findUserById(invitation.userId);
5780
+ if (!user) {
5781
+ throw new Error("User not found");
5782
+ }
5783
+ await this.query(
5784
+ `INSERT INTO user_invitations (
5785
+ id,
5786
+ user_id,
5787
+ code_hash,
5788
+ code_prefix,
5789
+ expires_at,
5790
+ redeemed_at,
5791
+ revoked_at,
5792
+ created_at,
5793
+ created_by_user_id,
5794
+ updated_by_user_id
5795
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
5796
+ [
5797
+ invitation.id,
5798
+ invitation.userId,
5799
+ invitation.codeHash,
5800
+ invitation.codePrefix,
5801
+ invitation.expiresAt,
5802
+ invitation.redeemedAt,
5803
+ invitation.revokedAt,
5804
+ invitation.createdAt,
5805
+ actingUserId,
5806
+ actingUserId
5807
+ ]
5808
+ );
5809
+ await this.recordAuditEntry(actingUserId, "create", "invitation", invitation.id);
5810
+ return invitation;
5811
+ }
5812
+ /**
5813
+ * Finds an invitation by stable identifier.
5814
+ *
5815
+ * @param id - Invitation identifier to look up.
5816
+ */
5817
+ async findInvitationById(id) {
5818
+ const result = await this.query(
5819
+ `${INVITATION_SELECT2} WHERE id = $1 LIMIT 1`,
5820
+ [id]
5821
+ );
5822
+ const row = result.rows[0];
5823
+ return row ? mapInvitationSqlRow(row) : null;
5824
+ }
5825
+ /**
5826
+ * Finds an invitation by the sha256 hash of its secret.
5827
+ *
5828
+ * @param codeHash - sha256 hex digest of the invitation secret.
5829
+ */
5830
+ async findInvitationByCodeHash(codeHash) {
5831
+ const result = await this.query(
5832
+ `${INVITATION_SELECT2} WHERE code_hash = $1 LIMIT 1`,
5833
+ [codeHash]
5834
+ );
5835
+ const row = result.rows[0];
5836
+ return row ? mapInvitationSqlRow(row) : null;
5837
+ }
5838
+ /**
5839
+ * Lists all invitations ordered by creation time descending.
5840
+ */
5841
+ async listInvitations() {
5842
+ const result = await this.query(
5843
+ `${INVITATION_SELECT2} ORDER BY created_at DESC`
5844
+ );
5845
+ return result.rows.map(mapInvitationSqlRow);
5846
+ }
5847
+ /**
5848
+ * Revokes a pending invitation by id.
5849
+ *
5850
+ * @param id - Invitation identifier to revoke.
5851
+ * @param actingUserId - User performing the revoke action.
5852
+ */
5853
+ async revokeInvitation(id, actingUserId) {
5854
+ const now = /* @__PURE__ */ new Date();
5855
+ const result = await this.query(
5856
+ `UPDATE user_invitations
5857
+ SET revoked_at = $1, updated_by_user_id = $2
5858
+ WHERE id = $3
5859
+ AND redeemed_at IS NULL
5860
+ AND revoked_at IS NULL
5861
+ AND expires_at > $1
5862
+ RETURNING id`,
5863
+ [now, actingUserId, id]
5864
+ );
5865
+ if (!result.rows[0]) {
5866
+ return false;
5867
+ }
5868
+ await this.recordAuditEntry(actingUserId, "update", "invitation", id);
5869
+ return true;
5870
+ }
5871
+ /**
5872
+ * Atomically consumes a pending invitation and issues a permanent API token.
5873
+ *
5874
+ * @param codeHash - sha256 hex digest of the invitation secret.
5875
+ * @param tokenName - Label stored on the newly created API token.
5876
+ * @param actingUserId - Internal user attributed with the redemption action.
5877
+ */
5878
+ async redeemInvitation(codeHash, tokenName, actingUserId) {
5879
+ const now = /* @__PURE__ */ new Date();
5880
+ const client = await this.requirePool().connect();
5881
+ try {
5882
+ await client.query("BEGIN");
5883
+ const claimResult = await client.query(
5884
+ `UPDATE user_invitations
5885
+ SET redeemed_at = $1, updated_by_user_id = $2
5886
+ WHERE code_hash = $3
5887
+ AND redeemed_at IS NULL
5888
+ AND revoked_at IS NULL
5889
+ AND expires_at > $1
5890
+ RETURNING ${INVITATION_SELECT_COLUMNS2}`,
5891
+ [now, actingUserId, codeHash]
5892
+ );
5893
+ const invitationRow = claimResult.rows[0];
5894
+ if (!invitationRow) {
5895
+ const existingResult = await client.query(
5896
+ `${INVITATION_SELECT2} WHERE code_hash = $1 LIMIT 1`,
5897
+ [codeHash]
5898
+ );
5899
+ const existingRow = existingResult.rows[0];
5900
+ if (!existingRow) {
5901
+ throw new InvitationUnavailableError("not_found");
5902
+ }
5903
+ const existing = mapInvitationSqlRow(existingRow);
5904
+ if (existing.redeemedAt) {
5905
+ throw new InvitationUnavailableError("redeemed");
5906
+ }
5907
+ if (existing.revokedAt) {
5908
+ throw new InvitationUnavailableError("revoked");
5909
+ }
5910
+ throw new InvitationUnavailableError("expired");
5911
+ }
5912
+ const invitation = mapInvitationSqlRow(invitationRow);
5913
+ const userResult = await client.query(`${USER_SELECT2} WHERE id = $1 LIMIT 1`, [
5914
+ invitation.userId
5915
+ ]);
5916
+ const userRow = userResult.rows[0];
5917
+ if (!userRow) {
5918
+ throw new Error("User not found");
5919
+ }
5920
+ const user = mapUserSqlRow(userRow);
5921
+ const effectiveTokenName = tokenName.trim().length > 0 ? tokenName.trim() : user.name;
5922
+ const { record, secret } = generateApiToken(user.id, effectiveTokenName);
5923
+ await client.query(
5924
+ `INSERT INTO api_tokens (
5925
+ id,
5926
+ user_id,
5927
+ name,
5928
+ token_hash,
5929
+ token_prefix,
5930
+ created_at,
5931
+ last_used_at,
5932
+ revoked_at,
5933
+ created_by_user_id,
5934
+ updated_by_user_id
5935
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
5936
+ [
5937
+ record.id,
5938
+ record.userId,
5939
+ record.name,
5940
+ record.tokenHash,
5941
+ record.tokenPrefix,
5942
+ record.createdAt,
5943
+ record.lastUsedAt,
5944
+ record.revokedAt,
5945
+ actingUserId,
5946
+ actingUserId
5947
+ ]
5948
+ );
5949
+ await client.query("COMMIT");
5950
+ await this.recordAuditEntry(actingUserId, "update", "invitation", invitation.id);
5951
+ await this.recordAuditEntry(actingUserId, "create", "api_token", record.id);
5952
+ return { user, token: record, secret };
5953
+ } catch (error) {
5954
+ await client.query("ROLLBACK");
5955
+ throw error;
5956
+ } finally {
5957
+ client.release();
5958
+ }
5959
+ }
5032
5960
  /**
5033
5961
  * Lists all collections ordered by name.
5034
5962
  */
@@ -5044,7 +5972,7 @@ var PostgresDatabase = class _PostgresDatabase {
5044
5972
  */
5045
5973
  async createCollection(name, actingUserId) {
5046
5974
  const trimmedName = trimRequiredName(name, "Collection name");
5047
- const id = randomUUID4();
5975
+ const id = randomUUID5();
5048
5976
  const now = /* @__PURE__ */ new Date();
5049
5977
  const result = await this.query(
5050
5978
  `INSERT INTO collections (
@@ -5179,7 +6107,7 @@ var PostgresDatabase = class _PostgresDatabase {
5179
6107
  */
5180
6108
  async createEnvironment(name, actingUserId) {
5181
6109
  const trimmedName = trimRequiredName(name, "Environment name");
5182
- const id = randomUUID4();
6110
+ const id = randomUUID5();
5183
6111
  const now = /* @__PURE__ */ new Date();
5184
6112
  const result = await this.query(
5185
6113
  `INSERT INTO environments (
@@ -5302,7 +6230,7 @@ var PostgresDatabase = class _PostgresDatabase {
5302
6230
  */
5303
6231
  async createSnippet(name, code, scope, actingUserId) {
5304
6232
  const trimmedName = trimRequiredName(name, "Snippet name");
5305
- const id = randomUUID4();
6233
+ const id = randomUUID5();
5306
6234
  const now = /* @__PURE__ */ new Date();
5307
6235
  const maxResult = await this.query(
5308
6236
  "SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM snippets"
@@ -5508,7 +6436,7 @@ var PostgresDatabase = class _PostgresDatabase {
5508
6436
  [input.collectionId, folderId]
5509
6437
  );
5510
6438
  const maxOrder = maxResult.rows[0]?.max_order ?? -1;
5511
- const id = randomUUID4();
6439
+ const id = randomUUID5();
5512
6440
  const result = await this.query(
5513
6441
  `INSERT INTO requests (
5514
6442
  id,
@@ -5602,7 +6530,7 @@ var PostgresDatabase = class _PostgresDatabase {
5602
6530
  */
5603
6531
  async createFolder(collectionId, name, actingUserId) {
5604
6532
  const trimmedName = trimRequiredName(name, "Folder name");
5605
- const id = randomUUID4();
6533
+ const id = randomUUID5();
5606
6534
  const now = /* @__PURE__ */ new Date();
5607
6535
  const maxResult = await this.query(
5608
6536
  "SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM folders WHERE collection_id = $1",
@@ -5849,7 +6777,7 @@ var PostgresDatabase = class _PostgresDatabase {
5849
6777
  async addLlmUsage(userId, period, promptTokens, completionTokens) {
5850
6778
  const totalDelta = promptTokens + completionTokens;
5851
6779
  const now = /* @__PURE__ */ new Date();
5852
- const id = randomUUID4();
6780
+ const id = randomUUID5();
5853
6781
  const result = await this.query(
5854
6782
  `INSERT INTO llm_usage (
5855
6783
  id,
@@ -5880,7 +6808,7 @@ var PostgresDatabase = class _PostgresDatabase {
5880
6808
  * @param input - Usage details for one successful completion step.
5881
6809
  */
5882
6810
  async createLlmUsageLog(input) {
5883
- const id = randomUUID4();
6811
+ const id = randomUUID5();
5884
6812
  const now = /* @__PURE__ */ new Date();
5885
6813
  const result = await this.query(
5886
6814
  `INSERT INTO llm_usage_log (
@@ -5955,7 +6883,7 @@ var PostgresDatabase = class _PostgresDatabase {
5955
6883
  async createRunResult(input, actingUserId) {
5956
6884
  const metadata = parseRunResultPayload(input.payload);
5957
6885
  const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
5958
- const id = randomUUID4();
6886
+ const id = randomUUID5();
5959
6887
  const now = /* @__PURE__ */ new Date();
5960
6888
  const result = await this.query(
5961
6889
  `INSERT INTO run_results (
@@ -6035,7 +6963,7 @@ var PostgresDatabase = class _PostgresDatabase {
6035
6963
  this.systemUserId = existing.id;
6036
6964
  return;
6037
6965
  }
6038
- const id = randomUUID4();
6966
+ const id = randomUUID5();
6039
6967
  const now = /* @__PURE__ */ new Date();
6040
6968
  const input = createSystemUserInput();
6041
6969
  await this.query(
@@ -6086,7 +7014,7 @@ var PostgresDatabase = class _PostgresDatabase {
6086
7014
  (userId) => this.findUserById(userId),
6087
7015
  actingUserId
6088
7016
  );
6089
- const id = randomUUID4();
7017
+ const id = randomUUID5();
6090
7018
  const now = /* @__PURE__ */ new Date();
6091
7019
  await this.query(
6092
7020
  `INSERT INTO audit_log (
@@ -6290,39 +7218,41 @@ function registerMigrateCommand(program, handler = migrateCommand) {
6290
7218
 
6291
7219
  // src/cli/userCommand.ts
6292
7220
  import { InvalidArgumentError } from "commander";
7221
+ import { randomUUID as randomUUID7 } from "crypto";
6293
7222
 
6294
- // src/server/auth/apiTokens.ts
6295
- import { createHash, randomBytes, randomUUID as randomUUID5 } from "crypto";
6296
- var TOKEN_PREFIX = "hbk_";
6297
- function hashToken(token) {
6298
- return createHash("sha256").update(token).digest("hex");
6299
- }
6300
- function generateApiToken(userId, name) {
6301
- const secretSuffix = randomBytes(32).toString("base64url");
6302
- const secret = `${TOKEN_PREFIX}${secretSuffix}`;
6303
- const tokenPrefix = `${TOKEN_PREFIX}${secretSuffix.slice(0, 8)}`;
7223
+ // src/server/auth/invitations.ts
7224
+ import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID6 } from "crypto";
7225
+ var INVITATION_PREFIX = "hbi_";
7226
+ var DEFAULT_INVITATION_EXPIRES_IN_HOURS = 24;
7227
+ function hashInvitationSecret(secret) {
7228
+ return createHash2("sha256").update(secret).digest("hex");
7229
+ }
7230
+ function isInvitationSecretFormat(secret) {
7231
+ return secret.startsWith(INVITATION_PREFIX) && secret.length > INVITATION_PREFIX.length + 8;
7232
+ }
7233
+ function resolveInvitationExpiresAt(expiresInHours) {
7234
+ const hours = expiresInHours !== void 0 && expiresInHours > 0 ? expiresInHours : DEFAULT_INVITATION_EXPIRES_IN_HOURS;
7235
+ return new Date(Date.now() + hours * 60 * 60 * 1e3);
7236
+ }
7237
+ function generateInvitation(userId, actingUserId, expiresAt) {
7238
+ const secretSuffix = randomBytes2(32).toString("base64url");
7239
+ const secret = `${INVITATION_PREFIX}${secretSuffix}`;
7240
+ const codePrefix = `${INVITATION_PREFIX}${secretSuffix.slice(0, 8)}`;
6304
7241
  const createdAt = /* @__PURE__ */ new Date();
6305
7242
  const record = {
6306
- id: randomUUID5(),
7243
+ id: randomUUID6(),
6307
7244
  userId,
6308
- name,
6309
- tokenHash: hashToken(secret),
6310
- tokenPrefix,
6311
- createdAt,
6312
- lastUsedAt: null,
7245
+ codeHash: hashInvitationSecret(secret),
7246
+ codePrefix,
7247
+ expiresAt,
7248
+ redeemedAt: null,
6313
7249
  revokedAt: null,
6314
- createdByUserId: null,
6315
- updatedByUserId: null
7250
+ createdAt,
7251
+ createdByUserId: actingUserId,
7252
+ updatedByUserId: actingUserId
6316
7253
  };
6317
7254
  return { record, secret };
6318
7255
  }
6319
- function extractBearer(headerValue) {
6320
- if (!headerValue) {
6321
- return null;
6322
- }
6323
- const match = /^Bearer\s+(\S+)$/i.exec(headerValue.trim());
6324
- return match?.[1] ?? null;
6325
- }
6326
7256
 
6327
7257
  // src/server/admin/userValidation.ts
6328
7258
  var ValidationError = class extends Error {
@@ -6578,6 +7508,13 @@ function parseMonthlyTokenLimit(value) {
6578
7508
  }
6579
7509
  return parsed;
6580
7510
  }
7511
+ function parsePositiveInt(value) {
7512
+ const parsed = Number(value.trim());
7513
+ if (!Number.isInteger(parsed) || parsed <= 0) {
7514
+ throw new InvalidArgumentError("Value must be a positive integer.");
7515
+ }
7516
+ return parsed;
7517
+ }
6581
7518
  function printUser(user, usage) {
6582
7519
  console.log(`- id: ${user.id}`);
6583
7520
  console.log(` name: ${user.name}`);
@@ -6683,6 +7620,100 @@ async function userCreateCommand(options) {
6683
7620
  console.log("");
6684
7621
  printCreatedApiToken(user, record, secret);
6685
7622
  }
7623
+ function printCreatedInvitation(user, invitation, secret) {
7624
+ console.log(`Created invitation (${invitation.id}) for user "${user.name}" (${user.id}).`);
7625
+ console.log(`Invitation prefix: ${invitation.codePrefix}`);
7626
+ console.log(`Expires: ${invitation.expiresAt.toISOString()}`);
7627
+ console.log("");
7628
+ console.log("Store this invitation secret now; it will not be shown again:");
7629
+ console.log(secret);
7630
+ }
7631
+ async function userInviteCreateCommand(options) {
7632
+ const config = loadServerConfig(options.config);
7633
+ const db = createDatabase(config.db);
7634
+ const access = mapValidationError(
7635
+ () => normalizeAccessForRole(
7636
+ options.role,
7637
+ options.collectionAccess,
7638
+ options.environmentAccess,
7639
+ options.snippetAccess
7640
+ )
7641
+ );
7642
+ await db.connect();
7643
+ const actingUserId = await requireSystemUserId(db);
7644
+ const catalogs = await loadAccessCatalogs(db, config.llm);
7645
+ const llmModels = readLlmModelsOption(options);
7646
+ const llm = mapValidationError(
7647
+ () => normalizeLlmForRole(options.role, options.llmAccess ?? false, llmModels)
7648
+ );
7649
+ validateSubmittedAccessListsOrThrow(
7650
+ {
7651
+ role: options.role,
7652
+ collectionAccess: access.collectionAccess,
7653
+ environmentAccess: access.environmentAccess,
7654
+ snippetAccess: access.snippetAccess,
7655
+ llmModels
7656
+ },
7657
+ catalogs
7658
+ );
7659
+ const userId = randomUUID7();
7660
+ const expiresAt = resolveInvitationExpiresAt(options.expiresInHours);
7661
+ const { record: invitation, secret } = generateInvitation(userId, actingUserId, expiresAt);
7662
+ const created = await db.createInvitedUser(
7663
+ userId,
7664
+ {
7665
+ name: options.name,
7666
+ role: options.role,
7667
+ collectionAccess: access.collectionAccess ?? [],
7668
+ environmentAccess: access.environmentAccess ?? [],
7669
+ snippetAccess: access.snippetAccess ?? [],
7670
+ llmAccess: llm.llmAccess,
7671
+ llmModels: llm.llmModels,
7672
+ llmMonthlyTokenLimit: options.llmMonthlyTokens ?? null
7673
+ },
7674
+ invitation,
7675
+ actingUserId
7676
+ );
7677
+ await db.disconnect();
7678
+ console.log(
7679
+ `Created invited user "${created.user.name}" (${created.user.id}) with role ${created.user.role}.`
7680
+ );
7681
+ printUser(created.user);
7682
+ console.log("");
7683
+ printCreatedInvitation(created.user, created.invitation, secret);
7684
+ }
7685
+ async function userInviteListCommand(options) {
7686
+ const config = loadServerConfig(options.config);
7687
+ const db = createDatabase(config.db);
7688
+ await db.connect();
7689
+ const [invitations, users] = await Promise.all([db.listInvitations(), db.listUsers()]);
7690
+ await db.disconnect();
7691
+ if (invitations.length === 0) {
7692
+ console.log("No invitations found.");
7693
+ return;
7694
+ }
7695
+ const usersById = new Map(users.map((user) => [user.id, user.name]));
7696
+ for (const invitation of invitations) {
7697
+ const userName = usersById.get(invitation.userId) ?? invitation.userId;
7698
+ console.log(
7699
+ `${invitation.id} ${userName} ${invitation.codePrefix} ${getInvitationStatus(invitation)}`
7700
+ );
7701
+ console.log(` expires: ${invitation.expiresAt.toISOString()}`);
7702
+ }
7703
+ }
7704
+ async function userInviteRevokeCommand(options) {
7705
+ const config = loadServerConfig(options.config);
7706
+ const db = createDatabase(config.db);
7707
+ await db.connect();
7708
+ const actingUserId = await requireSystemUserId(db);
7709
+ const revoked = await db.revokeInvitation(options.id, actingUserId);
7710
+ await db.disconnect();
7711
+ if (revoked) {
7712
+ console.log(`Revoked invitation ${options.id}.`);
7713
+ return;
7714
+ }
7715
+ console.log(`No pending invitation found with id ${options.id}.`);
7716
+ }
6686
7717
  async function userListCommand(options) {
6687
7718
  const config = loadServerConfig(options.config);
6688
7719
  const db = createDatabase(config.db);
@@ -6855,6 +7886,48 @@ async function userTokenRevokeCommand(options) {
6855
7886
  }
6856
7887
  function registerUserCommand(program, handlers = {}) {
6857
7888
  const user = program.command("user").description("Manage user accounts and their API tokens");
7889
+ const invite = user.command("invite").description("Manage onboarding invitations for user accounts");
7890
+ invite.command("create").description("Create a user account with a single-use onboarding invitation").requiredOption("--name <name>", "Unique display name", parseRequiredName).requiredOption("--role <role>", "Account role (admin or user)", parseUserRole2).option(
7891
+ "--collection-access <id>",
7892
+ "Collection id or * (repeatable)",
7893
+ parseAccessFlag,
7894
+ []
7895
+ ).option(
7896
+ "--environment-access <id>",
7897
+ "Environment id or * (repeatable)",
7898
+ parseAccessFlag,
7899
+ []
7900
+ ).option(
7901
+ "--snippet-access <id>",
7902
+ "Snippet id or * (repeatable)",
7903
+ parseAccessFlag,
7904
+ []
7905
+ ).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).option("--expires-in-hours <hours>", "Invitation lifetime in hours", parsePositiveInt).action(
7906
+ /**
7907
+ * Runs the user invite create subcommand after merging global CLI options.
7908
+ */
7909
+ async function userInviteCreateAction(options) {
7910
+ await (handlers.inviteCreate ?? userInviteCreateCommand)(mergeGlobalOptions(this, options));
7911
+ }
7912
+ );
7913
+ invite.command("list").description("List stored onboarding invitations").action(
7914
+ /**
7915
+ * Runs the user invite list subcommand after merging global CLI options.
7916
+ */
7917
+ async function userInviteListAction(options) {
7918
+ await (handlers.inviteList ?? userInviteListCommand)(mergeGlobalOptions(this, options));
7919
+ }
7920
+ );
7921
+ invite.command("revoke").description("Revoke a pending onboarding invitation by id").argument("<id>", "Invitation identifier to revoke").action(
7922
+ /**
7923
+ * Runs the user invite revoke subcommand after merging global CLI options.
7924
+ */
7925
+ async function userInviteRevokeAction(id, options) {
7926
+ await (handlers.inviteRevoke ?? userInviteRevokeCommand)(
7927
+ mergeGlobalOptions(this, { ...options, id })
7928
+ );
7929
+ }
7930
+ );
6858
7931
  user.command("create").description("Create a new user account").requiredOption("--name <name>", "Unique display name", parseRequiredName).requiredOption("--role <role>", "Account role (admin or user)", parseUserRole2).option(
6859
7932
  "--collection-access <id>",
6860
7933
  "Collection id or * (repeatable)",
@@ -7026,6 +8099,9 @@ function readPackageVersion() {
7026
8099
  return pkg.version;
7027
8100
  }
7028
8101
 
8102
+ // src/server/routes/admin.ts
8103
+ import { randomUUID as randomUUID8 } from "crypto";
8104
+
7029
8105
  // src/server/auth/accessControl.ts
7030
8106
  function isAdmin(user) {
7031
8107
  return user.role === "admin";
@@ -7679,34 +8755,101 @@ var reloadConfigSectionResultSchema = z9.object({
7679
8755
  status: z9.enum(["reloaded", "unchanged", "failed", "restart-required"]),
7680
8756
  error: z9.string().optional()
7681
8757
  });
7682
- var reloadConfigResponseSchema = z9.object({
7683
- sections: z9.array(reloadConfigSectionResultSchema),
7684
- fatalError: z9.string().optional()
8758
+ var reloadConfigResponseSchema = z9.object({
8759
+ sections: z9.array(reloadConfigSectionResultSchema),
8760
+ fatalError: z9.string().optional()
8761
+ });
8762
+ function serializeHubUser(user) {
8763
+ return {
8764
+ id: user.id,
8765
+ name: user.name,
8766
+ role: user.role,
8767
+ collectionAccess: user.collectionAccess,
8768
+ environmentAccess: user.environmentAccess,
8769
+ snippetAccess: user.snippetAccess,
8770
+ llmAccess: user.llmAccess,
8771
+ llmModels: user.llmModels,
8772
+ llmMonthlyTokenLimit: user.llmMonthlyTokenLimit,
8773
+ createdAt: user.createdAt.toISOString(),
8774
+ updatedAt: user.updatedAt.toISOString()
8775
+ };
8776
+ }
8777
+ function serializeApiToken(token) {
8778
+ return {
8779
+ id: token.id,
8780
+ userId: token.userId,
8781
+ name: token.name,
8782
+ tokenPrefix: token.tokenPrefix,
8783
+ createdAt: token.createdAt.toISOString(),
8784
+ lastUsedAt: token.lastUsedAt ? token.lastUsedAt.toISOString() : null,
8785
+ revokedAt: token.revokedAt ? token.revokedAt.toISOString() : null
8786
+ };
8787
+ }
8788
+
8789
+ // src/server/routes/schemas/invitations.ts
8790
+ import { z as z10 } from "zod/v4";
8791
+ var invitationStatusSchema = z10.enum(["pending", "redeemed", "revoked", "expired"]);
8792
+ var hubInvitationRecordSchema = z10.object({
8793
+ id: z10.string(),
8794
+ userId: z10.string(),
8795
+ codePrefix: z10.string(),
8796
+ expiresAt: timestampSchema,
8797
+ redeemedAt: timestampSchema.nullable(),
8798
+ revokedAt: timestampSchema.nullable(),
8799
+ createdAt: timestampSchema,
8800
+ status: invitationStatusSchema
8801
+ });
8802
+ var createAdminInvitedUserBodySchema = createAdminUserBodySchema.extend({
8803
+ expiresInHours: z10.number().int().positive().optional()
8804
+ });
8805
+ var createAdminInvitationResponseSchema = z10.object({
8806
+ user: hubUserRecordSchema,
8807
+ invitation: hubInvitationRecordSchema,
8808
+ secret: z10.string()
8809
+ });
8810
+ var listAdminInvitationsResponseSchema = z10.object({
8811
+ invitations: z10.array(hubInvitationRecordSchema)
8812
+ });
8813
+ var invitationSecretBodySchema = z10.object({
8814
+ secret: z10.string().trim().min(1)
8815
+ });
8816
+ var redeemInvitationBodySchema = invitationSecretBodySchema.extend({
8817
+ tokenName: z10.string().trim().min(1).optional()
8818
+ });
8819
+ var hubInvitationPreviewUserSchema = z10.object({
8820
+ name: z10.string(),
8821
+ role: userRoleSchema,
8822
+ collectionAccess: z10.array(z10.string()),
8823
+ environmentAccess: z10.array(z10.string()),
8824
+ snippetAccess: z10.array(z10.string()),
8825
+ llmAccess: z10.boolean(),
8826
+ llmModels: z10.array(z10.string())
8827
+ });
8828
+ var previewInvitationResponseSchema = z10.object({
8829
+ user: hubInvitationPreviewUserSchema,
8830
+ expiresAt: timestampSchema
7685
8831
  });
7686
- function serializeHubUser(user) {
8832
+ function serializeHubInvitation(invitation) {
8833
+ return {
8834
+ id: invitation.id,
8835
+ userId: invitation.userId,
8836
+ codePrefix: invitation.codePrefix,
8837
+ expiresAt: invitation.expiresAt.toISOString(),
8838
+ redeemedAt: invitation.redeemedAt?.toISOString() ?? null,
8839
+ revokedAt: invitation.revokedAt?.toISOString() ?? null,
8840
+ createdAt: invitation.createdAt.toISOString(),
8841
+ status: getInvitationStatus(invitation)
8842
+ };
8843
+ }
8844
+ function serializeInvitationPreviewUser(user) {
7687
8845
  return {
7688
- id: user.id,
7689
8846
  name: user.name,
7690
8847
  role: user.role,
7691
8848
  collectionAccess: user.collectionAccess,
7692
8849
  environmentAccess: user.environmentAccess,
7693
8850
  snippetAccess: user.snippetAccess,
7694
8851
  llmAccess: user.llmAccess,
7695
- llmModels: user.llmModels,
7696
- llmMonthlyTokenLimit: user.llmMonthlyTokenLimit,
7697
- createdAt: user.createdAt.toISOString(),
7698
- updatedAt: user.updatedAt.toISOString()
7699
- };
7700
- }
7701
- function serializeApiToken(token) {
7702
- return {
7703
- id: token.id,
7704
- userId: token.userId,
7705
- name: token.name,
7706
- tokenPrefix: token.tokenPrefix,
7707
- createdAt: token.createdAt.toISOString(),
7708
- lastUsedAt: token.lastUsedAt ? token.lastUsedAt.toISOString() : null,
7709
- revokedAt: token.revokedAt ? token.revokedAt.toISOString() : null
8852
+ llmModels: user.llmModels
7710
8853
  };
7711
8854
  }
7712
8855
 
@@ -7856,6 +8999,174 @@ async function registerAdminRoutes(app, options) {
7856
8999
  }
7857
9000
  }
7858
9001
  });
9002
+ routes.route({
9003
+ method: "POST",
9004
+ url: "/admin/invited-users",
9005
+ schema: {
9006
+ body: createAdminInvitedUserBodySchema,
9007
+ response: {
9008
+ 201: createAdminInvitationResponseSchema,
9009
+ 400: errorResponseSchema,
9010
+ 403: errorResponseSchema
9011
+ }
9012
+ },
9013
+ /**
9014
+ * Creates a user account and a single-use onboarding invitation without issuing an API token.
9015
+ */
9016
+ handler: async (request, reply) => {
9017
+ try {
9018
+ const user = requireAuthenticatedUser(request);
9019
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
9020
+ return;
9021
+ }
9022
+ const input = buildAdminUserCreateInput(request.body);
9023
+ const llm = getLlm();
9024
+ const [collections, environments, snippets] = await Promise.all([
9025
+ db.listCollections(),
9026
+ db.listEnvironments(),
9027
+ db.listSnippets()
9028
+ ]);
9029
+ const catalogs = buildAccessCatalogIds(
9030
+ collections,
9031
+ environments,
9032
+ snippets,
9033
+ llm ? listHubOfferedModels(llm).map((model) => model.id) : null
9034
+ );
9035
+ validateSubmittedAccessLists(
9036
+ {
9037
+ role: request.body.role,
9038
+ collectionAccess: request.body.collectionAccess,
9039
+ environmentAccess: request.body.environmentAccess,
9040
+ snippetAccess: request.body.snippetAccess,
9041
+ llmModels: request.body.llmModels
9042
+ },
9043
+ catalogs
9044
+ );
9045
+ const userId = randomUUID8();
9046
+ const expiresAt = resolveInvitationExpiresAt(request.body.expiresInHours);
9047
+ const { record: invitation, secret } = generateInvitation(userId, user.id, expiresAt);
9048
+ const created = await db.createInvitedUser(userId, input, invitation, user.id);
9049
+ return reply.code(201).send({
9050
+ user: serializeHubUser(created.user),
9051
+ invitation: serializeHubInvitation(created.invitation),
9052
+ secret
9053
+ });
9054
+ } catch (error) {
9055
+ if (handleValidationError(reply, error) || handleDbError(reply, error)) {
9056
+ return;
9057
+ }
9058
+ throw error;
9059
+ }
9060
+ }
9061
+ });
9062
+ routes.route({
9063
+ method: "POST",
9064
+ url: "/admin/users/:id/invitations",
9065
+ schema: {
9066
+ params: idParamSchema,
9067
+ body: createAdminInvitedUserBodySchema.pick({ expiresInHours: true }),
9068
+ response: {
9069
+ 201: createAdminInvitationResponseSchema,
9070
+ 400: errorResponseSchema,
9071
+ 403: errorResponseSchema,
9072
+ 404: errorResponseSchema
9073
+ }
9074
+ },
9075
+ /**
9076
+ * Issues a replacement onboarding invitation for an existing user account.
9077
+ */
9078
+ handler: async (request, reply) => {
9079
+ try {
9080
+ const admin = requireAuthenticatedUser(request);
9081
+ if (denyUnlessAllowed(reply, canUseManagementApi(admin))) {
9082
+ return;
9083
+ }
9084
+ const existing = await db.findUserById(request.params.id);
9085
+ if (!existing) {
9086
+ return reply.code(404).send({ error: "User not found" });
9087
+ }
9088
+ if (denySystemUserTarget(reply, existing, db.getSystemUserId())) {
9089
+ return;
9090
+ }
9091
+ const expiresAt = resolveInvitationExpiresAt(request.body.expiresInHours);
9092
+ const { record: invitation, secret } = generateInvitation(existing.id, admin.id, expiresAt);
9093
+ await db.createInvitation(invitation, admin.id);
9094
+ return reply.code(201).send({
9095
+ user: serializeHubUser(existing),
9096
+ invitation: serializeHubInvitation(invitation),
9097
+ secret
9098
+ });
9099
+ } catch (error) {
9100
+ if (handleDbError(reply, error)) {
9101
+ return;
9102
+ }
9103
+ throw error;
9104
+ }
9105
+ }
9106
+ });
9107
+ routes.route({
9108
+ method: "GET",
9109
+ url: "/admin/invitations",
9110
+ schema: {
9111
+ response: {
9112
+ 200: listAdminInvitationsResponseSchema,
9113
+ 403: errorResponseSchema
9114
+ }
9115
+ },
9116
+ /**
9117
+ * Lists onboarding invitations for operator review and recovery.
9118
+ */
9119
+ handler: async (request, reply) => {
9120
+ try {
9121
+ const user = requireAuthenticatedUser(request);
9122
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
9123
+ return;
9124
+ }
9125
+ const invitations = await db.listInvitations();
9126
+ return reply.send({
9127
+ invitations: invitations.map((invitation) => serializeHubInvitation(invitation))
9128
+ });
9129
+ } catch (error) {
9130
+ if (handleDbError(reply, error)) {
9131
+ return;
9132
+ }
9133
+ throw error;
9134
+ }
9135
+ }
9136
+ });
9137
+ routes.route({
9138
+ method: "DELETE",
9139
+ url: "/admin/invitations/:id",
9140
+ schema: {
9141
+ params: idParamSchema,
9142
+ response: {
9143
+ 204: emptyResponseSchema,
9144
+ 403: errorResponseSchema,
9145
+ 404: errorResponseSchema
9146
+ }
9147
+ },
9148
+ /**
9149
+ * Revokes a pending onboarding invitation so it can no longer be redeemed.
9150
+ */
9151
+ handler: async (request, reply) => {
9152
+ try {
9153
+ const user = requireAuthenticatedUser(request);
9154
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
9155
+ return;
9156
+ }
9157
+ const revoked = await db.revokeInvitation(request.params.id, user.id);
9158
+ if (!revoked) {
9159
+ return reply.code(404).send({ error: "Invitation not found or not revocable." });
9160
+ }
9161
+ return reply.code(204).send(null);
9162
+ } catch (error) {
9163
+ if (handleDbError(reply, error)) {
9164
+ return;
9165
+ }
9166
+ throw error;
9167
+ }
9168
+ }
9169
+ });
7859
9170
  routes.route({
7860
9171
  method: "GET",
7861
9172
  url: "/admin/collections",
@@ -8719,6 +10030,189 @@ async function registerAuthRoutes(app) {
8719
10030
  });
8720
10031
  }
8721
10032
 
10033
+ // src/db/invitationValidation.ts
10034
+ function assertInvitationPending(invitation, now = /* @__PURE__ */ new Date()) {
10035
+ if (invitation.redeemedAt) {
10036
+ throw new InvitationUnavailableError("redeemed");
10037
+ }
10038
+ if (invitation.revokedAt) {
10039
+ throw new InvitationUnavailableError("revoked");
10040
+ }
10041
+ if (invitation.expiresAt.getTime() <= now.getTime()) {
10042
+ throw new InvitationUnavailableError("expired");
10043
+ }
10044
+ }
10045
+
10046
+ // src/server/routes/invitationErrors.ts
10047
+ function handleInvitationError(reply, error) {
10048
+ if (!(error instanceof InvitationUnavailableError)) {
10049
+ return false;
10050
+ }
10051
+ const status = error.reason === "not_found" ? 404 : 410;
10052
+ void reply.code(status).send(errorResponseSchema.parse({ error: error.message }));
10053
+ return true;
10054
+ }
10055
+
10056
+ // src/server/routes/invitations.ts
10057
+ function buildInvitationThrottleKey(request) {
10058
+ return `${request.ip}:invitation`;
10059
+ }
10060
+ function createInvitationThrottleHook(throttleStore) {
10061
+ return async function invitationThrottle(request, reply) {
10062
+ const policy = throttleStore.getPolicy();
10063
+ const throttleKey = buildInvitationThrottleKey(request);
10064
+ try {
10065
+ if (await throttleStore.isBlocked(throttleKey)) {
10066
+ return reply.header("Retry-After", String(policy.blockSeconds)).code(429).send({ error: "Too Many Requests" });
10067
+ }
10068
+ } catch {
10069
+ return reply.code(503).send({ error: "Service Unavailable" });
10070
+ }
10071
+ };
10072
+ }
10073
+ async function recordInvitationFailure(throttleStore, request, reply) {
10074
+ const throttleKey = buildInvitationThrottleKey(request);
10075
+ try {
10076
+ await throttleStore.recordFailure(throttleKey);
10077
+ return false;
10078
+ } catch {
10079
+ void reply.code(503).send({ error: "Service Unavailable" });
10080
+ return true;
10081
+ }
10082
+ }
10083
+ async function resetInvitationThrottle(throttleStore, request, reply) {
10084
+ const throttleKey = buildInvitationThrottleKey(request);
10085
+ try {
10086
+ await throttleStore.reset(throttleKey);
10087
+ return false;
10088
+ } catch {
10089
+ void reply.code(503).send({ error: "Service Unavailable" });
10090
+ return true;
10091
+ }
10092
+ }
10093
+ async function registerInvitationRoutes(app, options) {
10094
+ const routes = app.withTypeProvider();
10095
+ app.addHook("onRequest", createInvitationThrottleHook(options.throttleStore));
10096
+ routes.route({
10097
+ method: "POST",
10098
+ url: "/auth/invitations/preview",
10099
+ schema: {
10100
+ body: invitationSecretBodySchema,
10101
+ response: {
10102
+ 200: previewInvitationResponseSchema,
10103
+ 400: errorResponseSchema,
10104
+ 404: errorResponseSchema,
10105
+ 410: errorResponseSchema,
10106
+ 429: errorResponseSchema,
10107
+ 503: errorResponseSchema
10108
+ }
10109
+ },
10110
+ /**
10111
+ * Returns invited user details for confirmation without consuming the invitation.
10112
+ */
10113
+ handler: async (request, reply) => {
10114
+ try {
10115
+ const secret = request.body.secret;
10116
+ if (!isInvitationSecretFormat(secret)) {
10117
+ return reply.code(400).send({ error: "Invalid invitation secret format." });
10118
+ }
10119
+ const invitation = await options.db.findInvitationByCodeHash(hashInvitationSecret(secret));
10120
+ if (!invitation) {
10121
+ if (await recordInvitationFailure(options.throttleStore, request, reply)) {
10122
+ return;
10123
+ }
10124
+ return reply.code(404).send({ error: "Invalid or expired invitation." });
10125
+ }
10126
+ try {
10127
+ assertInvitationPending(invitation);
10128
+ } catch (error) {
10129
+ if (await recordInvitationFailure(options.throttleStore, request, reply)) {
10130
+ return;
10131
+ }
10132
+ if (handleInvitationError(reply, error)) {
10133
+ return;
10134
+ }
10135
+ throw error;
10136
+ }
10137
+ const user = await options.db.findUserById(invitation.userId);
10138
+ if (!user) {
10139
+ return reply.code(404).send({ error: "Invalid or expired invitation." });
10140
+ }
10141
+ if (await resetInvitationThrottle(options.throttleStore, request, reply)) {
10142
+ return;
10143
+ }
10144
+ return reply.send({
10145
+ user: serializeInvitationPreviewUser(user),
10146
+ expiresAt: invitation.expiresAt.toISOString()
10147
+ });
10148
+ } catch (error) {
10149
+ if (handleDbError(reply, error) || handleInvitationError(reply, error)) {
10150
+ return;
10151
+ }
10152
+ throw error;
10153
+ }
10154
+ }
10155
+ });
10156
+ routes.route({
10157
+ method: "POST",
10158
+ url: "/auth/invitations/redeem",
10159
+ schema: {
10160
+ body: redeemInvitationBodySchema,
10161
+ response: {
10162
+ 201: createdApiTokenResponseSchema,
10163
+ 400: errorResponseSchema,
10164
+ 404: errorResponseSchema,
10165
+ 410: errorResponseSchema,
10166
+ 429: errorResponseSchema,
10167
+ 503: errorResponseSchema
10168
+ }
10169
+ },
10170
+ /**
10171
+ * Consumes a pending invitation and returns a one-time permanent API token secret.
10172
+ */
10173
+ handler: async (request, reply) => {
10174
+ try {
10175
+ const secret = request.body.secret;
10176
+ if (!isInvitationSecretFormat(secret)) {
10177
+ return reply.code(400).send({ error: "Invalid invitation secret format." });
10178
+ }
10179
+ const systemUserId = options.db.getSystemUserId();
10180
+ if (!systemUserId) {
10181
+ throw new Error("System user is not provisioned");
10182
+ }
10183
+ const tokenName = request.body.tokenName?.trim();
10184
+ const codeHash = hashInvitationSecret(secret);
10185
+ let redeemed;
10186
+ try {
10187
+ redeemed = await options.db.redeemInvitation(codeHash, tokenName ?? "", systemUserId);
10188
+ } catch (error) {
10189
+ if (error instanceof InvitationUnavailableError) {
10190
+ if (await recordInvitationFailure(options.throttleStore, request, reply)) {
10191
+ return;
10192
+ }
10193
+ }
10194
+ if (handleInvitationError(reply, error)) {
10195
+ return;
10196
+ }
10197
+ throw error;
10198
+ }
10199
+ if (await resetInvitationThrottle(options.throttleStore, request, reply)) {
10200
+ return;
10201
+ }
10202
+ return reply.code(201).send({
10203
+ token: serializeApiToken(redeemed.token),
10204
+ secret: redeemed.secret
10205
+ });
10206
+ } catch (error) {
10207
+ if (handleDbError(reply, error) || handleInvitationError(reply, error)) {
10208
+ return;
10209
+ }
10210
+ throw error;
10211
+ }
10212
+ }
10213
+ });
10214
+ }
10215
+
8722
10216
  // src/server/routes/collections.ts
8723
10217
  async function registerCollectionRoutes(app, db) {
8724
10218
  const routes = app.withTypeProvider();
@@ -9339,10 +10833,10 @@ async function registerFolderRoutes(app, db) {
9339
10833
  }
9340
10834
 
9341
10835
  // src/server/routes/health.ts
9342
- import { z as z10 } from "zod/v4";
9343
- var healthResponseSchema = z10.object({
9344
- status: z10.literal("ok"),
9345
- version: z10.string()
10836
+ import { z as z11 } from "zod/v4";
10837
+ var healthResponseSchema = z11.object({
10838
+ status: z11.literal("ok"),
10839
+ version: z11.string()
9346
10840
  });
9347
10841
  async function registerHealthRoute(app, version2) {
9348
10842
  app.withTypeProvider().route({
@@ -10462,10 +11956,10 @@ async function registerLlmRoutes(app, options) {
10462
11956
  }
10463
11957
 
10464
11958
  // src/server/routes/schemas/plugins.ts
10465
- import { z as z11 } from "zod/v4";
10466
- var pluginSourcesResponseSchema = z11.object({
10467
- catalogs: z11.array(z11.string()),
10468
- trusted: z11.array(z11.string())
11959
+ import { z as z12 } from "zod/v4";
11960
+ var pluginSourcesResponseSchema = z12.object({
11961
+ catalogs: z12.array(z12.string()),
11962
+ trusted: z12.array(z12.string())
10469
11963
  });
10470
11964
 
10471
11965
  // src/server/routes/plugins.ts
@@ -10559,6 +12053,10 @@ function createBearerAuthHook(db, throttleStore) {
10559
12053
  // src/server/routes/index.ts
10560
12054
  async function registerPublicRoutes(app, options) {
10561
12055
  await registerHealthRoute(app, options.version);
12056
+ await registerInvitationRoutes(app, {
12057
+ db: options.db,
12058
+ throttleStore: options.throttleStore
12059
+ });
10562
12060
  }
10563
12061
  async function registerProtectedRoutes(app, options) {
10564
12062
  registerBearerAuthDecorator(app);