@digilogiclabs/platform-core 1.7.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.mjs CHANGED
@@ -13893,6 +13893,252 @@ var MemoryCompliance = class {
13893
13893
  }
13894
13894
  };
13895
13895
 
13896
+ // src/interfaces/IBeta.ts
13897
+ function generateBetaCode(prefix = "BETA") {
13898
+ const hex = Array.from(
13899
+ { length: 8 },
13900
+ () => Math.floor(Math.random() * 16).toString(16)
13901
+ ).join("").toUpperCase();
13902
+ return `${prefix}-${hex}`;
13903
+ }
13904
+ function normalizeBetaCode(code) {
13905
+ return code.trim().toUpperCase();
13906
+ }
13907
+ function generateBetaId() {
13908
+ return `beta_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
13909
+ }
13910
+ var MemoryBeta = class {
13911
+ settings;
13912
+ codes = /* @__PURE__ */ new Map();
13913
+ testers = /* @__PURE__ */ new Map();
13914
+ config;
13915
+ constructor(config = {}) {
13916
+ this.config = {
13917
+ defaultBetaMode: config.defaultBetaMode ?? true,
13918
+ defaultRequireInviteCode: config.defaultRequireInviteCode ?? true,
13919
+ defaultBetaMessage: config.defaultBetaMessage ?? "We're in beta! Thanks for being an early tester.",
13920
+ codePrefix: config.codePrefix ?? "BETA",
13921
+ maxCodesPerBatch: config.maxCodesPerBatch ?? 50
13922
+ };
13923
+ this.settings = {
13924
+ betaMode: this.config.defaultBetaMode,
13925
+ requireInviteCode: this.config.defaultRequireInviteCode,
13926
+ betaMessage: this.config.defaultBetaMessage
13927
+ };
13928
+ }
13929
+ // ─────────────────────────────────────────────────────────────
13930
+ // Settings
13931
+ // ─────────────────────────────────────────────────────────────
13932
+ async getSettings() {
13933
+ return { ...this.settings };
13934
+ }
13935
+ async updateSettings(options) {
13936
+ if (options.betaMode !== void 0) {
13937
+ this.settings.betaMode = options.betaMode;
13938
+ }
13939
+ if (options.requireInviteCode !== void 0) {
13940
+ this.settings.requireInviteCode = options.requireInviteCode;
13941
+ }
13942
+ if (options.betaMessage !== void 0) {
13943
+ this.settings.betaMessage = options.betaMessage;
13944
+ }
13945
+ }
13946
+ // ─────────────────────────────────────────────────────────────
13947
+ // Code Management
13948
+ // ─────────────────────────────────────────────────────────────
13949
+ async createCodes(options) {
13950
+ const count = Math.min(options.count ?? 1, this.config.maxCodesPerBatch);
13951
+ const prefix = options.prefix ?? this.config.codePrefix;
13952
+ const results = [];
13953
+ for (let i = 0; i < count; i++) {
13954
+ const codeStr = options.code ? normalizeBetaCode(options.code) : generateBetaCode(prefix);
13955
+ if (this.codes.has(codeStr)) {
13956
+ throw new Error(`Code already exists: ${codeStr}`);
13957
+ }
13958
+ const invite = {
13959
+ id: generateBetaId(),
13960
+ code: codeStr,
13961
+ maxUses: options.maxUses ?? 1,
13962
+ currentUses: 0,
13963
+ expiresAt: options.expiresAt ?? null,
13964
+ createdBy: options.createdBy,
13965
+ notes: options.notes ?? "",
13966
+ isActive: true,
13967
+ createdAt: /* @__PURE__ */ new Date()
13968
+ };
13969
+ this.codes.set(codeStr, invite);
13970
+ results.push(invite);
13971
+ if (options.code) {
13972
+ break;
13973
+ }
13974
+ }
13975
+ return results;
13976
+ }
13977
+ async listCodes(options = {}) {
13978
+ let codes = Array.from(this.codes.values());
13979
+ if (options.isActive !== void 0) {
13980
+ codes = codes.filter((c) => c.isActive === options.isActive);
13981
+ }
13982
+ if (options.status) {
13983
+ const now = /* @__PURE__ */ new Date();
13984
+ codes = codes.filter((c) => {
13985
+ switch (options.status) {
13986
+ case "unused":
13987
+ return c.isActive && c.currentUses === 0;
13988
+ case "partial":
13989
+ return c.isActive && c.currentUses > 0 && c.currentUses < c.maxUses;
13990
+ case "exhausted":
13991
+ return c.currentUses >= c.maxUses;
13992
+ case "expired":
13993
+ return c.expiresAt !== null && c.expiresAt < now;
13994
+ case "revoked":
13995
+ return !c.isActive;
13996
+ default:
13997
+ return true;
13998
+ }
13999
+ });
14000
+ }
14001
+ codes.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
14002
+ const offset = options.offset ?? 0;
14003
+ const limit = options.limit ?? 100;
14004
+ return codes.slice(offset, offset + limit);
14005
+ }
14006
+ async getCode(code) {
14007
+ return this.codes.get(normalizeBetaCode(code)) ?? null;
14008
+ }
14009
+ async revokeCode(code) {
14010
+ const normalized = normalizeBetaCode(code);
14011
+ const invite = this.codes.get(normalized);
14012
+ if (invite) {
14013
+ invite.isActive = false;
14014
+ }
14015
+ }
14016
+ // ─────────────────────────────────────────────────────────────
14017
+ // Validation & Consumption
14018
+ // ─────────────────────────────────────────────────────────────
14019
+ async validateCode(code) {
14020
+ if (!this.settings.requireInviteCode) {
14021
+ return { valid: true, message: "Invite codes are not required." };
14022
+ }
14023
+ const normalized = normalizeBetaCode(code);
14024
+ const invite = this.codes.get(normalized);
14025
+ if (!invite) {
14026
+ return { valid: false, message: "Invalid invite code." };
14027
+ }
14028
+ if (!invite.isActive) {
14029
+ return { valid: false, message: "This invite code has been revoked." };
14030
+ }
14031
+ if (invite.expiresAt && invite.expiresAt < /* @__PURE__ */ new Date()) {
14032
+ return { valid: false, message: "This invite code has expired." };
14033
+ }
14034
+ if (invite.currentUses >= invite.maxUses) {
14035
+ return { valid: false, message: "This invite code has reached its usage limit." };
14036
+ }
14037
+ return {
14038
+ valid: true,
14039
+ message: "Valid invite code.",
14040
+ code: invite.code,
14041
+ remainingUses: invite.maxUses - invite.currentUses
14042
+ };
14043
+ }
14044
+ async consumeCode(code, userId) {
14045
+ const validation = await this.validateCode(code);
14046
+ if (!validation.valid) {
14047
+ return { success: false, message: validation.message };
14048
+ }
14049
+ const normalized = normalizeBetaCode(code);
14050
+ const invite = this.codes.get(normalized);
14051
+ if (!invite) {
14052
+ return { success: false, message: "Invalid invite code." };
14053
+ }
14054
+ invite.currentUses += 1;
14055
+ this.testers.set(userId, {
14056
+ userId,
14057
+ inviteCode: invite.code,
14058
+ isBetaTester: true,
14059
+ betaJoinedAt: /* @__PURE__ */ new Date()
14060
+ });
14061
+ return { success: true, message: "Invite code consumed successfully." };
14062
+ }
14063
+ // ─────────────────────────────────────────────────────────────
14064
+ // User Tracking
14065
+ // ─────────────────────────────────────────────────────────────
14066
+ async isBetaTester(userId) {
14067
+ const tester = this.testers.get(userId);
14068
+ return tester?.isBetaTester ?? false;
14069
+ }
14070
+ async getBetaTester(userId) {
14071
+ return this.testers.get(userId) ?? null;
14072
+ }
14073
+ async listBetaTesters(options = {}) {
14074
+ const testers = Array.from(this.testers.values()).filter((t) => t.isBetaTester).sort((a, b) => b.betaJoinedAt.getTime() - a.betaJoinedAt.getTime());
14075
+ const offset = options.offset ?? 0;
14076
+ const limit = options.limit ?? 100;
14077
+ return testers.slice(offset, offset + limit);
14078
+ }
14079
+ // ─────────────────────────────────────────────────────────────
14080
+ // Analytics
14081
+ // ─────────────────────────────────────────────────────────────
14082
+ async getStats() {
14083
+ const codes = Array.from(this.codes.values());
14084
+ const activeCodes = codes.filter((c) => c.isActive);
14085
+ const testers = Array.from(this.testers.values()).filter((t) => t.isBetaTester);
14086
+ const joinDates = testers.map((t) => t.betaJoinedAt).sort((a, b) => a.getTime() - b.getTime());
14087
+ return {
14088
+ totalBetaTesters: testers.length,
14089
+ totalCodes: codes.length,
14090
+ activeCodes: activeCodes.length,
14091
+ totalUses: codes.reduce((sum, c) => sum + c.currentUses, 0),
14092
+ totalRemaining: activeCodes.reduce(
14093
+ (sum, c) => sum + (c.maxUses - c.currentUses),
14094
+ 0
14095
+ ),
14096
+ firstBetaSignup: joinDates[0] ?? null,
14097
+ latestBetaSignup: joinDates[joinDates.length - 1] ?? null
14098
+ };
14099
+ }
14100
+ async getCodeUsageReports() {
14101
+ return Array.from(this.codes.values()).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).map((c) => ({
14102
+ code: c.code,
14103
+ notes: c.notes,
14104
+ maxUses: c.maxUses,
14105
+ currentUses: c.currentUses,
14106
+ remainingUses: c.maxUses - c.currentUses,
14107
+ usagePercent: c.maxUses > 0 ? Math.round(c.currentUses / c.maxUses * 100) : 0,
14108
+ isActive: c.isActive,
14109
+ expiresAt: c.expiresAt,
14110
+ createdAt: c.createdAt
14111
+ }));
14112
+ }
14113
+ // ─────────────────────────────────────────────────────────────
14114
+ // Health
14115
+ // ─────────────────────────────────────────────────────────────
14116
+ async healthCheck() {
14117
+ return true;
14118
+ }
14119
+ // ─────────────────────────────────────────────────────────────
14120
+ // Testing Helpers
14121
+ // ─────────────────────────────────────────────────────────────
14122
+ /** Clear all data (for testing) */
14123
+ clear() {
14124
+ this.codes.clear();
14125
+ this.testers.clear();
14126
+ this.settings = {
14127
+ betaMode: this.config.defaultBetaMode,
14128
+ requireInviteCode: this.config.defaultRequireInviteCode,
14129
+ betaMessage: this.config.defaultBetaMessage
14130
+ };
14131
+ }
14132
+ /** Get the number of stored codes */
14133
+ get codeCount() {
14134
+ return this.codes.size;
14135
+ }
14136
+ /** Get the number of beta testers */
14137
+ get testerCount() {
14138
+ return this.testers.size;
14139
+ }
14140
+ };
14141
+
13896
14142
  // src/index.ts
13897
14143
  init_IAI();
13898
14144
  init_IRAG();
@@ -16353,8 +16599,8 @@ var MemoryRateLimiterStorage = class {
16353
16599
  const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
16354
16600
  this.cleanupInterval = setInterval(() => {
16355
16601
  const now = Date.now();
16356
- for (const [key, window] of this.windows) {
16357
- if (window.resetAt < now) {
16602
+ for (const [key, window2] of this.windows) {
16603
+ if (window2.resetAt < now) {
16358
16604
  this.windows.delete(key);
16359
16605
  }
16360
16606
  }
@@ -16365,20 +16611,20 @@ var MemoryRateLimiterStorage = class {
16365
16611
  }
16366
16612
  async increment(key, windowMs) {
16367
16613
  const now = Date.now();
16368
- let window = this.windows.get(key);
16369
- if (!window || window.resetAt < now) {
16370
- window = { count: 0, resetAt: now + windowMs };
16371
- this.windows.set(key, window);
16614
+ let window2 = this.windows.get(key);
16615
+ if (!window2 || window2.resetAt < now) {
16616
+ window2 = { count: 0, resetAt: now + windowMs };
16617
+ this.windows.set(key, window2);
16372
16618
  }
16373
- window.count++;
16374
- return [window.count, window.resetAt];
16619
+ window2.count++;
16620
+ return [window2.count, window2.resetAt];
16375
16621
  }
16376
16622
  async get(key) {
16377
- const window = this.windows.get(key);
16378
- if (!window || window.resetAt < Date.now()) {
16623
+ const window2 = this.windows.get(key);
16624
+ if (!window2 || window2.resetAt < Date.now()) {
16379
16625
  return null;
16380
16626
  }
16381
- return window;
16627
+ return window2;
16382
16628
  }
16383
16629
  /**
16384
16630
  * Clear all windows (for testing)
@@ -18638,6 +18884,119 @@ function createAuditLogger(options = {}) {
18638
18884
  return { log, createTimedAudit };
18639
18885
  }
18640
18886
 
18887
+ // src/auth/beta-client.ts
18888
+ var DEFAULT_CONFIG = {
18889
+ baseUrl: "",
18890
+ settingsEndpoint: "/api/beta-settings",
18891
+ validateEndpoint: "/api/validate-beta-code",
18892
+ storageKey: "beta_code",
18893
+ failSafeDefaults: {
18894
+ betaMode: true,
18895
+ requireInviteCode: true,
18896
+ betaMessage: ""
18897
+ }
18898
+ };
18899
+ function createBetaClient(config = {}) {
18900
+ const cfg = {
18901
+ ...DEFAULT_CONFIG,
18902
+ ...config,
18903
+ failSafeDefaults: {
18904
+ ...DEFAULT_CONFIG.failSafeDefaults,
18905
+ ...config.failSafeDefaults
18906
+ }
18907
+ };
18908
+ return {
18909
+ fetchSettings: () => fetchBetaSettings(cfg),
18910
+ validateCode: (code) => validateBetaCode(code, cfg),
18911
+ storeCode: (code) => storeBetaCode(code, cfg),
18912
+ getStoredCode: () => getStoredBetaCode(cfg),
18913
+ clearStoredCode: () => clearStoredBetaCode(cfg)
18914
+ };
18915
+ }
18916
+ async function fetchBetaSettings(config = {}) {
18917
+ const cfg = { ...DEFAULT_CONFIG, ...config };
18918
+ try {
18919
+ const response = await fetch(
18920
+ `${cfg.baseUrl}${cfg.settingsEndpoint}`,
18921
+ {
18922
+ method: "GET",
18923
+ headers: { "Content-Type": "application/json" },
18924
+ cache: "no-store"
18925
+ }
18926
+ );
18927
+ if (!response.ok) {
18928
+ throw new Error(`Failed to fetch beta settings: ${response.status}`);
18929
+ }
18930
+ const data = await response.json();
18931
+ return {
18932
+ betaMode: data.betaMode ?? cfg.failSafeDefaults.betaMode ?? true,
18933
+ requireInviteCode: data.requireInviteCode ?? cfg.failSafeDefaults.requireInviteCode ?? true,
18934
+ betaMessage: data.betaMessage ?? cfg.failSafeDefaults.betaMessage ?? ""
18935
+ };
18936
+ } catch (error) {
18937
+ console.error("Error fetching beta settings:", error);
18938
+ return {
18939
+ betaMode: cfg.failSafeDefaults.betaMode ?? true,
18940
+ requireInviteCode: cfg.failSafeDefaults.requireInviteCode ?? true,
18941
+ betaMessage: cfg.failSafeDefaults.betaMessage ?? ""
18942
+ };
18943
+ }
18944
+ }
18945
+ async function validateBetaCode(code, config = {}) {
18946
+ const cfg = { ...DEFAULT_CONFIG, ...config };
18947
+ if (!code || code.trim().length < 3) {
18948
+ return {
18949
+ valid: false,
18950
+ message: "Please enter a valid invite code."
18951
+ };
18952
+ }
18953
+ try {
18954
+ const response = await fetch(
18955
+ `${cfg.baseUrl}${cfg.validateEndpoint}`,
18956
+ {
18957
+ method: "POST",
18958
+ headers: { "Content-Type": "application/json" },
18959
+ body: JSON.stringify({ code: code.trim().toUpperCase() })
18960
+ }
18961
+ );
18962
+ if (response.status === 429) {
18963
+ return {
18964
+ valid: false,
18965
+ message: "Too many attempts. Please try again later."
18966
+ };
18967
+ }
18968
+ if (!response.ok) {
18969
+ throw new Error(`Validation request failed: ${response.status}`);
18970
+ }
18971
+ return await response.json();
18972
+ } catch (error) {
18973
+ console.error("Error validating invite code:", error);
18974
+ return {
18975
+ valid: false,
18976
+ message: "Unable to validate code. Please try again."
18977
+ };
18978
+ }
18979
+ }
18980
+ function storeBetaCode(code, config = {}) {
18981
+ const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
18982
+ if (typeof window !== "undefined") {
18983
+ sessionStorage.setItem(key, code.trim().toUpperCase());
18984
+ }
18985
+ }
18986
+ function getStoredBetaCode(config = {}) {
18987
+ const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
18988
+ if (typeof window !== "undefined") {
18989
+ return sessionStorage.getItem(key);
18990
+ }
18991
+ return null;
18992
+ }
18993
+ function clearStoredBetaCode(config = {}) {
18994
+ const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
18995
+ if (typeof window !== "undefined") {
18996
+ sessionStorage.removeItem(key);
18997
+ }
18998
+ }
18999
+
18641
19000
  // src/env.ts
18642
19001
  function getRequiredEnv(key) {
18643
19002
  const value = process.env[key];
@@ -30689,6 +31048,498 @@ function generateId2() {
30689
31048
  return randomBytes36(8).toString("hex") + Date.now().toString(36);
30690
31049
  }
30691
31050
 
31051
+ // src/adapters/postgres-beta/PostgresBeta.ts
31052
+ function mapRowToInvite(row) {
31053
+ return {
31054
+ id: row.id,
31055
+ code: row.code,
31056
+ maxUses: row.max_uses,
31057
+ currentUses: row.current_uses,
31058
+ expiresAt: row.expires_at ? new Date(row.expires_at) : null,
31059
+ createdBy: row.created_by,
31060
+ notes: row.notes ?? "",
31061
+ isActive: row.is_active,
31062
+ createdAt: new Date(row.created_at)
31063
+ };
31064
+ }
31065
+ function mapRowToTester(row) {
31066
+ return {
31067
+ userId: row.user_id,
31068
+ inviteCode: row.invite_code,
31069
+ isBetaTester: row.is_beta_tester,
31070
+ betaJoinedAt: new Date(row.beta_joined_at)
31071
+ };
31072
+ }
31073
+ var PostgresBeta = class {
31074
+ pool;
31075
+ schema;
31076
+ settingsSource;
31077
+ envPrefix;
31078
+ staticCodes;
31079
+ config;
31080
+ constructor(pgConfig) {
31081
+ this.pool = pgConfig.pool;
31082
+ this.schema = pgConfig.schema ?? "public";
31083
+ this.settingsSource = pgConfig.settingsSource ?? "database";
31084
+ this.envPrefix = pgConfig.envPrefix ?? "BETA";
31085
+ this.staticCodes = new Set(
31086
+ (pgConfig.staticCodes ?? []).map((c) => normalizeBetaCode(c))
31087
+ );
31088
+ this.config = {
31089
+ defaultBetaMode: pgConfig.defaultBetaMode ?? true,
31090
+ defaultRequireInviteCode: pgConfig.defaultRequireInviteCode ?? true,
31091
+ defaultBetaMessage: pgConfig.defaultBetaMessage ?? "We're in beta! Thanks for being an early tester.",
31092
+ codePrefix: pgConfig.codePrefix ?? "BETA",
31093
+ maxCodesPerBatch: pgConfig.maxCodesPerBatch ?? 50
31094
+ };
31095
+ }
31096
+ // ─────────────────────────────────────────────────────────────
31097
+ // Table helpers
31098
+ // ─────────────────────────────────────────────────────────────
31099
+ t(table) {
31100
+ return `${this.schema}.${table}`;
31101
+ }
31102
+ // ─────────────────────────────────────────────────────────────
31103
+ // Initialization
31104
+ // ─────────────────────────────────────────────────────────────
31105
+ /**
31106
+ * Initialize beta tables. Call once at app startup.
31107
+ * Creates tables if they don't exist and seeds default settings.
31108
+ */
31109
+ async initialize() {
31110
+ const client = await this.pool.connect();
31111
+ try {
31112
+ await client.query(`
31113
+ CREATE TABLE IF NOT EXISTS ${this.t("beta_settings")} (
31114
+ key VARCHAR(100) PRIMARY KEY,
31115
+ value TEXT NOT NULL,
31116
+ description TEXT,
31117
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
31118
+ updated_by VARCHAR(255)
31119
+ )
31120
+ `);
31121
+ await client.query(`
31122
+ INSERT INTO ${this.t("beta_settings")} (key, value, description) VALUES
31123
+ ('beta_mode', $1, 'Whether the app is in beta mode'),
31124
+ ('require_invite_code', $2, 'Whether an invite code is required to sign up'),
31125
+ ('beta_message', $3, 'Message displayed during beta')
31126
+ ON CONFLICT (key) DO NOTHING
31127
+ `, [
31128
+ String(this.config.defaultBetaMode),
31129
+ String(this.config.defaultRequireInviteCode),
31130
+ this.config.defaultBetaMessage
31131
+ ]);
31132
+ await client.query(`
31133
+ CREATE TABLE IF NOT EXISTS ${this.t("beta_invites")} (
31134
+ id VARCHAR(255) PRIMARY KEY,
31135
+ code VARCHAR(100) NOT NULL UNIQUE,
31136
+ max_uses INTEGER NOT NULL DEFAULT 1,
31137
+ current_uses INTEGER NOT NULL DEFAULT 0,
31138
+ expires_at TIMESTAMP WITH TIME ZONE,
31139
+ created_by VARCHAR(255) NOT NULL,
31140
+ notes TEXT DEFAULT '',
31141
+ is_active BOOLEAN NOT NULL DEFAULT true,
31142
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
31143
+ CONSTRAINT chk_beta_uses CHECK (current_uses <= max_uses)
31144
+ )
31145
+ `);
31146
+ await client.query(`
31147
+ CREATE INDEX IF NOT EXISTS idx_beta_invites_code_active
31148
+ ON ${this.t("beta_invites")}(code) WHERE is_active = true
31149
+ `);
31150
+ await client.query(`
31151
+ CREATE TABLE IF NOT EXISTS ${this.t("beta_testers")} (
31152
+ user_id VARCHAR(255) PRIMARY KEY,
31153
+ invite_code VARCHAR(100) NOT NULL,
31154
+ is_beta_tester BOOLEAN NOT NULL DEFAULT true,
31155
+ beta_joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
31156
+ )
31157
+ `);
31158
+ await client.query(`
31159
+ CREATE INDEX IF NOT EXISTS idx_beta_testers_active
31160
+ ON ${this.t("beta_testers")}(is_beta_tester, beta_joined_at DESC)
31161
+ WHERE is_beta_tester = true
31162
+ `);
31163
+ } finally {
31164
+ client.release();
31165
+ }
31166
+ }
31167
+ // ─────────────────────────────────────────────────────────────
31168
+ // Settings
31169
+ // ─────────────────────────────────────────────────────────────
31170
+ async getSettings() {
31171
+ if (this.settingsSource === "env") {
31172
+ return this.getEnvSettings();
31173
+ }
31174
+ const result = await this.pool.query(
31175
+ `SELECT key, value FROM ${this.t("beta_settings")} WHERE key IN ('beta_mode', 'require_invite_code', 'beta_message')`
31176
+ );
31177
+ const settings = {};
31178
+ for (const row of result.rows) {
31179
+ settings[row.key] = row.value;
31180
+ }
31181
+ return {
31182
+ betaMode: settings["beta_mode"] !== "false",
31183
+ requireInviteCode: settings["require_invite_code"] !== "false",
31184
+ betaMessage: settings["beta_message"] ?? this.config.defaultBetaMessage
31185
+ };
31186
+ }
31187
+ getEnvSettings() {
31188
+ const prefix = this.envPrefix;
31189
+ return {
31190
+ betaMode: process.env[`${prefix}_MODE`] !== "false",
31191
+ requireInviteCode: process.env[`${prefix}_REQUIRE_INVITE_CODE`] !== "false",
31192
+ betaMessage: process.env[`${prefix}_MESSAGE`] ?? this.config.defaultBetaMessage
31193
+ };
31194
+ }
31195
+ async updateSettings(options) {
31196
+ if (this.settingsSource === "env") {
31197
+ return;
31198
+ }
31199
+ const client = await this.pool.connect();
31200
+ try {
31201
+ const updates = [];
31202
+ if (options.betaMode !== void 0) {
31203
+ updates.push({ key: "beta_mode", value: String(options.betaMode) });
31204
+ }
31205
+ if (options.requireInviteCode !== void 0) {
31206
+ updates.push({ key: "require_invite_code", value: String(options.requireInviteCode) });
31207
+ }
31208
+ if (options.betaMessage !== void 0) {
31209
+ updates.push({ key: "beta_message", value: options.betaMessage });
31210
+ }
31211
+ for (const { key, value } of updates) {
31212
+ await client.query(
31213
+ `UPDATE ${this.t("beta_settings")} SET value = $1, updated_at = NOW(), updated_by = $2 WHERE key = $3`,
31214
+ [value, options.updatedBy ?? "system", key]
31215
+ );
31216
+ }
31217
+ } finally {
31218
+ client.release();
31219
+ }
31220
+ }
31221
+ // ─────────────────────────────────────────────────────────────
31222
+ // Code Management
31223
+ // ─────────────────────────────────────────────────────────────
31224
+ async createCodes(options) {
31225
+ const count = Math.min(options.count ?? 1, this.config.maxCodesPerBatch);
31226
+ const prefix = options.prefix ?? this.config.codePrefix;
31227
+ const results = [];
31228
+ const client = await this.pool.connect();
31229
+ try {
31230
+ for (let i = 0; i < count; i++) {
31231
+ const codeStr = options.code ? normalizeBetaCode(options.code) : generateBetaCode(prefix);
31232
+ const id = generateBetaId();
31233
+ let attempts = 0;
31234
+ let inserted = false;
31235
+ let currentCode = codeStr;
31236
+ while (!inserted && attempts < 5) {
31237
+ try {
31238
+ const result = await client.query(
31239
+ `INSERT INTO ${this.t("beta_invites")} (id, code, max_uses, expires_at, created_by, notes)
31240
+ VALUES ($1, $2, $3, $4, $5, $6)
31241
+ RETURNING *`,
31242
+ [
31243
+ id,
31244
+ currentCode,
31245
+ options.maxUses ?? 1,
31246
+ options.expiresAt ?? null,
31247
+ options.createdBy,
31248
+ options.notes ?? ""
31249
+ ]
31250
+ );
31251
+ const row = result.rows[0];
31252
+ if (row) results.push(mapRowToInvite(row));
31253
+ inserted = true;
31254
+ } catch (err) {
31255
+ const pgErr = err;
31256
+ if (pgErr.code === "23505" && !options.code) {
31257
+ currentCode = generateBetaCode(prefix);
31258
+ attempts++;
31259
+ } else {
31260
+ throw err;
31261
+ }
31262
+ }
31263
+ }
31264
+ if (!inserted) {
31265
+ throw new Error(`Failed to generate unique code after ${attempts} attempts`);
31266
+ }
31267
+ if (options.code) break;
31268
+ }
31269
+ } finally {
31270
+ client.release();
31271
+ }
31272
+ return results;
31273
+ }
31274
+ async listCodes(options = {}) {
31275
+ const conditions = [];
31276
+ const params = [];
31277
+ let paramIdx = 1;
31278
+ if (options.isActive !== void 0) {
31279
+ conditions.push(`is_active = $${paramIdx++}`);
31280
+ params.push(options.isActive);
31281
+ }
31282
+ if (options.status) {
31283
+ switch (options.status) {
31284
+ case "unused":
31285
+ conditions.push("is_active = true");
31286
+ conditions.push("current_uses = 0");
31287
+ break;
31288
+ case "partial":
31289
+ conditions.push("is_active = true");
31290
+ conditions.push("current_uses > 0");
31291
+ conditions.push("current_uses < max_uses");
31292
+ break;
31293
+ case "exhausted":
31294
+ conditions.push("current_uses >= max_uses");
31295
+ break;
31296
+ case "expired":
31297
+ conditions.push("expires_at IS NOT NULL");
31298
+ conditions.push("expires_at < NOW()");
31299
+ break;
31300
+ case "revoked":
31301
+ conditions.push("is_active = false");
31302
+ break;
31303
+ }
31304
+ }
31305
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
31306
+ const limit = options.limit ?? 100;
31307
+ const offset = options.offset ?? 0;
31308
+ const result = await this.pool.query(
31309
+ `SELECT * FROM ${this.t("beta_invites")} ${where}
31310
+ ORDER BY created_at DESC
31311
+ LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
31312
+ [...params, limit, offset]
31313
+ );
31314
+ return result.rows.map(mapRowToInvite);
31315
+ }
31316
+ async getCode(code) {
31317
+ const normalized = normalizeBetaCode(code);
31318
+ const result = await this.pool.query(
31319
+ `SELECT * FROM ${this.t("beta_invites")} WHERE code = $1`,
31320
+ [normalized]
31321
+ );
31322
+ const row = result.rows[0];
31323
+ if (!row) return null;
31324
+ return mapRowToInvite(row);
31325
+ }
31326
+ async revokeCode(code) {
31327
+ const normalized = normalizeBetaCode(code);
31328
+ await this.pool.query(
31329
+ `UPDATE ${this.t("beta_invites")} SET is_active = false WHERE code = $1`,
31330
+ [normalized]
31331
+ );
31332
+ }
31333
+ // ─────────────────────────────────────────────────────────────
31334
+ // Validation & Consumption
31335
+ // ─────────────────────────────────────────────────────────────
31336
+ async validateCode(code) {
31337
+ const settings = await this.getSettings();
31338
+ if (!settings.requireInviteCode) {
31339
+ return { valid: true, message: "Invite codes are not required." };
31340
+ }
31341
+ const normalized = normalizeBetaCode(code);
31342
+ if (this.staticCodes.has(normalized)) {
31343
+ return {
31344
+ valid: true,
31345
+ message: "Valid invite code.",
31346
+ code: normalized
31347
+ };
31348
+ }
31349
+ const result = await this.pool.query(
31350
+ `SELECT * FROM ${this.t("beta_invites")}
31351
+ WHERE code = $1
31352
+ AND is_active = true
31353
+ AND (expires_at IS NULL OR expires_at > NOW())
31354
+ AND current_uses < max_uses`,
31355
+ [normalized]
31356
+ );
31357
+ if (result.rows.length === 0) {
31358
+ const exists = await this.pool.query(
31359
+ `SELECT is_active, expires_at, current_uses, max_uses FROM ${this.t("beta_invites")} WHERE code = $1`,
31360
+ [normalized]
31361
+ );
31362
+ if (exists.rows.length === 0) {
31363
+ return { valid: false, message: "Invalid invite code." };
31364
+ }
31365
+ const existRow = exists.rows[0];
31366
+ if (!existRow) {
31367
+ return { valid: false, message: "Invalid invite code." };
31368
+ }
31369
+ if (!existRow.is_active) {
31370
+ return { valid: false, message: "This invite code has been revoked." };
31371
+ }
31372
+ if (existRow.expires_at && new Date(existRow.expires_at) < /* @__PURE__ */ new Date()) {
31373
+ return { valid: false, message: "This invite code has expired." };
31374
+ }
31375
+ if (existRow.current_uses >= existRow.max_uses) {
31376
+ return { valid: false, message: "This invite code has reached its usage limit." };
31377
+ }
31378
+ return { valid: false, message: "Invalid invite code." };
31379
+ }
31380
+ const invite = result.rows[0];
31381
+ if (!invite) {
31382
+ return { valid: false, message: "Invalid invite code." };
31383
+ }
31384
+ return {
31385
+ valid: true,
31386
+ message: "Valid invite code.",
31387
+ code: invite.code,
31388
+ remainingUses: invite.max_uses - invite.current_uses
31389
+ };
31390
+ }
31391
+ async consumeCode(code, userId) {
31392
+ const normalized = normalizeBetaCode(code);
31393
+ if (this.staticCodes.has(normalized)) {
31394
+ await this.tagBetaTester(userId, normalized);
31395
+ return { success: true, message: "Invite code consumed successfully." };
31396
+ }
31397
+ const client = await this.pool.connect();
31398
+ try {
31399
+ await client.query("BEGIN");
31400
+ const result = await client.query(
31401
+ `SELECT * FROM ${this.t("beta_invites")}
31402
+ WHERE code = $1
31403
+ AND is_active = true
31404
+ AND (expires_at IS NULL OR expires_at > NOW())
31405
+ AND current_uses < max_uses
31406
+ FOR UPDATE`,
31407
+ [normalized]
31408
+ );
31409
+ if (result.rows.length === 0) {
31410
+ await client.query("ROLLBACK");
31411
+ return { success: false, message: "Invalid or exhausted invite code." };
31412
+ }
31413
+ await client.query(
31414
+ `UPDATE ${this.t("beta_invites")} SET current_uses = current_uses + 1 WHERE code = $1`,
31415
+ [normalized]
31416
+ );
31417
+ await client.query(
31418
+ `INSERT INTO ${this.t("beta_testers")} (user_id, invite_code, is_beta_tester, beta_joined_at)
31419
+ VALUES ($1, $2, true, NOW())
31420
+ ON CONFLICT (user_id) DO UPDATE SET invite_code = $2, is_beta_tester = true, beta_joined_at = NOW()`,
31421
+ [userId, normalized]
31422
+ );
31423
+ await client.query("COMMIT");
31424
+ return { success: true, message: "Invite code consumed successfully." };
31425
+ } catch (error) {
31426
+ await client.query("ROLLBACK");
31427
+ throw error;
31428
+ } finally {
31429
+ client.release();
31430
+ }
31431
+ }
31432
+ async tagBetaTester(userId, code) {
31433
+ await this.pool.query(
31434
+ `INSERT INTO ${this.t("beta_testers")} (user_id, invite_code, is_beta_tester, beta_joined_at)
31435
+ VALUES ($1, $2, true, NOW())
31436
+ ON CONFLICT (user_id) DO UPDATE SET invite_code = $2, is_beta_tester = true, beta_joined_at = NOW()`,
31437
+ [userId, code]
31438
+ );
31439
+ }
31440
+ // ─────────────────────────────────────────────────────────────
31441
+ // User Tracking
31442
+ // ─────────────────────────────────────────────────────────────
31443
+ async isBetaTester(userId) {
31444
+ const result = await this.pool.query(
31445
+ `SELECT is_beta_tester FROM ${this.t("beta_testers")} WHERE user_id = $1 AND is_beta_tester = true`,
31446
+ [userId]
31447
+ );
31448
+ return result.rows.length > 0;
31449
+ }
31450
+ async getBetaTester(userId) {
31451
+ const result = await this.pool.query(
31452
+ `SELECT * FROM ${this.t("beta_testers")} WHERE user_id = $1`,
31453
+ [userId]
31454
+ );
31455
+ const row = result.rows[0];
31456
+ if (!row) return null;
31457
+ return mapRowToTester(row);
31458
+ }
31459
+ async listBetaTesters(options = {}) {
31460
+ const limit = options.limit ?? 100;
31461
+ const offset = options.offset ?? 0;
31462
+ const result = await this.pool.query(
31463
+ `SELECT * FROM ${this.t("beta_testers")}
31464
+ WHERE is_beta_tester = true
31465
+ ORDER BY beta_joined_at DESC
31466
+ LIMIT $1 OFFSET $2`,
31467
+ [limit, offset]
31468
+ );
31469
+ return result.rows.map(mapRowToTester);
31470
+ }
31471
+ // ─────────────────────────────────────────────────────────────
31472
+ // Analytics
31473
+ // ─────────────────────────────────────────────────────────────
31474
+ async getStats() {
31475
+ const [testersResult, codesResult] = await Promise.all([
31476
+ this.pool.query(
31477
+ `SELECT
31478
+ COUNT(*) as total,
31479
+ MIN(beta_joined_at) as first_signup,
31480
+ MAX(beta_joined_at) as latest_signup
31481
+ FROM ${this.t("beta_testers")}
31482
+ WHERE is_beta_tester = true`
31483
+ ),
31484
+ this.pool.query(
31485
+ `SELECT
31486
+ COUNT(*) as total_codes,
31487
+ COUNT(*) FILTER (WHERE is_active = true) as active_codes,
31488
+ COALESCE(SUM(current_uses), 0) as total_uses,
31489
+ COALESCE(SUM(CASE WHEN is_active = true THEN max_uses - current_uses ELSE 0 END), 0) as total_remaining
31490
+ FROM ${this.t("beta_invites")}`
31491
+ )
31492
+ ]);
31493
+ const tRow = testersResult.rows[0] ?? {};
31494
+ const cRow = codesResult.rows[0] ?? {};
31495
+ return {
31496
+ totalBetaTesters: Number(tRow.total ?? 0),
31497
+ totalCodes: Number(cRow.total_codes ?? 0),
31498
+ activeCodes: Number(cRow.active_codes ?? 0),
31499
+ totalUses: Number(cRow.total_uses ?? 0),
31500
+ totalRemaining: Number(cRow.total_remaining ?? 0),
31501
+ firstBetaSignup: tRow.first_signup ? new Date(tRow.first_signup) : null,
31502
+ latestBetaSignup: tRow.latest_signup ? new Date(tRow.latest_signup) : null
31503
+ };
31504
+ }
31505
+ async getCodeUsageReports() {
31506
+ const result = await this.pool.query(
31507
+ `SELECT
31508
+ code, notes, max_uses, current_uses,
31509
+ (max_uses - current_uses) as remaining_uses,
31510
+ CASE WHEN max_uses > 0
31511
+ THEN ROUND((current_uses::numeric / max_uses) * 100)
31512
+ ELSE 0
31513
+ END as usage_percent,
31514
+ is_active, expires_at, created_at
31515
+ FROM ${this.t("beta_invites")}
31516
+ ORDER BY created_at DESC`
31517
+ );
31518
+ return result.rows.map((row) => ({
31519
+ code: row.code,
31520
+ notes: row.notes ?? "",
31521
+ maxUses: row.max_uses,
31522
+ currentUses: row.current_uses,
31523
+ remainingUses: Number(row.remaining_uses ?? 0),
31524
+ usagePercent: Number(row.usage_percent ?? 0),
31525
+ isActive: row.is_active,
31526
+ expiresAt: row.expires_at ? new Date(row.expires_at) : null,
31527
+ createdAt: new Date(row.created_at)
31528
+ }));
31529
+ }
31530
+ // ─────────────────────────────────────────────────────────────
31531
+ // Health
31532
+ // ─────────────────────────────────────────────────────────────
31533
+ async healthCheck() {
31534
+ try {
31535
+ await this.pool.query("SELECT 1");
31536
+ return true;
31537
+ } catch {
31538
+ return false;
31539
+ }
31540
+ }
31541
+ };
31542
+
30692
31543
  // src/app-logger.ts
