@digilogiclabs/platform-core 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7191,6 +7191,7 @@ __export(src_exports, {
7191
7191
  MemoryAuditLog: () => MemoryAuditLog,
7192
7192
  MemoryAuth: () => MemoryAuth,
7193
7193
  MemoryAuthSSO: () => MemoryAuthSSO,
7194
+ MemoryBeta: () => MemoryBeta,
7194
7195
  MemoryBilling: () => MemoryBilling,
7195
7196
  MemoryCache: () => MemoryCache,
7196
7197
  MemoryCompliance: () => MemoryCompliance,
@@ -7231,6 +7232,7 @@ __export(src_exports, {
7231
7232
  PhoneSchema: () => PhoneSchema,
7232
7233
  PineconeRAG: () => PineconeRAG,
7233
7234
  PlatformConfigSchema: () => PlatformConfigSchema,
7235
+ PostgresBeta: () => PostgresBeta,
7234
7236
  PostgresDatabase: () => PostgresDatabase,
7235
7237
  PostgresTenant: () => PostgresTenant,
7236
7238
  QueueConfigSchema: () => QueueConfigSchema,
@@ -7286,6 +7288,7 @@ __export(src_exports, {
7286
7288
  checkEnvVars: () => checkEnvVars,
7287
7289
  checkRateLimit: () => checkRateLimit,
7288
7290
  classifyError: () => classifyError,
7291
+ clearStoredBetaCode: () => clearStoredBetaCode,
7289
7292
  closeSharedRedis: () => closeSharedRedis,
7290
7293
  composeHookRegistries: () => composeHookRegistries,
7291
7294
  constantTimeEqual: () => constantTimeEqual,
@@ -7298,6 +7301,10 @@ __export(src_exports, {
7298
7301
  createAuditActor: () => createAuditActor,
7299
7302
  createAuditLogger: () => createAuditLogger,
7300
7303
  createAuthError: () => createAuthError,
7304
+ createBetaClient: () => createBetaClient,
7305
+ createBetaInvitesTable: () => createBetaInvitesTable,
7306
+ createBetaSettingsTable: () => createBetaSettingsTable,
7307
+ createBetaTestersTable: () => createBetaTestersTable,
7301
7308
  createBulkhead: () => createBulkhead,
7302
7309
  createCacheMiddleware: () => createCacheMiddleware,
7303
7310
  createCachedFallback: () => createCachedFallback,
@@ -7356,9 +7363,12 @@ __export(src_exports, {
7356
7363
  extractAuditRequestId: () => extractAuditRequestId,
7357
7364
  extractAuditUserAgent: () => extractAuditUserAgent,
7358
7365
  extractClientIp: () => extractClientIp,
7366
+ fetchBetaSettings: () => fetchBetaSettings,
7359
7367
  filterChannelsByPreferences: () => filterChannelsByPreferences,
7360
7368
  formatAmount: () => formatAmount,
7361
7369
  generateAuditId: () => generateAuditId,
7370
+ generateBetaCode: () => generateBetaCode,
7371
+ generateBetaId: () => generateBetaId,
7362
7372
  generateChecksum: () => generateChecksum,
7363
7373
  generateDeliveryId: () => generateDeliveryId,
7364
7374
  generateErrorId: () => generateErrorId,
@@ -7388,6 +7398,7 @@ __export(src_exports, {
7388
7398
  getRequestId: () => getRequestId,
7389
7399
  getRequiredEnv: () => getRequiredEnv,
7390
7400
  getSharedRedis: () => getSharedRedis,
7401
+ getStoredBetaCode: () => getStoredBetaCode,
7391
7402
  getTenantId: () => getTenantId,
7392
7403
  getTokenEndpoint: () => getTokenEndpoint,
7393
7404
  getTraceId: () => getTraceId,
@@ -7407,6 +7418,7 @@ __export(src_exports, {
7407
7418
  loadConfig: () => loadConfig,
7408
7419
  matchAction: () => matchAction,
7409
7420
  matchEventType: () => matchEventType,
7421
+ normalizeBetaCode: () => normalizeBetaCode,
7410
7422
  parseKeycloakRoles: () => parseKeycloakRoles,
7411
7423
  raceTimeout: () => raceTimeout,
7412
7424
  refreshKeycloakToken: () => refreshKeycloakToken,
@@ -7420,9 +7432,11 @@ __export(src_exports, {
7420
7432
  sanitizeApiError: () => sanitizeApiError,
7421
7433
  sanitizeForEmail: () => sanitizeForEmail,
7422
7434
  sqlMigration: () => sqlMigration,
7435
+ storeBetaCode: () => storeBetaCode,
7423
7436
  stripHtml: () => stripHtml,
7424
7437
  timedHealthCheck: () => timedHealthCheck,
7425
7438
  toHealthCheckResult: () => toHealthCheckResult,
7439
+ validateBetaCode: () => validateBetaCode,
7426
7440
  validateConfig: () => validateConfig,
7427
7441
  validateEnvVars: () => validateEnvVars,
7428
7442
  withCorrelation: () => withCorrelation,
@@ -14210,6 +14224,252 @@ var MemoryCompliance = class {
14210
14224
  }
14211
14225
  };
14212
14226
 
14227
+ // src/interfaces/IBeta.ts
14228
+ function generateBetaCode(prefix = "BETA") {
14229
+ const hex = Array.from(
14230
+ { length: 8 },
14231
+ () => Math.floor(Math.random() * 16).toString(16)
14232
+ ).join("").toUpperCase();
14233
+ return `${prefix}-${hex}`;
14234
+ }
14235
+ function normalizeBetaCode(code) {
14236
+ return code.trim().toUpperCase();
14237
+ }
14238
+ function generateBetaId() {
14239
+ return `beta_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
14240
+ }
14241
+ var MemoryBeta = class {
14242
+ settings;
14243
+ codes = /* @__PURE__ */ new Map();
14244
+ testers = /* @__PURE__ */ new Map();
14245
+ config;
14246
+ constructor(config = {}) {
14247
+ this.config = {
14248
+ defaultBetaMode: config.defaultBetaMode ?? true,
14249
+ defaultRequireInviteCode: config.defaultRequireInviteCode ?? true,
14250
+ defaultBetaMessage: config.defaultBetaMessage ?? "We're in beta! Thanks for being an early tester.",
14251
+ codePrefix: config.codePrefix ?? "BETA",
14252
+ maxCodesPerBatch: config.maxCodesPerBatch ?? 50
14253
+ };
14254
+ this.settings = {
14255
+ betaMode: this.config.defaultBetaMode,
14256
+ requireInviteCode: this.config.defaultRequireInviteCode,
14257
+ betaMessage: this.config.defaultBetaMessage
14258
+ };
14259
+ }
14260
+ // ─────────────────────────────────────────────────────────────
14261
+ // Settings
14262
+ // ─────────────────────────────────────────────────────────────
14263
+ async getSettings() {
14264
+ return { ...this.settings };
14265
+ }
14266
+ async updateSettings(options) {
14267
+ if (options.betaMode !== void 0) {
14268
+ this.settings.betaMode = options.betaMode;
14269
+ }
14270
+ if (options.requireInviteCode !== void 0) {
14271
+ this.settings.requireInviteCode = options.requireInviteCode;
14272
+ }
14273
+ if (options.betaMessage !== void 0) {
14274
+ this.settings.betaMessage = options.betaMessage;
14275
+ }
14276
+ }
14277
+ // ─────────────────────────────────────────────────────────────
14278
+ // Code Management
14279
+ // ─────────────────────────────────────────────────────────────
14280
+ async createCodes(options) {
14281
+ const count = Math.min(options.count ?? 1, this.config.maxCodesPerBatch);
14282
+ const prefix = options.prefix ?? this.config.codePrefix;
14283
+ const results = [];
14284
+ for (let i = 0; i < count; i++) {
14285
+ const codeStr = options.code ? normalizeBetaCode(options.code) : generateBetaCode(prefix);
14286
+ if (this.codes.has(codeStr)) {
14287
+ throw new Error(`Code already exists: ${codeStr}`);
14288
+ }
14289
+ const invite = {
14290
+ id: generateBetaId(),
14291
+ code: codeStr,
14292
+ maxUses: options.maxUses ?? 1,
14293
+ currentUses: 0,
14294
+ expiresAt: options.expiresAt ?? null,
14295
+ createdBy: options.createdBy,
14296
+ notes: options.notes ?? "",
14297
+ isActive: true,
14298
+ createdAt: /* @__PURE__ */ new Date()
14299
+ };
14300
+ this.codes.set(codeStr, invite);
14301
+ results.push(invite);
14302
+ if (options.code) {
14303
+ break;
14304
+ }
14305
+ }
14306
+ return results;
14307
+ }
14308
+ async listCodes(options = {}) {
14309
+ let codes = Array.from(this.codes.values());
14310
+ if (options.isActive !== void 0) {
14311
+ codes = codes.filter((c) => c.isActive === options.isActive);
14312
+ }
14313
+ if (options.status) {
14314
+ const now = /* @__PURE__ */ new Date();
14315
+ codes = codes.filter((c) => {
14316
+ switch (options.status) {
14317
+ case "unused":
14318
+ return c.isActive && c.currentUses === 0;
14319
+ case "partial":
14320
+ return c.isActive && c.currentUses > 0 && c.currentUses < c.maxUses;
14321
+ case "exhausted":
14322
+ return c.currentUses >= c.maxUses;
14323
+ case "expired":
14324
+ return c.expiresAt !== null && c.expiresAt < now;
14325
+ case "revoked":
14326
+ return !c.isActive;
14327
+ default:
14328
+ return true;
14329
+ }
14330
+ });
14331
+ }
14332
+ codes.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
14333
+ const offset = options.offset ?? 0;
14334
+ const limit = options.limit ?? 100;
14335
+ return codes.slice(offset, offset + limit);
14336
+ }
14337
+ async getCode(code) {
14338
+ return this.codes.get(normalizeBetaCode(code)) ?? null;
14339
+ }
14340
+ async revokeCode(code) {
14341
+ const normalized = normalizeBetaCode(code);
14342
+ const invite = this.codes.get(normalized);
14343
+ if (invite) {
14344
+ invite.isActive = false;
14345
+ }
14346
+ }
14347
+ // ─────────────────────────────────────────────────────────────
14348
+ // Validation & Consumption
14349
+ // ─────────────────────────────────────────────────────────────
14350
+ async validateCode(code) {
14351
+ if (!this.settings.requireInviteCode) {
14352
+ return { valid: true, message: "Invite codes are not required." };
14353
+ }
14354
+ const normalized = normalizeBetaCode(code);
14355
+ const invite = this.codes.get(normalized);
14356
+ if (!invite) {
14357
+ return { valid: false, message: "Invalid invite code." };
14358
+ }
14359
+ if (!invite.isActive) {
14360
+ return { valid: false, message: "This invite code has been revoked." };
14361
+ }
14362
+ if (invite.expiresAt && invite.expiresAt < /* @__PURE__ */ new Date()) {
14363
+ return { valid: false, message: "This invite code has expired." };
14364
+ }
14365
+ if (invite.currentUses >= invite.maxUses) {
14366
+ return { valid: false, message: "This invite code has reached its usage limit." };
14367
+ }
14368
+ return {
14369
+ valid: true,
14370
+ message: "Valid invite code.",
14371
+ code: invite.code,
14372
+ remainingUses: invite.maxUses - invite.currentUses
14373
+ };
14374
+ }
14375
+ async consumeCode(code, userId) {
14376
+ const validation = await this.validateCode(code);
14377
+ if (!validation.valid) {
14378
+ return { success: false, message: validation.message };
14379
+ }
14380
+ const normalized = normalizeBetaCode(code);
14381
+ const invite = this.codes.get(normalized);
14382
+ if (!invite) {
14383
+ return { success: false, message: "Invalid invite code." };
14384
+ }
14385
+ invite.currentUses += 1;
14386
+ this.testers.set(userId, {
14387
+ userId,
14388
+ inviteCode: invite.code,
14389
+ isBetaTester: true,
14390
+ betaJoinedAt: /* @__PURE__ */ new Date()
14391
+ });
14392
+ return { success: true, message: "Invite code consumed successfully." };
14393
+ }
14394
+ // ─────────────────────────────────────────────────────────────
14395
+ // User Tracking
14396
+ // ─────────────────────────────────────────────────────────────
14397
+ async isBetaTester(userId) {
14398
+ const tester = this.testers.get(userId);
14399
+ return tester?.isBetaTester ?? false;
14400
+ }
14401
+ async getBetaTester(userId) {
14402
+ return this.testers.get(userId) ?? null;
14403
+ }
14404
+ async listBetaTesters(options = {}) {
14405
+ const testers = Array.from(this.testers.values()).filter((t) => t.isBetaTester).sort((a, b) => b.betaJoinedAt.getTime() - a.betaJoinedAt.getTime());
14406
+ const offset = options.offset ?? 0;
14407
+ const limit = options.limit ?? 100;
14408
+ return testers.slice(offset, offset + limit);
14409
+ }
14410
+ // ─────────────────────────────────────────────────────────────
14411
+ // Analytics
14412
+ // ─────────────────────────────────────────────────────────────
14413
+ async getStats() {
14414
+ const codes = Array.from(this.codes.values());
14415
+ const activeCodes = codes.filter((c) => c.isActive);
14416
+ const testers = Array.from(this.testers.values()).filter((t) => t.isBetaTester);
14417
+ const joinDates = testers.map((t) => t.betaJoinedAt).sort((a, b) => a.getTime() - b.getTime());
14418
+ return {
14419
+ totalBetaTesters: testers.length,
14420
+ totalCodes: codes.length,
14421
+ activeCodes: activeCodes.length,
14422
+ totalUses: codes.reduce((sum, c) => sum + c.currentUses, 0),
14423
+ totalRemaining: activeCodes.reduce(
14424
+ (sum, c) => sum + (c.maxUses - c.currentUses),
14425
+ 0
14426
+ ),
14427
+ firstBetaSignup: joinDates[0] ?? null,
14428
+ latestBetaSignup: joinDates[joinDates.length - 1] ?? null
14429
+ };
14430
+ }
14431
+ async getCodeUsageReports() {
14432
+ return Array.from(this.codes.values()).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).map((c) => ({
14433
+ code: c.code,
14434
+ notes: c.notes,
14435
+ maxUses: c.maxUses,
14436
+ currentUses: c.currentUses,
14437
+ remainingUses: c.maxUses - c.currentUses,
14438
+ usagePercent: c.maxUses > 0 ? Math.round(c.currentUses / c.maxUses * 100) : 0,
14439
+ isActive: c.isActive,
14440
+ expiresAt: c.expiresAt,
14441
+ createdAt: c.createdAt
14442
+ }));
14443
+ }
14444
+ // ─────────────────────────────────────────────────────────────
14445
+ // Health
14446
+ // ─────────────────────────────────────────────────────────────
14447
+ async healthCheck() {
14448
+ return true;
14449
+ }
14450
+ // ─────────────────────────────────────────────────────────────
14451
+ // Testing Helpers
14452
+ // ─────────────────────────────────────────────────────────────
14453
+ /** Clear all data (for testing) */
14454
+ clear() {
14455
+ this.codes.clear();
14456
+ this.testers.clear();
14457
+ this.settings = {
14458
+ betaMode: this.config.defaultBetaMode,
14459
+ requireInviteCode: this.config.defaultRequireInviteCode,
14460
+ betaMessage: this.config.defaultBetaMessage
14461
+ };
14462
+ }
14463
+ /** Get the number of stored codes */
14464
+ get codeCount() {
14465
+ return this.codes.size;
14466
+ }
14467
+ /** Get the number of beta testers */
14468
+ get testerCount() {
14469
+ return this.testers.size;
14470
+ }
14471
+ };
14472
+
14213
14473
  // src/index.ts
14214
14474
  init_IAI();
14215
14475
  init_IRAG();
@@ -16665,8 +16925,8 @@ var MemoryRateLimiterStorage = class {
16665
16925
  const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
16666
16926
  this.cleanupInterval = setInterval(() => {
16667
16927
  const now = Date.now();
16668
- for (const [key, window] of this.windows) {
16669
- if (window.resetAt < now) {
16928
+ for (const [key, window2] of this.windows) {
16929
+ if (window2.resetAt < now) {
16670
16930
  this.windows.delete(key);
16671
16931
  }
16672
16932
  }
@@ -16677,20 +16937,20 @@ var MemoryRateLimiterStorage = class {
16677
16937
  }
16678
16938
  async increment(key, windowMs) {
16679
16939
  const now = Date.now();
16680
- let window = this.windows.get(key);
16681
- if (!window || window.resetAt < now) {
16682
- window = { count: 0, resetAt: now + windowMs };
16683
- this.windows.set(key, window);
16940
+ let window2 = this.windows.get(key);
16941
+ if (!window2 || window2.resetAt < now) {
16942
+ window2 = { count: 0, resetAt: now + windowMs };
16943
+ this.windows.set(key, window2);
16684
16944
  }
16685
- window.count++;
16686
- return [window.count, window.resetAt];
16945
+ window2.count++;
16946
+ return [window2.count, window2.resetAt];
16687
16947
  }
16688
16948
  async get(key) {
16689
- const window = this.windows.get(key);
16690
- if (!window || window.resetAt < Date.now()) {
16949
+ const window2 = this.windows.get(key);
16950
+ if (!window2 || window2.resetAt < Date.now()) {
16691
16951
  return null;
16692
16952
  }
16693
- return window;
16953
+ return window2;
16694
16954
  }
16695
16955
  /**
16696
16956
  * Clear all windows (for testing)
@@ -18277,7 +18537,11 @@ function buildKeycloakCallbacks(config) {
18277
18537
  * Compatible with Auth.js v5 JWT callback signature.
18278
18538
  */
18279
18539
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18280
- async jwt({ token, user, account }) {
18540
+ async jwt({
18541
+ token,
18542
+ user,
18543
+ account
18544
+ }) {
18281
18545
  if (user) {
18282
18546
  token.id = token.sub ?? user.id;
18283
18547
  }
@@ -18946,6 +19210,119 @@ function createAuditLogger(options = {}) {
18946
19210
  return { log, createTimedAudit };
18947
19211
  }
18948
19212
 
19213
+ // src/auth/beta-client.ts
19214
+ var DEFAULT_CONFIG = {
19215
+ baseUrl: "",
19216
+ settingsEndpoint: "/api/beta-settings",
19217
+ validateEndpoint: "/api/validate-beta-code",
19218
+ storageKey: "beta_code",
19219
+ failSafeDefaults: {
19220
+ betaMode: true,
19221
+ requireInviteCode: true,
19222
+ betaMessage: ""
19223
+ }
19224
+ };
19225
+ function createBetaClient(config = {}) {
19226
+ const cfg = {
19227
+ ...DEFAULT_CONFIG,
19228
+ ...config,
19229
+ failSafeDefaults: {
19230
+ ...DEFAULT_CONFIG.failSafeDefaults,
19231
+ ...config.failSafeDefaults
19232
+ }
19233
+ };
19234
+ return {
19235
+ fetchSettings: () => fetchBetaSettings(cfg),
19236
+ validateCode: (code) => validateBetaCode(code, cfg),
19237
+ storeCode: (code) => storeBetaCode(code, cfg),
19238
+ getStoredCode: () => getStoredBetaCode(cfg),
19239
+ clearStoredCode: () => clearStoredBetaCode(cfg)
19240
+ };
19241
+ }
19242
+ async function fetchBetaSettings(config = {}) {
19243
+ const cfg = { ...DEFAULT_CONFIG, ...config };
19244
+ try {
19245
+ const response = await fetch(
19246
+ `${cfg.baseUrl}${cfg.settingsEndpoint}`,
19247
+ {
19248
+ method: "GET",
19249
+ headers: { "Content-Type": "application/json" },
19250
+ cache: "no-store"
19251
+ }
19252
+ );
19253
+ if (!response.ok) {
19254
+ throw new Error(`Failed to fetch beta settings: ${response.status}`);
19255
+ }
19256
+ const data = await response.json();
19257
+ return {
19258
+ betaMode: data.betaMode ?? cfg.failSafeDefaults.betaMode ?? true,
19259
+ requireInviteCode: data.requireInviteCode ?? cfg.failSafeDefaults.requireInviteCode ?? true,
19260
+ betaMessage: data.betaMessage ?? cfg.failSafeDefaults.betaMessage ?? ""
19261
+ };
19262
+ } catch (error) {
19263
+ console.error("Error fetching beta settings:", error);
19264
+ return {
19265
+ betaMode: cfg.failSafeDefaults.betaMode ?? true,
19266
+ requireInviteCode: cfg.failSafeDefaults.requireInviteCode ?? true,
19267
+ betaMessage: cfg.failSafeDefaults.betaMessage ?? ""
19268
+ };
19269
+ }
19270
+ }
19271
+ async function validateBetaCode(code, config = {}) {
19272
+ const cfg = { ...DEFAULT_CONFIG, ...config };
19273
+ if (!code || code.trim().length < 3) {
19274
+ return {
19275
+ valid: false,
19276
+ message: "Please enter a valid invite code."
19277
+ };
19278
+ }
19279
+ try {
19280
+ const response = await fetch(
19281
+ `${cfg.baseUrl}${cfg.validateEndpoint}`,
19282
+ {
19283
+ method: "POST",
19284
+ headers: { "Content-Type": "application/json" },
19285
+ body: JSON.stringify({ code: code.trim().toUpperCase() })
19286
+ }
19287
+ );
19288
+ if (response.status === 429) {
19289
+ return {
19290
+ valid: false,
19291
+ message: "Too many attempts. Please try again later."
19292
+ };
19293
+ }
19294
+ if (!response.ok) {
19295
+ throw new Error(`Validation request failed: ${response.status}`);
19296
+ }
19297
+ return await response.json();
19298
+ } catch (error) {
19299
+ console.error("Error validating invite code:", error);
19300
+ return {
19301
+ valid: false,
19302
+ message: "Unable to validate code. Please try again."
19303
+ };
19304
+ }
19305
+ }
19306
+ function storeBetaCode(code, config = {}) {
19307
+ const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
19308
+ if (typeof window !== "undefined") {
19309
+ sessionStorage.setItem(key, code.trim().toUpperCase());
19310
+ }
19311
+ }
19312
+ function getStoredBetaCode(config = {}) {
19313
+ const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
19314
+ if (typeof window !== "undefined") {
19315
+ return sessionStorage.getItem(key);
19316
+ }
19317
+ return null;
19318
+ }
19319
+ function clearStoredBetaCode(config = {}) {
19320
+ const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
19321
+ if (typeof window !== "undefined") {
19322
+ sessionStorage.removeItem(key);
19323
+ }
19324
+ }
19325
+
18949
19326
  // src/env.ts
18950
19327
  function getRequiredEnv(key) {
18951
19328
  const value = process.env[key];
@@ -30997,6 +31374,498 @@ function generateId2() {
30997
31374
  return (0, import_crypto38.randomBytes)(8).toString("hex") + Date.now().toString(36);
30998
31375
  }
30999
31376
 
31377
+ // src/adapters/postgres-beta/PostgresBeta.ts
31378
+ function mapRowToInvite(row) {
31379
+ return {
31380
+ id: row.id,
31381
+ code: row.code,
31382
+ maxUses: row.max_uses,
31383
+ currentUses: row.current_uses,
31384
+ expiresAt: row.expires_at ? new Date(row.expires_at) : null,
31385
+ createdBy: row.created_by,
31386
+ notes: row.notes ?? "",
31387
+ isActive: row.is_active,
31388
+ createdAt: new Date(row.created_at)
31389
+ };
31390
+ }
31391
+ function mapRowToTester(row) {
31392
+ return {
31393
+ userId: row.user_id,
31394
+ inviteCode: row.invite_code,
31395
+ isBetaTester: row.is_beta_tester,
31396
+ betaJoinedAt: new Date(row.beta_joined_at)
31397
+ };
31398
+ }
31399
+ var PostgresBeta = class {
31400
+ pool;
31401
+ schema;
31402
+ settingsSource;
31403
+ envPrefix;
31404
+ staticCodes;
31405
+ config;
31406
+ constructor(pgConfig) {
31407
+ this.pool = pgConfig.pool;
31408
+ this.schema = pgConfig.schema ?? "public";
31409
+ this.settingsSource = pgConfig.settingsSource ?? "database";
31410
+ this.envPrefix = pgConfig.envPrefix ?? "BETA";
31411
+ this.staticCodes = new Set(
31412
+ (pgConfig.staticCodes ?? []).map((c) => normalizeBetaCode(c))
31413
+ );
31414
+ this.config = {
31415
+ defaultBetaMode: pgConfig.defaultBetaMode ?? true,
31416
+ defaultRequireInviteCode: pgConfig.defaultRequireInviteCode ?? true,
31417
+ defaultBetaMessage: pgConfig.defaultBetaMessage ?? "We're in beta! Thanks for being an early tester.",
31418
+ codePrefix: pgConfig.codePrefix ?? "BETA",
31419
+ maxCodesPerBatch: pgConfig.maxCodesPerBatch ?? 50
31420
+ };
31421
+ }
31422
+ // ─────────────────────────────────────────────────────────────
31423
+ // Table helpers
31424
+ // ─────────────────────────────────────────────────────────────
31425
+ t(table) {
31426
+ return `${this.schema}.${table}`;
31427
+ }
31428
+ // ─────────────────────────────────────────────────────────────
31429
+ // Initialization
31430
+ // ─────────────────────────────────────────────────────────────
31431
+ /**
31432
+ * Initialize beta tables. Call once at app startup.
31433
+ * Creates tables if they don't exist and seeds default settings.
31434
+ */
31435
+ async initialize() {
31436
+ const client = await this.pool.connect();
31437
+ try {
31438
+ await client.query(`
31439
+ CREATE TABLE IF NOT EXISTS ${this.t("beta_settings")} (
31440
+ key VARCHAR(100) PRIMARY KEY,
31441
+ value TEXT NOT NULL,
31442
+ description TEXT,
31443
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
31444
+ updated_by VARCHAR(255)
31445
+ )
31446
+ `);
31447
+ await client.query(`
31448
+ INSERT INTO ${this.t("beta_settings")} (key, value, description) VALUES
31449
+ ('beta_mode', $1, 'Whether the app is in beta mode'),
31450
+ ('require_invite_code', $2, 'Whether an invite code is required to sign up'),
31451
+ ('beta_message', $3, 'Message displayed during beta')
31452
+ ON CONFLICT (key) DO NOTHING
31453
+ `, [
31454
+ String(this.config.defaultBetaMode),
31455
+ String(this.config.defaultRequireInviteCode),
31456
+ this.config.defaultBetaMessage
31457
+ ]);
31458
+ await client.query(`
31459
+ CREATE TABLE IF NOT EXISTS ${this.t("beta_invites")} (
31460
+ id VARCHAR(255) PRIMARY KEY,
31461
+ code VARCHAR(100) NOT NULL UNIQUE,
31462
+ max_uses INTEGER NOT NULL DEFAULT 1,
31463
+ current_uses INTEGER NOT NULL DEFAULT 0,
31464
+ expires_at TIMESTAMP WITH TIME ZONE,
31465
+ created_by VARCHAR(255) NOT NULL,
31466
+ notes TEXT DEFAULT '',
31467
+ is_active BOOLEAN NOT NULL DEFAULT true,
31468
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
31469
+ CONSTRAINT chk_beta_uses CHECK (current_uses <= max_uses)
31470
+ )
31471
+ `);
31472
+ await client.query(`
31473
+ CREATE INDEX IF NOT EXISTS idx_beta_invites_code_active
31474
+ ON ${this.t("beta_invites")}(code) WHERE is_active = true
31475
+ `);
31476
+ await client.query(`
31477
+ CREATE TABLE IF NOT EXISTS ${this.t("beta_testers")} (
31478
+ user_id VARCHAR(255) PRIMARY KEY,
31479
+ invite_code VARCHAR(100) NOT NULL,
31480
+ is_beta_tester BOOLEAN NOT NULL DEFAULT true,
31481
+ beta_joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
31482
+ )
31483
+ `);
31484
+ await client.query(`
31485
+ CREATE INDEX IF NOT EXISTS idx_beta_testers_active
31486
+ ON ${this.t("beta_testers")}(is_beta_tester, beta_joined_at DESC)
31487
+ WHERE is_beta_tester = true
31488
+ `);
31489
+ } finally {
31490
+ client.release();
31491
+ }
31492
+ }
31493
+ // ─────────────────────────────────────────────────────────────
31494
+ // Settings
31495
+ // ─────────────────────────────────────────────────────────────
31496
+ async getSettings() {
31497
+ if (this.settingsSource === "env") {
31498
+ return this.getEnvSettings();
31499
+ }
31500
+ const result = await this.pool.query(
31501
+ `SELECT key, value FROM ${this.t("beta_settings")} WHERE key IN ('beta_mode', 'require_invite_code', 'beta_message')`
31502
+ );
31503
+ const settings = {};
31504
+ for (const row of result.rows) {
31505
+ settings[row.key] = row.value;
31506
+ }
31507
+ return {
31508
+ betaMode: settings["beta_mode"] !== "false",
31509
+ requireInviteCode: settings["require_invite_code"] !== "false",
31510
+ betaMessage: settings["beta_message"] ?? this.config.defaultBetaMessage
31511
+ };
31512
+ }
31513
+ getEnvSettings() {
31514
+ const prefix = this.envPrefix;
31515
+ return {
31516
+ betaMode: process.env[`${prefix}_MODE`] !== "false",
31517
+ requireInviteCode: process.env[`${prefix}_REQUIRE_INVITE_CODE`] !== "false",
31518
+ betaMessage: process.env[`${prefix}_MESSAGE`] ?? this.config.defaultBetaMessage
31519
+ };
31520
+ }
31521
+ async updateSettings(options) {
31522
+ if (this.settingsSource === "env") {
31523
+ return;
31524
+ }
31525
+ const client = await this.pool.connect();
31526
+ try {
31527
+ const updates = [];
31528
+ if (options.betaMode !== void 0) {
31529
+ updates.push({ key: "beta_mode", value: String(options.betaMode) });
31530
+ }
31531
+ if (options.requireInviteCode !== void 0) {
31532
+ updates.push({ key: "require_invite_code", value: String(options.requireInviteCode) });
31533
+ }
31534
+ if (options.betaMessage !== void 0) {
31535
+ updates.push({ key: "beta_message", value: options.betaMessage });
31536
+ }
31537
+ for (const { key, value } of updates) {
31538
+ await client.query(
31539
+ `UPDATE ${this.t("beta_settings")} SET value = $1, updated_at = NOW(), updated_by = $2 WHERE key = $3`,
31540
+ [value, options.updatedBy ?? "system", key]
31541
+ );
31542
+ }
31543
+ } finally {
31544
+ client.release();
31545
+ }
31546
+ }
31547
+ // ─────────────────────────────────────────────────────────────
31548
+ // Code Management
31549
+ // ─────────────────────────────────────────────────────────────
31550
+ async createCodes(options) {
31551
+ const count = Math.min(options.count ?? 1, this.config.maxCodesPerBatch);
31552
+ const prefix = options.prefix ?? this.config.codePrefix;
31553
+ const results = [];
31554
+ const client = await this.pool.connect();
31555
+ try {
31556
+ for (let i = 0; i < count; i++) {
31557
+ const codeStr = options.code ? normalizeBetaCode(options.code) : generateBetaCode(prefix);
31558
+ const id = generateBetaId();
31559
+ let attempts = 0;
31560
+ let inserted = false;
31561
+ let currentCode = codeStr;
31562
+ while (!inserted && attempts < 5) {
31563
+ try {
31564
+ const result = await client.query(
31565
+ `INSERT INTO ${this.t("beta_invites")} (id, code, max_uses, expires_at, created_by, notes)
31566
+ VALUES ($1, $2, $3, $4, $5, $6)
31567
+ RETURNING *`,
31568
+ [
31569
+ id,
31570
+ currentCode,
31571
+ options.maxUses ?? 1,
31572
+ options.expiresAt ?? null,
31573
+ options.createdBy,
31574
+ options.notes ?? ""
31575
+ ]
31576
+ );
31577
+ const row = result.rows[0];
31578
+ if (row) results.push(mapRowToInvite(row));
31579
+ inserted = true;
31580
+ } catch (err) {
31581
+ const pgErr = err;
31582
+ if (pgErr.code === "23505" && !options.code) {
31583
+ currentCode = generateBetaCode(prefix);
31584
+ attempts++;
31585
+ } else {
31586
+ throw err;
31587
+ }
31588
+ }
31589
+ }
31590
+ if (!inserted) {
31591
+ throw new Error(`Failed to generate unique code after ${attempts} attempts`);
31592
+ }
31593
+ if (options.code) break;
31594
+ }
31595
+ } finally {
31596
+ client.release();
31597
+ }
31598
+ return results;
31599
+ }
31600
+ async listCodes(options = {}) {
31601
+ const conditions = [];
31602
+ const params = [];
31603
+ let paramIdx = 1;
31604
+ if (options.isActive !== void 0) {
31605
+ conditions.push(`is_active = $${paramIdx++}`);
31606
+ params.push(options.isActive);
31607
+ }
31608
+ if (options.status) {
31609
+ switch (options.status) {
31610
+ case "unused":
31611
+ conditions.push("is_active = true");
31612
+ conditions.push("current_uses = 0");
31613
+ break;
31614
+ case "partial":
31615
+ conditions.push("is_active = true");
31616
+ conditions.push("current_uses > 0");
31617
+ conditions.push("current_uses < max_uses");
31618
+ break;
31619
+ case "exhausted":
31620
+ conditions.push("current_uses >= max_uses");
31621
+ break;
31622
+ case "expired":
31623
+ conditions.push("expires_at IS NOT NULL");
31624
+ conditions.push("expires_at < NOW()");
31625
+ break;
31626
+ case "revoked":
31627
+ conditions.push("is_active = false");
31628
+ break;
31629
+ }
31630
+ }
31631
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
31632
+ const limit = options.limit ?? 100;
31633
+ const offset = options.offset ?? 0;
31634
+ const result = await this.pool.query(
31635
+ `SELECT * FROM ${this.t("beta_invites")} ${where}
31636
+ ORDER BY created_at DESC
31637
+ LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
31638
+ [...params, limit, offset]
31639
+ );
31640
+ return result.rows.map(mapRowToInvite);
31641
+ }
31642
+ async getCode(code) {
31643
+ const normalized = normalizeBetaCode(code);
31644
+ const result = await this.pool.query(
31645
+ `SELECT * FROM ${this.t("beta_invites")} WHERE code = $1`,
31646
+ [normalized]
31647
+ );
31648
+ const row = result.rows[0];
31649
+ if (!row) return null;
31650
+ return mapRowToInvite(row);
31651
+ }
31652
+ async revokeCode(code) {
31653
+ const normalized = normalizeBetaCode(code);
31654
+ await this.pool.query(
31655
+ `UPDATE ${this.t("beta_invites")} SET is_active = false WHERE code = $1`,
31656
+ [normalized]
31657
+ );
31658
+ }
31659
+ // ─────────────────────────────────────────────────────────────
31660
+ // Validation & Consumption
31661
+ // ─────────────────────────────────────────────────────────────
31662
+ async validateCode(code) {
31663
+ const settings = await this.getSettings();
31664
+ if (!settings.requireInviteCode) {
31665
+ return { valid: true, message: "Invite codes are not required." };
31666
+ }
31667
+ const normalized = normalizeBetaCode(code);
31668
+ if (this.staticCodes.has(normalized)) {
31669
+ return {
31670
+ valid: true,
31671
+ message: "Valid invite code.",
31672
+ code: normalized
31673
+ };
31674
+ }
31675
+ const result = await this.pool.query(
31676
+ `SELECT * FROM ${this.t("beta_invites")}
31677
+ WHERE code = $1
31678
+ AND is_active = true
31679
+ AND (expires_at IS NULL OR expires_at > NOW())
31680
+ AND current_uses < max_uses`,
31681
+ [normalized]
31682
+ );
31683
+ if (result.rows.length === 0) {
31684
+ const exists = await this.pool.query(
31685
+ `SELECT is_active, expires_at, current_uses, max_uses FROM ${this.t("beta_invites")} WHERE code = $1`,
31686
+ [normalized]
31687
+ );
31688
+ if (exists.rows.length === 0) {
31689
+ return { valid: false, message: "Invalid invite code." };
31690
+ }
31691
+ const existRow = exists.rows[0];
31692
+ if (!existRow) {
31693
+ return { valid: false, message: "Invalid invite code." };
31694
+ }
31695
+ if (!existRow.is_active) {
31696
+ return { valid: false, message: "This invite code has been revoked." };
31697
+ }
31698
+ if (existRow.expires_at && new Date(existRow.expires_at) < /* @__PURE__ */ new Date()) {
31699
+ return { valid: false, message: "This invite code has expired." };
31700
+ }
31701
+ if (existRow.current_uses >= existRow.max_uses) {
31702
+ return { valid: false, message: "This invite code has reached its usage limit." };
31703
+ }
31704
+ return { valid: false, message: "Invalid invite code." };
31705
+ }
31706
+ const invite = result.rows[0];
31707
+ if (!invite) {
31708
+ return { valid: false, message: "Invalid invite code." };
31709
+ }
31710
+ return {
31711
+ valid: true,
31712
+ message: "Valid invite code.",
31713
+ code: invite.code,
31714
+ remainingUses: invite.max_uses - invite.current_uses
31715
+ };
31716
+ }
31717
+ async consumeCode(code, userId) {
31718
+ const normalized = normalizeBetaCode(code);
31719
+ if (this.staticCodes.has(normalized)) {
31720
+ await this.tagBetaTester(userId, normalized);
31721
+ return { success: true, message: "Invite code consumed successfully." };
31722
+ }
31723
+ const client = await this.pool.connect();
31724
+ try {
31725
+ await client.query("BEGIN");
31726
+ const result = await client.query(
31727
+ `SELECT * FROM ${this.t("beta_invites")}
31728
+ WHERE code = $1
31729
+ AND is_active = true
31730
+ AND (expires_at IS NULL OR expires_at > NOW())
31731
+ AND current_uses < max_uses
31732
+ FOR UPDATE`,
31733
+ [normalized]
31734
+ );
31735
+ if (result.rows.length === 0) {
31736
+ await client.query("ROLLBACK");
31737
+ return { success: false, message: "Invalid or exhausted invite code." };
31738
+ }
31739
+ await client.query(
31740
+ `UPDATE ${this.t("beta_invites")} SET current_uses = current_uses + 1 WHERE code = $1`,
31741
+ [normalized]
31742
+ );
31743
+ await client.query(
31744
+ `INSERT INTO ${this.t("beta_testers")} (user_id, invite_code, is_beta_tester, beta_joined_at)
31745
+ VALUES ($1, $2, true, NOW())
31746
+ ON CONFLICT (user_id) DO UPDATE SET invite_code = $2, is_beta_tester = true, beta_joined_at = NOW()`,
31747
+ [userId, normalized]
31748
+ );
31749
+ await client.query("COMMIT");
31750
+ return { success: true, message: "Invite code consumed successfully." };
31751
+ } catch (error) {
31752
+ await client.query("ROLLBACK");
31753
+ throw error;
31754
+ } finally {
31755
+ client.release();
31756
+ }
31757
+ }
31758
+ async tagBetaTester(userId, code) {
31759
+ await this.pool.query(
31760
+ `INSERT INTO ${this.t("beta_testers")} (user_id, invite_code, is_beta_tester, beta_joined_at)
31761
+ VALUES ($1, $2, true, NOW())
31762
+ ON CONFLICT (user_id) DO UPDATE SET invite_code = $2, is_beta_tester = true, beta_joined_at = NOW()`,
31763
+ [userId, code]
31764
+ );
31765
+ }
31766
+ // ─────────────────────────────────────────────────────────────
31767
+ // User Tracking
31768
+ // ─────────────────────────────────────────────────────────────
31769
+ async isBetaTester(userId) {
31770
+ const result = await this.pool.query(
31771
+ `SELECT is_beta_tester FROM ${this.t("beta_testers")} WHERE user_id = $1 AND is_beta_tester = true`,
31772
+ [userId]
31773
+ );
31774
+ return result.rows.length > 0;
31775
+ }
31776
+ async getBetaTester(userId) {
31777
+ const result = await this.pool.query(
31778
+ `SELECT * FROM ${this.t("beta_testers")} WHERE user_id = $1`,
31779
+ [userId]
31780
+ );
31781
+ const row = result.rows[0];
31782
+ if (!row) return null;
31783
+ return mapRowToTester(row);
31784
+ }
31785
+ async listBetaTesters(options = {}) {
31786
+ const limit = options.limit ?? 100;
31787
+ const offset = options.offset ?? 0;
31788
+ const result = await this.pool.query(
31789
+ `SELECT * FROM ${this.t("beta_testers")}
31790
+ WHERE is_beta_tester = true
31791
+ ORDER BY beta_joined_at DESC
31792
+ LIMIT $1 OFFSET $2`,
31793
+ [limit, offset]
31794
+ );
31795
+ return result.rows.map(mapRowToTester);
31796
+ }
31797
+ // ─────────────────────────────────────────────────────────────
31798
+ // Analytics
31799
+ // ─────────────────────────────────────────────────────────────
31800
+ async getStats() {
31801
+ const [testersResult, codesResult] = await Promise.all([
31802
+ this.pool.query(
31803
+ `SELECT
31804
+ COUNT(*) as total,
31805
+ MIN(beta_joined_at) as first_signup,
31806
+ MAX(beta_joined_at) as latest_signup
31807
+ FROM ${this.t("beta_testers")}
31808
+ WHERE is_beta_tester = true`
31809
+ ),
31810
+ this.pool.query(
31811
+ `SELECT
31812
+ COUNT(*) as total_codes,
31813
+ COUNT(*) FILTER (WHERE is_active = true) as active_codes,
31814
+ COALESCE(SUM(current_uses), 0) as total_uses,
31815
+ COALESCE(SUM(CASE WHEN is_active = true THEN max_uses - current_uses ELSE 0 END), 0) as total_remaining
31816
+ FROM ${this.t("beta_invites")}`
31817
+ )
31818
+ ]);
31819
+ const tRow = testersResult.rows[0] ?? {};
31820
+ const cRow = codesResult.rows[0] ?? {};
31821
+ return {
31822
+ totalBetaTesters: Number(tRow.total ?? 0),
31823
+ totalCodes: Number(cRow.total_codes ?? 0),
31824
+ activeCodes: Number(cRow.active_codes ?? 0),
31825
+ totalUses: Number(cRow.total_uses ?? 0),
31826
+ totalRemaining: Number(cRow.total_remaining ?? 0),
31827
+ firstBetaSignup: tRow.first_signup ? new Date(tRow.first_signup) : null,
31828
+ latestBetaSignup: tRow.latest_signup ? new Date(tRow.latest_signup) : null
31829
+ };
31830
+ }
31831
+ async getCodeUsageReports() {
31832
+ const result = await this.pool.query(
31833
+ `SELECT
31834
+ code, notes, max_uses, current_uses,
31835
+ (max_uses - current_uses) as remaining_uses,
31836
+ CASE WHEN max_uses > 0
31837
+ THEN ROUND((current_uses::numeric / max_uses) * 100)
31838
+ ELSE 0
31839
+ END as usage_percent,
31840
+ is_active, expires_at, created_at
31841
+ FROM ${this.t("beta_invites")}
31842
+ ORDER BY created_at DESC`
31843
+ );
31844
+ return result.rows.map((row) => ({
31845
+ code: row.code,
31846
+ notes: row.notes ?? "",
31847
+ maxUses: row.max_uses,
31848
+ currentUses: row.current_uses,
31849
+ remainingUses: Number(row.remaining_uses ?? 0),
31850
+ usagePercent: Number(row.usage_percent ?? 0),
31851
+ isActive: row.is_active,
31852
+ expiresAt: row.expires_at ? new Date(row.expires_at) : null,
31853
+ createdAt: new Date(row.created_at)
31854
+ }));
31855
+ }
31856
+ // ─────────────────────────────────────────────────────────────
31857
+ // Health
31858
+ // ─────────────────────────────────────────────────────────────
31859
+ async healthCheck() {
31860
+ try {
31861
+ await this.pool.query("SELECT 1");
31862
+ return true;
31863
+ } catch {
31864
+ return false;
31865
+ }
31866
+ }
31867
+ };
31868
+
31000
31869
  // src/app-logger.ts
31001
31870
  var LEVEL_PRIORITY2 = {
31002
31871
  debug: 0,
@@ -31173,7 +32042,7 @@ async function closeSharedRedis() {
31173
32042
  }
31174
32043
 
31175
32044
  // src/migrations/Migrator.ts
31176
- var DEFAULT_CONFIG = {
32045
+ var DEFAULT_CONFIG2 = {
31177
32046
  tableName: "_migrations",
31178
32047
  schema: "public",
31179
32048
  lockTimeout: 60,
@@ -31200,7 +32069,7 @@ var Migrator = class {
31200
32069
  locked = false;
31201
32070
  constructor(db, config = {}) {
31202
32071
  this.db = db;
31203
- this.config = { ...DEFAULT_CONFIG, ...config };
32072
+ this.config = { ...DEFAULT_CONFIG2, ...config };
31204
32073
  }
31205
32074
  /**
31206
32075
  * Get the fully qualified migration table name
@@ -31896,6 +32765,67 @@ var createSsoSessionsTable = {
31896
32765
  `,
31897
32766
  down: "DROP TABLE IF EXISTS sso_sessions CASCADE"
31898
32767
  };
32768
+ var createBetaSettingsTable = {
32769
+ version: "20241217_009",
32770
+ name: "create_beta_settings_table",
32771
+ up: `
32772
+ CREATE TABLE IF NOT EXISTS beta_settings (
32773
+ key VARCHAR(100) PRIMARY KEY,
32774
+ value TEXT NOT NULL,
32775
+ description TEXT,
32776
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
32777
+ updated_by VARCHAR(255)
32778
+ );
32779
+
32780
+ INSERT INTO beta_settings (key, value, description) VALUES
32781
+ ('beta_mode', 'true', 'Whether the app is in beta mode'),
32782
+ ('require_invite_code', 'true', 'Whether an invite code is required to sign up'),
32783
+ ('beta_message', 'We''re in beta! Thanks for being an early tester.', 'Message displayed during beta')
32784
+ ON CONFLICT (key) DO NOTHING;
32785
+ `,
32786
+ down: "DROP TABLE IF EXISTS beta_settings CASCADE"
32787
+ };
32788
+ var createBetaInvitesTable = {
32789
+ version: "20241217_010",
32790
+ name: "create_beta_invites_table",
32791
+ up: `
32792
+ CREATE TABLE IF NOT EXISTS beta_invites (
32793
+ id VARCHAR(255) PRIMARY KEY,
32794
+ code VARCHAR(100) NOT NULL UNIQUE,
32795
+ max_uses INTEGER NOT NULL DEFAULT 1,
32796
+ current_uses INTEGER NOT NULL DEFAULT 0,
32797
+ expires_at TIMESTAMP WITH TIME ZONE,
32798
+ created_by VARCHAR(255) NOT NULL,
32799
+ notes TEXT DEFAULT '',
32800
+ is_active BOOLEAN NOT NULL DEFAULT true,
32801
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
32802
+ CONSTRAINT chk_beta_uses CHECK (current_uses <= max_uses)
32803
+ );
32804
+
32805
+ CREATE INDEX IF NOT EXISTS idx_beta_invites_code_active
32806
+ ON beta_invites(code) WHERE is_active = true;
32807
+ CREATE INDEX IF NOT EXISTS idx_beta_invites_active
32808
+ ON beta_invites(is_active, created_at DESC);
32809
+ `,
32810
+ down: "DROP TABLE IF EXISTS beta_invites CASCADE"
32811
+ };
32812
+ var createBetaTestersTable = {
32813
+ version: "20241217_011",
32814
+ name: "create_beta_testers_table",
32815
+ up: `
32816
+ CREATE TABLE IF NOT EXISTS beta_testers (
32817
+ user_id VARCHAR(255) PRIMARY KEY,
32818
+ invite_code VARCHAR(100) NOT NULL REFERENCES beta_invites(code) ON DELETE SET NULL,
32819
+ is_beta_tester BOOLEAN NOT NULL DEFAULT true,
32820
+ beta_joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
32821
+ );
32822
+
32823
+ CREATE INDEX IF NOT EXISTS idx_beta_testers_active
32824
+ ON beta_testers(is_beta_tester, beta_joined_at DESC)
32825
+ WHERE is_beta_tester = true;
32826
+ `,
32827
+ down: "DROP TABLE IF EXISTS beta_testers CASCADE"
32828
+ };
31899
32829
  var enterpriseMigrations = [
31900
32830
  createSsoOidcConfigsTable,
31901
32831
  createDomainVerificationsTable,
@@ -31904,7 +32834,10 @@ var enterpriseMigrations = [
31904
32834
  createTenantMembersTable,
31905
32835
  createTenantInvitationsTable,
31906
32836
  createTenantUsageTable,
31907
- createSsoSessionsTable
32837
+ createSsoSessionsTable,
32838
+ createBetaSettingsTable,
32839
+ createBetaInvitesTable,
32840
+ createBetaTestersTable
31908
32841
  ];
31909
32842
  function getEnterpriseMigrations(features) {
31910
32843
  const migrations = [];
@@ -31924,6 +32857,13 @@ function getEnterpriseMigrations(features) {
31924
32857
  createTenantUsageTable
31925
32858
  );
31926
32859
  }
32860
+ if (features.beta) {
32861
+ migrations.push(
32862
+ createBetaSettingsTable,
32863
+ createBetaInvitesTable,
32864
+ createBetaTestersTable
32865
+ );
32866
+ }
31927
32867
  return migrations;
31928
32868
  }
31929
32869
  // Annotate the CommonJS export names for ESM import in node:
@@ -31986,6 +32926,7 @@ function getEnterpriseMigrations(features) {
31986
32926
  MemoryAuditLog,
31987
32927
  MemoryAuth,
31988
32928
  MemoryAuthSSO,
32929
+ MemoryBeta,
31989
32930
  MemoryBilling,
31990
32931
  MemoryCache,
31991
32932
  MemoryCompliance,
@@ -32026,6 +32967,7 @@ function getEnterpriseMigrations(features) {
32026
32967
  PhoneSchema,
32027
32968
  PineconeRAG,
32028
32969
  PlatformConfigSchema,
32970
+ PostgresBeta,
32029
32971
  PostgresDatabase,
32030
32972
  PostgresTenant,
32031
32973
  QueueConfigSchema,
@@ -32081,6 +33023,7 @@ function getEnterpriseMigrations(features) {
32081
33023
  checkEnvVars,
32082
33024
  checkRateLimit,
32083
33025
  classifyError,
33026
+ clearStoredBetaCode,
32084
33027
  closeSharedRedis,
32085
33028
  composeHookRegistries,
32086
33029
  constantTimeEqual,
@@ -32093,6 +33036,10 @@ function getEnterpriseMigrations(features) {
32093
33036
  createAuditActor,
32094
33037
  createAuditLogger,
32095
33038
  createAuthError,
33039
+ createBetaClient,
33040
+ createBetaInvitesTable,
33041
+ createBetaSettingsTable,
33042
+ createBetaTestersTable,
32096
33043
  createBulkhead,
32097
33044
  createCacheMiddleware,
32098
33045
  createCachedFallback,
@@ -32151,9 +33098,12 @@ function getEnterpriseMigrations(features) {
32151
33098
  extractAuditRequestId,
32152
33099
  extractAuditUserAgent,
32153
33100
  extractClientIp,
33101
+ fetchBetaSettings,
32154
33102
  filterChannelsByPreferences,
32155
33103
  formatAmount,
32156
33104
  generateAuditId,
33105
+ generateBetaCode,
33106
+ generateBetaId,
32157
33107
  generateChecksum,
32158
33108
  generateDeliveryId,
32159
33109
  generateErrorId,
@@ -32183,6 +33133,7 @@ function getEnterpriseMigrations(features) {
32183
33133
  getRequestId,
32184
33134
  getRequiredEnv,
32185
33135
  getSharedRedis,
33136
+ getStoredBetaCode,
32186
33137
  getTenantId,
32187
33138
  getTokenEndpoint,
32188
33139
  getTraceId,
@@ -32202,6 +33153,7 @@ function getEnterpriseMigrations(features) {
32202
33153
  loadConfig,
32203
33154
  matchAction,
32204
33155
  matchEventType,
33156
+ normalizeBetaCode,
32205
33157
  parseKeycloakRoles,
32206
33158
  raceTimeout,
32207
33159
  refreshKeycloakToken,
@@ -32215,9 +33167,11 @@ function getEnterpriseMigrations(features) {
32215
33167
  sanitizeApiError,
32216
33168
  sanitizeForEmail,
32217
33169
  sqlMigration,
33170
+ storeBetaCode,
32218
33171
  stripHtml,
32219
33172
  timedHealthCheck,
32220
33173
  toHealthCheckResult,
33174
+ validateBetaCode,
32221
33175
  validateConfig,
32222
33176
  validateEnvVars,
32223
33177
  withCorrelation,