@digilogiclabs/platform-core 1.7.0 → 1.9.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 +3 -2
- package/dist/auth.d.ts +3 -2
- package/dist/auth.js +119 -0
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +113 -0
- package/dist/auth.mjs.map +1 -1
- package/dist/email-templates.d.mts +210 -0
- package/dist/email-templates.d.ts +210 -0
- package/dist/email-templates.js +338 -0
- package/dist/email-templates.js.map +1 -0
- package/dist/email-templates.mjs +297 -0
- package/dist/email-templates.mjs.map +1 -0
- package/dist/{env-DerQ7Da-.d.mts → env-DHPZR3Lv.d.mts} +345 -74
- package/dist/{env-DerQ7Da-.d.ts → env-DHPZR3Lv.d.ts} +345 -74
- 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 +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.js +974 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +960 -14
- 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/dist/security-BvLXaQkv.d.mts +88 -0
- package/dist/security-BvLXaQkv.d.ts +88 -0
- package/package.json +6 -1
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,257 @@ 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 {
|
|
14367
|
+
valid: false,
|
|
14368
|
+
message: "This invite code has reached its usage limit."
|
|
14369
|
+
};
|
|
14370
|
+
}
|
|
14371
|
+
return {
|
|
14372
|
+
valid: true,
|
|
14373
|
+
message: "Valid invite code.",
|
|
14374
|
+
code: invite.code,
|
|
14375
|
+
remainingUses: invite.maxUses - invite.currentUses
|
|
14376
|
+
};
|
|
14377
|
+
}
|
|
14378
|
+
async consumeCode(code, userId) {
|
|
14379
|
+
const validation = await this.validateCode(code);
|
|
14380
|
+
if (!validation.valid) {
|
|
14381
|
+
return { success: false, message: validation.message };
|
|
14382
|
+
}
|
|
14383
|
+
const normalized = normalizeBetaCode(code);
|
|
14384
|
+
const invite = this.codes.get(normalized);
|
|
14385
|
+
if (!invite) {
|
|
14386
|
+
return { success: false, message: "Invalid invite code." };
|
|
14387
|
+
}
|
|
14388
|
+
invite.currentUses += 1;
|
|
14389
|
+
this.testers.set(userId, {
|
|
14390
|
+
userId,
|
|
14391
|
+
inviteCode: invite.code,
|
|
14392
|
+
isBetaTester: true,
|
|
14393
|
+
betaJoinedAt: /* @__PURE__ */ new Date()
|
|
14394
|
+
});
|
|
14395
|
+
return { success: true, message: "Invite code consumed successfully." };
|
|
14396
|
+
}
|
|
14397
|
+
// ─────────────────────────────────────────────────────────────
|
|
14398
|
+
// User Tracking
|
|
14399
|
+
// ─────────────────────────────────────────────────────────────
|
|
14400
|
+
async isBetaTester(userId) {
|
|
14401
|
+
const tester = this.testers.get(userId);
|
|
14402
|
+
return tester?.isBetaTester ?? false;
|
|
14403
|
+
}
|
|
14404
|
+
async getBetaTester(userId) {
|
|
14405
|
+
return this.testers.get(userId) ?? null;
|
|
14406
|
+
}
|
|
14407
|
+
async listBetaTesters(options = {}) {
|
|
14408
|
+
const testers = Array.from(this.testers.values()).filter((t) => t.isBetaTester).sort((a, b) => b.betaJoinedAt.getTime() - a.betaJoinedAt.getTime());
|
|
14409
|
+
const offset = options.offset ?? 0;
|
|
14410
|
+
const limit = options.limit ?? 100;
|
|
14411
|
+
return testers.slice(offset, offset + limit);
|
|
14412
|
+
}
|
|
14413
|
+
// ─────────────────────────────────────────────────────────────
|
|
14414
|
+
// Analytics
|
|
14415
|
+
// ─────────────────────────────────────────────────────────────
|
|
14416
|
+
async getStats() {
|
|
14417
|
+
const codes = Array.from(this.codes.values());
|
|
14418
|
+
const activeCodes = codes.filter((c) => c.isActive);
|
|
14419
|
+
const testers = Array.from(this.testers.values()).filter(
|
|
14420
|
+
(t) => t.isBetaTester
|
|
14421
|
+
);
|
|
14422
|
+
const joinDates = testers.map((t) => t.betaJoinedAt).sort((a, b) => a.getTime() - b.getTime());
|
|
14423
|
+
return {
|
|
14424
|
+
totalBetaTesters: testers.length,
|
|
14425
|
+
totalCodes: codes.length,
|
|
14426
|
+
activeCodes: activeCodes.length,
|
|
14427
|
+
totalUses: codes.reduce((sum, c) => sum + c.currentUses, 0),
|
|
14428
|
+
totalRemaining: activeCodes.reduce(
|
|
14429
|
+
(sum, c) => sum + (c.maxUses - c.currentUses),
|
|
14430
|
+
0
|
|
14431
|
+
),
|
|
14432
|
+
firstBetaSignup: joinDates[0] ?? null,
|
|
14433
|
+
latestBetaSignup: joinDates[joinDates.length - 1] ?? null
|
|
14434
|
+
};
|
|
14435
|
+
}
|
|
14436
|
+
async getCodeUsageReports() {
|
|
14437
|
+
return Array.from(this.codes.values()).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).map((c) => ({
|
|
14438
|
+
code: c.code,
|
|
14439
|
+
notes: c.notes,
|
|
14440
|
+
maxUses: c.maxUses,
|
|
14441
|
+
currentUses: c.currentUses,
|
|
14442
|
+
remainingUses: c.maxUses - c.currentUses,
|
|
14443
|
+
usagePercent: c.maxUses > 0 ? Math.round(c.currentUses / c.maxUses * 100) : 0,
|
|
14444
|
+
isActive: c.isActive,
|
|
14445
|
+
expiresAt: c.expiresAt,
|
|
14446
|
+
createdAt: c.createdAt
|
|
14447
|
+
}));
|
|
14448
|
+
}
|
|
14449
|
+
// ─────────────────────────────────────────────────────────────
|
|
14450
|
+
// Health
|
|
14451
|
+
// ─────────────────────────────────────────────────────────────
|
|
14452
|
+
async healthCheck() {
|
|
14453
|
+
return true;
|
|
14454
|
+
}
|
|
14455
|
+
// ─────────────────────────────────────────────────────────────
|
|
14456
|
+
// Testing Helpers
|
|
14457
|
+
// ─────────────────────────────────────────────────────────────
|
|
14458
|
+
/** Clear all data (for testing) */
|
|
14459
|
+
clear() {
|
|
14460
|
+
this.codes.clear();
|
|
14461
|
+
this.testers.clear();
|
|
14462
|
+
this.settings = {
|
|
14463
|
+
betaMode: this.config.defaultBetaMode,
|
|
14464
|
+
requireInviteCode: this.config.defaultRequireInviteCode,
|
|
14465
|
+
betaMessage: this.config.defaultBetaMessage
|
|
14466
|
+
};
|
|
14467
|
+
}
|
|
14468
|
+
/** Get the number of stored codes */
|
|
14469
|
+
get codeCount() {
|
|
14470
|
+
return this.codes.size;
|
|
14471
|
+
}
|
|
14472
|
+
/** Get the number of beta testers */
|
|
14473
|
+
get testerCount() {
|
|
14474
|
+
return this.testers.size;
|
|
14475
|
+
}
|
|
14476
|
+
};
|
|
14477
|
+
|
|
14213
14478
|
// src/index.ts
|
|
14214
14479
|
init_IAI();
|
|
14215
14480
|
init_IRAG();
|
|
@@ -16665,8 +16930,8 @@ var MemoryRateLimiterStorage = class {
|
|
|
16665
16930
|
const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
|
|
16666
16931
|
this.cleanupInterval = setInterval(() => {
|
|
16667
16932
|
const now = Date.now();
|
|
16668
|
-
for (const [key,
|
|
16669
|
-
if (
|
|
16933
|
+
for (const [key, window2] of this.windows) {
|
|
16934
|
+
if (window2.resetAt < now) {
|
|
16670
16935
|
this.windows.delete(key);
|
|
16671
16936
|
}
|
|
16672
16937
|
}
|
|
@@ -16677,20 +16942,20 @@ var MemoryRateLimiterStorage = class {
|
|
|
16677
16942
|
}
|
|
16678
16943
|
async increment(key, windowMs) {
|
|
16679
16944
|
const now = Date.now();
|
|
16680
|
-
let
|
|
16681
|
-
if (!
|
|
16682
|
-
|
|
16683
|
-
this.windows.set(key,
|
|
16945
|
+
let window2 = this.windows.get(key);
|
|
16946
|
+
if (!window2 || window2.resetAt < now) {
|
|
16947
|
+
window2 = { count: 0, resetAt: now + windowMs };
|
|
16948
|
+
this.windows.set(key, window2);
|
|
16684
16949
|
}
|
|
16685
|
-
|
|
16686
|
-
return [
|
|
16950
|
+
window2.count++;
|
|
16951
|
+
return [window2.count, window2.resetAt];
|
|
16687
16952
|
}
|
|
16688
16953
|
async get(key) {
|
|
16689
|
-
const
|
|
16690
|
-
if (!
|
|
16954
|
+
const window2 = this.windows.get(key);
|
|
16955
|
+
if (!window2 || window2.resetAt < Date.now()) {
|
|
16691
16956
|
return null;
|
|
16692
16957
|
}
|
|
16693
|
-
return
|
|
16958
|
+
return window2;
|
|
16694
16959
|
}
|
|
16695
16960
|
/**
|
|
16696
16961
|
* Clear all windows (for testing)
|
|
@@ -18950,6 +19215,113 @@ function createAuditLogger(options = {}) {
|
|
|
18950
19215
|
return { log, createTimedAudit };
|
|
18951
19216
|
}
|
|
18952
19217
|
|
|
19218
|
+
// src/auth/beta-client.ts
|
|
19219
|
+
var DEFAULT_CONFIG = {
|
|
19220
|
+
baseUrl: "",
|
|
19221
|
+
settingsEndpoint: "/api/beta-settings",
|
|
19222
|
+
validateEndpoint: "/api/validate-beta-code",
|
|
19223
|
+
storageKey: "beta_code",
|
|
19224
|
+
failSafeDefaults: {
|
|
19225
|
+
betaMode: true,
|
|
19226
|
+
requireInviteCode: true,
|
|
19227
|
+
betaMessage: ""
|
|
19228
|
+
}
|
|
19229
|
+
};
|
|
19230
|
+
function createBetaClient(config = {}) {
|
|
19231
|
+
const cfg = {
|
|
19232
|
+
...DEFAULT_CONFIG,
|
|
19233
|
+
...config,
|
|
19234
|
+
failSafeDefaults: {
|
|
19235
|
+
...DEFAULT_CONFIG.failSafeDefaults,
|
|
19236
|
+
...config.failSafeDefaults
|
|
19237
|
+
}
|
|
19238
|
+
};
|
|
19239
|
+
return {
|
|
19240
|
+
fetchSettings: () => fetchBetaSettings(cfg),
|
|
19241
|
+
validateCode: (code) => validateBetaCode(code, cfg),
|
|
19242
|
+
storeCode: (code) => storeBetaCode(code, cfg),
|
|
19243
|
+
getStoredCode: () => getStoredBetaCode(cfg),
|
|
19244
|
+
clearStoredCode: () => clearStoredBetaCode(cfg)
|
|
19245
|
+
};
|
|
19246
|
+
}
|
|
19247
|
+
async function fetchBetaSettings(config = {}) {
|
|
19248
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
19249
|
+
try {
|
|
19250
|
+
const response = await fetch(`${cfg.baseUrl}${cfg.settingsEndpoint}`, {
|
|
19251
|
+
method: "GET",
|
|
19252
|
+
headers: { "Content-Type": "application/json" },
|
|
19253
|
+
cache: "no-store"
|
|
19254
|
+
});
|
|
19255
|
+
if (!response.ok) {
|
|
19256
|
+
throw new Error(`Failed to fetch beta settings: ${response.status}`);
|
|
19257
|
+
}
|
|
19258
|
+
const data = await response.json();
|
|
19259
|
+
return {
|
|
19260
|
+
betaMode: data.betaMode ?? cfg.failSafeDefaults.betaMode ?? true,
|
|
19261
|
+
requireInviteCode: data.requireInviteCode ?? cfg.failSafeDefaults.requireInviteCode ?? true,
|
|
19262
|
+
betaMessage: data.betaMessage ?? cfg.failSafeDefaults.betaMessage ?? ""
|
|
19263
|
+
};
|
|
19264
|
+
} catch (error) {
|
|
19265
|
+
console.error("Error fetching beta settings:", error);
|
|
19266
|
+
return {
|
|
19267
|
+
betaMode: cfg.failSafeDefaults.betaMode ?? true,
|
|
19268
|
+
requireInviteCode: cfg.failSafeDefaults.requireInviteCode ?? true,
|
|
19269
|
+
betaMessage: cfg.failSafeDefaults.betaMessage ?? ""
|
|
19270
|
+
};
|
|
19271
|
+
}
|
|
19272
|
+
}
|
|
19273
|
+
async function validateBetaCode(code, config = {}) {
|
|
19274
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
19275
|
+
if (!code || code.trim().length < 3) {
|
|
19276
|
+
return {
|
|
19277
|
+
valid: false,
|
|
19278
|
+
message: "Please enter a valid invite code."
|
|
19279
|
+
};
|
|
19280
|
+
}
|
|
19281
|
+
try {
|
|
19282
|
+
const response = await fetch(`${cfg.baseUrl}${cfg.validateEndpoint}`, {
|
|
19283
|
+
method: "POST",
|
|
19284
|
+
headers: { "Content-Type": "application/json" },
|
|
19285
|
+
body: JSON.stringify({ code: code.trim().toUpperCase() })
|
|
19286
|
+
});
|
|
19287
|
+
if (response.status === 429) {
|
|
19288
|
+
return {
|
|
19289
|
+
valid: false,
|
|
19290
|
+
message: "Too many attempts. Please try again later."
|
|
19291
|
+
};
|
|
19292
|
+
}
|
|
19293
|
+
if (!response.ok) {
|
|
19294
|
+
throw new Error(`Validation request failed: ${response.status}`);
|
|
19295
|
+
}
|
|
19296
|
+
return await response.json();
|
|
19297
|
+
} catch (error) {
|
|
19298
|
+
console.error("Error validating invite code:", error);
|
|
19299
|
+
return {
|
|
19300
|
+
valid: false,
|
|
19301
|
+
message: "Unable to validate code. Please try again."
|
|
19302
|
+
};
|
|
19303
|
+
}
|
|
19304
|
+
}
|
|
19305
|
+
function storeBetaCode(code, config = {}) {
|
|
19306
|
+
const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
|
|
19307
|
+
if (typeof window !== "undefined") {
|
|
19308
|
+
sessionStorage.setItem(key, code.trim().toUpperCase());
|
|
19309
|
+
}
|
|
19310
|
+
}
|
|
19311
|
+
function getStoredBetaCode(config = {}) {
|
|
19312
|
+
const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
|
|
19313
|
+
if (typeof window !== "undefined") {
|
|
19314
|
+
return sessionStorage.getItem(key);
|
|
19315
|
+
}
|
|
19316
|
+
return null;
|
|
19317
|
+
}
|
|
19318
|
+
function clearStoredBetaCode(config = {}) {
|
|
19319
|
+
const key = config.storageKey ?? DEFAULT_CONFIG.storageKey;
|
|
19320
|
+
if (typeof window !== "undefined") {
|
|
19321
|
+
sessionStorage.removeItem(key);
|
|
19322
|
+
}
|
|
19323
|
+
}
|
|
19324
|
+
|
|
18953
19325
|
// src/env.ts
|
|
18954
19326
|
function getRequiredEnv(key) {
|
|
18955
19327
|
const value = process.env[key];
|
|
@@ -31001,6 +31373,509 @@ function generateId2() {
|
|
|
31001
31373
|
return (0, import_crypto38.randomBytes)(8).toString("hex") + Date.now().toString(36);
|
|
31002
31374
|
}
|
|
31003
31375
|
|
|
31376
|
+
// src/adapters/postgres-beta/PostgresBeta.ts
|
|
31377
|
+
function mapRowToInvite(row) {
|
|
31378
|
+
return {
|
|
31379
|
+
id: row.id,
|
|
31380
|
+
code: row.code,
|
|
31381
|
+
maxUses: row.max_uses,
|
|
31382
|
+
currentUses: row.current_uses,
|
|
31383
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : null,
|
|
31384
|
+
createdBy: row.created_by,
|
|
31385
|
+
notes: row.notes ?? "",
|
|
31386
|
+
isActive: row.is_active,
|
|
31387
|
+
createdAt: new Date(row.created_at)
|
|
31388
|
+
};
|
|
31389
|
+
}
|
|
31390
|
+
function mapRowToTester(row) {
|
|
31391
|
+
return {
|
|
31392
|
+
userId: row.user_id,
|
|
31393
|
+
inviteCode: row.invite_code,
|
|
31394
|
+
isBetaTester: row.is_beta_tester,
|
|
31395
|
+
betaJoinedAt: new Date(row.beta_joined_at)
|
|
31396
|
+
};
|
|
31397
|
+
}
|
|
31398
|
+
var PostgresBeta = class {
|
|
31399
|
+
pool;
|
|
31400
|
+
schema;
|
|
31401
|
+
settingsSource;
|
|
31402
|
+
envPrefix;
|
|
31403
|
+
staticCodes;
|
|
31404
|
+
config;
|
|
31405
|
+
constructor(pgConfig) {
|
|
31406
|
+
this.pool = pgConfig.pool;
|
|
31407
|
+
this.schema = pgConfig.schema ?? "public";
|
|
31408
|
+
this.settingsSource = pgConfig.settingsSource ?? "database";
|
|
31409
|
+
this.envPrefix = pgConfig.envPrefix ?? "BETA";
|
|
31410
|
+
this.staticCodes = new Set(
|
|
31411
|
+
(pgConfig.staticCodes ?? []).map((c) => normalizeBetaCode(c))
|
|
31412
|
+
);
|
|
31413
|
+
this.config = {
|
|
31414
|
+
defaultBetaMode: pgConfig.defaultBetaMode ?? true,
|
|
31415
|
+
defaultRequireInviteCode: pgConfig.defaultRequireInviteCode ?? true,
|
|
31416
|
+
defaultBetaMessage: pgConfig.defaultBetaMessage ?? "We're in beta! Thanks for being an early tester.",
|
|
31417
|
+
codePrefix: pgConfig.codePrefix ?? "BETA",
|
|
31418
|
+
maxCodesPerBatch: pgConfig.maxCodesPerBatch ?? 50
|
|
31419
|
+
};
|
|
31420
|
+
}
|
|
31421
|
+
// ─────────────────────────────────────────────────────────────
|
|
31422
|
+
// Table helpers
|
|
31423
|
+
// ─────────────────────────────────────────────────────────────
|
|
31424
|
+
t(table) {
|
|
31425
|
+
return `${this.schema}.${table}`;
|
|
31426
|
+
}
|
|
31427
|
+
// ─────────────────────────────────────────────────────────────
|
|
31428
|
+
// Initialization
|
|
31429
|
+
// ─────────────────────────────────────────────────────────────
|
|
31430
|
+
/**
|
|
31431
|
+
* Initialize beta tables. Call once at app startup.
|
|
31432
|
+
* Creates tables if they don't exist and seeds default settings.
|
|
31433
|
+
*/
|
|
31434
|
+
async initialize() {
|
|
31435
|
+
const client = await this.pool.connect();
|
|
31436
|
+
try {
|
|
31437
|
+
await client.query(`
|
|
31438
|
+
CREATE TABLE IF NOT EXISTS ${this.t("beta_settings")} (
|
|
31439
|
+
key VARCHAR(100) PRIMARY KEY,
|
|
31440
|
+
value TEXT NOT NULL,
|
|
31441
|
+
description TEXT,
|
|
31442
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
31443
|
+
updated_by VARCHAR(255)
|
|
31444
|
+
)
|
|
31445
|
+
`);
|
|
31446
|
+
await client.query(
|
|
31447
|
+
`
|
|
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
|
+
[
|
|
31455
|
+
String(this.config.defaultBetaMode),
|
|
31456
|
+
String(this.config.defaultRequireInviteCode),
|
|
31457
|
+
this.config.defaultBetaMessage
|
|
31458
|
+
]
|
|
31459
|
+
);
|
|
31460
|
+
await client.query(`
|
|
31461
|
+
CREATE TABLE IF NOT EXISTS ${this.t("beta_invites")} (
|
|
31462
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
31463
|
+
code VARCHAR(100) NOT NULL UNIQUE,
|
|
31464
|
+
max_uses INTEGER NOT NULL DEFAULT 1,
|
|
31465
|
+
current_uses INTEGER NOT NULL DEFAULT 0,
|
|
31466
|
+
expires_at TIMESTAMP WITH TIME ZONE,
|
|
31467
|
+
created_by VARCHAR(255) NOT NULL,
|
|
31468
|
+
notes TEXT DEFAULT '',
|
|
31469
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
31470
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
31471
|
+
CONSTRAINT chk_beta_uses CHECK (current_uses <= max_uses)
|
|
31472
|
+
)
|
|
31473
|
+
`);
|
|
31474
|
+
await client.query(`
|
|
31475
|
+
CREATE INDEX IF NOT EXISTS idx_beta_invites_code_active
|
|
31476
|
+
ON ${this.t("beta_invites")}(code) WHERE is_active = true
|
|
31477
|
+
`);
|
|
31478
|
+
await client.query(`
|
|
31479
|
+
CREATE TABLE IF NOT EXISTS ${this.t("beta_testers")} (
|
|
31480
|
+
user_id VARCHAR(255) PRIMARY KEY,
|
|
31481
|
+
invite_code VARCHAR(100) NOT NULL,
|
|
31482
|
+
is_beta_tester BOOLEAN NOT NULL DEFAULT true,
|
|
31483
|
+
beta_joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
31484
|
+
)
|
|
31485
|
+
`);
|
|
31486
|
+
await client.query(`
|
|
31487
|
+
CREATE INDEX IF NOT EXISTS idx_beta_testers_active
|
|
31488
|
+
ON ${this.t("beta_testers")}(is_beta_tester, beta_joined_at DESC)
|
|
31489
|
+
WHERE is_beta_tester = true
|
|
31490
|
+
`);
|
|
31491
|
+
} finally {
|
|
31492
|
+
client.release();
|
|
31493
|
+
}
|
|
31494
|
+
}
|
|
31495
|
+
// ─────────────────────────────────────────────────────────────
|
|
31496
|
+
// Settings
|
|
31497
|
+
// ─────────────────────────────────────────────────────────────
|
|
31498
|
+
async getSettings() {
|
|
31499
|
+
if (this.settingsSource === "env") {
|
|
31500
|
+
return this.getEnvSettings();
|
|
31501
|
+
}
|
|
31502
|
+
const result = await this.pool.query(
|
|
31503
|
+
`SELECT key, value FROM ${this.t("beta_settings")} WHERE key IN ('beta_mode', 'require_invite_code', 'beta_message')`
|
|
31504
|
+
);
|
|
31505
|
+
const settings = {};
|
|
31506
|
+
for (const row of result.rows) {
|
|
31507
|
+
settings[row.key] = row.value;
|
|
31508
|
+
}
|
|
31509
|
+
return {
|
|
31510
|
+
betaMode: settings["beta_mode"] !== "false",
|
|
31511
|
+
requireInviteCode: settings["require_invite_code"] !== "false",
|
|
31512
|
+
betaMessage: settings["beta_message"] ?? this.config.defaultBetaMessage
|
|
31513
|
+
};
|
|
31514
|
+
}
|
|
31515
|
+
getEnvSettings() {
|
|
31516
|
+
const prefix = this.envPrefix;
|
|
31517
|
+
return {
|
|
31518
|
+
betaMode: process.env[`${prefix}_MODE`] !== "false",
|
|
31519
|
+
requireInviteCode: process.env[`${prefix}_REQUIRE_INVITE_CODE`] !== "false",
|
|
31520
|
+
betaMessage: process.env[`${prefix}_MESSAGE`] ?? this.config.defaultBetaMessage
|
|
31521
|
+
};
|
|
31522
|
+
}
|
|
31523
|
+
async updateSettings(options) {
|
|
31524
|
+
if (this.settingsSource === "env") {
|
|
31525
|
+
return;
|
|
31526
|
+
}
|
|
31527
|
+
const client = await this.pool.connect();
|
|
31528
|
+
try {
|
|
31529
|
+
const updates = [];
|
|
31530
|
+
if (options.betaMode !== void 0) {
|
|
31531
|
+
updates.push({ key: "beta_mode", value: String(options.betaMode) });
|
|
31532
|
+
}
|
|
31533
|
+
if (options.requireInviteCode !== void 0) {
|
|
31534
|
+
updates.push({
|
|
31535
|
+
key: "require_invite_code",
|
|
31536
|
+
value: String(options.requireInviteCode)
|
|
31537
|
+
});
|
|
31538
|
+
}
|
|
31539
|
+
if (options.betaMessage !== void 0) {
|
|
31540
|
+
updates.push({ key: "beta_message", value: options.betaMessage });
|
|
31541
|
+
}
|
|
31542
|
+
for (const { key, value } of updates) {
|
|
31543
|
+
await client.query(
|
|
31544
|
+
`UPDATE ${this.t("beta_settings")} SET value = $1, updated_at = NOW(), updated_by = $2 WHERE key = $3`,
|
|
31545
|
+
[value, options.updatedBy ?? "system", key]
|
|
31546
|
+
);
|
|
31547
|
+
}
|
|
31548
|
+
} finally {
|
|
31549
|
+
client.release();
|
|
31550
|
+
}
|
|
31551
|
+
}
|
|
31552
|
+
// ─────────────────────────────────────────────────────────────
|
|
31553
|
+
// Code Management
|
|
31554
|
+
// ─────────────────────────────────────────────────────────────
|
|
31555
|
+
async createCodes(options) {
|
|
31556
|
+
const count = Math.min(options.count ?? 1, this.config.maxCodesPerBatch);
|
|
31557
|
+
const prefix = options.prefix ?? this.config.codePrefix;
|
|
31558
|
+
const results = [];
|
|
31559
|
+
const client = await this.pool.connect();
|
|
31560
|
+
try {
|
|
31561
|
+
for (let i = 0; i < count; i++) {
|
|
31562
|
+
const codeStr = options.code ? normalizeBetaCode(options.code) : generateBetaCode(prefix);
|
|
31563
|
+
const id = generateBetaId();
|
|
31564
|
+
let attempts = 0;
|
|
31565
|
+
let inserted = false;
|
|
31566
|
+
let currentCode = codeStr;
|
|
31567
|
+
while (!inserted && attempts < 5) {
|
|
31568
|
+
try {
|
|
31569
|
+
const result = await client.query(
|
|
31570
|
+
`INSERT INTO ${this.t("beta_invites")} (id, code, max_uses, expires_at, created_by, notes)
|
|
31571
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
31572
|
+
RETURNING *`,
|
|
31573
|
+
[
|
|
31574
|
+
id,
|
|
31575
|
+
currentCode,
|
|
31576
|
+
options.maxUses ?? 1,
|
|
31577
|
+
options.expiresAt ?? null,
|
|
31578
|
+
options.createdBy,
|
|
31579
|
+
options.notes ?? ""
|
|
31580
|
+
]
|
|
31581
|
+
);
|
|
31582
|
+
const row = result.rows[0];
|
|
31583
|
+
if (row) results.push(mapRowToInvite(row));
|
|
31584
|
+
inserted = true;
|
|
31585
|
+
} catch (err) {
|
|
31586
|
+
const pgErr = err;
|
|
31587
|
+
if (pgErr.code === "23505" && !options.code) {
|
|
31588
|
+
currentCode = generateBetaCode(prefix);
|
|
31589
|
+
attempts++;
|
|
31590
|
+
} else {
|
|
31591
|
+
throw err;
|
|
31592
|
+
}
|
|
31593
|
+
}
|
|
31594
|
+
}
|
|
31595
|
+
if (!inserted) {
|
|
31596
|
+
throw new Error(
|
|
31597
|
+
`Failed to generate unique code after ${attempts} attempts`
|
|
31598
|
+
);
|
|
31599
|
+
}
|
|
31600
|
+
if (options.code) break;
|
|
31601
|
+
}
|
|
31602
|
+
} finally {
|
|
31603
|
+
client.release();
|
|
31604
|
+
}
|
|
31605
|
+
return results;
|
|
31606
|
+
}
|
|
31607
|
+
async listCodes(options = {}) {
|
|
31608
|
+
const conditions = [];
|
|
31609
|
+
const params = [];
|
|
31610
|
+
let paramIdx = 1;
|
|
31611
|
+
if (options.isActive !== void 0) {
|
|
31612
|
+
conditions.push(`is_active = $${paramIdx++}`);
|
|
31613
|
+
params.push(options.isActive);
|
|
31614
|
+
}
|
|
31615
|
+
if (options.status) {
|
|
31616
|
+
switch (options.status) {
|
|
31617
|
+
case "unused":
|
|
31618
|
+
conditions.push("is_active = true");
|
|
31619
|
+
conditions.push("current_uses = 0");
|
|
31620
|
+
break;
|
|
31621
|
+
case "partial":
|
|
31622
|
+
conditions.push("is_active = true");
|
|
31623
|
+
conditions.push("current_uses > 0");
|
|
31624
|
+
conditions.push("current_uses < max_uses");
|
|
31625
|
+
break;
|
|
31626
|
+
case "exhausted":
|
|
31627
|
+
conditions.push("current_uses >= max_uses");
|
|
31628
|
+
break;
|
|
31629
|
+
case "expired":
|
|
31630
|
+
conditions.push("expires_at IS NOT NULL");
|
|
31631
|
+
conditions.push("expires_at < NOW()");
|
|
31632
|
+
break;
|
|
31633
|
+
case "revoked":
|
|
31634
|
+
conditions.push("is_active = false");
|
|
31635
|
+
break;
|
|
31636
|
+
}
|
|
31637
|
+
}
|
|
31638
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
31639
|
+
const limit = options.limit ?? 100;
|
|
31640
|
+
const offset = options.offset ?? 0;
|
|
31641
|
+
const result = await this.pool.query(
|
|
31642
|
+
`SELECT * FROM ${this.t("beta_invites")} ${where}
|
|
31643
|
+
ORDER BY created_at DESC
|
|
31644
|
+
LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
|
|
31645
|
+
[...params, limit, offset]
|
|
31646
|
+
);
|
|
31647
|
+
return result.rows.map(mapRowToInvite);
|
|
31648
|
+
}
|
|
31649
|
+
async getCode(code) {
|
|
31650
|
+
const normalized = normalizeBetaCode(code);
|
|
31651
|
+
const result = await this.pool.query(
|
|
31652
|
+
`SELECT * FROM ${this.t("beta_invites")} WHERE code = $1`,
|
|
31653
|
+
[normalized]
|
|
31654
|
+
);
|
|
31655
|
+
const row = result.rows[0];
|
|
31656
|
+
if (!row) return null;
|
|
31657
|
+
return mapRowToInvite(row);
|
|
31658
|
+
}
|
|
31659
|
+
async revokeCode(code) {
|
|
31660
|
+
const normalized = normalizeBetaCode(code);
|
|
31661
|
+
await this.pool.query(
|
|
31662
|
+
`UPDATE ${this.t("beta_invites")} SET is_active = false WHERE code = $1`,
|
|
31663
|
+
[normalized]
|
|
31664
|
+
);
|
|
31665
|
+
}
|
|
31666
|
+
// ─────────────────────────────────────────────────────────────
|
|
31667
|
+
// Validation & Consumption
|
|
31668
|
+
// ─────────────────────────────────────────────────────────────
|
|
31669
|
+
async validateCode(code) {
|
|
31670
|
+
const settings = await this.getSettings();
|
|
31671
|
+
if (!settings.requireInviteCode) {
|
|
31672
|
+
return { valid: true, message: "Invite codes are not required." };
|
|
31673
|
+
}
|
|
31674
|
+
const normalized = normalizeBetaCode(code);
|
|
31675
|
+
if (this.staticCodes.has(normalized)) {
|
|
31676
|
+
return {
|
|
31677
|
+
valid: true,
|
|
31678
|
+
message: "Valid invite code.",
|
|
31679
|
+
code: normalized
|
|
31680
|
+
};
|
|
31681
|
+
}
|
|
31682
|
+
const result = await this.pool.query(
|
|
31683
|
+
`SELECT * FROM ${this.t("beta_invites")}
|
|
31684
|
+
WHERE code = $1
|
|
31685
|
+
AND is_active = true
|
|
31686
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
31687
|
+
AND current_uses < max_uses`,
|
|
31688
|
+
[normalized]
|
|
31689
|
+
);
|
|
31690
|
+
if (result.rows.length === 0) {
|
|
31691
|
+
const exists = await this.pool.query(
|
|
31692
|
+
`SELECT is_active, expires_at, current_uses, max_uses FROM ${this.t("beta_invites")} WHERE code = $1`,
|
|
31693
|
+
[normalized]
|
|
31694
|
+
);
|
|
31695
|
+
if (exists.rows.length === 0) {
|
|
31696
|
+
return { valid: false, message: "Invalid invite code." };
|
|
31697
|
+
}
|
|
31698
|
+
const existRow = exists.rows[0];
|
|
31699
|
+
if (!existRow) {
|
|
31700
|
+
return { valid: false, message: "Invalid invite code." };
|
|
31701
|
+
}
|
|
31702
|
+
if (!existRow.is_active) {
|
|
31703
|
+
return { valid: false, message: "This invite code has been revoked." };
|
|
31704
|
+
}
|
|
31705
|
+
if (existRow.expires_at && new Date(existRow.expires_at) < /* @__PURE__ */ new Date()) {
|
|
31706
|
+
return { valid: false, message: "This invite code has expired." };
|
|
31707
|
+
}
|
|
31708
|
+
if (existRow.current_uses >= existRow.max_uses) {
|
|
31709
|
+
return {
|
|
31710
|
+
valid: false,
|
|
31711
|
+
message: "This invite code has reached its usage limit."
|
|
31712
|
+
};
|
|
31713
|
+
}
|
|
31714
|
+
return { valid: false, message: "Invalid invite code." };
|
|
31715
|
+
}
|
|
31716
|
+
const invite = result.rows[0];
|
|
31717
|
+
if (!invite) {
|
|
31718
|
+
return { valid: false, message: "Invalid invite code." };
|
|
31719
|
+
}
|
|
31720
|
+
return {
|
|
31721
|
+
valid: true,
|
|
31722
|
+
message: "Valid invite code.",
|
|
31723
|
+
code: invite.code,
|
|
31724
|
+
remainingUses: invite.max_uses - invite.current_uses
|
|
31725
|
+
};
|
|
31726
|
+
}
|
|
31727
|
+
async consumeCode(code, userId) {
|
|
31728
|
+
const normalized = normalizeBetaCode(code);
|
|
31729
|
+
if (this.staticCodes.has(normalized)) {
|
|
31730
|
+
await this.tagBetaTester(userId, normalized);
|
|
31731
|
+
return { success: true, message: "Invite code consumed successfully." };
|
|
31732
|
+
}
|
|
31733
|
+
const client = await this.pool.connect();
|
|
31734
|
+
try {
|
|
31735
|
+
await client.query("BEGIN");
|
|
31736
|
+
const result = await client.query(
|
|
31737
|
+
`SELECT * FROM ${this.t("beta_invites")}
|
|
31738
|
+
WHERE code = $1
|
|
31739
|
+
AND is_active = true
|
|
31740
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
31741
|
+
AND current_uses < max_uses
|
|
31742
|
+
FOR UPDATE`,
|
|
31743
|
+
[normalized]
|
|
31744
|
+
);
|
|
31745
|
+
if (result.rows.length === 0) {
|
|
31746
|
+
await client.query("ROLLBACK");
|
|
31747
|
+
return { success: false, message: "Invalid or exhausted invite code." };
|
|
31748
|
+
}
|
|
31749
|
+
await client.query(
|
|
31750
|
+
`UPDATE ${this.t("beta_invites")} SET current_uses = current_uses + 1 WHERE code = $1`,
|
|
31751
|
+
[normalized]
|
|
31752
|
+
);
|
|
31753
|
+
await client.query(
|
|
31754
|
+
`INSERT INTO ${this.t("beta_testers")} (user_id, invite_code, is_beta_tester, beta_joined_at)
|
|
31755
|
+
VALUES ($1, $2, true, NOW())
|
|
31756
|
+
ON CONFLICT (user_id) DO UPDATE SET invite_code = $2, is_beta_tester = true, beta_joined_at = NOW()`,
|
|
31757
|
+
[userId, normalized]
|
|
31758
|
+
);
|
|
31759
|
+
await client.query("COMMIT");
|
|
31760
|
+
return { success: true, message: "Invite code consumed successfully." };
|
|
31761
|
+
} catch (error) {
|
|
31762
|
+
await client.query("ROLLBACK");
|
|
31763
|
+
throw error;
|
|
31764
|
+
} finally {
|
|
31765
|
+
client.release();
|
|
31766
|
+
}
|
|
31767
|
+
}
|
|
31768
|
+
async tagBetaTester(userId, code) {
|
|
31769
|
+
await this.pool.query(
|
|
31770
|
+
`INSERT INTO ${this.t("beta_testers")} (user_id, invite_code, is_beta_tester, beta_joined_at)
|
|
31771
|
+
VALUES ($1, $2, true, NOW())
|
|
31772
|
+
ON CONFLICT (user_id) DO UPDATE SET invite_code = $2, is_beta_tester = true, beta_joined_at = NOW()`,
|
|
31773
|
+
[userId, code]
|
|
31774
|
+
);
|
|
31775
|
+
}
|
|
31776
|
+
// ─────────────────────────────────────────────────────────────
|
|
31777
|
+
// User Tracking
|
|
31778
|
+
// ─────────────────────────────────────────────────────────────
|
|
31779
|
+
async isBetaTester(userId) {
|
|
31780
|
+
const result = await this.pool.query(
|
|
31781
|
+
`SELECT is_beta_tester FROM ${this.t("beta_testers")} WHERE user_id = $1 AND is_beta_tester = true`,
|
|
31782
|
+
[userId]
|
|
31783
|
+
);
|
|
31784
|
+
return result.rows.length > 0;
|
|
31785
|
+
}
|
|
31786
|
+
async getBetaTester(userId) {
|
|
31787
|
+
const result = await this.pool.query(
|
|
31788
|
+
`SELECT * FROM ${this.t("beta_testers")} WHERE user_id = $1`,
|
|
31789
|
+
[userId]
|
|
31790
|
+
);
|
|
31791
|
+
const row = result.rows[0];
|
|
31792
|
+
if (!row) return null;
|
|
31793
|
+
return mapRowToTester(row);
|
|
31794
|
+
}
|
|
31795
|
+
async listBetaTesters(options = {}) {
|
|
31796
|
+
const limit = options.limit ?? 100;
|
|
31797
|
+
const offset = options.offset ?? 0;
|
|
31798
|
+
const result = await this.pool.query(
|
|
31799
|
+
`SELECT * FROM ${this.t("beta_testers")}
|
|
31800
|
+
WHERE is_beta_tester = true
|
|
31801
|
+
ORDER BY beta_joined_at DESC
|
|
31802
|
+
LIMIT $1 OFFSET $2`,
|
|
31803
|
+
[limit, offset]
|
|
31804
|
+
);
|
|
31805
|
+
return result.rows.map(mapRowToTester);
|
|
31806
|
+
}
|
|
31807
|
+
// ─────────────────────────────────────────────────────────────
|
|
31808
|
+
// Analytics
|
|
31809
|
+
// ─────────────────────────────────────────────────────────────
|
|
31810
|
+
async getStats() {
|
|
31811
|
+
const [testersResult, codesResult] = await Promise.all([
|
|
31812
|
+
this.pool.query(
|
|
31813
|
+
`SELECT
|
|
31814
|
+
COUNT(*) as total,
|
|
31815
|
+
MIN(beta_joined_at) as first_signup,
|
|
31816
|
+
MAX(beta_joined_at) as latest_signup
|
|
31817
|
+
FROM ${this.t("beta_testers")}
|
|
31818
|
+
WHERE is_beta_tester = true`
|
|
31819
|
+
),
|
|
31820
|
+
this.pool.query(
|
|
31821
|
+
`SELECT
|
|
31822
|
+
COUNT(*) as total_codes,
|
|
31823
|
+
COUNT(*) FILTER (WHERE is_active = true) as active_codes,
|
|
31824
|
+
COALESCE(SUM(current_uses), 0) as total_uses,
|
|
31825
|
+
COALESCE(SUM(CASE WHEN is_active = true THEN max_uses - current_uses ELSE 0 END), 0) as total_remaining
|
|
31826
|
+
FROM ${this.t("beta_invites")}`
|
|
31827
|
+
)
|
|
31828
|
+
]);
|
|
31829
|
+
const tRow = testersResult.rows[0] ?? {};
|
|
31830
|
+
const cRow = codesResult.rows[0] ?? {};
|
|
31831
|
+
return {
|
|
31832
|
+
totalBetaTesters: Number(tRow.total ?? 0),
|
|
31833
|
+
totalCodes: Number(cRow.total_codes ?? 0),
|
|
31834
|
+
activeCodes: Number(cRow.active_codes ?? 0),
|
|
31835
|
+
totalUses: Number(cRow.total_uses ?? 0),
|
|
31836
|
+
totalRemaining: Number(cRow.total_remaining ?? 0),
|
|
31837
|
+
firstBetaSignup: tRow.first_signup ? new Date(tRow.first_signup) : null,
|
|
31838
|
+
latestBetaSignup: tRow.latest_signup ? new Date(tRow.latest_signup) : null
|
|
31839
|
+
};
|
|
31840
|
+
}
|
|
31841
|
+
async getCodeUsageReports() {
|
|
31842
|
+
const result = await this.pool.query(
|
|
31843
|
+
`SELECT
|
|
31844
|
+
code, notes, max_uses, current_uses,
|
|
31845
|
+
(max_uses - current_uses) as remaining_uses,
|
|
31846
|
+
CASE WHEN max_uses > 0
|
|
31847
|
+
THEN ROUND((current_uses::numeric / max_uses) * 100)
|
|
31848
|
+
ELSE 0
|
|
31849
|
+
END as usage_percent,
|
|
31850
|
+
is_active, expires_at, created_at
|
|
31851
|
+
FROM ${this.t("beta_invites")}
|
|
31852
|
+
ORDER BY created_at DESC`
|
|
31853
|
+
);
|
|
31854
|
+
return result.rows.map((row) => ({
|
|
31855
|
+
code: row.code,
|
|
31856
|
+
notes: row.notes ?? "",
|
|
31857
|
+
maxUses: row.max_uses,
|
|
31858
|
+
currentUses: row.current_uses,
|
|
31859
|
+
remainingUses: Number(row.remaining_uses ?? 0),
|
|
31860
|
+
usagePercent: Number(row.usage_percent ?? 0),
|
|
31861
|
+
isActive: row.is_active,
|
|
31862
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : null,
|
|
31863
|
+
createdAt: new Date(row.created_at)
|
|
31864
|
+
}));
|
|
31865
|
+
}
|
|
31866
|
+
// ─────────────────────────────────────────────────────────────
|
|
31867
|
+
// Health
|
|
31868
|
+
// ─────────────────────────────────────────────────────────────
|
|
31869
|
+
async healthCheck() {
|
|
31870
|
+
try {
|
|
31871
|
+
await this.pool.query("SELECT 1");
|
|
31872
|
+
return true;
|
|
31873
|
+
} catch {
|
|
31874
|
+
return false;
|
|
31875
|
+
}
|
|
31876
|
+
}
|
|
31877
|
+
};
|
|
31878
|
+
|
|
31004
31879
|
// src/app-logger.ts
|
|
31005
31880
|
var LEVEL_PRIORITY2 = {
|
|
31006
31881
|
debug: 0,
|
|
@@ -31177,7 +32052,7 @@ async function closeSharedRedis() {
|
|
|
31177
32052
|
}
|
|
31178
32053
|
|
|
31179
32054
|
// src/migrations/Migrator.ts
|
|
31180
|
-
var
|
|
32055
|
+
var DEFAULT_CONFIG2 = {
|
|
31181
32056
|
tableName: "_migrations",
|
|
31182
32057
|
schema: "public",
|
|
31183
32058
|
lockTimeout: 60,
|
|
@@ -31204,7 +32079,7 @@ var Migrator = class {
|
|
|
31204
32079
|
locked = false;
|
|
31205
32080
|
constructor(db, config = {}) {
|
|
31206
32081
|
this.db = db;
|
|
31207
|
-
this.config = { ...
|
|
32082
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
31208
32083
|
}
|
|
31209
32084
|
/**
|
|
31210
32085
|
* Get the fully qualified migration table name
|
|
@@ -31900,6 +32775,67 @@ var createSsoSessionsTable = {
|
|
|
31900
32775
|
`,
|
|
31901
32776
|
down: "DROP TABLE IF EXISTS sso_sessions CASCADE"
|
|
31902
32777
|
};
|
|
32778
|
+
var createBetaSettingsTable = {
|
|
32779
|
+
version: "20241217_009",
|
|
32780
|
+
name: "create_beta_settings_table",
|
|
32781
|
+
up: `
|
|
32782
|
+
CREATE TABLE IF NOT EXISTS beta_settings (
|
|
32783
|
+
key VARCHAR(100) PRIMARY KEY,
|
|
32784
|
+
value TEXT NOT NULL,
|
|
32785
|
+
description TEXT,
|
|
32786
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
32787
|
+
updated_by VARCHAR(255)
|
|
32788
|
+
);
|
|
32789
|
+
|
|
32790
|
+
INSERT INTO beta_settings (key, value, description) VALUES
|
|
32791
|
+
('beta_mode', 'true', 'Whether the app is in beta mode'),
|
|
32792
|
+
('require_invite_code', 'true', 'Whether an invite code is required to sign up'),
|
|
32793
|
+
('beta_message', 'We''re in beta! Thanks for being an early tester.', 'Message displayed during beta')
|
|
32794
|
+
ON CONFLICT (key) DO NOTHING;
|
|
32795
|
+
`,
|
|
32796
|
+
down: "DROP TABLE IF EXISTS beta_settings CASCADE"
|
|
32797
|
+
};
|
|
32798
|
+
var createBetaInvitesTable = {
|
|
32799
|
+
version: "20241217_010",
|
|
32800
|
+
name: "create_beta_invites_table",
|
|
32801
|
+
up: `
|
|
32802
|
+
CREATE TABLE IF NOT EXISTS beta_invites (
|
|
32803
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
32804
|
+
code VARCHAR(100) NOT NULL UNIQUE,
|
|
32805
|
+
max_uses INTEGER NOT NULL DEFAULT 1,
|
|
32806
|
+
current_uses INTEGER NOT NULL DEFAULT 0,
|
|
32807
|
+
expires_at TIMESTAMP WITH TIME ZONE,
|
|
32808
|
+
created_by VARCHAR(255) NOT NULL,
|
|
32809
|
+
notes TEXT DEFAULT '',
|
|
32810
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
32811
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
32812
|
+
CONSTRAINT chk_beta_uses CHECK (current_uses <= max_uses)
|
|
32813
|
+
);
|
|
32814
|
+
|
|
32815
|
+
CREATE INDEX IF NOT EXISTS idx_beta_invites_code_active
|
|
32816
|
+
ON beta_invites(code) WHERE is_active = true;
|
|
32817
|
+
CREATE INDEX IF NOT EXISTS idx_beta_invites_active
|
|
32818
|
+
ON beta_invites(is_active, created_at DESC);
|
|
32819
|
+
`,
|
|
32820
|
+
down: "DROP TABLE IF EXISTS beta_invites CASCADE"
|
|
32821
|
+
};
|
|
32822
|
+
var createBetaTestersTable = {
|
|
32823
|
+
version: "20241217_011",
|
|
32824
|
+
name: "create_beta_testers_table",
|
|
32825
|
+
up: `
|
|
32826
|
+
CREATE TABLE IF NOT EXISTS beta_testers (
|
|
32827
|
+
user_id VARCHAR(255) PRIMARY KEY,
|
|
32828
|
+
invite_code VARCHAR(100) NOT NULL REFERENCES beta_invites(code) ON DELETE SET NULL,
|
|
32829
|
+
is_beta_tester BOOLEAN NOT NULL DEFAULT true,
|
|
32830
|
+
beta_joined_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
32831
|
+
);
|
|
32832
|
+
|
|
32833
|
+
CREATE INDEX IF NOT EXISTS idx_beta_testers_active
|
|
32834
|
+
ON beta_testers(is_beta_tester, beta_joined_at DESC)
|
|
32835
|
+
WHERE is_beta_tester = true;
|
|
32836
|
+
`,
|
|
32837
|
+
down: "DROP TABLE IF EXISTS beta_testers CASCADE"
|
|
32838
|
+
};
|
|
31903
32839
|
var enterpriseMigrations = [
|
|
31904
32840
|
createSsoOidcConfigsTable,
|
|
31905
32841
|
createDomainVerificationsTable,
|
|
@@ -31908,7 +32844,10 @@ var enterpriseMigrations = [
|
|
|
31908
32844
|
createTenantMembersTable,
|
|
31909
32845
|
createTenantInvitationsTable,
|
|
31910
32846
|
createTenantUsageTable,
|
|
31911
|
-
createSsoSessionsTable
|
|
32847
|
+
createSsoSessionsTable,
|
|
32848
|
+
createBetaSettingsTable,
|
|
32849
|
+
createBetaInvitesTable,
|
|
32850
|
+
createBetaTestersTable
|
|
31912
32851
|
];
|
|
31913
32852
|
function getEnterpriseMigrations(features) {
|
|
31914
32853
|
const migrations = [];
|
|
@@ -31928,6 +32867,13 @@ function getEnterpriseMigrations(features) {
|
|
|
31928
32867
|
createTenantUsageTable
|
|
31929
32868
|
);
|
|
31930
32869
|
}
|
|
32870
|
+
if (features.beta) {
|
|
32871
|
+
migrations.push(
|
|
32872
|
+
createBetaSettingsTable,
|
|
32873
|
+
createBetaInvitesTable,
|
|
32874
|
+
createBetaTestersTable
|
|
32875
|
+
);
|
|
32876
|
+
}
|
|
31931
32877
|
return migrations;
|
|
31932
32878
|
}
|
|
31933
32879
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -31990,6 +32936,7 @@ function getEnterpriseMigrations(features) {
|
|
|
31990
32936
|
MemoryAuditLog,
|
|
31991
32937
|
MemoryAuth,
|
|
31992
32938
|
MemoryAuthSSO,
|
|
32939
|
+
MemoryBeta,
|
|
31993
32940
|
MemoryBilling,
|
|
31994
32941
|
MemoryCache,
|
|
31995
32942
|
MemoryCompliance,
|
|
@@ -32030,6 +32977,7 @@ function getEnterpriseMigrations(features) {
|
|
|
32030
32977
|
PhoneSchema,
|
|
32031
32978
|
PineconeRAG,
|
|
32032
32979
|
PlatformConfigSchema,
|
|
32980
|
+
PostgresBeta,
|
|
32033
32981
|
PostgresDatabase,
|
|
32034
32982
|
PostgresTenant,
|
|
32035
32983
|
QueueConfigSchema,
|
|
@@ -32085,6 +33033,7 @@ function getEnterpriseMigrations(features) {
|
|
|
32085
33033
|
checkEnvVars,
|
|
32086
33034
|
checkRateLimit,
|
|
32087
33035
|
classifyError,
|
|
33036
|
+
clearStoredBetaCode,
|
|
32088
33037
|
closeSharedRedis,
|
|
32089
33038
|
composeHookRegistries,
|
|
32090
33039
|
constantTimeEqual,
|
|
@@ -32097,6 +33046,10 @@ function getEnterpriseMigrations(features) {
|
|
|
32097
33046
|
createAuditActor,
|
|
32098
33047
|
createAuditLogger,
|
|
32099
33048
|
createAuthError,
|
|
33049
|
+
createBetaClient,
|
|
33050
|
+
createBetaInvitesTable,
|
|
33051
|
+
createBetaSettingsTable,
|
|
33052
|
+
createBetaTestersTable,
|
|
32100
33053
|
createBulkhead,
|
|
32101
33054
|
createCacheMiddleware,
|
|
32102
33055
|
createCachedFallback,
|
|
@@ -32155,9 +33108,12 @@ function getEnterpriseMigrations(features) {
|
|
|
32155
33108
|
extractAuditRequestId,
|
|
32156
33109
|
extractAuditUserAgent,
|
|
32157
33110
|
extractClientIp,
|
|
33111
|
+
fetchBetaSettings,
|
|
32158
33112
|
filterChannelsByPreferences,
|
|
32159
33113
|
formatAmount,
|
|
32160
33114
|
generateAuditId,
|
|
33115
|
+
generateBetaCode,
|
|
33116
|
+
generateBetaId,
|
|
32161
33117
|
generateChecksum,
|
|
32162
33118
|
generateDeliveryId,
|
|
32163
33119
|
generateErrorId,
|
|
@@ -32187,6 +33143,7 @@ function getEnterpriseMigrations(features) {
|
|
|
32187
33143
|
getRequestId,
|
|
32188
33144
|
getRequiredEnv,
|
|
32189
33145
|
getSharedRedis,
|
|
33146
|
+
getStoredBetaCode,
|
|
32190
33147
|
getTenantId,
|
|
32191
33148
|
getTokenEndpoint,
|
|
32192
33149
|
getTraceId,
|
|
@@ -32206,6 +33163,7 @@ function getEnterpriseMigrations(features) {
|
|
|
32206
33163
|
loadConfig,
|
|
32207
33164
|
matchAction,
|
|
32208
33165
|
matchEventType,
|
|
33166
|
+
normalizeBetaCode,
|
|
32209
33167
|
parseKeycloakRoles,
|
|
32210
33168
|
raceTimeout,
|
|
32211
33169
|
refreshKeycloakToken,
|
|
@@ -32219,9 +33177,11 @@ function getEnterpriseMigrations(features) {
|
|
|
32219
33177
|
sanitizeApiError,
|
|
32220
33178
|
sanitizeForEmail,
|
|
32221
33179
|
sqlMigration,
|
|
33180
|
+
storeBetaCode,
|
|
32222
33181
|
stripHtml,
|
|
32223
33182
|
timedHealthCheck,
|
|
32224
33183
|
toHealthCheckResult,
|
|
33184
|
+
validateBetaCode,
|
|
32225
33185
|
validateConfig,
|
|
32226
33186
|
validateEnvVars,
|
|
32227
33187
|
withCorrelation,
|