30693
31544
  var LEVEL_PRIORITY2 = {
30694
31545
  debug: 0,
@@ -30865,7 +31716,7 @@ async function closeSharedRedis() {
30865
31716
  }
30866
31717
 
30867
31718
  // src/migrations/Migrator.ts
30868
- var DEFAULT_CONFIG = {
31719
+ var DEFAULT_CONFIG2 = {
30869
31720
  tableName: "_migrations",
30870
31721
  schema: "public",
30871
31722
  lockTimeout: 60,
@@ -30892,7 +31743,7 @@ var Migrator = class {
30892
31743
  locked = false;
30893
31744
  constructor(db, config = {}) {
30894
31745
  this.db = db;
30895
- this.config = { ...DEFAULT_CONFIG, ...config };
31746
+ this.config = { ...DEFAULT_CONFIG2, ...config };
30896
31747
  }
30897
31748
  /**
30898
31749
  * Get the fully qualified migration table name
@@ -31588,6 +32439,67 @@ var createSsoSessionsTable = {
31588
32439
  `,
31589
32440
  down: "DROP TABLE IF EXISTS sso_sessions CASCADE"
31590
32441
  };
32442
+ var createBetaSettingsTable = {
32443
+ version: "20241217_009",
32444
+ name: "create_beta_settings_table",
32445
+ up: `
32446
+ CREATE TABLE IF NOT EXISTS beta_settings (
32447
+ key VARCHAR(100) PRIMARY KEY,
32448
+ value TEXT NOT NULL,
32449
+ description TEXT,
32450
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
32451
+ updated_by VARCHAR(255)
32452
+ );
32453
+
32454
+ INSERT INTO beta_settings (key, value, description) VALUES
32455
+ ('beta_mode', 'true', 'Whether the app is in beta mode'),
32456
+ ('require_invite_code', 'true', 'Whether an invite code is required to sign up'),
32457
+ ('beta_message', 'We''re in beta! Thanks for being an early tester.', 'Message displayed during beta')
32458
+ ON CONFLICT (key) DO NOTHING;
32459
+ `,
32460
+ down: "DROP TABLE IF EXISTS beta_settings CASCADE"
32461
+ };
32462
+ var createBetaInvitesTable = {
32463
+ version: "20241217_010",
32464
+ name: "create_beta_invites_table",
32465
+ up: `
32466
+ CREATE TABLE IF NOT EXISTS beta_invites (
32467
+ id VARCHAR(255) PRIMARY KEY,
32468
+ code VARCHAR(100) NOT NULL UNIQUE,
32469
+ max_uses INTEGER NOT NULL DEFAULT 1,
32470
+ current_uses INTEGER NOT NULL DEFAULT 0,
32471
+ expires_at TIMESTAMP WITH TIME ZONE,
32472
+ created_by VARCHAR(255) NOT NULL,
32473
+ notes TEXT DEFAULT '',
32474
+ is_active BOOLEAN NOT NULL DEFAULT true,
32475
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
32476
+ CONSTRAINT chk_beta_uses CHECK (current_uses <= max_uses)
32477
+ );
32478
+
32479
+ CREATE INDEX IF NOT EXISTS idx_beta_invites_code_active
32480
+ ON beta_invites(code) WHERE is_active = true;
32481
+ CREATE INDEX IF NOT EXISTS idx_beta_invites_active
32482
+ ON beta_invites(is_active, created_at DESC);
32483
+ `,
32484
+ down: "DROP TABLE IF EXISTS beta_invites CASCADE"
32485
+ };
32486
+ var createBetaTestersTable = {
32487
+ version: "20241217_011",
32488
+ name: "create_beta_testers_table",
32489
+ up: `
32490
+ CREATE TABLE IF NOT EXISTS beta_testers (
32491
+ user_id VARCHAR(255) PRIMARY KEY,
32492
+ invite_code VARCHAR(100) NOT NULL REFERENCES beta_invites(code) ON DELETE SET NULL,
32493
+ is_beta_tester BOOLEAN NOT NULL DEFAULT true,
32494
+ beta_joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
32495
+ );
32496
+
32497
+ CREATE INDEX IF NOT EXISTS idx_beta_testers_active
32498
+ ON beta_testers(is_beta_tester, beta_joined_at DESC)
32499
+ WHERE is_beta_tester = true;
32500
+ `,
32501
+ down: "DROP TABLE IF EXISTS beta_testers CASCADE"
32502
+ };
31591
32503
  var enterpriseMigrations = [
31592
32504
  createSsoOidcConfigsTable,
31593
32505
  createDomainVerificationsTable,
@@ -31596,7 +32508,10 @@ var enterpriseMigrations = [
31596
32508
  createTenantMembersTable,
31597
32509
  createTenantInvitationsTable,
31598
32510
  createTenantUsageTable,
31599
- createSsoSessionsTable
32511
+ createSsoSessionsTable,
32512
+ createBetaSettingsTable,
32513
+ createBetaInvitesTable,
32514
+ createBetaTestersTable
31600
32515
  ];
31601
32516
  function getEnterpriseMigrations(features) {
31602
32517
  const migrations = [];
@@ -31616,6 +32531,13 @@ function getEnterpriseMigrations(features) {
31616
32531
  createTenantUsageTable
31617
32532
  );
31618
32533
  }
32534
+ if (features.beta) {
32535
+ migrations.push(
32536
+ createBetaSettingsTable,
32537
+ createBetaInvitesTable,
32538
+ createBetaTestersTable
32539
+ );
32540
+ }
31619
32541
  return migrations;
31620
32542
  }
31621
32543
  export {
@@ -31677,6 +32599,7 @@ export {
31677
32599
  MemoryAuditLog,
31678
32600
  MemoryAuth,
31679
32601
  MemoryAuthSSO,
32602
+ MemoryBeta,
31680
32603
  MemoryBilling,
31681
32604
  MemoryCache,
31682
32605
  MemoryCompliance,
@@ -31717,6 +32640,7 @@ export {
31717
32640
  PhoneSchema,
31718
32641
  PineconeRAG,
31719
32642
  PlatformConfigSchema,
32643
+ PostgresBeta,
31720
32644
  PostgresDatabase,
31721
32645
  PostgresTenant,
31722
32646
  QueueConfigSchema,
@@ -31772,6 +32696,7 @@ export {
31772
32696
  checkEnvVars,
31773
32697
  checkRateLimit,
31774
32698
  classifyError,
32699
+ clearStoredBetaCode,
31775
32700
  closeSharedRedis,
31776
32701
  composeHookRegistries,
31777
32702
  constantTimeEqual,
@@ -31784,6 +32709,10 @@ export {
31784
32709
  createAuditActor,
31785
32710
  createAuditLogger,
31786
32711
  createAuthError,
32712
+ createBetaClient,
32713
+ createBetaInvitesTable,
32714
+ createBetaSettingsTable,
32715
+ createBetaTestersTable,
31787
32716
  createBulkhead,
31788
32717
  createCacheMiddleware,
31789
32718
  createCachedFallback,
@@ -31842,9 +32771,12 @@ export {
31842
32771
  extractAuditRequestId,
31843
32772
  extractAuditUserAgent,
31844
32773
  extractClientIp,
32774
+ fetchBetaSettings,
31845
32775
  filterChannelsByPreferences,
31846
32776
  formatAmount,
31847
32777
  generateAuditId,
32778
+ generateBetaCode,
32779
+ generateBetaId,
31848
32780
  generateChecksum,
31849
32781
  generateDeliveryId,
31850
32782
  generateErrorId,
@@ -31874,6 +32806,7 @@ export {
31874
32806
  getRequestId,
31875
32807
  getRequiredEnv,
31876
32808
  getSharedRedis,
32809
+ getStoredBetaCode,
31877
32810
  getTenantId,
31878
32811
  getTokenEndpoint,
31879
32812
  getTraceId,
@@ -31893,6 +32826,7 @@ export {
31893
32826
  loadConfig,
31894
32827
  matchAction,
31895
32828
  matchEventType,
32829
+ normalizeBetaCode,
31896
32830
  parseKeycloakRoles,
31897
32831
  raceTimeout,
31898
32832
  refreshKeycloakToken,
@@ -31906,9 +32840,11 @@ export {
31906
32840
  sanitizeApiError,
31907
32841
  sanitizeForEmail,
31908
32842
  sqlMigration,
32843
+ storeBetaCode,
31909
32844
  stripHtml,
31910
32845
  timedHealthCheck,
31911
32846
  toHealthCheckResult,
32847
+ validateBetaCode,
31912
32848
  validateConfig,
31913
32849
  validateEnvVars,
31914
32850
  withCorrelation,