@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/auth.d.mts +174 -1
- package/dist/auth.d.ts +174 -1
- package/dist/auth.js +233 -3
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +220 -2
- package/dist/auth.mjs.map +1 -1
- package/dist/{index-CkyVz0hQ.d.mts → env-jqNJdZVt.d.mts} +360 -2
- package/dist/{index-CkyVz0hQ.d.ts → env-jqNJdZVt.d.ts} +360 -2
- package/dist/{index-CepDdu7h.d.mts → index-DzQ0Js5Z.d.mts} +13 -1
- package/dist/{index-CepDdu7h.d.ts → index-DzQ0Js5Z.d.ts} +13 -1
- package/dist/index.d.mts +98 -3
- package/dist/index.d.ts +98 -3
- package/dist/index.js +969 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +955 -15
- package/dist/index.mjs.map +1 -1
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +72 -1
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/index.mjs +72 -1
- package/dist/migrations/index.mjs.map +1 -1
- package/package.json +1 -1
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,
|
|
16357
|
-
if (
|
|
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
|
|
16369
|
-
if (!
|
|
16370
|
-
|
|
16371
|
-
this.windows.set(key,
|
|
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
|
-
|
|
16374
|
-
return [
|
|
16619
|
+
window2.count++;
|
|
16620
|
+
return [window2.count, window2.resetAt];
|
|
16375
16621
|
}
|
|
16376
16622
|
async get(key) {
|
|
16377
|
-
const
|
|
16378
|
-
if (!
|
|
16623
|
+
const window2 = this.windows.get(key);
|
|
16624
|
+
if (!window2 || window2.resetAt < Date.now()) {
|
|
16379
16625
|
return null;
|
|
16380
16626
|
}
|
|
16381
|
-
return
|
|
16627
|
+
return window2;
|
|
16382
16628
|
}
|
|
16383
16629
|
/**
|
|
16384
16630
|
* Clear all windows (for testing)
|
|
@@ -17965,7 +18211,11 @@ function buildKeycloakCallbacks(config) {
|
|
|
17965
18211
|
* Compatible with Auth.js v5 JWT callback signature.
|
|
17966
18212
|
*/
|
|
17967
18213
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17968
|
-
async jwt({
|
|
18214
|
+
async jwt({
|
|
18215
|
+
token,
|
|
18216
|
+
user,
|
|
18217
|
+
account
|
|
18218
|
+
}) {
|
|
17969
18219
|
if (user) {
|
|
17970
18220
|
token.id = token.sub ?? user.id;
|
|
17971
18221
|
}
|
|
@@ -18634,6 +18884,119 @@ function createAuditLogger(options = {}) {
|
|
|
18634
18884
|
return { log, createTimedAudit };
|
|
18635
18885
|
}
|
|
18636
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
|
+
|
|
18637
19000
|
// src/env.ts
|
|
18638
19001
|
function getRequiredEnv(key) {
|
|
18639
19002
|
const value = process.env[key];
|
|
@@ -30685,6 +31048,498 @@ function generateId2() {
|
|
|
30685
31048
|
return randomBytes36(8).toString("hex") + Date.now().toString(36);
|
|
30686
31049
|
}
|
|
30687
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
|
+
|
|
30688
31543
|
// src/app-logger.ts
|
|
30689
31544
|
var LEVEL_PRIORITY2 = {
|
|
30690
31545
|
debug: 0,
|
|
@@ -30861,7 +31716,7 @@ async function closeSharedRedis() {
|
|
|
30861
31716
|
}
|
|
30862
31717
|
|
|
30863
31718
|
// src/migrations/Migrator.ts
|
|
30864
|
-
var
|
|
31719
|
+
var DEFAULT_CONFIG2 = {
|
|
30865
31720
|
tableName: "_migrations",
|
|
30866
31721
|
schema: "public",
|
|
30867
31722
|
lockTimeout: 60,
|
|
@@ -30888,7 +31743,7 @@ var Migrator = class {
|
|
|
30888
31743
|
locked = false;
|
|
30889
31744
|
constructor(db, config = {}) {
|
|
30890
31745
|
this.db = db;
|
|
30891
|
-
this.config = { ...
|
|
31746
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
30892
31747
|
}
|
|
30893
31748
|
/**
|
|
30894
31749
|
* Get the fully qualified migration table name
|
|
@@ -31584,6 +32439,67 @@ var createSsoSessionsTable = {
|
|
|
31584
32439
|
`,
|
|
31585
32440
|
down: "DROP TABLE IF EXISTS sso_sessions CASCADE"
|
|
31586
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
|
+
};
|
|
31587
32503
|
var enterpriseMigrations = [
|
|
31588
32504
|
createSsoOidcConfigsTable,
|
|
31589
32505
|
createDomainVerificationsTable,
|
|
@@ -31592,7 +32508,10 @@ var enterpriseMigrations = [
|
|
|
31592
32508
|
createTenantMembersTable,
|
|
31593
32509
|
createTenantInvitationsTable,
|
|
31594
32510
|
createTenantUsageTable,
|
|
31595
|
-
createSsoSessionsTable
|
|
32511
|
+
createSsoSessionsTable,
|
|
32512
|
+
createBetaSettingsTable,
|
|
32513
|
+
createBetaInvitesTable,
|
|
32514
|
+
createBetaTestersTable
|
|
31596
32515
|
];
|
|
31597
32516
|
function getEnterpriseMigrations(features) {
|
|
31598
32517
|
const migrations = [];
|
|
@@ -31612,6 +32531,13 @@ function getEnterpriseMigrations(features) {
|
|
|
31612
32531
|
createTenantUsageTable
|
|
31613
32532
|
);
|
|
31614
32533
|
}
|
|
32534
|
+
if (features.beta) {
|
|
32535
|
+
migrations.push(
|
|
32536
|
+
createBetaSettingsTable,
|
|
32537
|
+
createBetaInvitesTable,
|
|
32538
|
+
createBetaTestersTable
|
|
32539
|
+
);
|
|
32540
|
+
}
|
|
31615
32541
|
return migrations;
|
|
31616
32542
|
}
|
|
31617
32543
|
export {
|
|
@@ -31673,6 +32599,7 @@ export {
|
|
|
31673
32599
|
MemoryAuditLog,
|
|
31674
32600
|
MemoryAuth,
|
|
31675
32601
|
MemoryAuthSSO,
|
|
32602
|
+
MemoryBeta,
|
|
31676
32603
|
MemoryBilling,
|
|
31677
32604
|
MemoryCache,
|
|
31678
32605
|
MemoryCompliance,
|
|
@@ -31713,6 +32640,7 @@ export {
|
|
|
31713
32640
|
PhoneSchema,
|
|
31714
32641
|
PineconeRAG,
|
|
31715
32642
|
PlatformConfigSchema,
|
|
32643
|
+
PostgresBeta,
|
|
31716
32644
|
PostgresDatabase,
|
|
31717
32645
|
PostgresTenant,
|
|
31718
32646
|
QueueConfigSchema,
|
|
@@ -31768,6 +32696,7 @@ export {
|
|
|
31768
32696
|
checkEnvVars,
|
|
31769
32697
|
checkRateLimit,
|
|
31770
32698
|
classifyError,
|
|
32699
|
+
clearStoredBetaCode,
|
|
31771
32700
|
closeSharedRedis,
|
|
31772
32701
|
composeHookRegistries,
|
|
31773
32702
|
constantTimeEqual,
|
|
@@ -31780,6 +32709,10 @@ export {
|
|
|
31780
32709
|
createAuditActor,
|
|
31781
32710
|
createAuditLogger,
|
|
31782
32711
|
createAuthError,
|
|
32712
|
+
createBetaClient,
|
|
32713
|
+
createBetaInvitesTable,
|
|
32714
|
+
createBetaSettingsTable,
|
|
32715
|
+
createBetaTestersTable,
|
|
31783
32716
|
createBulkhead,
|
|
31784
32717
|
createCacheMiddleware,
|
|
31785
32718
|
createCachedFallback,
|
|
@@ -31838,9 +32771,12 @@ export {
|
|
|
31838
32771
|
extractAuditRequestId,
|
|
31839
32772
|
extractAuditUserAgent,
|
|
31840
32773
|
extractClientIp,
|
|
32774
|
+
fetchBetaSettings,
|
|
31841
32775
|
filterChannelsByPreferences,
|
|
31842
32776
|
formatAmount,
|
|
31843
32777
|
generateAuditId,
|
|
32778
|
+
generateBetaCode,
|
|
32779
|
+
generateBetaId,
|
|
31844
32780
|
generateChecksum,
|
|
31845
32781
|
generateDeliveryId,
|
|
31846
32782
|
generateErrorId,
|
|
@@ -31870,6 +32806,7 @@ export {
|
|
|
31870
32806
|
getRequestId,
|
|
31871
32807
|
getRequiredEnv,
|
|
31872
32808
|
getSharedRedis,
|
|
32809
|
+
getStoredBetaCode,
|
|
31873
32810
|
getTenantId,
|
|
31874
32811
|
getTokenEndpoint,
|
|
31875
32812
|
getTraceId,
|
|
@@ -31889,6 +32826,7 @@ export {
|
|
|
31889
32826
|
loadConfig,
|
|
31890
32827
|
matchAction,
|
|
31891
32828
|
matchEventType,
|
|
32829
|
+
normalizeBetaCode,
|
|
31892
32830
|
parseKeycloakRoles,
|
|
31893
32831
|
raceTimeout,
|
|
31894
32832
|
refreshKeycloakToken,
|
|
@@ -31902,9 +32840,11 @@ export {
|
|
|
31902
32840
|
sanitizeApiError,
|
|
31903
32841
|
sanitizeForEmail,
|
|
31904
32842
|
sqlMigration,
|
|
32843
|
+
storeBetaCode,
|
|
31905
32844
|
stripHtml,
|
|
31906
32845
|
timedHealthCheck,
|
|
31907
32846
|
toHealthCheckResult,
|
|
32847
|
+
validateBetaCode,
|
|
31908
32848
|
validateConfig,
|
|
31909
32849
|
validateEnvVars,
|
|
31910
32850
|
withCorrelation,
|