@snapback/cli 1.1.12 → 1.1.15
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/README.md +79 -18
- package/dist/SkippedTestDetector-AXTMWWHC.js +5 -0
- package/dist/SkippedTestDetector-QLSQV7K7.js +5 -0
- package/dist/analysis-6WTBZJH3.js +6 -0
- package/dist/analysis-C472LUGW.js +2475 -0
- package/dist/auth-HFJRXXG2.js +1446 -0
- package/dist/auto-provision-organization-SF6XM7X4.js +161 -0
- package/dist/chunk-23G5VYA3.js +4259 -0
- package/dist/{chunk-QAKFE3NE.js → chunk-4YTE4JEW.js} +3 -4
- package/dist/chunk-5EOPYJ4Y.js +12 -0
- package/dist/{chunk-G7QXHNGB.js → chunk-5SQA44V7.js} +1125 -32
- package/dist/{chunk-BW7RALUZ.js → chunk-7ADPL4Q3.js} +11 -4
- package/dist/chunk-CBGOC6RV.js +293 -0
- package/dist/chunk-DNEADD2G.js +3499 -0
- package/dist/{chunk-NKBZIXCN.js → chunk-DPWFZNMY.js} +122 -15
- package/dist/chunk-GQ73B37K.js +314 -0
- package/dist/chunk-HR34NJP7.js +6133 -0
- package/dist/chunk-ICKSHS3A.js +2264 -0
- package/dist/{chunk-KPETDXQO.js → chunk-OI2HNNT6.js} +565 -50
- package/dist/chunk-PL4HF4M2.js +593 -0
- package/dist/chunk-WS36HDEU.js +3735 -0
- package/dist/chunk-XYU5FFE3.js +111 -0
- package/dist/chunk-ZBQDE6WJ.js +108 -0
- package/dist/client-WIO6W447.js +8 -0
- package/dist/dist-E7E2T3DQ.js +9 -0
- package/dist/dist-TEWNOZYS.js +5 -0
- package/dist/dist-YZBJAYEJ.js +12 -0
- package/dist/index.js +65215 -26627
- package/dist/local-service-adapter-3JHN6G4O.js +6 -0
- package/dist/pioneer-oauth-hook-V2JKEXM7.js +12 -0
- package/dist/{secure-credentials-6UMEU22H.js → secure-credentials-UEPG7GWW.js} +15 -8
- package/dist/snapback-dir-MG7DTRMF.js +6 -0
- package/package.json +8 -42
- package/scripts/postinstall.mjs +2 -3
- package/dist/SkippedTestDetector-B3JZUE5G.js +0 -5
- package/dist/SkippedTestDetector-B3JZUE5G.js.map +0 -1
- package/dist/analysis-Z53F5FT2.js +0 -6
- package/dist/analysis-Z53F5FT2.js.map +0 -1
- package/dist/chunk-6MR2TINI.js +0 -27
- package/dist/chunk-6MR2TINI.js.map +0 -1
- package/dist/chunk-BW7RALUZ.js.map +0 -1
- package/dist/chunk-G7QXHNGB.js.map +0 -1
- package/dist/chunk-ISVRGBWT.js +0 -16223
- package/dist/chunk-ISVRGBWT.js.map +0 -1
- package/dist/chunk-KPETDXQO.js.map +0 -1
- package/dist/chunk-NKBZIXCN.js.map +0 -1
- package/dist/chunk-QAKFE3NE.js.map +0 -1
- package/dist/chunk-YOVA65PS.js +0 -12745
- package/dist/chunk-YOVA65PS.js.map +0 -1
- package/dist/dist-7UKXVKH3.js +0 -5
- package/dist/dist-7UKXVKH3.js.map +0 -1
- package/dist/dist-VDK7WEF4.js +0 -5
- package/dist/dist-VDK7WEF4.js.map +0 -1
- package/dist/dist-WKLJSPJT.js +0 -8
- package/dist/dist-WKLJSPJT.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/secure-credentials-6UMEU22H.js.map +0 -1
- package/dist/snapback-dir-T3CRQRY6.js +0 -6
- package/dist/snapback-dir-T3CRQRY6.js.map +0 -1
|
@@ -0,0 +1,4259 @@
|
|
|
1
|
+
#!/usr/bin/env node --no-warnings=ExperimentalWarning
|
|
2
|
+
import { isRedisAvailable, getCache, setCache, deleteCache } from './chunk-GQ73B37K.js';
|
|
3
|
+
import { snapshots, snapshotFiles, agentSuggestions, quarantineEvents, postAcceptOutcomes, policyEvaluations, loops, feedback, combinedSchema, user, aiChat, organization, member, invitation, purchase, session, account, verification, passkey, db, userAttributions, subscriptions, trials, pioneers, usageLimits, creditsLedger, mcpObservations, mcpToolInvocations, extensionSyncState, pioneerActions, sagas, apiKeys, TIER_STALENESS_THRESHOLD_MS, WORKSPACE_LINK_TTL_MS, patterns, closeDatabaseConnection, checkDatabaseConnection } from './chunk-HR34NJP7.js';
|
|
4
|
+
import { logger } from './chunk-PL4HF4M2.js';
|
|
5
|
+
import { shouldMergeAttribution, createLogger, LogLevel, isFeatureAvailableAtTier, getTierFeatures, getTierLimit, EventBus, generateIdempotencyKey, getActionPoints, calculatePioneerTier, PIONEER_TIER_THRESHOLDS, isFirstTimeAction, TIER_UPGRADE_SAGA } from './chunk-WS36HDEU.js';
|
|
6
|
+
import { __name } from './chunk-7ADPL4Q3.js';
|
|
7
|
+
import { eq, desc, and, gte, lte, sum, gt, sql, inArray } from 'drizzle-orm';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import slugify from '@sindresorhus/slugify';
|
|
10
|
+
import { nanoid } from 'nanoid';
|
|
11
|
+
import * as crypto2 from 'crypto';
|
|
12
|
+
import { randomBytes } from 'crypto';
|
|
13
|
+
import { pgTable, timestamp, boolean, text, varchar, index, jsonb } from 'drizzle-orm/pg-core';
|
|
14
|
+
import { createSelectSchema, createUpdateSchema, createInsertSchema } from 'drizzle-zod';
|
|
15
|
+
import 'fs';
|
|
16
|
+
import 'fs/promises';
|
|
17
|
+
import 'os';
|
|
18
|
+
import 'path';
|
|
19
|
+
import { PostHog } from 'posthog-node';
|
|
20
|
+
|
|
21
|
+
process.env.SNAPBACK_CLI='true';
|
|
22
|
+
var SnapshotStoreDb = class {
|
|
23
|
+
static {
|
|
24
|
+
__name(this, "SnapshotStoreDb");
|
|
25
|
+
}
|
|
26
|
+
db;
|
|
27
|
+
constructor(db2) {
|
|
28
|
+
this.db = db2;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a new snapshot
|
|
32
|
+
*/
|
|
33
|
+
async createSnapshot(snapshot) {
|
|
34
|
+
const id = crypto.randomUUID();
|
|
35
|
+
const now = /* @__PURE__ */ new Date();
|
|
36
|
+
await this.db.insert(snapshots).values({
|
|
37
|
+
id,
|
|
38
|
+
userId: snapshot.userId,
|
|
39
|
+
apiKeyId: snapshot.apiKeyId,
|
|
40
|
+
name: snapshot.name,
|
|
41
|
+
description: snapshot.description,
|
|
42
|
+
trigger: snapshot.triggerType,
|
|
43
|
+
fileCount: snapshot.fileCount,
|
|
44
|
+
totalSizeBytes: snapshot.totalSizeBytes,
|
|
45
|
+
riskScore: snapshot.riskScore,
|
|
46
|
+
createdAt: now,
|
|
47
|
+
expiresAt: snapshot.expiresAt,
|
|
48
|
+
workspaceId: snapshot.workspaceId
|
|
49
|
+
});
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Add files to a snapshot
|
|
54
|
+
*/
|
|
55
|
+
async addFilesToSnapshot(snapshotId, files) {
|
|
56
|
+
const values = files.map((file) => ({
|
|
57
|
+
id: crypto.randomUUID(),
|
|
58
|
+
snapshotId,
|
|
59
|
+
filePath: file.filePath,
|
|
60
|
+
fileHash: file.fileHash,
|
|
61
|
+
fileSizeBytes: file.fileSizeBytes,
|
|
62
|
+
changeType: file.changeType,
|
|
63
|
+
linesChanged: file.linesChanged,
|
|
64
|
+
containsSecrets: file.containsSecrets,
|
|
65
|
+
riskLevel: file.riskLevel,
|
|
66
|
+
cloudBackupUrl: file.cloudBackupUrl,
|
|
67
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
68
|
+
}));
|
|
69
|
+
if (values.length > 0) {
|
|
70
|
+
await this.db.insert(snapshotFiles).values(values);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* List snapshots for a user
|
|
75
|
+
*/
|
|
76
|
+
async listSnapshots(userId, limit = 50) {
|
|
77
|
+
const result = await this.db.select().from(snapshots).where(eq(snapshots.userId, userId)).orderBy(desc(snapshots.createdAt)).limit(limit);
|
|
78
|
+
return result.map((row) => ({
|
|
79
|
+
id: row.id,
|
|
80
|
+
userId: row.userId,
|
|
81
|
+
apiKeyId: row.apiKeyId,
|
|
82
|
+
workspaceId: row.workspaceId || void 0,
|
|
83
|
+
name: row.name || void 0,
|
|
84
|
+
description: row.description || void 0,
|
|
85
|
+
triggerType: row.trigger,
|
|
86
|
+
fileCount: row.fileCount,
|
|
87
|
+
totalSizeBytes: row.totalSizeBytes,
|
|
88
|
+
riskScore: row.riskScore || void 0,
|
|
89
|
+
createdAt: row.createdAt,
|
|
90
|
+
expiresAt: row.expiresAt || void 0
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fetch a snapshot by ID
|
|
95
|
+
*/
|
|
96
|
+
async fetchSnapshot(id) {
|
|
97
|
+
const result = await this.db.select().from(snapshots).where(eq(snapshots.id, id)).limit(1);
|
|
98
|
+
if (result.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const row = result[0];
|
|
102
|
+
if (!row) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
id: row.id,
|
|
107
|
+
userId: row.userId,
|
|
108
|
+
apiKeyId: row.apiKeyId,
|
|
109
|
+
workspaceId: row.workspaceId || void 0,
|
|
110
|
+
name: row.name || void 0,
|
|
111
|
+
description: row.description || void 0,
|
|
112
|
+
triggerType: row.trigger,
|
|
113
|
+
fileCount: row.fileCount,
|
|
114
|
+
totalSizeBytes: row.totalSizeBytes,
|
|
115
|
+
riskScore: row.riskScore || void 0,
|
|
116
|
+
createdAt: row.createdAt,
|
|
117
|
+
expiresAt: row.expiresAt || void 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Fetch files for a snapshot
|
|
122
|
+
*/
|
|
123
|
+
async fetchSnapshotFiles(snapshotId) {
|
|
124
|
+
const result = await this.db.select().from(snapshotFiles).where(eq(snapshotFiles.snapshotId, snapshotId));
|
|
125
|
+
return result.map((row) => ({
|
|
126
|
+
id: row.id,
|
|
127
|
+
snapshotId: row.snapshotId,
|
|
128
|
+
filePath: row.filePath,
|
|
129
|
+
fileHash: row.fileHash,
|
|
130
|
+
fileSizeBytes: row.fileSizeBytes,
|
|
131
|
+
changeType: row.changeType || void 0,
|
|
132
|
+
linesChanged: row.linesChanged || void 0,
|
|
133
|
+
containsSecrets: row.containsSecrets || void 0,
|
|
134
|
+
riskLevel: row.riskLevel || void 0,
|
|
135
|
+
cloudBackupUrl: row.cloudBackupUrl || void 0,
|
|
136
|
+
createdAt: row.createdAt
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
function redactString(value) {
|
|
141
|
+
return value.replace(/./g, "*");
|
|
142
|
+
}
|
|
143
|
+
__name(redactString, "redactString");
|
|
144
|
+
function redactObject(obj) {
|
|
145
|
+
if (!obj || typeof obj !== "object") {
|
|
146
|
+
return obj;
|
|
147
|
+
}
|
|
148
|
+
const redacted = Array.isArray(obj) ? [] : {};
|
|
149
|
+
for (const key in obj) {
|
|
150
|
+
if (typeof obj[key] === "string") {
|
|
151
|
+
redacted[key] = redactString(obj[key]);
|
|
152
|
+
} else if (typeof obj[key] === "object") {
|
|
153
|
+
redacted[key] = redactObject(obj[key]);
|
|
154
|
+
} else {
|
|
155
|
+
redacted[key] = obj[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return redacted;
|
|
159
|
+
}
|
|
160
|
+
__name(redactObject, "redactObject");
|
|
161
|
+
var SLOW_MS = 200;
|
|
162
|
+
function applyRedaction(event) {
|
|
163
|
+
const redacted = {
|
|
164
|
+
...event
|
|
165
|
+
};
|
|
166
|
+
for (const key of [
|
|
167
|
+
"suggestionText",
|
|
168
|
+
"filePath",
|
|
169
|
+
"userFeedback",
|
|
170
|
+
"errorMessage",
|
|
171
|
+
"feedbackText"
|
|
172
|
+
]) {
|
|
173
|
+
if (key in redacted && redacted[key]) {
|
|
174
|
+
redacted[key] = redactString(String(redacted[key]));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if ("violations" in redacted && redacted.violations) {
|
|
178
|
+
redacted.violations = redactObject(redacted.violations);
|
|
179
|
+
}
|
|
180
|
+
if ("remediationSteps" in redacted && redacted.remediationSteps) {
|
|
181
|
+
redacted.remediationSteps = redactObject(redacted.remediationSteps);
|
|
182
|
+
}
|
|
183
|
+
return redacted;
|
|
184
|
+
}
|
|
185
|
+
__name(applyRedaction, "applyRedaction");
|
|
186
|
+
function logSlowQuery(operationName, durationMs) {
|
|
187
|
+
if (durationMs > SLOW_MS) {
|
|
188
|
+
console.warn(`Slow query detected: ${operationName} took ${durationMs}ms (threshold: ${SLOW_MS}ms)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
__name(logSlowQuery, "logSlowQuery");
|
|
192
|
+
var TelemetrySinkDb = class {
|
|
193
|
+
static {
|
|
194
|
+
__name(this, "TelemetrySinkDb");
|
|
195
|
+
}
|
|
196
|
+
db;
|
|
197
|
+
constructor(db2) {
|
|
198
|
+
this.db = db2;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Insert agent suggestion event with idempotency check
|
|
202
|
+
*/
|
|
203
|
+
async insertAgentSuggestion(event) {
|
|
204
|
+
const startTime = Date.now();
|
|
205
|
+
try {
|
|
206
|
+
const existing = await this.db.select().from(agentSuggestions).where(eq(agentSuggestions.requestId, event.requestId)).limit(1);
|
|
207
|
+
if (existing.length > 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const redacted = applyRedaction(event);
|
|
211
|
+
await this.db.insert(agentSuggestions).values({
|
|
212
|
+
id: crypto.randomUUID(),
|
|
213
|
+
userId: redacted.userId,
|
|
214
|
+
apiKeyId: redacted.apiKeyId,
|
|
215
|
+
sessionId: redacted.sessionId,
|
|
216
|
+
requestId: redacted.requestId,
|
|
217
|
+
suggestionId: redacted.suggestionId,
|
|
218
|
+
suggestionText: redacted.suggestionText,
|
|
219
|
+
suggestionType: redacted.suggestionType,
|
|
220
|
+
filePath: redacted.filePath,
|
|
221
|
+
lineStart: redacted.lineStart,
|
|
222
|
+
lineEnd: redacted.lineEnd,
|
|
223
|
+
characterStart: redacted.characterStart,
|
|
224
|
+
characterEnd: redacted.characterEnd,
|
|
225
|
+
accepted: redacted.accepted,
|
|
226
|
+
dismissed: redacted.dismissed,
|
|
227
|
+
timestamp: redacted.timestamp,
|
|
228
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
229
|
+
});
|
|
230
|
+
const duration = Date.now() - startTime;
|
|
231
|
+
logSlowQuery("insertAgentSuggestion", duration);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
await this.db.insert(quarantineEvents).values({
|
|
234
|
+
id: crypto.randomUUID(),
|
|
235
|
+
userId: event.userId,
|
|
236
|
+
apiKeyId: event.apiKeyId,
|
|
237
|
+
originalEvent: event,
|
|
238
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
239
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
240
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
241
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
242
|
+
});
|
|
243
|
+
console.error("Failed to insert agent suggestion", {
|
|
244
|
+
requestId: event.requestId,
|
|
245
|
+
error: error instanceof Error ? error.message : String(error)
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Insert post-accept outcome event with idempotency check
|
|
251
|
+
*/
|
|
252
|
+
async insertPostAcceptOutcome(event) {
|
|
253
|
+
const startTime = Date.now();
|
|
254
|
+
try {
|
|
255
|
+
const existing = await this.db.select().from(postAcceptOutcomes).where(eq(postAcceptOutcomes.suggestionId, event.suggestionId)).limit(1);
|
|
256
|
+
if (existing.length > 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const redacted = applyRedaction(event);
|
|
260
|
+
await this.db.insert(postAcceptOutcomes).values({
|
|
261
|
+
id: crypto.randomUUID(),
|
|
262
|
+
userId: redacted.userId,
|
|
263
|
+
apiKeyId: redacted.apiKeyId,
|
|
264
|
+
suggestionId: redacted.suggestionId,
|
|
265
|
+
editsMade: redacted.editsMade,
|
|
266
|
+
timeToEditMs: redacted.timeToEditMs,
|
|
267
|
+
timeToSubmitMs: redacted.timeToSubmitMs,
|
|
268
|
+
userFeedback: redacted.userFeedback,
|
|
269
|
+
timestamp: redacted.timestamp,
|
|
270
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
271
|
+
});
|
|
272
|
+
const duration = Date.now() - startTime;
|
|
273
|
+
logSlowQuery("insertPostAcceptOutcome", duration);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
await this.db.insert(quarantineEvents).values({
|
|
276
|
+
id: crypto.randomUUID(),
|
|
277
|
+
userId: event.userId,
|
|
278
|
+
apiKeyId: event.apiKeyId,
|
|
279
|
+
originalEvent: event,
|
|
280
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
281
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
282
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
283
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
284
|
+
});
|
|
285
|
+
console.error("Failed to insert post-accept outcome", {
|
|
286
|
+
suggestionId: event.suggestionId,
|
|
287
|
+
error: error instanceof Error ? error.message : String(error)
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Insert policy evaluation event with idempotency check
|
|
293
|
+
*/
|
|
294
|
+
async insertPolicyEvaluation(event) {
|
|
295
|
+
const startTime = Date.now();
|
|
296
|
+
try {
|
|
297
|
+
const existing = await this.db.select().from(policyEvaluations).where(eq(policyEvaluations.requestId, event.requestId)).limit(1);
|
|
298
|
+
if (existing.length > 0) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const redacted = applyRedaction(event);
|
|
302
|
+
await this.db.insert(policyEvaluations).values({
|
|
303
|
+
id: crypto.randomUUID(),
|
|
304
|
+
userId: redacted.userId,
|
|
305
|
+
apiKeyId: redacted.apiKeyId,
|
|
306
|
+
sessionId: redacted.sessionId,
|
|
307
|
+
requestId: redacted.requestId,
|
|
308
|
+
policyName: redacted.policyName,
|
|
309
|
+
policyVersion: redacted.policyVersion,
|
|
310
|
+
evaluationResult: redacted.evaluationResult,
|
|
311
|
+
violations: redacted.violations,
|
|
312
|
+
remediationSteps: redacted.remediationSteps,
|
|
313
|
+
timestamp: redacted.timestamp,
|
|
314
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
315
|
+
});
|
|
316
|
+
const duration = Date.now() - startTime;
|
|
317
|
+
logSlowQuery("insertPolicyEvaluation", duration);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
await this.db.insert(quarantineEvents).values({
|
|
320
|
+
id: crypto.randomUUID(),
|
|
321
|
+
userId: event.userId,
|
|
322
|
+
apiKeyId: event.apiKeyId,
|
|
323
|
+
originalEvent: event,
|
|
324
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
325
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
326
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
327
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
328
|
+
});
|
|
329
|
+
console.error("Failed to insert policy evaluation", {
|
|
330
|
+
requestId: event.requestId,
|
|
331
|
+
error: error instanceof Error ? error.message : String(error)
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Insert loop event with idempotency check
|
|
337
|
+
*/
|
|
338
|
+
async insertLoop(event) {
|
|
339
|
+
const startTime = Date.now();
|
|
340
|
+
try {
|
|
341
|
+
const existing = await this.db.select().from(loops).where(eq(loops.requestId, event.requestId)).limit(1);
|
|
342
|
+
if (existing.length > 0) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const redacted = applyRedaction(event);
|
|
346
|
+
await this.db.insert(loops).values({
|
|
347
|
+
id: crypto.randomUUID(),
|
|
348
|
+
userId: redacted.userId,
|
|
349
|
+
apiKeyId: redacted.apiKeyId,
|
|
350
|
+
sessionId: redacted.sessionId,
|
|
351
|
+
requestId: redacted.requestId,
|
|
352
|
+
loopType: redacted.loopType,
|
|
353
|
+
iterationCount: redacted.iterationCount,
|
|
354
|
+
durationMs: redacted.durationMs,
|
|
355
|
+
success: redacted.success,
|
|
356
|
+
errorMessage: redacted.errorMessage,
|
|
357
|
+
timestamp: redacted.timestamp,
|
|
358
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
359
|
+
});
|
|
360
|
+
const duration = Date.now() - startTime;
|
|
361
|
+
logSlowQuery("insertLoop", duration);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
await this.db.insert(quarantineEvents).values({
|
|
364
|
+
id: crypto.randomUUID(),
|
|
365
|
+
userId: event.userId,
|
|
366
|
+
apiKeyId: event.apiKeyId,
|
|
367
|
+
originalEvent: event,
|
|
368
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
369
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
370
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
371
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
372
|
+
});
|
|
373
|
+
console.error("Failed to insert loop", {
|
|
374
|
+
requestId: event.requestId,
|
|
375
|
+
error: error instanceof Error ? error.message : String(error)
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Insert feedback event with idempotency check
|
|
381
|
+
*/
|
|
382
|
+
async insertFeedback(event) {
|
|
383
|
+
const startTime = Date.now();
|
|
384
|
+
try {
|
|
385
|
+
const existing = await this.db.select().from(feedback).where(eq(feedback.requestId, event.requestId)).limit(1);
|
|
386
|
+
if (existing.length > 0) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const redacted = applyRedaction(event);
|
|
390
|
+
await this.db.insert(feedback).values({
|
|
391
|
+
id: crypto.randomUUID(),
|
|
392
|
+
userId: redacted.userId,
|
|
393
|
+
apiKeyId: redacted.apiKeyId,
|
|
394
|
+
sessionId: redacted.sessionId,
|
|
395
|
+
requestId: redacted.requestId,
|
|
396
|
+
feedbackType: redacted.feedbackType,
|
|
397
|
+
feedbackText: redacted.feedbackText,
|
|
398
|
+
rating: redacted.rating,
|
|
399
|
+
metadata: redacted.metadata,
|
|
400
|
+
timestamp: redacted.timestamp,
|
|
401
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
402
|
+
});
|
|
403
|
+
const duration = Date.now() - startTime;
|
|
404
|
+
logSlowQuery("insertFeedback", duration);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
await this.db.insert(quarantineEvents).values({
|
|
407
|
+
id: crypto.randomUUID(),
|
|
408
|
+
userId: event.userId,
|
|
409
|
+
apiKeyId: event.apiKeyId,
|
|
410
|
+
originalEvent: event,
|
|
411
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
412
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
413
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
414
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
415
|
+
});
|
|
416
|
+
console.error("Failed to insert feedback", {
|
|
417
|
+
requestId: event.requestId,
|
|
418
|
+
error: error instanceof Error ? error.message : String(error)
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Batch insert agent suggestions
|
|
424
|
+
*/
|
|
425
|
+
async batchInsertAgentSuggestions(events) {
|
|
426
|
+
const startTime = Date.now();
|
|
427
|
+
if (events.length === 0) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const values = events.map((event) => {
|
|
432
|
+
const redacted = applyRedaction(event);
|
|
433
|
+
return {
|
|
434
|
+
id: crypto.randomUUID(),
|
|
435
|
+
userId: redacted.userId,
|
|
436
|
+
apiKeyId: redacted.apiKeyId,
|
|
437
|
+
sessionId: redacted.sessionId,
|
|
438
|
+
requestId: redacted.requestId,
|
|
439
|
+
suggestionId: redacted.suggestionId,
|
|
440
|
+
suggestionText: redacted.suggestionText,
|
|
441
|
+
suggestionType: redacted.suggestionType,
|
|
442
|
+
filePath: redacted.filePath,
|
|
443
|
+
lineStart: redacted.lineStart,
|
|
444
|
+
lineEnd: redacted.lineEnd,
|
|
445
|
+
characterStart: redacted.characterStart,
|
|
446
|
+
characterEnd: redacted.characterEnd,
|
|
447
|
+
accepted: redacted.accepted,
|
|
448
|
+
dismissed: redacted.dismissed,
|
|
449
|
+
timestamp: redacted.timestamp,
|
|
450
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
await this.db.insert(agentSuggestions).values(values);
|
|
454
|
+
const duration = Date.now() - startTime;
|
|
455
|
+
logSlowQuery(`batchInsertAgentSuggestions(${events.length} items)`, duration);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
for (const event of events) {
|
|
458
|
+
await this.db.insert(quarantineEvents).values({
|
|
459
|
+
id: crypto.randomUUID(),
|
|
460
|
+
userId: event.userId,
|
|
461
|
+
apiKeyId: event.apiKeyId,
|
|
462
|
+
originalEvent: event,
|
|
463
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
464
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
465
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
466
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
console.error("Failed to batch insert agent suggestions", {
|
|
470
|
+
count: events.length,
|
|
471
|
+
error: error instanceof Error ? error.message : String(error)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Batch insert policy evaluations
|
|
477
|
+
*/
|
|
478
|
+
async batchInsertPolicyEvaluations(events) {
|
|
479
|
+
const startTime = Date.now();
|
|
480
|
+
if (events.length === 0) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const values = events.map((event) => {
|
|
485
|
+
const redacted = applyRedaction(event);
|
|
486
|
+
return {
|
|
487
|
+
id: crypto.randomUUID(),
|
|
488
|
+
userId: redacted.userId,
|
|
489
|
+
apiKeyId: redacted.apiKeyId,
|
|
490
|
+
sessionId: redacted.sessionId,
|
|
491
|
+
requestId: redacted.requestId,
|
|
492
|
+
policyName: redacted.policyName,
|
|
493
|
+
policyVersion: redacted.policyVersion,
|
|
494
|
+
evaluationResult: redacted.evaluationResult,
|
|
495
|
+
violations: redacted.violations,
|
|
496
|
+
remediationSteps: redacted.remediationSteps,
|
|
497
|
+
timestamp: redacted.timestamp,
|
|
498
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
await this.db.insert(policyEvaluations).values(values);
|
|
502
|
+
const duration = Date.now() - startTime;
|
|
503
|
+
logSlowQuery(`batchInsertPolicyEvaluations(${events.length} items)`, duration);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
for (const event of events) {
|
|
506
|
+
await this.db.insert(quarantineEvents).values({
|
|
507
|
+
id: crypto.randomUUID(),
|
|
508
|
+
userId: event.userId,
|
|
509
|
+
apiKeyId: event.apiKeyId,
|
|
510
|
+
originalEvent: event,
|
|
511
|
+
errorReason: error instanceof Error ? error.message : String(error),
|
|
512
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
513
|
+
attemptedAt: /* @__PURE__ */ new Date(),
|
|
514
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
console.error("Failed to batch insert policy evaluations", {
|
|
518
|
+
count: events.length,
|
|
519
|
+
error: error instanceof Error ? error.message : String(error)
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// ../../packages/platform/dist/db/database-service.js
|
|
526
|
+
var databaseService = {
|
|
527
|
+
drizzle: db,
|
|
528
|
+
isConnected: /* @__PURE__ */ __name(async () => {
|
|
529
|
+
return await checkDatabaseConnection();
|
|
530
|
+
}, "isConnected"),
|
|
531
|
+
disconnect: /* @__PURE__ */ __name(async () => {
|
|
532
|
+
await closeDatabaseConnection();
|
|
533
|
+
}, "disconnect")
|
|
534
|
+
};
|
|
535
|
+
var healthCheck = /* @__PURE__ */ __name(async () => {
|
|
536
|
+
const connected = await checkDatabaseConnection();
|
|
537
|
+
return {
|
|
538
|
+
connected,
|
|
539
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
540
|
+
};
|
|
541
|
+
}, "healthCheck");
|
|
542
|
+
var { userDetectionCapabilities, capabilityAudit } = combinedSchema;
|
|
543
|
+
var userIdSchema = z.string().min(1, "User ID required").transform((val) => val.trim()).refine((val) => val.length > 0, "User ID cannot be whitespace");
|
|
544
|
+
var countSchema = z.number().int().positive("Count must be a positive integer");
|
|
545
|
+
var accuracyScoreSchema = z.number().min(0, "Accuracy score must be between 0.0 and 1.0").max(1, "Accuracy score must be between 0.0 and 1.0");
|
|
546
|
+
var capabilityCache = /* @__PURE__ */ new Map();
|
|
547
|
+
var CACHE_TTL_MS = 60 * 1e3;
|
|
548
|
+
var MAX_CACHE_SIZE = 1e4;
|
|
549
|
+
var cacheAccessOrder = [];
|
|
550
|
+
var cacheHits = 0;
|
|
551
|
+
var cacheMisses = 0;
|
|
552
|
+
function getCacheMetrics() {
|
|
553
|
+
const total = cacheHits + cacheMisses;
|
|
554
|
+
return {
|
|
555
|
+
hits: cacheHits,
|
|
556
|
+
misses: cacheMisses,
|
|
557
|
+
hitRate: total > 0 ? cacheHits / total : 0
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
__name(getCacheMetrics, "getCacheMetrics");
|
|
561
|
+
function resetCacheMetrics() {
|
|
562
|
+
cacheHits = 0;
|
|
563
|
+
cacheMisses = 0;
|
|
564
|
+
}
|
|
565
|
+
__name(resetCacheMetrics, "resetCacheMetrics");
|
|
566
|
+
function invalidateCapabilityCache(userId) {
|
|
567
|
+
capabilityCache.delete(userId);
|
|
568
|
+
const index2 = cacheAccessOrder.indexOf(userId);
|
|
569
|
+
if (index2 >= 0) {
|
|
570
|
+
cacheAccessOrder.splice(index2, 1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
__name(invalidateCapabilityCache, "invalidateCapabilityCache");
|
|
574
|
+
function clearCapabilityCache() {
|
|
575
|
+
capabilityCache.clear();
|
|
576
|
+
cacheAccessOrder.length = 0;
|
|
577
|
+
cacheHits = 0;
|
|
578
|
+
cacheMisses = 0;
|
|
579
|
+
}
|
|
580
|
+
__name(clearCapabilityCache, "clearCapabilityCache");
|
|
581
|
+
function updateCacheWithLRU(userId, data) {
|
|
582
|
+
const existingIndex = cacheAccessOrder.indexOf(userId);
|
|
583
|
+
if (existingIndex >= 0) {
|
|
584
|
+
cacheAccessOrder.splice(existingIndex, 1);
|
|
585
|
+
}
|
|
586
|
+
cacheAccessOrder.push(userId);
|
|
587
|
+
if (cacheAccessOrder.length > MAX_CACHE_SIZE) {
|
|
588
|
+
const evictKey = cacheAccessOrder.shift();
|
|
589
|
+
if (evictKey) {
|
|
590
|
+
capabilityCache.delete(evictKey);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
capabilityCache.set(userId, data);
|
|
594
|
+
}
|
|
595
|
+
__name(updateCacheWithLRU, "updateCacheWithLRU");
|
|
596
|
+
async function getCapabilities(userId, options = {}) {
|
|
597
|
+
const validatedUserId = userIdSchema.parse(userId);
|
|
598
|
+
if (!options.skipCache) {
|
|
599
|
+
const cached = capabilityCache.get(validatedUserId);
|
|
600
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
601
|
+
cacheHits++;
|
|
602
|
+
return cached.data;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
cacheMisses++;
|
|
606
|
+
if (!db) {
|
|
607
|
+
throw new Error("Database not available");
|
|
608
|
+
}
|
|
609
|
+
let capabilities = await db.query.userDetectionCapabilities.findFirst({
|
|
610
|
+
where: /* @__PURE__ */ __name((cap, { eq: eq15 }) => eq15(cap.userId, validatedUserId), "where")
|
|
611
|
+
});
|
|
612
|
+
if (!capabilities) {
|
|
613
|
+
const [created] = await db.insert(userDetectionCapabilities).values({
|
|
614
|
+
userId: validatedUserId,
|
|
615
|
+
tier: "free"
|
|
616
|
+
}).onConflictDoUpdate({
|
|
617
|
+
target: userDetectionCapabilities.userId,
|
|
618
|
+
set: {
|
|
619
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
620
|
+
}
|
|
621
|
+
}).returning();
|
|
622
|
+
capabilities = created;
|
|
623
|
+
}
|
|
624
|
+
if (!capabilities) {
|
|
625
|
+
throw new Error("Failed to fetch or create capabilities");
|
|
626
|
+
}
|
|
627
|
+
updateCacheWithLRU(validatedUserId, {
|
|
628
|
+
data: capabilities,
|
|
629
|
+
timestamp: Date.now()
|
|
630
|
+
});
|
|
631
|
+
return capabilities;
|
|
632
|
+
}
|
|
633
|
+
__name(getCapabilities, "getCapabilities");
|
|
634
|
+
async function updateCapabilities(userId, updates, expectedVersion) {
|
|
635
|
+
if (!db) {
|
|
636
|
+
throw new Error("Database not available");
|
|
637
|
+
}
|
|
638
|
+
const validatedUserId = userIdSchema.parse(userId);
|
|
639
|
+
const updateValues = {
|
|
640
|
+
lastUpdated: /* @__PURE__ */ new Date(),
|
|
641
|
+
version: sql`${userDetectionCapabilities.version} + 1`
|
|
642
|
+
};
|
|
643
|
+
if (updates.falsePositivePatterns !== void 0) {
|
|
644
|
+
updateValues.falsePositivePatterns = updates.falsePositivePatterns;
|
|
645
|
+
}
|
|
646
|
+
if (updates.customRiskIndicators !== void 0) {
|
|
647
|
+
updateValues.customRiskIndicators = updates.customRiskIndicators;
|
|
648
|
+
}
|
|
649
|
+
if (updates.thresholdOverrides !== void 0) {
|
|
650
|
+
updateValues.thresholdOverrides = updates.thresholdOverrides;
|
|
651
|
+
}
|
|
652
|
+
if (updates.accuracyScore !== void 0) {
|
|
653
|
+
const validatedScore = accuracyScoreSchema.parse(updates.accuracyScore);
|
|
654
|
+
updateValues.accuracyScore = validatedScore.toFixed(4);
|
|
655
|
+
}
|
|
656
|
+
if (updates.toolAccuracy !== void 0) {
|
|
657
|
+
updateValues.toolAccuracy = updates.toolAccuracy;
|
|
658
|
+
}
|
|
659
|
+
if (updates.totalDetectionsAnalyzed !== void 0) {
|
|
660
|
+
updateValues.totalDetectionsAnalyzed = updates.totalDetectionsAnalyzed;
|
|
661
|
+
}
|
|
662
|
+
if (updates.tier !== void 0) {
|
|
663
|
+
updateValues.tier = updates.tier;
|
|
664
|
+
}
|
|
665
|
+
let result;
|
|
666
|
+
if (expectedVersion != null) {
|
|
667
|
+
result = await db.update(userDetectionCapabilities).set(updateValues).where(sql`${userDetectionCapabilities.userId} = ${validatedUserId} AND ${userDetectionCapabilities.version} = ${expectedVersion}`).returning();
|
|
668
|
+
} else {
|
|
669
|
+
result = await db.update(userDetectionCapabilities).set(updateValues).where(eq(userDetectionCapabilities.userId, validatedUserId)).returning();
|
|
670
|
+
}
|
|
671
|
+
invalidateCapabilityCache(validatedUserId);
|
|
672
|
+
return result.length > 0 ? result[0] ?? null : null;
|
|
673
|
+
}
|
|
674
|
+
__name(updateCapabilities, "updateCapabilities");
|
|
675
|
+
async function appendFalsePositivePatterns(userId, patterns2) {
|
|
676
|
+
if (!db || patterns2.length === 0) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const current = await getCapabilities(userId, {
|
|
680
|
+
skipCache: true
|
|
681
|
+
});
|
|
682
|
+
const existingKeys = new Set(current.falsePositivePatterns?.map((p) => `${p.patternKey}:${p.aiTool}`) ?? []);
|
|
683
|
+
const newPatterns = patterns2.filter((p) => !existingKeys.has(`${p.patternKey}:${p.aiTool}`));
|
|
684
|
+
const mergedPatterns = [
|
|
685
|
+
...current.falsePositivePatterns ?? [],
|
|
686
|
+
...newPatterns
|
|
687
|
+
];
|
|
688
|
+
const version = current.version ?? void 0;
|
|
689
|
+
return await updateCapabilities(userId, {
|
|
690
|
+
falsePositivePatterns: mergedPatterns
|
|
691
|
+
}, version);
|
|
692
|
+
}
|
|
693
|
+
__name(appendFalsePositivePatterns, "appendFalsePositivePatterns");
|
|
694
|
+
async function incrementDetectionsAnalyzed(userId, count) {
|
|
695
|
+
if (!db) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const validatedCount = countSchema.parse(count);
|
|
699
|
+
await db.update(userDetectionCapabilities).set({
|
|
700
|
+
totalDetectionsAnalyzed: sql`${userDetectionCapabilities.totalDetectionsAnalyzed} + ${validatedCount}`,
|
|
701
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
702
|
+
}).where(eq(userDetectionCapabilities.userId, userId));
|
|
703
|
+
invalidateCapabilityCache(userId);
|
|
704
|
+
}
|
|
705
|
+
__name(incrementDetectionsAnalyzed, "incrementDetectionsAnalyzed");
|
|
706
|
+
async function handleTierUpgrade(userId, newTier, options = {}) {
|
|
707
|
+
if (!db) {
|
|
708
|
+
throw new Error("Database not available");
|
|
709
|
+
}
|
|
710
|
+
const current = await getCapabilities(userId, {
|
|
711
|
+
skipCache: true
|
|
712
|
+
});
|
|
713
|
+
const oldTier = current.tier ?? "free";
|
|
714
|
+
const version = current.version ?? void 0;
|
|
715
|
+
const updated = await updateCapabilities(userId, {
|
|
716
|
+
tier: newTier
|
|
717
|
+
}, version);
|
|
718
|
+
if (!updated) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
await logCapabilityAudit({
|
|
722
|
+
userId,
|
|
723
|
+
capabilityType: "tier_upgraded",
|
|
724
|
+
change: {
|
|
725
|
+
type: "tier_upgraded",
|
|
726
|
+
tier: {
|
|
727
|
+
oldTier,
|
|
728
|
+
newTier
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
reason: options.reason ?? "User subscription upgraded",
|
|
732
|
+
sessionId: options.sessionId,
|
|
733
|
+
workspaceId: options.workspaceId
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
__name(handleTierUpgrade, "handleTierUpgrade");
|
|
737
|
+
async function handleTierDowngrade(userId, options = {}) {
|
|
738
|
+
if (!db) {
|
|
739
|
+
throw new Error("Database not available");
|
|
740
|
+
}
|
|
741
|
+
const current = await getCapabilities(userId, {
|
|
742
|
+
skipCache: true
|
|
743
|
+
});
|
|
744
|
+
const oldTier = current.tier ?? "free";
|
|
745
|
+
const clearedCapabilities = [];
|
|
746
|
+
if ((current.customRiskIndicators?.length ?? 0) > 0) {
|
|
747
|
+
clearedCapabilities.push("customRiskIndicators");
|
|
748
|
+
}
|
|
749
|
+
if (Object.keys(current.thresholdOverrides ?? {}).length > 0) {
|
|
750
|
+
clearedCapabilities.push("thresholdOverrides");
|
|
751
|
+
}
|
|
752
|
+
if (Object.keys(current.toolAccuracy ?? {}).length > 0) {
|
|
753
|
+
clearedCapabilities.push("toolAccuracy");
|
|
754
|
+
}
|
|
755
|
+
const version = current.version ?? void 0;
|
|
756
|
+
const updated = await updateCapabilities(userId, {
|
|
757
|
+
tier: "free",
|
|
758
|
+
customRiskIndicators: [],
|
|
759
|
+
thresholdOverrides: {},
|
|
760
|
+
toolAccuracy: {}
|
|
761
|
+
}, version);
|
|
762
|
+
if (!updated) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
await logCapabilityAudit({
|
|
766
|
+
userId,
|
|
767
|
+
capabilityType: "tier_downgraded",
|
|
768
|
+
change: {
|
|
769
|
+
type: "tier_downgraded",
|
|
770
|
+
tier: {
|
|
771
|
+
oldTier,
|
|
772
|
+
newTier: "free",
|
|
773
|
+
clearedCapabilities
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
reason: options.reason ?? "User subscription downgraded",
|
|
777
|
+
sessionId: options.sessionId,
|
|
778
|
+
workspaceId: options.workspaceId
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
__name(handleTierDowngrade, "handleTierDowngrade");
|
|
782
|
+
function computeCapabilityAuditIdempotencyKey(params) {
|
|
783
|
+
if (params.idempotencyKey) {
|
|
784
|
+
return params.idempotencyKey;
|
|
785
|
+
}
|
|
786
|
+
const parts = [
|
|
787
|
+
params.userId,
|
|
788
|
+
params.capabilityType,
|
|
789
|
+
params.sessionId ?? "",
|
|
790
|
+
params.workspaceId ?? "",
|
|
791
|
+
params.clientType ?? "",
|
|
792
|
+
params.reason ?? ""
|
|
793
|
+
];
|
|
794
|
+
if (params.change) {
|
|
795
|
+
parts.push(JSON.stringify(params.change));
|
|
796
|
+
}
|
|
797
|
+
if (params.performanceBefore) {
|
|
798
|
+
parts.push(JSON.stringify(params.performanceBefore));
|
|
799
|
+
}
|
|
800
|
+
if (params.performanceAfter) {
|
|
801
|
+
parts.push(JSON.stringify(params.performanceAfter));
|
|
802
|
+
}
|
|
803
|
+
return parts.join("|");
|
|
804
|
+
}
|
|
805
|
+
__name(computeCapabilityAuditIdempotencyKey, "computeCapabilityAuditIdempotencyKey");
|
|
806
|
+
async function logCapabilityAudit(params) {
|
|
807
|
+
if (!db) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const idempotencyKey = computeCapabilityAuditIdempotencyKey(params);
|
|
811
|
+
const values = {
|
|
812
|
+
userId: params.userId,
|
|
813
|
+
capabilityType: params.capabilityType,
|
|
814
|
+
change: params.change,
|
|
815
|
+
reason: params.reason,
|
|
816
|
+
performanceBefore: params.performanceBefore,
|
|
817
|
+
performanceAfter: params.performanceAfter,
|
|
818
|
+
sessionId: params.sessionId,
|
|
819
|
+
workspaceId: params.workspaceId,
|
|
820
|
+
clientType: params.clientType,
|
|
821
|
+
idempotencyKey
|
|
822
|
+
};
|
|
823
|
+
await db.insert(capabilityAudit).values(values).onConflictDoNothing({
|
|
824
|
+
target: capabilityAudit.idempotencyKey
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
__name(logCapabilityAudit, "logCapabilityAudit");
|
|
828
|
+
async function getCapabilityAuditHistory(userId, limit = 50) {
|
|
829
|
+
if (!db) {
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
return await db.query.capabilityAudit.findMany({
|
|
833
|
+
where: /* @__PURE__ */ __name((audit, { eq: eq15 }) => eq15(audit.userId, userId), "where"),
|
|
834
|
+
orderBy: /* @__PURE__ */ __name((audit, { desc: desc4 }) => desc4(audit.createdAt), "orderBy"),
|
|
835
|
+
limit
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
__name(getCapabilityAuditHistory, "getCapabilityAuditHistory");
|
|
839
|
+
async function recordFalsePositiveSignal(userId, signal, options = {}) {
|
|
840
|
+
if (!db) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const current = await getCapabilities(userId, {
|
|
844
|
+
skipCache: true
|
|
845
|
+
});
|
|
846
|
+
const patterns2 = [
|
|
847
|
+
...current.falsePositivePatterns ?? []
|
|
848
|
+
];
|
|
849
|
+
const existingIndex = patterns2.findIndex((p) => p.patternKey === signal.patternKey && p.aiTool === signal.aiTool && p.filePattern === signal.filePattern);
|
|
850
|
+
let learned;
|
|
851
|
+
if (existingIndex >= 0) {
|
|
852
|
+
const existingPattern = patterns2[existingIndex];
|
|
853
|
+
if (!existingPattern) {
|
|
854
|
+
throw new Error("Pattern not found at expected index");
|
|
855
|
+
}
|
|
856
|
+
learned = mergeSignalIntoPattern(existingPattern, signal);
|
|
857
|
+
patterns2[existingIndex] = learned;
|
|
858
|
+
} else {
|
|
859
|
+
learned = signalToPattern(signal);
|
|
860
|
+
patterns2.push(learned);
|
|
861
|
+
}
|
|
862
|
+
const version = current.version ?? void 0;
|
|
863
|
+
const updated = await updateCapabilities(userId, {
|
|
864
|
+
falsePositivePatterns: patterns2
|
|
865
|
+
}, version);
|
|
866
|
+
if (!updated) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
await logCapabilityAudit({
|
|
870
|
+
userId,
|
|
871
|
+
capabilityType: "pattern_learned",
|
|
872
|
+
change: {
|
|
873
|
+
type: "pattern_learned",
|
|
874
|
+
patterns: [
|
|
875
|
+
{
|
|
876
|
+
patternKey: learned.patternKey,
|
|
877
|
+
aiTool: learned.aiTool,
|
|
878
|
+
weight: learned.weight,
|
|
879
|
+
decayedWeight: learned.decayedWeight,
|
|
880
|
+
source: signal.type
|
|
881
|
+
}
|
|
882
|
+
]
|
|
883
|
+
},
|
|
884
|
+
reason: options.reason ?? "False positive feedback recorded",
|
|
885
|
+
performanceBefore: void 0,
|
|
886
|
+
performanceAfter: void 0,
|
|
887
|
+
sessionId: options.sessionId,
|
|
888
|
+
workspaceId: options.workspaceId,
|
|
889
|
+
clientType: options.clientType
|
|
890
|
+
});
|
|
891
|
+
return updated;
|
|
892
|
+
}
|
|
893
|
+
__name(recordFalsePositiveSignal, "recordFalsePositiveSignal");
|
|
894
|
+
async function resetCapabilities(userId, options = {}) {
|
|
895
|
+
if (!db) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
const current = await getCapabilities(userId, {
|
|
899
|
+
skipCache: true
|
|
900
|
+
});
|
|
901
|
+
const version = current.version ?? void 0;
|
|
902
|
+
const updated = await updateCapabilities(userId, {
|
|
903
|
+
falsePositivePatterns: [],
|
|
904
|
+
customRiskIndicators: [],
|
|
905
|
+
thresholdOverrides: {},
|
|
906
|
+
accuracyScore: 0,
|
|
907
|
+
toolAccuracy: {},
|
|
908
|
+
totalDetectionsAnalyzed: 0,
|
|
909
|
+
tier: "free"
|
|
910
|
+
}, version);
|
|
911
|
+
if (!updated) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
await logCapabilityAudit({
|
|
915
|
+
userId,
|
|
916
|
+
capabilityType: "capabilities_reset",
|
|
917
|
+
change: {
|
|
918
|
+
type: "capabilities_reset",
|
|
919
|
+
reason: options.reason ?? "Capabilities reset to baseline"
|
|
920
|
+
},
|
|
921
|
+
reason: options.reason ?? "Capabilities reset to baseline",
|
|
922
|
+
performanceBefore: void 0,
|
|
923
|
+
performanceAfter: void 0,
|
|
924
|
+
sessionId: options.sessionId,
|
|
925
|
+
workspaceId: options.workspaceId,
|
|
926
|
+
clientType: options.clientType
|
|
927
|
+
});
|
|
928
|
+
return updated;
|
|
929
|
+
}
|
|
930
|
+
__name(resetCapabilities, "resetCapabilities");
|
|
931
|
+
var IMPLICIT_WEIGHT = 1;
|
|
932
|
+
var EXPLICIT_WEIGHT = 3;
|
|
933
|
+
var DECAY_HALF_LIFE_DAYS = 14;
|
|
934
|
+
function calculateDecayedWeight(signal) {
|
|
935
|
+
const baseWeight = signal.type === "explicit" ? EXPLICIT_WEIGHT : IMPLICIT_WEIGHT;
|
|
936
|
+
const timestamp3 = signal.timestamp ?? Date.now();
|
|
937
|
+
const ageMs = Date.now() - timestamp3;
|
|
938
|
+
const ageDays = ageMs / (24 * 60 * 60 * 1e3);
|
|
939
|
+
const decayFactor = 0.5 ** (ageDays / DECAY_HALF_LIFE_DAYS);
|
|
940
|
+
return baseWeight * decayFactor;
|
|
941
|
+
}
|
|
942
|
+
__name(calculateDecayedWeight, "calculateDecayedWeight");
|
|
943
|
+
function signalToPattern(signal) {
|
|
944
|
+
const weight = signal.type === "explicit" ? EXPLICIT_WEIGHT : IMPLICIT_WEIGHT;
|
|
945
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
946
|
+
return {
|
|
947
|
+
patternKey: signal.patternKey,
|
|
948
|
+
aiTool: signal.aiTool,
|
|
949
|
+
filePattern: signal.filePattern,
|
|
950
|
+
proceedCount: 1,
|
|
951
|
+
weight,
|
|
952
|
+
decayedWeight: calculateDecayedWeight(signal),
|
|
953
|
+
firstSeen: now,
|
|
954
|
+
lastSeen: now
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
__name(signalToPattern, "signalToPattern");
|
|
958
|
+
function mergeSignalIntoPattern(existing, signal) {
|
|
959
|
+
const newWeight = signal.type === "explicit" ? EXPLICIT_WEIGHT : IMPLICIT_WEIGHT;
|
|
960
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
961
|
+
return {
|
|
962
|
+
...existing,
|
|
963
|
+
proceedCount: existing.proceedCount + 1,
|
|
964
|
+
weight: existing.weight + newWeight,
|
|
965
|
+
decayedWeight: calculateDecayedWeight({
|
|
966
|
+
...signal,
|
|
967
|
+
timestamp: Date.now()
|
|
968
|
+
}),
|
|
969
|
+
lastSeen: now
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
__name(mergeSignalIntoPattern, "mergeSignalIntoPattern");
|
|
973
|
+
var { organization: organization2, member: member2 } = combinedSchema;
|
|
974
|
+
async function getOrganizations({ limit, offset, query }) {
|
|
975
|
+
if (!db) {
|
|
976
|
+
throw new Error("Database not available");
|
|
977
|
+
}
|
|
978
|
+
return await db.query.organization.findMany({
|
|
979
|
+
where: query ? (org, { like }) => like(org.name, `%${query}%`) : void 0,
|
|
980
|
+
limit,
|
|
981
|
+
offset,
|
|
982
|
+
extras: {
|
|
983
|
+
membersCount: sql`(SELECT COUNT(*) FROM ${member2} WHERE ${member2.organizationId} = ${organization2.id})`.as("membersCount")
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
__name(getOrganizations, "getOrganizations");
|
|
988
|
+
async function countAllOrganizations() {
|
|
989
|
+
if (!db) {
|
|
990
|
+
throw new Error("Database not available");
|
|
991
|
+
}
|
|
992
|
+
return await db.$count(organization2);
|
|
993
|
+
}
|
|
994
|
+
__name(countAllOrganizations, "countAllOrganizations");
|
|
995
|
+
async function getOrganizationById(id) {
|
|
996
|
+
if (!db) {
|
|
997
|
+
throw new Error("Database not available");
|
|
998
|
+
}
|
|
999
|
+
return await db.query.organization.findFirst({
|
|
1000
|
+
where: /* @__PURE__ */ __name((org, { eq: eq15 }) => eq15(org.id, id), "where"),
|
|
1001
|
+
with: {
|
|
1002
|
+
members: true,
|
|
1003
|
+
invitations: true
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
__name(getOrganizationById, "getOrganizationById");
|
|
1008
|
+
async function getOrganizationsWithMembers(userId) {
|
|
1009
|
+
if (!db) {
|
|
1010
|
+
throw new Error("Database not available");
|
|
1011
|
+
}
|
|
1012
|
+
return await db.query.organization.findMany({
|
|
1013
|
+
with: {
|
|
1014
|
+
members: {
|
|
1015
|
+
where: /* @__PURE__ */ __name((member3, { eq: eq15 }) => eq15(member3.userId, userId), "where"),
|
|
1016
|
+
with: {
|
|
1017
|
+
user: true
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
__name(getOrganizationsWithMembers, "getOrganizationsWithMembers");
|
|
1024
|
+
async function getInvitationById(id) {
|
|
1025
|
+
if (!db) {
|
|
1026
|
+
throw new Error("Database not available");
|
|
1027
|
+
}
|
|
1028
|
+
return await db.query.invitation.findFirst({
|
|
1029
|
+
where: /* @__PURE__ */ __name((invitation2, { eq: eq15 }) => eq15(invitation2.id, id), "where"),
|
|
1030
|
+
with: {
|
|
1031
|
+
organization: true
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
__name(getInvitationById, "getInvitationById");
|
|
1036
|
+
async function getOrganizationBySlug(slug) {
|
|
1037
|
+
if (!db) {
|
|
1038
|
+
throw new Error("Database not available");
|
|
1039
|
+
}
|
|
1040
|
+
return await db.query.organization.findFirst({
|
|
1041
|
+
where: /* @__PURE__ */ __name((org, { eq: eq15 }) => eq15(org.slug, slug), "where")
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
__name(getOrganizationBySlug, "getOrganizationBySlug");
|
|
1045
|
+
async function getOrganizationMembership(organizationId, userId) {
|
|
1046
|
+
if (!db) {
|
|
1047
|
+
throw new Error("Database not available");
|
|
1048
|
+
}
|
|
1049
|
+
return await db.query.member.findFirst({
|
|
1050
|
+
where: /* @__PURE__ */ __name((member3, { and: and6, eq: eq15 }) => and6(eq15(member3.organizationId, organizationId), eq15(member3.userId, userId)), "where"),
|
|
1051
|
+
with: {
|
|
1052
|
+
organization: true
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
__name(getOrganizationMembership, "getOrganizationMembership");
|
|
1057
|
+
async function getOrganizationWithPurchasesAndMembersCount(organizationId) {
|
|
1058
|
+
if (!db) {
|
|
1059
|
+
throw new Error("Database not available");
|
|
1060
|
+
}
|
|
1061
|
+
return await db.query.organization.findFirst({
|
|
1062
|
+
where: /* @__PURE__ */ __name((org, { eq: eq15 }) => eq15(org.id, organizationId), "where"),
|
|
1063
|
+
with: {
|
|
1064
|
+
purchases: true
|
|
1065
|
+
},
|
|
1066
|
+
extras: {
|
|
1067
|
+
membersCount: sql`(SELECT COUNT(*) FROM ${member2} WHERE ${member2.organizationId} = ${organization2.id})`.as("membersCount")
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
__name(getOrganizationWithPurchasesAndMembersCount, "getOrganizationWithPurchasesAndMembersCount");
|
|
1072
|
+
async function getPendingInvitationByEmail(email) {
|
|
1073
|
+
if (!db) {
|
|
1074
|
+
throw new Error("Database not available");
|
|
1075
|
+
}
|
|
1076
|
+
return await db.query.invitation.findFirst({
|
|
1077
|
+
where: /* @__PURE__ */ __name((invitation2, { and: and6, eq: eq15 }) => and6(eq15(invitation2.email, email), eq15(invitation2.status, "pending")), "where")
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
__name(getPendingInvitationByEmail, "getPendingInvitationByEmail");
|
|
1081
|
+
async function updateOrganization(updatedOrganization) {
|
|
1082
|
+
if (!db) {
|
|
1083
|
+
throw new Error("Database not available");
|
|
1084
|
+
}
|
|
1085
|
+
return await db.update(organization2).set(updatedOrganization).where(eq(organization2.id, updatedOrganization.id));
|
|
1086
|
+
}
|
|
1087
|
+
__name(updateOrganization, "updateOrganization");
|
|
1088
|
+
async function generateOrganizationSlug(name) {
|
|
1089
|
+
const baseSlug = slugify(name, {
|
|
1090
|
+
lowercase: true
|
|
1091
|
+
});
|
|
1092
|
+
let slug = baseSlug;
|
|
1093
|
+
let hasAvailableSlug = false;
|
|
1094
|
+
for (let i = 0; i < 3; i++) {
|
|
1095
|
+
const existing = await getOrganizationBySlug(slug);
|
|
1096
|
+
if (!existing) {
|
|
1097
|
+
hasAvailableSlug = true;
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
slug = `${baseSlug}-${nanoid(5)}`;
|
|
1101
|
+
}
|
|
1102
|
+
if (!hasAvailableSlug) {
|
|
1103
|
+
throw new Error("Could not generate unique slug");
|
|
1104
|
+
}
|
|
1105
|
+
return slug;
|
|
1106
|
+
}
|
|
1107
|
+
__name(generateOrganizationSlug, "generateOrganizationSlug");
|
|
1108
|
+
var PRIVACY_SALT = process.env.PRIVACY_SALT || "default-salt";
|
|
1109
|
+
function anonymizeUserId(userId) {
|
|
1110
|
+
const hash = crypto2.createHash("sha256").update(userId + PRIVACY_SALT).digest("hex");
|
|
1111
|
+
return `anon_${hash.slice(0, 16)}`;
|
|
1112
|
+
}
|
|
1113
|
+
__name(anonymizeUserId, "anonymizeUserId");
|
|
1114
|
+
function anonymizeEmail(email) {
|
|
1115
|
+
const [local, domain] = email.split("@");
|
|
1116
|
+
if (!domain) {
|
|
1117
|
+
return anonymizeUserId(email);
|
|
1118
|
+
}
|
|
1119
|
+
const domainHash = crypto2.createHash("sha256").update(domain + PRIVACY_SALT).digest("hex").slice(0, 8);
|
|
1120
|
+
const maskedLocal = `${local.charAt(0)}***`;
|
|
1121
|
+
return `${maskedLocal}@${domainHash}`;
|
|
1122
|
+
}
|
|
1123
|
+
__name(anonymizeEmail, "anonymizeEmail");
|
|
1124
|
+
function sanitizeForLogging(obj) {
|
|
1125
|
+
const sensitiveFields = [
|
|
1126
|
+
"password",
|
|
1127
|
+
"email",
|
|
1128
|
+
"token",
|
|
1129
|
+
"apiKey",
|
|
1130
|
+
"key",
|
|
1131
|
+
"secret",
|
|
1132
|
+
"refreshToken",
|
|
1133
|
+
"accessToken",
|
|
1134
|
+
"salt",
|
|
1135
|
+
"hash"
|
|
1136
|
+
];
|
|
1137
|
+
const result = {
|
|
1138
|
+
...obj
|
|
1139
|
+
};
|
|
1140
|
+
for (const field of sensitiveFields) {
|
|
1141
|
+
if (field in result) {
|
|
1142
|
+
result[field] = "[REDACTED]";
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return result;
|
|
1146
|
+
}
|
|
1147
|
+
__name(sanitizeForLogging, "sanitizeForLogging");
|
|
1148
|
+
async function logAnonymizedEvent(event, data, userId) {
|
|
1149
|
+
try {
|
|
1150
|
+
const anonymousId = userId ? anonymizeUserId(userId) : void 0;
|
|
1151
|
+
const sanitized = sanitizeForLogging({
|
|
1152
|
+
...data,
|
|
1153
|
+
userId: void 0,
|
|
1154
|
+
email: void 0,
|
|
1155
|
+
apiKeyId: void 0,
|
|
1156
|
+
token: void 0
|
|
1157
|
+
});
|
|
1158
|
+
logger.info(`Analytics: ${event}`, {
|
|
1159
|
+
event,
|
|
1160
|
+
anonymousId,
|
|
1161
|
+
...sanitized,
|
|
1162
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1163
|
+
});
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
logger.error("Failed to log anonymized event", {
|
|
1166
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1167
|
+
event
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
__name(logAnonymizedEvent, "logAnonymizedEvent");
|
|
1172
|
+
async function exportUserData(userId) {
|
|
1173
|
+
try {
|
|
1174
|
+
if (!db) {
|
|
1175
|
+
logger.error("Database not initialized");
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
const userRecord = await db.select().from(user).where(eq(user.id, userId)).then((rows) => rows[0] || null);
|
|
1179
|
+
if (!userRecord) {
|
|
1180
|
+
logger.warn("User not found for data export", {
|
|
1181
|
+
userId
|
|
1182
|
+
});
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
const [userSessions, userAccounts, userApiKeys, userSubscriptions] = await Promise.all([
|
|
1186
|
+
db.select().from(session).where(eq(session.userId, userId)),
|
|
1187
|
+
db.select().from(account).where(eq(account.userId, userId)),
|
|
1188
|
+
db.select().from(apiKeys).where(eq(apiKeys.userId, userId)),
|
|
1189
|
+
db.select().from(subscriptions).where(eq(subscriptions.userId, userId))
|
|
1190
|
+
]);
|
|
1191
|
+
const sanitizedUser = sanitizeForLogging(userRecord);
|
|
1192
|
+
const sanitizedApiKeys = userApiKeys.map((k) => sanitizeForLogging(k));
|
|
1193
|
+
return {
|
|
1194
|
+
user: sanitizedUser,
|
|
1195
|
+
sessions: userSessions,
|
|
1196
|
+
accounts: userAccounts,
|
|
1197
|
+
apiKeys: sanitizedApiKeys,
|
|
1198
|
+
subscriptions: userSubscriptions,
|
|
1199
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1200
|
+
};
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
logger.error("Failed to export user data", {
|
|
1203
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1204
|
+
userId
|
|
1205
|
+
});
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
__name(exportUserData, "exportUserData");
|
|
1210
|
+
async function deleteUserData(userId) {
|
|
1211
|
+
try {
|
|
1212
|
+
if (!db) {
|
|
1213
|
+
logger.error("Database not initialized");
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
logger.warn("Deleting all user data (GDPR Right to Erasure)", {
|
|
1217
|
+
userId
|
|
1218
|
+
});
|
|
1219
|
+
await db.delete(user).where(eq(user.id, userId));
|
|
1220
|
+
logger.info("User data deleted successfully", {
|
|
1221
|
+
userId
|
|
1222
|
+
});
|
|
1223
|
+
return true;
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
logger.error("Failed to delete user data", {
|
|
1226
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1227
|
+
userId
|
|
1228
|
+
});
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
__name(deleteUserData, "deleteUserData");
|
|
1233
|
+
async function deleteUserApiKeys(userId) {
|
|
1234
|
+
try {
|
|
1235
|
+
if (!db) {
|
|
1236
|
+
logger.error("Database not initialized");
|
|
1237
|
+
return 0;
|
|
1238
|
+
}
|
|
1239
|
+
const result = await db.delete(apiKeys).where(eq(apiKeys.userId, userId));
|
|
1240
|
+
logger.info("User API keys deleted", {
|
|
1241
|
+
userId
|
|
1242
|
+
});
|
|
1243
|
+
return result.rowCount || 0;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
logger.error("Failed to delete user API keys", {
|
|
1246
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1247
|
+
userId
|
|
1248
|
+
});
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
__name(deleteUserApiKeys, "deleteUserApiKeys");
|
|
1253
|
+
async function anonymizeUserData(userId) {
|
|
1254
|
+
try {
|
|
1255
|
+
if (!db) {
|
|
1256
|
+
logger.error("Database not initialized");
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
logger.info("Anonymizing user data", {
|
|
1260
|
+
userId
|
|
1261
|
+
});
|
|
1262
|
+
await db.update(user).set({
|
|
1263
|
+
email: `deleted+${anonymizeUserId(userId)}@snapback.local`,
|
|
1264
|
+
name: "Deleted User",
|
|
1265
|
+
image: null,
|
|
1266
|
+
username: null
|
|
1267
|
+
}).where(eq(user.id, userId));
|
|
1268
|
+
await db.delete(session).where(eq(session.userId, userId));
|
|
1269
|
+
await db.delete(apiKeys).where(eq(apiKeys.userId, userId));
|
|
1270
|
+
logger.info("User data anonymized successfully", {
|
|
1271
|
+
userId
|
|
1272
|
+
});
|
|
1273
|
+
return true;
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
logger.error("Failed to anonymize user data", {
|
|
1276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1277
|
+
userId
|
|
1278
|
+
});
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
__name(anonymizeUserData, "anonymizeUserData");
|
|
1283
|
+
async function getUserPrivacyPreferences(userId) {
|
|
1284
|
+
try {
|
|
1285
|
+
if (!db) {
|
|
1286
|
+
logger.error("Database not initialized");
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
const userRecord = await db.select().from(user).where(eq(user.id, userId)).then((rows) => rows[0] || null);
|
|
1290
|
+
if (!userRecord) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
analyticsConsent: true,
|
|
1295
|
+
marketingConsent: false,
|
|
1296
|
+
sharingConsent: false
|
|
1297
|
+
};
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
logger.error("Failed to get privacy preferences", {
|
|
1300
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1301
|
+
userId
|
|
1302
|
+
});
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
__name(getUserPrivacyPreferences, "getUserPrivacyPreferences");
|
|
1307
|
+
function shouldRetainData(createdAt, retentionDays = 90) {
|
|
1308
|
+
const retentionMs = retentionDays * 24 * 60 * 60 * 1e3;
|
|
1309
|
+
const ageMs = Date.now() - createdAt.getTime();
|
|
1310
|
+
return ageMs < retentionMs;
|
|
1311
|
+
}
|
|
1312
|
+
__name(shouldRetainData, "shouldRetainData");
|
|
1313
|
+
async function cleanupExpiredData(retentionDays = 90) {
|
|
1314
|
+
try {
|
|
1315
|
+
if (!db) {
|
|
1316
|
+
logger.error("Database not initialized");
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3);
|
|
1320
|
+
await db.delete(session).where(lte(session.expiresAt, cutoffDate));
|
|
1321
|
+
logger.info("Data cleanup completed", {
|
|
1322
|
+
retentionDays,
|
|
1323
|
+
cutoffDate: cutoffDate.toISOString()
|
|
1324
|
+
});
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
logger.error("Data cleanup failed", {
|
|
1327
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
__name(cleanupExpiredData, "cleanupExpiredData");
|
|
1332
|
+
var { purchase: purchase2 } = combinedSchema;
|
|
1333
|
+
async function getPurchasesByOrganizationId(organizationId) {
|
|
1334
|
+
if (!db) {
|
|
1335
|
+
throw new Error("Database not available");
|
|
1336
|
+
}
|
|
1337
|
+
return db.query.purchase.findMany({
|
|
1338
|
+
where: /* @__PURE__ */ __name((purchase3, { eq: eq15 }) => eq15(purchase3.organizationId, organizationId), "where")
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
__name(getPurchasesByOrganizationId, "getPurchasesByOrganizationId");
|
|
1342
|
+
async function getPurchasesByUserId(userId) {
|
|
1343
|
+
if (!db) {
|
|
1344
|
+
throw new Error("Database not available");
|
|
1345
|
+
}
|
|
1346
|
+
return db.query.purchase.findMany({
|
|
1347
|
+
where: /* @__PURE__ */ __name((purchase3, { eq: eq15 }) => eq15(purchase3.userId, userId), "where")
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
__name(getPurchasesByUserId, "getPurchasesByUserId");
|
|
1351
|
+
async function getPurchaseById(id) {
|
|
1352
|
+
if (!db) {
|
|
1353
|
+
throw new Error("Database not available");
|
|
1354
|
+
}
|
|
1355
|
+
return db.query.purchase.findFirst({
|
|
1356
|
+
where: /* @__PURE__ */ __name((purchase3, { eq: eq15 }) => eq15(purchase3.id, id), "where")
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
__name(getPurchaseById, "getPurchaseById");
|
|
1360
|
+
async function getPurchaseBySubscriptionId(subscriptionId) {
|
|
1361
|
+
if (!db) {
|
|
1362
|
+
throw new Error("Database not available");
|
|
1363
|
+
}
|
|
1364
|
+
return db.query.purchase.findFirst({
|
|
1365
|
+
where: /* @__PURE__ */ __name((purchase3, { eq: eq15 }) => eq15(purchase3.subscriptionId, subscriptionId), "where")
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
__name(getPurchaseBySubscriptionId, "getPurchaseBySubscriptionId");
|
|
1369
|
+
async function createPurchase(insertedPurchase) {
|
|
1370
|
+
if (!db) {
|
|
1371
|
+
throw new Error("Database not available");
|
|
1372
|
+
}
|
|
1373
|
+
const result = await db.insert(purchase2).values(insertedPurchase).returning({
|
|
1374
|
+
id: purchase2.id
|
|
1375
|
+
});
|
|
1376
|
+
const firstResult = result[0];
|
|
1377
|
+
if (!firstResult) {
|
|
1378
|
+
throw new Error("Failed to create purchase");
|
|
1379
|
+
}
|
|
1380
|
+
const { id } = firstResult;
|
|
1381
|
+
return getPurchaseById(id);
|
|
1382
|
+
}
|
|
1383
|
+
__name(createPurchase, "createPurchase");
|
|
1384
|
+
async function updatePurchase(updatedPurchase) {
|
|
1385
|
+
if (!db) {
|
|
1386
|
+
throw new Error("Database not available");
|
|
1387
|
+
}
|
|
1388
|
+
const result = await db.update(purchase2).set(updatedPurchase).returning({
|
|
1389
|
+
id: purchase2.id
|
|
1390
|
+
});
|
|
1391
|
+
const firstResult = result[0];
|
|
1392
|
+
if (!firstResult) {
|
|
1393
|
+
throw new Error("Failed to update purchase");
|
|
1394
|
+
}
|
|
1395
|
+
const { id } = firstResult;
|
|
1396
|
+
return getPurchaseById(id);
|
|
1397
|
+
}
|
|
1398
|
+
__name(updatePurchase, "updatePurchase");
|
|
1399
|
+
async function deletePurchaseBySubscriptionId(subscriptionId) {
|
|
1400
|
+
if (!db) {
|
|
1401
|
+
throw new Error("Database not available");
|
|
1402
|
+
}
|
|
1403
|
+
await db.delete(purchase2).where(eq(purchase2.subscriptionId, subscriptionId));
|
|
1404
|
+
}
|
|
1405
|
+
__name(deletePurchaseBySubscriptionId, "deletePurchaseBySubscriptionId");
|
|
1406
|
+
var { user: user2, account: account2 } = combinedSchema;
|
|
1407
|
+
var searchUsersSchema = z.object({
|
|
1408
|
+
query: z.string().min(1).max(100).optional(),
|
|
1409
|
+
limit: z.number().min(1).max(100).default(50),
|
|
1410
|
+
offset: z.number().min(0).default(0)
|
|
1411
|
+
});
|
|
1412
|
+
async function getUsers({ limit, offset, query }) {
|
|
1413
|
+
if (!db) {
|
|
1414
|
+
throw new Error("Database not available");
|
|
1415
|
+
}
|
|
1416
|
+
const validatedParams = searchUsersSchema.parse({
|
|
1417
|
+
query,
|
|
1418
|
+
limit,
|
|
1419
|
+
offset
|
|
1420
|
+
});
|
|
1421
|
+
const whereClause = query ? (user3, { like, sql: sql5 }) => like(user3.name, sql5`${"%"}${validatedParams.query}${"%"}`) : void 0;
|
|
1422
|
+
return await db.query.user.findMany({
|
|
1423
|
+
where: whereClause,
|
|
1424
|
+
limit: validatedParams.limit,
|
|
1425
|
+
offset: validatedParams.offset
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
__name(getUsers, "getUsers");
|
|
1429
|
+
async function countAllUsers() {
|
|
1430
|
+
if (!db) {
|
|
1431
|
+
throw new Error("Database not available");
|
|
1432
|
+
}
|
|
1433
|
+
return db.$count(user2);
|
|
1434
|
+
}
|
|
1435
|
+
__name(countAllUsers, "countAllUsers");
|
|
1436
|
+
async function getUserById(id) {
|
|
1437
|
+
if (!db) {
|
|
1438
|
+
throw new Error("Database not available");
|
|
1439
|
+
}
|
|
1440
|
+
return await db.query.user.findFirst({
|
|
1441
|
+
where: /* @__PURE__ */ __name((user3, { eq: eq15 }) => eq15(user3.id, id), "where")
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
__name(getUserById, "getUserById");
|
|
1445
|
+
async function getUserByEmail(email) {
|
|
1446
|
+
if (!db) {
|
|
1447
|
+
throw new Error("Database not available");
|
|
1448
|
+
}
|
|
1449
|
+
return await db.query.user.findFirst({
|
|
1450
|
+
where: /* @__PURE__ */ __name((user3, { eq: eq15 }) => eq15(user3.email, email), "where")
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
__name(getUserByEmail, "getUserByEmail");
|
|
1454
|
+
async function createUser({ email, name, role, emailVerified, onboardingComplete }) {
|
|
1455
|
+
if (!db) {
|
|
1456
|
+
throw new Error("Database not available");
|
|
1457
|
+
}
|
|
1458
|
+
const result = await db.insert(user2).values({
|
|
1459
|
+
email,
|
|
1460
|
+
name,
|
|
1461
|
+
role,
|
|
1462
|
+
emailVerified,
|
|
1463
|
+
onboardingComplete,
|
|
1464
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1465
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1466
|
+
}).returning({
|
|
1467
|
+
id: user2.id
|
|
1468
|
+
});
|
|
1469
|
+
const firstResult = result[0];
|
|
1470
|
+
if (!firstResult) {
|
|
1471
|
+
throw new Error("Failed to create user");
|
|
1472
|
+
}
|
|
1473
|
+
const { id } = firstResult;
|
|
1474
|
+
const newUser = await getUserById(id);
|
|
1475
|
+
return newUser;
|
|
1476
|
+
}
|
|
1477
|
+
__name(createUser, "createUser");
|
|
1478
|
+
async function getAccountById(id) {
|
|
1479
|
+
if (!db) {
|
|
1480
|
+
throw new Error("Database not available");
|
|
1481
|
+
}
|
|
1482
|
+
return await db.query.account.findFirst({
|
|
1483
|
+
where: /* @__PURE__ */ __name((account3, { eq: eq15 }) => eq15(account3.id, id), "where")
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
__name(getAccountById, "getAccountById");
|
|
1487
|
+
async function createUserAccount({ userId, providerId, accountId, hashedPassword }) {
|
|
1488
|
+
if (!db) {
|
|
1489
|
+
throw new Error("Database not available");
|
|
1490
|
+
}
|
|
1491
|
+
const result = await db.insert(account2).values({
|
|
1492
|
+
userId,
|
|
1493
|
+
accountId,
|
|
1494
|
+
providerId,
|
|
1495
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1496
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1497
|
+
password: hashedPassword
|
|
1498
|
+
}).returning({
|
|
1499
|
+
id: account2.id
|
|
1500
|
+
});
|
|
1501
|
+
const firstResult = result[0];
|
|
1502
|
+
if (!firstResult) {
|
|
1503
|
+
throw new Error("Failed to create account");
|
|
1504
|
+
}
|
|
1505
|
+
const { id } = firstResult;
|
|
1506
|
+
const newAccount = await getAccountById(id);
|
|
1507
|
+
return newAccount;
|
|
1508
|
+
}
|
|
1509
|
+
__name(createUserAccount, "createUserAccount");
|
|
1510
|
+
async function updateUser(updatedUser) {
|
|
1511
|
+
if (!db) {
|
|
1512
|
+
throw new Error("Database not available");
|
|
1513
|
+
}
|
|
1514
|
+
return db.update(user2).set(updatedUser).where(eq(user2.id, updatedUser.id));
|
|
1515
|
+
}
|
|
1516
|
+
__name(updateUser, "updateUser");
|
|
1517
|
+
var { workspaceLinks } = combinedSchema;
|
|
1518
|
+
var workspaceIdSchema = z.string().regex(/^([a-f0-9]{12}|ws[g]?_[a-f0-9]{32})$/, "Invalid workspace ID format: must be 12-char hex (unified) or ws_/wsg_ + 32-char hex (legacy)");
|
|
1519
|
+
var linkWorkspaceSchema = z.object({
|
|
1520
|
+
workspaceId: workspaceIdSchema,
|
|
1521
|
+
userId: z.string().min(1),
|
|
1522
|
+
tier: z.enum([
|
|
1523
|
+
"free",
|
|
1524
|
+
"pro",
|
|
1525
|
+
"enterprise"
|
|
1526
|
+
]).optional().default("free"),
|
|
1527
|
+
displayName: z.string().max(255).optional(),
|
|
1528
|
+
expiresAt: z.date().optional()
|
|
1529
|
+
});
|
|
1530
|
+
var updateTierSchema = z.object({
|
|
1531
|
+
workspaceId: workspaceIdSchema,
|
|
1532
|
+
tier: z.enum([
|
|
1533
|
+
"free",
|
|
1534
|
+
"pro",
|
|
1535
|
+
"enterprise"
|
|
1536
|
+
])
|
|
1537
|
+
});
|
|
1538
|
+
async function getWorkspaceLinkById(workspaceId) {
|
|
1539
|
+
if (!db) {
|
|
1540
|
+
throw new Error("Database not available");
|
|
1541
|
+
}
|
|
1542
|
+
const validatedId = workspaceIdSchema.parse(workspaceId);
|
|
1543
|
+
return await db.query.workspaceLinks.findFirst({
|
|
1544
|
+
where: /* @__PURE__ */ __name((link, { eq: eq15 }) => eq15(link.workspaceId, validatedId), "where")
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
__name(getWorkspaceLinkById, "getWorkspaceLinkById");
|
|
1548
|
+
async function resolveTierByWorkspaceId(workspaceId) {
|
|
1549
|
+
if (!db) {
|
|
1550
|
+
return {
|
|
1551
|
+
found: false,
|
|
1552
|
+
tier: "free"
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
const validatedId = workspaceIdSchema.parse(workspaceId);
|
|
1557
|
+
const link = await db.query.workspaceLinks.findFirst({
|
|
1558
|
+
where: /* @__PURE__ */ __name((link2, { eq: eq15 }) => eq15(link2.workspaceId, validatedId), "where")
|
|
1559
|
+
});
|
|
1560
|
+
if (!link) {
|
|
1561
|
+
return {
|
|
1562
|
+
found: false,
|
|
1563
|
+
tier: "free"
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
if (link.expiresAt && link.expiresAt < /* @__PURE__ */ new Date()) {
|
|
1567
|
+
await db.delete(workspaceLinks).where(eq(workspaceLinks.workspaceId, validatedId));
|
|
1568
|
+
console.log(`[Workspace Links] Deleted expired link: ${validatedId.slice(0, 10)}...`);
|
|
1569
|
+
return {
|
|
1570
|
+
found: false,
|
|
1571
|
+
tier: "free"
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
const staleCutoff = new Date(Date.now() - TIER_STALENESS_THRESHOLD_MS);
|
|
1575
|
+
const tierRefreshedAt = link.tierRefreshedAt ?? link.createdAt;
|
|
1576
|
+
let tierRefreshed = false;
|
|
1577
|
+
let currentTier = link.tier;
|
|
1578
|
+
if (tierRefreshedAt < staleCutoff) {
|
|
1579
|
+
const [subscription] = await db.select({
|
|
1580
|
+
plan: subscriptions.plan
|
|
1581
|
+
}).from(subscriptions).where(eq(subscriptions.userId, link.userId)).limit(1);
|
|
1582
|
+
const freshTier = mapPlanToTier(subscription?.plan);
|
|
1583
|
+
await db.update(workspaceLinks).set({
|
|
1584
|
+
tier: freshTier,
|
|
1585
|
+
tierRefreshedAt: /* @__PURE__ */ new Date(),
|
|
1586
|
+
lastSeenAt: /* @__PURE__ */ new Date()
|
|
1587
|
+
}).where(eq(workspaceLinks.workspaceId, validatedId));
|
|
1588
|
+
currentTier = freshTier;
|
|
1589
|
+
tierRefreshed = true;
|
|
1590
|
+
console.log(`[Workspace Links] Refreshed stale tier for ${validatedId.slice(0, 10)}...: ${link.tier} -> ${freshTier}`);
|
|
1591
|
+
} else {
|
|
1592
|
+
db.update(workspaceLinks).set({
|
|
1593
|
+
lastSeenAt: /* @__PURE__ */ new Date()
|
|
1594
|
+
}).where(eq(workspaceLinks.workspaceId, validatedId)).catch(() => {
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
found: true,
|
|
1599
|
+
tier: currentTier,
|
|
1600
|
+
userId: link.userId,
|
|
1601
|
+
displayName: link.displayName ?? void 0,
|
|
1602
|
+
tierRefreshed
|
|
1603
|
+
};
|
|
1604
|
+
} catch (_error) {
|
|
1605
|
+
return {
|
|
1606
|
+
found: false,
|
|
1607
|
+
tier: "free"
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
__name(resolveTierByWorkspaceId, "resolveTierByWorkspaceId");
|
|
1612
|
+
function mapPlanToTier(plan) {
|
|
1613
|
+
switch (plan) {
|
|
1614
|
+
case "pro":
|
|
1615
|
+
case "solo":
|
|
1616
|
+
return "pro";
|
|
1617
|
+
case "team":
|
|
1618
|
+
case "enterprise":
|
|
1619
|
+
return "enterprise";
|
|
1620
|
+
default:
|
|
1621
|
+
return "free";
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
__name(mapPlanToTier, "mapPlanToTier");
|
|
1625
|
+
async function linkWorkspace(params) {
|
|
1626
|
+
if (!db) {
|
|
1627
|
+
throw new Error("Database not available");
|
|
1628
|
+
}
|
|
1629
|
+
const validated = linkWorkspaceSchema.parse(params);
|
|
1630
|
+
const expiresAt = validated.expiresAt ?? new Date(Date.now() + WORKSPACE_LINK_TTL_MS);
|
|
1631
|
+
const existing = await getWorkspaceLinkById(validated.workspaceId);
|
|
1632
|
+
if (existing) {
|
|
1633
|
+
await db.update(workspaceLinks).set({
|
|
1634
|
+
userId: validated.userId,
|
|
1635
|
+
tier: validated.tier,
|
|
1636
|
+
displayName: validated.displayName,
|
|
1637
|
+
lastSeenAt: /* @__PURE__ */ new Date(),
|
|
1638
|
+
tierRefreshedAt: /* @__PURE__ */ new Date(),
|
|
1639
|
+
expiresAt
|
|
1640
|
+
}).where(eq(workspaceLinks.workspaceId, validated.workspaceId));
|
|
1641
|
+
return await getWorkspaceLinkById(validated.workspaceId);
|
|
1642
|
+
}
|
|
1643
|
+
await db.insert(workspaceLinks).values({
|
|
1644
|
+
workspaceId: validated.workspaceId,
|
|
1645
|
+
userId: validated.userId,
|
|
1646
|
+
tier: validated.tier,
|
|
1647
|
+
displayName: validated.displayName,
|
|
1648
|
+
tierRefreshedAt: /* @__PURE__ */ new Date(),
|
|
1649
|
+
expiresAt
|
|
1650
|
+
});
|
|
1651
|
+
return await getWorkspaceLinkById(validated.workspaceId);
|
|
1652
|
+
}
|
|
1653
|
+
__name(linkWorkspace, "linkWorkspace");
|
|
1654
|
+
async function updateWorkspaceTier(params) {
|
|
1655
|
+
if (!db) {
|
|
1656
|
+
throw new Error("Database not available");
|
|
1657
|
+
}
|
|
1658
|
+
const validated = updateTierSchema.parse(params);
|
|
1659
|
+
const result = await db.update(workspaceLinks).set({
|
|
1660
|
+
tier: validated.tier,
|
|
1661
|
+
lastSeenAt: /* @__PURE__ */ new Date()
|
|
1662
|
+
}).where(eq(workspaceLinks.workspaceId, validated.workspaceId)).returning({
|
|
1663
|
+
workspaceId: workspaceLinks.workspaceId
|
|
1664
|
+
});
|
|
1665
|
+
return result.length > 0;
|
|
1666
|
+
}
|
|
1667
|
+
__name(updateWorkspaceTier, "updateWorkspaceTier");
|
|
1668
|
+
async function unlinkWorkspace(workspaceId) {
|
|
1669
|
+
if (!db) {
|
|
1670
|
+
throw new Error("Database not available");
|
|
1671
|
+
}
|
|
1672
|
+
const validatedId = workspaceIdSchema.parse(workspaceId);
|
|
1673
|
+
const result = await db.delete(workspaceLinks).where(eq(workspaceLinks.workspaceId, validatedId)).returning({
|
|
1674
|
+
workspaceId: workspaceLinks.workspaceId
|
|
1675
|
+
});
|
|
1676
|
+
return result.length > 0;
|
|
1677
|
+
}
|
|
1678
|
+
__name(unlinkWorkspace, "unlinkWorkspace");
|
|
1679
|
+
async function getWorkspaceLinksByUserId(userId) {
|
|
1680
|
+
if (!db) {
|
|
1681
|
+
throw new Error("Database not available");
|
|
1682
|
+
}
|
|
1683
|
+
return await db.query.workspaceLinks.findMany({
|
|
1684
|
+
where: /* @__PURE__ */ __name((link, { eq: eq15 }) => eq15(link.userId, userId), "where"),
|
|
1685
|
+
orderBy: /* @__PURE__ */ __name((link, { desc: desc4 }) => desc4(link.lastSeenAt), "orderBy")
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
__name(getWorkspaceLinksByUserId, "getWorkspaceLinksByUserId");
|
|
1689
|
+
async function unlinkAllWorkspacesForUser(userId) {
|
|
1690
|
+
if (!db) {
|
|
1691
|
+
throw new Error("Database not available");
|
|
1692
|
+
}
|
|
1693
|
+
const result = await db.delete(workspaceLinks).where(eq(workspaceLinks.userId, userId)).returning({
|
|
1694
|
+
workspaceId: workspaceLinks.workspaceId
|
|
1695
|
+
});
|
|
1696
|
+
return result.length;
|
|
1697
|
+
}
|
|
1698
|
+
__name(unlinkAllWorkspacesForUser, "unlinkAllWorkspacesForUser");
|
|
1699
|
+
async function searchSimilarPatterns(queryVector, limit = 10, similarityThreshold = 0.75, patternType, isGlobal) {
|
|
1700
|
+
if (queryVector.length !== 256) {
|
|
1701
|
+
throw new Error(`Expected 256-dimensional vector, got ${queryVector.length}`);
|
|
1702
|
+
}
|
|
1703
|
+
const vectorLiteral = `[${queryVector.join(",")}]`;
|
|
1704
|
+
let query = db.select({
|
|
1705
|
+
id: patterns.id,
|
|
1706
|
+
patternSignature: patterns.patternSignature,
|
|
1707
|
+
patternType: patterns.patternType,
|
|
1708
|
+
// Cosine similarity: 1 - distance (higher is more similar)
|
|
1709
|
+
similarity: sql`1 - (${patterns.embedding} <=> ${vectorLiteral}::vector)`,
|
|
1710
|
+
occurrenceCount: patterns.occurrenceCount,
|
|
1711
|
+
successRate: patterns.successRate,
|
|
1712
|
+
isGlobal: patterns.isGlobal
|
|
1713
|
+
}).from(patterns).where(sql`1 - (${patterns.embedding} <=> ${vectorLiteral}::vector) >= ${similarityThreshold}`);
|
|
1714
|
+
if (patternType) {
|
|
1715
|
+
query = query.where(sql`${patterns.patternType} = ${patternType}`);
|
|
1716
|
+
}
|
|
1717
|
+
if (isGlobal !== void 0) {
|
|
1718
|
+
query = query.where(sql`${patterns.isGlobal} = ${isGlobal}`);
|
|
1719
|
+
}
|
|
1720
|
+
const results = await query.orderBy(sql`${patterns.embedding} <=> ${vectorLiteral}::vector`).limit(limit);
|
|
1721
|
+
return results;
|
|
1722
|
+
}
|
|
1723
|
+
__name(searchSimilarPatterns, "searchSimilarPatterns");
|
|
1724
|
+
async function findSimilarPatterns(patternId, limit = 5, excludeSelf = true) {
|
|
1725
|
+
const referencePattern = await db.select({
|
|
1726
|
+
embedding: patterns.embedding
|
|
1727
|
+
}).from(patterns).where(sql`${patterns.id} = ${patternId}`).limit(1);
|
|
1728
|
+
if (!referencePattern.length || !referencePattern[0].embedding) {
|
|
1729
|
+
throw new Error(`Pattern ${patternId} not found or has no embedding`);
|
|
1730
|
+
}
|
|
1731
|
+
const queryVector = referencePattern[0].embedding;
|
|
1732
|
+
const vectorLiteral = `[${queryVector.join(",")}]`;
|
|
1733
|
+
let query = db.select({
|
|
1734
|
+
id: patterns.id,
|
|
1735
|
+
patternSignature: patterns.patternSignature,
|
|
1736
|
+
patternType: patterns.patternType,
|
|
1737
|
+
similarity: sql`1 - (${patterns.embedding} <=> ${vectorLiteral}::vector)`,
|
|
1738
|
+
occurrenceCount: patterns.occurrenceCount,
|
|
1739
|
+
successRate: patterns.successRate,
|
|
1740
|
+
isGlobal: patterns.isGlobal
|
|
1741
|
+
}).from(patterns).where(sql`${patterns.embedding} IS NOT NULL`);
|
|
1742
|
+
if (excludeSelf) {
|
|
1743
|
+
query = query.where(sql`${patterns.id} != ${patternId}`);
|
|
1744
|
+
}
|
|
1745
|
+
const results = await query.orderBy(sql`${patterns.embedding} <=> ${vectorLiteral}::vector`).limit(limit);
|
|
1746
|
+
return results;
|
|
1747
|
+
}
|
|
1748
|
+
__name(findSimilarPatterns, "findSimilarPatterns");
|
|
1749
|
+
async function insertPatternWithEmbedding(patternSignature, embedding, patternType, userId, toolAffinity, fileTypes) {
|
|
1750
|
+
if (embedding.length !== 256) {
|
|
1751
|
+
throw new Error(`Expected 256-dimensional vector, got ${embedding.length}`);
|
|
1752
|
+
}
|
|
1753
|
+
const result = await db.insert(patterns).values({
|
|
1754
|
+
patternSignature,
|
|
1755
|
+
embedding,
|
|
1756
|
+
patternType,
|
|
1757
|
+
userId,
|
|
1758
|
+
toolAffinity: toolAffinity ?? [],
|
|
1759
|
+
fileTypes: fileTypes ?? [],
|
|
1760
|
+
isGlobal: !userId
|
|
1761
|
+
}).returning({
|
|
1762
|
+
id: patterns.id
|
|
1763
|
+
});
|
|
1764
|
+
return result[0].id;
|
|
1765
|
+
}
|
|
1766
|
+
__name(insertPatternWithEmbedding, "insertPatternWithEmbedding");
|
|
1767
|
+
async function updatePatternEmbedding(patternId, embedding) {
|
|
1768
|
+
if (embedding.length !== 256) {
|
|
1769
|
+
throw new Error(`Expected 256-dimensional vector, got ${embedding.length}`);
|
|
1770
|
+
}
|
|
1771
|
+
await db.update(patterns).set({
|
|
1772
|
+
embedding
|
|
1773
|
+
}).where(sql`${patterns.id} = ${patternId}`);
|
|
1774
|
+
}
|
|
1775
|
+
__name(updatePatternEmbedding, "updatePatternEmbedding");
|
|
1776
|
+
async function isPgvectorEnabled() {
|
|
1777
|
+
try {
|
|
1778
|
+
const result = await db.execute(sql`
|
|
1779
|
+
SELECT 1 FROM pg_extension WHERE extname = 'vector'
|
|
1780
|
+
`);
|
|
1781
|
+
return result.rows.length > 0;
|
|
1782
|
+
} catch {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
__name(isPgvectorEnabled, "isPgvectorEnabled");
|
|
1787
|
+
async function getVectorStats() {
|
|
1788
|
+
const [totalResult, embeddingResult, indexResult] = await Promise.all([
|
|
1789
|
+
db.select({
|
|
1790
|
+
count: sql`count(*)`
|
|
1791
|
+
}).from(patterns),
|
|
1792
|
+
db.select({
|
|
1793
|
+
count: sql`count(*)`
|
|
1794
|
+
}).from(patterns).where(sql`${patterns.embedding} IS NOT NULL`),
|
|
1795
|
+
db.execute(sql`
|
|
1796
|
+
SELECT indexname, indexdef
|
|
1797
|
+
FROM pg_indexes
|
|
1798
|
+
WHERE tablename = 'patterns'
|
|
1799
|
+
AND indexdef LIKE '%hnsw%'
|
|
1800
|
+
`)
|
|
1801
|
+
]);
|
|
1802
|
+
const indexRow = indexResult.rows[0];
|
|
1803
|
+
return {
|
|
1804
|
+
totalPatterns: totalResult[0]?.count ?? 0,
|
|
1805
|
+
patternsWithEmbeddings: embeddingResult[0]?.count ?? 0,
|
|
1806
|
+
indexName: indexRow?.indexname ?? null,
|
|
1807
|
+
indexType: indexRow?.indexdef?.includes("hnsw") ? "hnsw" : null
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
__name(getVectorStats, "getVectorStats");
|
|
1811
|
+
var extensionLinkTokens = pgTable("extension_link_tokens", {
|
|
1812
|
+
id: varchar("id", {
|
|
1813
|
+
length: 255
|
|
1814
|
+
}).$defaultFn(() => nanoid()).primaryKey(),
|
|
1815
|
+
tokenHash: text("token_hash").notNull(),
|
|
1816
|
+
userId: varchar("user_id", {
|
|
1817
|
+
length: 255
|
|
1818
|
+
}).notNull().references(() => user.id, {
|
|
1819
|
+
onDelete: "cascade"
|
|
1820
|
+
}),
|
|
1821
|
+
workspaceId: varchar("workspace_id", {
|
|
1822
|
+
length: 255
|
|
1823
|
+
}),
|
|
1824
|
+
client: text("client").notNull(),
|
|
1825
|
+
used: boolean("used").notNull().default(false),
|
|
1826
|
+
expiresAt: timestamp("expires_at", {
|
|
1827
|
+
withTimezone: true
|
|
1828
|
+
}).notNull(),
|
|
1829
|
+
createdAt: timestamp("created_at", {
|
|
1830
|
+
withTimezone: true
|
|
1831
|
+
}).notNull().defaultNow()
|
|
1832
|
+
}, (table) => ({
|
|
1833
|
+
// Partial index for fast active token lookup (used=false, not expired)
|
|
1834
|
+
tokenHashIdx: index("idx_extension_link_tokens_hash").on(table.tokenHash),
|
|
1835
|
+
// Index for cleanup jobs
|
|
1836
|
+
expiryIdx: index("idx_extension_link_tokens_expiry").on(table.expiresAt)
|
|
1837
|
+
}));
|
|
1838
|
+
var extensionSessions = pgTable("extension_sessions", {
|
|
1839
|
+
id: varchar("id", {
|
|
1840
|
+
length: 255
|
|
1841
|
+
}).$defaultFn(() => nanoid()).primaryKey(),
|
|
1842
|
+
userId: varchar("user_id", {
|
|
1843
|
+
length: 255
|
|
1844
|
+
}).notNull().references(() => user.id, {
|
|
1845
|
+
onDelete: "cascade"
|
|
1846
|
+
}),
|
|
1847
|
+
workspaceId: varchar("workspace_id", {
|
|
1848
|
+
length: 255
|
|
1849
|
+
}),
|
|
1850
|
+
client: text("client").notNull(),
|
|
1851
|
+
refreshTokenHash: text("refresh_token_hash").notNull(),
|
|
1852
|
+
createdAt: timestamp("created_at", {
|
|
1853
|
+
withTimezone: true
|
|
1854
|
+
}).notNull().defaultNow(),
|
|
1855
|
+
lastUsedAt: timestamp("last_used_at", {
|
|
1856
|
+
withTimezone: true
|
|
1857
|
+
}),
|
|
1858
|
+
revokedAt: timestamp("revoked_at", {
|
|
1859
|
+
withTimezone: true
|
|
1860
|
+
}),
|
|
1861
|
+
expiresAt: timestamp("expires_at", {
|
|
1862
|
+
withTimezone: true
|
|
1863
|
+
}).notNull(),
|
|
1864
|
+
metadata: jsonb("metadata").$type()
|
|
1865
|
+
}, (table) => ({
|
|
1866
|
+
// Unique index for fast refresh token lookup (only non-revoked)
|
|
1867
|
+
refreshHashIdx: index("idx_extension_sessions_refresh_hash").on(table.refreshTokenHash),
|
|
1868
|
+
// Index for user session queries (Phase 2 UI)
|
|
1869
|
+
userIdx: index("idx_extension_sessions_user").on(table.userId),
|
|
1870
|
+
// Index for active sessions
|
|
1871
|
+
activeIdx: index("idx_extension_sessions_active").on(table.userId, table.revokedAt)
|
|
1872
|
+
}));
|
|
1873
|
+
|
|
1874
|
+
// ../../packages/platform/dist/db/test-utils.js
|
|
1875
|
+
var testInTransaction = /* @__PURE__ */ __name((testName, testFn) => {
|
|
1876
|
+
return async () => {
|
|
1877
|
+
const module = await import('./client-WIO6W447.js');
|
|
1878
|
+
const db2 = module.db;
|
|
1879
|
+
try {
|
|
1880
|
+
await testFn(db2);
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
console.error(`Test failed: ${testName}`, error);
|
|
1883
|
+
throw error;
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
}, "testInTransaction");
|
|
1887
|
+
var createTestUser = /* @__PURE__ */ __name(async (_tx, userData) => {
|
|
1888
|
+
const mockUser = {
|
|
1889
|
+
id: `user_${Date.now()}`,
|
|
1890
|
+
email: userData.email,
|
|
1891
|
+
emailVerified: userData.emailVerified || false,
|
|
1892
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1893
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1894
|
+
};
|
|
1895
|
+
return mockUser;
|
|
1896
|
+
}, "createTestUser");
|
|
1897
|
+
var truncateAllTables = /* @__PURE__ */ __name(async () => {
|
|
1898
|
+
console.warn("truncateAllTables is not implemented - tests may have side effects");
|
|
1899
|
+
}, "truncateAllTables");
|
|
1900
|
+
var getTestDb = /* @__PURE__ */ __name(async () => {
|
|
1901
|
+
const module = await import('./client-WIO6W447.js');
|
|
1902
|
+
return module.db;
|
|
1903
|
+
}, "getTestDb");
|
|
1904
|
+
var closeTestDb = /* @__PURE__ */ __name(async () => {
|
|
1905
|
+
console.log("closeTestDb called");
|
|
1906
|
+
}, "closeTestDb");
|
|
1907
|
+
|
|
1908
|
+
// ../../packages/platform/dist/db/zod.js
|
|
1909
|
+
var AiChatSchema = createSelectSchema(aiChat);
|
|
1910
|
+
var UserSchema = createSelectSchema(user);
|
|
1911
|
+
var UserUpdateSchema = createUpdateSchema(user, {
|
|
1912
|
+
id: z.string()
|
|
1913
|
+
});
|
|
1914
|
+
var OrganizationSchema = createSelectSchema(organization);
|
|
1915
|
+
var OrganizationUpdateSchema = createUpdateSchema(organization, {
|
|
1916
|
+
id: z.string()
|
|
1917
|
+
});
|
|
1918
|
+
var MemberSchema = createSelectSchema(member);
|
|
1919
|
+
var InvitationSchema = createSelectSchema(invitation);
|
|
1920
|
+
var PurchaseSchema = createSelectSchema(purchase);
|
|
1921
|
+
var PurchaseInsertSchema = createInsertSchema(purchase);
|
|
1922
|
+
var PurchaseUpdateSchema = createUpdateSchema(purchase, {
|
|
1923
|
+
id: z.string()
|
|
1924
|
+
});
|
|
1925
|
+
var SessionSchema = createSelectSchema(session);
|
|
1926
|
+
var AccountSchema = createSelectSchema(account);
|
|
1927
|
+
var VerificationSchema = createSelectSchema(verification);
|
|
1928
|
+
var PasskeySchema = createSelectSchema(passkey);
|
|
1929
|
+
var AttributionServiceImpl = class {
|
|
1930
|
+
static {
|
|
1931
|
+
__name(this, "AttributionServiceImpl");
|
|
1932
|
+
}
|
|
1933
|
+
// In-memory storage (stub - will be replaced with database)
|
|
1934
|
+
attributions = /* @__PURE__ */ new Map();
|
|
1935
|
+
fingerprintIndex = /* @__PURE__ */ new Map();
|
|
1936
|
+
/**
|
|
1937
|
+
* Transfer attribution from web to platform
|
|
1938
|
+
*/
|
|
1939
|
+
async transferAttribution(request) {
|
|
1940
|
+
const { userId, fingerprint, attribution } = request;
|
|
1941
|
+
if (!db) {
|
|
1942
|
+
return this.transferAttributionInMemory(request);
|
|
1943
|
+
}
|
|
1944
|
+
try {
|
|
1945
|
+
const [existingByFingerprint] = await db.select().from(userAttributions).where(eq(userAttributions.fingerprint, fingerprint)).limit(1);
|
|
1946
|
+
if (existingByFingerprint) {
|
|
1947
|
+
const shouldMerge = shouldMergeAttribution({
|
|
1948
|
+
attributionId: existingByFingerprint.id,
|
|
1949
|
+
source: existingByFingerprint.source,
|
|
1950
|
+
createdAt: existingByFingerprint.createdAt.toISOString(),
|
|
1951
|
+
campaignId: existingByFingerprint.campaignId || void 0
|
|
1952
|
+
}, attribution);
|
|
1953
|
+
if (shouldMerge) {
|
|
1954
|
+
await db.update(userAttributions).set({
|
|
1955
|
+
utmParams: {
|
|
1956
|
+
...existingByFingerprint.utmParams,
|
|
1957
|
+
...attribution.utmParams
|
|
1958
|
+
},
|
|
1959
|
+
conversionData: {
|
|
1960
|
+
...existingByFingerprint.conversionData,
|
|
1961
|
+
...attribution.conversionData
|
|
1962
|
+
}
|
|
1963
|
+
}).where(eq(userAttributions.id, existingByFingerprint.id));
|
|
1964
|
+
console.log(`[Attribution] Merged attribution for user ${userId} (fingerprint: ${fingerprint})`);
|
|
1965
|
+
return {
|
|
1966
|
+
success: true,
|
|
1967
|
+
attributionId: existingByFingerprint.id,
|
|
1968
|
+
action: "merged",
|
|
1969
|
+
existingAttribution: {
|
|
1970
|
+
attributionId: existingByFingerprint.id,
|
|
1971
|
+
source: existingByFingerprint.source,
|
|
1972
|
+
createdAt: existingByFingerprint.createdAt.toISOString(),
|
|
1973
|
+
campaignId: existingByFingerprint.campaignId || void 0
|
|
1974
|
+
},
|
|
1975
|
+
message: "Attribution updated with new touch point"
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
console.log(`[Attribution] Ignored duplicate attribution for user ${userId}`);
|
|
1979
|
+
return {
|
|
1980
|
+
success: true,
|
|
1981
|
+
attributionId: existingByFingerprint.id,
|
|
1982
|
+
action: "ignored",
|
|
1983
|
+
existingAttribution: {
|
|
1984
|
+
attributionId: existingByFingerprint.id,
|
|
1985
|
+
source: existingByFingerprint.source,
|
|
1986
|
+
createdAt: existingByFingerprint.createdAt.toISOString(),
|
|
1987
|
+
campaignId: existingByFingerprint.campaignId || void 0
|
|
1988
|
+
},
|
|
1989
|
+
message: "Attribution already exists for this fingerprint"
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
const [existingByUser] = await db.select().from(userAttributions).where(eq(userAttributions.userId, userId)).limit(1);
|
|
1993
|
+
if (existingByUser) {
|
|
1994
|
+
return {
|
|
1995
|
+
success: true,
|
|
1996
|
+
attributionId: existingByUser.id,
|
|
1997
|
+
action: "ignored",
|
|
1998
|
+
existingAttribution: {
|
|
1999
|
+
attributionId: existingByUser.id,
|
|
2000
|
+
source: existingByUser.source,
|
|
2001
|
+
createdAt: existingByUser.createdAt.toISOString(),
|
|
2002
|
+
campaignId: existingByUser.campaignId || void 0
|
|
2003
|
+
},
|
|
2004
|
+
message: "User already has attribution"
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
const [newAttribution] = await db.insert(userAttributions).values({
|
|
2008
|
+
userId,
|
|
2009
|
+
source: attribution.source,
|
|
2010
|
+
campaignId: attribution.campaignId,
|
|
2011
|
+
fingerprint,
|
|
2012
|
+
conversionData: attribution.conversionData,
|
|
2013
|
+
utmParams: attribution.utmParams,
|
|
2014
|
+
referralCode: attribution.referralCode,
|
|
2015
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
2016
|
+
}).returning();
|
|
2017
|
+
if (!newAttribution) {
|
|
2018
|
+
throw new Error("Failed to create attribution record");
|
|
2019
|
+
}
|
|
2020
|
+
console.log(`[Attribution] Created new attribution for user ${userId} - Source: ${attribution.source}`);
|
|
2021
|
+
return {
|
|
2022
|
+
success: true,
|
|
2023
|
+
attributionId: newAttribution.id,
|
|
2024
|
+
action: "created",
|
|
2025
|
+
message: "Attribution recorded successfully"
|
|
2026
|
+
};
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
console.error(`[Attribution] Error transferring attribution for user ${userId}:`, error);
|
|
2029
|
+
return this.transferAttributionInMemory(request);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Get attribution for a user
|
|
2034
|
+
*/
|
|
2035
|
+
async getAttribution(userId) {
|
|
2036
|
+
if (!db) {
|
|
2037
|
+
return this.attributions.get(userId) || null;
|
|
2038
|
+
}
|
|
2039
|
+
try {
|
|
2040
|
+
const [attribution] = await db.select().from(userAttributions).where(eq(userAttributions.userId, userId)).limit(1);
|
|
2041
|
+
if (!attribution) {
|
|
2042
|
+
return null;
|
|
2043
|
+
}
|
|
2044
|
+
return {
|
|
2045
|
+
id: attribution.id,
|
|
2046
|
+
userId: attribution.userId,
|
|
2047
|
+
source: attribution.source,
|
|
2048
|
+
campaignId: attribution.campaignId || void 0,
|
|
2049
|
+
fingerprint: attribution.fingerprint,
|
|
2050
|
+
conversionData: attribution.conversionData,
|
|
2051
|
+
utmParams: attribution.utmParams,
|
|
2052
|
+
createdAt: attribution.createdAt,
|
|
2053
|
+
referralCode: attribution.referralCode || void 0,
|
|
2054
|
+
convertedAt: attribution.convertedAt || void 0
|
|
2055
|
+
};
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
console.error(`[Attribution] Error fetching attribution for user ${userId}:`, error);
|
|
2058
|
+
return this.attributions.get(userId) || null;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Mark user as converted (purchased subscription)
|
|
2063
|
+
*/
|
|
2064
|
+
async markConverted(userId) {
|
|
2065
|
+
if (!db) {
|
|
2066
|
+
const attribution = this.attributions.get(userId);
|
|
2067
|
+
if (!attribution) {
|
|
2068
|
+
return false;
|
|
2069
|
+
}
|
|
2070
|
+
attribution.convertedAt = /* @__PURE__ */ new Date();
|
|
2071
|
+
console.log(`[Attribution] Marked user ${userId} as converted - Source: ${attribution.source}`);
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
2074
|
+
try {
|
|
2075
|
+
const result = await db.update(userAttributions).set({
|
|
2076
|
+
convertedAt: /* @__PURE__ */ new Date()
|
|
2077
|
+
}).where(eq(userAttributions.userId, userId)).returning();
|
|
2078
|
+
const updated = result[0];
|
|
2079
|
+
if (!updated) {
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
2082
|
+
console.log(`[Attribution] Marked user ${userId} as converted - Source: ${updated.source}`);
|
|
2083
|
+
return true;
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
console.error(`[Attribution] Error marking user ${userId} as converted:`, error);
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Get conversion metrics by source
|
|
2091
|
+
*/
|
|
2092
|
+
async getConversionMetrics(dateRange) {
|
|
2093
|
+
if (!db) {
|
|
2094
|
+
return this.getConversionMetricsInMemory(dateRange);
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
let query = db.select().from(userAttributions);
|
|
2098
|
+
if (dateRange) {
|
|
2099
|
+
query = query.where(and(gte(userAttributions.createdAt, dateRange.from), lte(userAttributions.createdAt, dateRange.to)));
|
|
2100
|
+
}
|
|
2101
|
+
const attributions = await query;
|
|
2102
|
+
const metricsBySource = /* @__PURE__ */ new Map();
|
|
2103
|
+
for (const record of attributions) {
|
|
2104
|
+
const metrics = metricsBySource.get(record.source) || {
|
|
2105
|
+
total: 0,
|
|
2106
|
+
conversions: 0,
|
|
2107
|
+
timesToConvert: []
|
|
2108
|
+
};
|
|
2109
|
+
metrics.total++;
|
|
2110
|
+
if (record.convertedAt) {
|
|
2111
|
+
metrics.conversions++;
|
|
2112
|
+
const daysToConvert = (record.convertedAt.getTime() - record.createdAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2113
|
+
metrics.timesToConvert.push(daysToConvert);
|
|
2114
|
+
}
|
|
2115
|
+
metricsBySource.set(record.source, metrics);
|
|
2116
|
+
}
|
|
2117
|
+
const results = [];
|
|
2118
|
+
for (const [source, data] of metricsBySource.entries()) {
|
|
2119
|
+
const avgTimeToConvert = data.timesToConvert.length > 0 ? data.timesToConvert.reduce((a, b) => a + b, 0) / data.timesToConvert.length : void 0;
|
|
2120
|
+
results.push({
|
|
2121
|
+
source,
|
|
2122
|
+
totalUsers: data.total,
|
|
2123
|
+
conversions: data.conversions,
|
|
2124
|
+
conversionRate: data.conversions / data.total,
|
|
2125
|
+
avgTimeToConvert
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
return results.sort((a, b) => b.totalUsers - a.totalUsers);
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
console.error("[Attribution] Error fetching conversion metrics:", error);
|
|
2131
|
+
return this.getConversionMetricsInMemory(dateRange);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Fallback: Transfer attribution in-memory
|
|
2136
|
+
*/
|
|
2137
|
+
transferAttributionInMemory(request) {
|
|
2138
|
+
const { userId, fingerprint, attribution } = request;
|
|
2139
|
+
const existingUserId = this.fingerprintIndex.get(fingerprint);
|
|
2140
|
+
if (existingUserId) {
|
|
2141
|
+
const existing = this.attributions.get(existingUserId);
|
|
2142
|
+
if (existing) {
|
|
2143
|
+
const shouldMerge = shouldMergeAttribution({
|
|
2144
|
+
attributionId: existing.id,
|
|
2145
|
+
source: existing.source,
|
|
2146
|
+
createdAt: existing.createdAt.toISOString(),
|
|
2147
|
+
campaignId: existing.campaignId
|
|
2148
|
+
}, attribution);
|
|
2149
|
+
if (shouldMerge) {
|
|
2150
|
+
existing.utmParams = {
|
|
2151
|
+
...existing.utmParams,
|
|
2152
|
+
...attribution.utmParams
|
|
2153
|
+
};
|
|
2154
|
+
existing.conversionData = {
|
|
2155
|
+
...existing.conversionData,
|
|
2156
|
+
...attribution.conversionData
|
|
2157
|
+
};
|
|
2158
|
+
return {
|
|
2159
|
+
success: true,
|
|
2160
|
+
attributionId: existing.id,
|
|
2161
|
+
action: "merged",
|
|
2162
|
+
existingAttribution: {
|
|
2163
|
+
attributionId: existing.id,
|
|
2164
|
+
source: existing.source,
|
|
2165
|
+
createdAt: existing.createdAt.toISOString(),
|
|
2166
|
+
campaignId: existing.campaignId
|
|
2167
|
+
},
|
|
2168
|
+
message: "Attribution updated with new touch point"
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
return {
|
|
2172
|
+
success: true,
|
|
2173
|
+
attributionId: existing.id,
|
|
2174
|
+
action: "ignored",
|
|
2175
|
+
existingAttribution: {
|
|
2176
|
+
attributionId: existing.id,
|
|
2177
|
+
source: existing.source,
|
|
2178
|
+
createdAt: existing.createdAt.toISOString(),
|
|
2179
|
+
campaignId: existing.campaignId
|
|
2180
|
+
},
|
|
2181
|
+
message: "Attribution already exists for this fingerprint"
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const attributionId = `attr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2186
|
+
const record = {
|
|
2187
|
+
id: attributionId,
|
|
2188
|
+
userId,
|
|
2189
|
+
source: attribution.source,
|
|
2190
|
+
campaignId: attribution.campaignId,
|
|
2191
|
+
fingerprint,
|
|
2192
|
+
conversionData: attribution.conversionData,
|
|
2193
|
+
utmParams: attribution.utmParams,
|
|
2194
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2195
|
+
referralCode: attribution.referralCode
|
|
2196
|
+
};
|
|
2197
|
+
this.attributions.set(userId, record);
|
|
2198
|
+
this.fingerprintIndex.set(fingerprint, userId);
|
|
2199
|
+
return {
|
|
2200
|
+
success: true,
|
|
2201
|
+
attributionId,
|
|
2202
|
+
action: "created",
|
|
2203
|
+
message: "Attribution recorded successfully"
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Fallback: Get conversion metrics in-memory
|
|
2208
|
+
*/
|
|
2209
|
+
getConversionMetricsInMemory(dateRange) {
|
|
2210
|
+
const metricsBySource = /* @__PURE__ */ new Map();
|
|
2211
|
+
for (const record of this.attributions.values()) {
|
|
2212
|
+
if (dateRange) {
|
|
2213
|
+
if (record.createdAt < dateRange.from || record.createdAt > dateRange.to) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
const metrics = metricsBySource.get(record.source) || {
|
|
2218
|
+
total: 0,
|
|
2219
|
+
conversions: 0,
|
|
2220
|
+
timesToConvert: []
|
|
2221
|
+
};
|
|
2222
|
+
metrics.total++;
|
|
2223
|
+
if (record.convertedAt) {
|
|
2224
|
+
metrics.conversions++;
|
|
2225
|
+
const daysToConvert = (record.convertedAt.getTime() - record.createdAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
2226
|
+
metrics.timesToConvert.push(daysToConvert);
|
|
2227
|
+
}
|
|
2228
|
+
metricsBySource.set(record.source, metrics);
|
|
2229
|
+
}
|
|
2230
|
+
const results = [];
|
|
2231
|
+
for (const [source, data] of metricsBySource.entries()) {
|
|
2232
|
+
const avgTimeToConvert = data.timesToConvert.length > 0 ? data.timesToConvert.reduce((a, b) => a + b, 0) / data.timesToConvert.length : void 0;
|
|
2233
|
+
results.push({
|
|
2234
|
+
source,
|
|
2235
|
+
totalUsers: data.total,
|
|
2236
|
+
conversions: data.conversions,
|
|
2237
|
+
conversionRate: data.conversions / data.total,
|
|
2238
|
+
avgTimeToConvert
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
return results.sort((a, b) => b.totalUsers - a.totalUsers);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2245
|
+
// ../../packages/config/dist/subscription-config.js
|
|
2246
|
+
var PRO_TRIAL_DAYS = 14;
|
|
2247
|
+
var TIER_CREDIT_ALLOWANCES = {
|
|
2248
|
+
free: 25,
|
|
2249
|
+
pro: 200,
|
|
2250
|
+
team: 200,
|
|
2251
|
+
enterprise: 0
|
|
2252
|
+
};
|
|
2253
|
+
var CREDIT_OVERAGE_SOFT_CAP = -100;
|
|
2254
|
+
|
|
2255
|
+
// ../../packages/config/dist/config.js
|
|
2256
|
+
var config = {
|
|
2257
|
+
appName: "SnapBack",
|
|
2258
|
+
tagline: "AI-Native DevOps",
|
|
2259
|
+
description: "Protection for your code.",
|
|
2260
|
+
organizations: {
|
|
2261
|
+
enable: true,
|
|
2262
|
+
enableBilling: true,
|
|
2263
|
+
enableUsersToCreateOrganizations: true,
|
|
2264
|
+
requireOrganization: false,
|
|
2265
|
+
hideOrganization: false,
|
|
2266
|
+
forbiddenOrganizationSlugs: [
|
|
2267
|
+
"admin",
|
|
2268
|
+
"root",
|
|
2269
|
+
"api",
|
|
2270
|
+
"app"
|
|
2271
|
+
]
|
|
2272
|
+
},
|
|
2273
|
+
users: {
|
|
2274
|
+
enableBilling: true,
|
|
2275
|
+
enableOnboarding: true
|
|
2276
|
+
},
|
|
2277
|
+
auth: {
|
|
2278
|
+
enableSignup: true,
|
|
2279
|
+
enableMagicLink: true,
|
|
2280
|
+
enableSocialLogin: true,
|
|
2281
|
+
enablePasskeys: false,
|
|
2282
|
+
enablePasswordLogin: true,
|
|
2283
|
+
enableTwoFactor: false,
|
|
2284
|
+
redirectAfterSignIn: "/app",
|
|
2285
|
+
redirectAfterLogout: "/",
|
|
2286
|
+
sessionCookieMaxAge: 60 * 60 * 24 * 30
|
|
2287
|
+
},
|
|
2288
|
+
mails: {
|
|
2289
|
+
from: "no-reply@snapback.dev"
|
|
2290
|
+
},
|
|
2291
|
+
storage: {
|
|
2292
|
+
bucketNames: {
|
|
2293
|
+
avatars: "snapback-avatars",
|
|
2294
|
+
checkpoints: "snapback-checkpoints",
|
|
2295
|
+
snapshots: "snapback-snapshots"
|
|
2296
|
+
}
|
|
2297
|
+
},
|
|
2298
|
+
ui: {
|
|
2299
|
+
enabledThemes: [
|
|
2300
|
+
"light",
|
|
2301
|
+
"dark"
|
|
2302
|
+
],
|
|
2303
|
+
defaultTheme: "dark",
|
|
2304
|
+
saas: {
|
|
2305
|
+
enabled: true,
|
|
2306
|
+
useSidebarLayout: true
|
|
2307
|
+
},
|
|
2308
|
+
marketing: {
|
|
2309
|
+
enabled: true
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
contactForm: {
|
|
2313
|
+
enabled: true,
|
|
2314
|
+
to: "support@snapback.dev",
|
|
2315
|
+
subject: "Contact from SnapBack"
|
|
2316
|
+
},
|
|
2317
|
+
payments: {
|
|
2318
|
+
plans: {
|
|
2319
|
+
free: {
|
|
2320
|
+
name: "Free",
|
|
2321
|
+
description: "Essential protection for individuals.",
|
|
2322
|
+
features: [
|
|
2323
|
+
"Unlimited local checkpoints",
|
|
2324
|
+
"Basic AI detection",
|
|
2325
|
+
"Community support"
|
|
2326
|
+
],
|
|
2327
|
+
isFree: true,
|
|
2328
|
+
prices: [
|
|
2329
|
+
{
|
|
2330
|
+
productId: "price_free",
|
|
2331
|
+
amount: 0,
|
|
2332
|
+
currency: "USD",
|
|
2333
|
+
type: "recurring",
|
|
2334
|
+
interval: "month"
|
|
2335
|
+
}
|
|
2336
|
+
]
|
|
2337
|
+
},
|
|
2338
|
+
pro: {
|
|
2339
|
+
name: "Pro",
|
|
2340
|
+
description: "Advanced protection for professional developers.",
|
|
2341
|
+
features: [
|
|
2342
|
+
"Cloud backup & sync",
|
|
2343
|
+
"Advanced AI analysis",
|
|
2344
|
+
"Priority support",
|
|
2345
|
+
"Unlimited history"
|
|
2346
|
+
],
|
|
2347
|
+
recommended: true,
|
|
2348
|
+
prices: [
|
|
2349
|
+
{
|
|
2350
|
+
productId: "price_pro_monthly",
|
|
2351
|
+
amount: 2e3,
|
|
2352
|
+
currency: "USD",
|
|
2353
|
+
type: "recurring",
|
|
2354
|
+
interval: "month",
|
|
2355
|
+
trialPeriodDays: PRO_TRIAL_DAYS
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
productId: "price_pro_yearly",
|
|
2359
|
+
amount: 2e4,
|
|
2360
|
+
currency: "USD",
|
|
2361
|
+
type: "recurring",
|
|
2362
|
+
interval: "year",
|
|
2363
|
+
trialPeriodDays: PRO_TRIAL_DAYS
|
|
2364
|
+
}
|
|
2365
|
+
]
|
|
2366
|
+
},
|
|
2367
|
+
team: {
|
|
2368
|
+
name: "Team",
|
|
2369
|
+
description: "Collaborative security for teams.",
|
|
2370
|
+
features: [
|
|
2371
|
+
"Everything in Pro",
|
|
2372
|
+
"Team dashboard",
|
|
2373
|
+
"Centralized policy management",
|
|
2374
|
+
"Audit logs"
|
|
2375
|
+
],
|
|
2376
|
+
prices: [
|
|
2377
|
+
{
|
|
2378
|
+
productId: "price_team_monthly",
|
|
2379
|
+
amount: 4900,
|
|
2380
|
+
currency: "USD",
|
|
2381
|
+
type: "recurring",
|
|
2382
|
+
interval: "month",
|
|
2383
|
+
seatBased: true
|
|
2384
|
+
}
|
|
2385
|
+
]
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
};
|
|
2390
|
+
|
|
2391
|
+
// ../../packages/config/dist/feature-flags.js
|
|
2392
|
+
process.env.ENABLE_EXTENSION_AUTH === "true";
|
|
2393
|
+
process.env.ENABLE_API_KEYS === "true";
|
|
2394
|
+
process.env.ENABLE_RATE_LIMITING === "true";
|
|
2395
|
+
process.env.ENABLE_INTELLIGENCE_LAYER === "true";
|
|
2396
|
+
process.env.ENABLE_TRUST_CALIBRATION === "true";
|
|
2397
|
+
process.env.ENABLE_PATTERN_LIBRARY === "true";
|
|
2398
|
+
process.env.ENABLE_PREDICTION_ENGINE === "true";
|
|
2399
|
+
process.env.ENABLE_GITHUB_INTEGRATION === "true";
|
|
2400
|
+
var ENABLE_SSO = process.env.ENABLE_SSO === "true";
|
|
2401
|
+
var ENABLE_CAPTCHA = process.env.ENABLE_CAPTCHA === "true";
|
|
2402
|
+
var ENABLE_MULTI_SESSION = process.env.ENABLE_MULTI_SESSION === "true";
|
|
2403
|
+
var ENABLE_ENHANCED_2FA = process.env.ENABLE_ENHANCED_2FA === "true";
|
|
2404
|
+
var ProtectionLevelSchema = z.enum([
|
|
2405
|
+
"watch",
|
|
2406
|
+
"warn",
|
|
2407
|
+
"block"
|
|
2408
|
+
]);
|
|
2409
|
+
var ProtectionRuleSchema = z.object({
|
|
2410
|
+
pattern: z.string().describe("Glob pattern (e.g., '*.env*', 'package.json')"),
|
|
2411
|
+
level: ProtectionLevelSchema,
|
|
2412
|
+
reason: z.string().optional().describe("Why this pattern is protected"),
|
|
2413
|
+
precedence: z.number().int().min(0).max(1e3).default(0)
|
|
2414
|
+
});
|
|
2415
|
+
var EngineConfigSchema = z.object({
|
|
2416
|
+
maxDepth: z.number().int().min(0).max(10).default(2).describe("Max dependency tree depth for analysis"),
|
|
2417
|
+
burstThreshold: z.number().int().min(1).max(100).default(30).describe("Min simultaneous file changes to trigger burst detection"),
|
|
2418
|
+
cooldowns: z.object({
|
|
2419
|
+
block: z.number().int().min(0).default(6e4),
|
|
2420
|
+
warn: z.number().int().min(0).default(3e4),
|
|
2421
|
+
watch: z.number().int().min(0).default(0)
|
|
2422
|
+
}).default({
|
|
2423
|
+
block: 6e4,
|
|
2424
|
+
warn: 3e4,
|
|
2425
|
+
watch: 0
|
|
2426
|
+
}).describe("Cooldown durations (ms) between alerts per level")
|
|
2427
|
+
});
|
|
2428
|
+
var IgnorePatternsSchema = z.array(z.string()).default([]).describe("Glob patterns to exclude from protection (e.g., node_modules, .git)");
|
|
2429
|
+
var PrivacySettingsSchema = z.object({
|
|
2430
|
+
consent: z.boolean().default(false).describe("User has given privacy consent"),
|
|
2431
|
+
clipboard: z.boolean().default(false).describe("Allow clipboard monitoring"),
|
|
2432
|
+
watcher: z.boolean().default(false).describe("Allow file watcher"),
|
|
2433
|
+
gitWrapper: z.boolean().default(false).describe("Allow git wrapper integration"),
|
|
2434
|
+
lastReminded: z.string().optional().describe("ISO timestamp of last consent reminder")
|
|
2435
|
+
}).default({});
|
|
2436
|
+
var NotificationsSettingsSchema = z.object({
|
|
2437
|
+
enabled: z.boolean().default(true),
|
|
2438
|
+
quietHours: z.object({
|
|
2439
|
+
start: z.string().default("22:00"),
|
|
2440
|
+
end: z.string().default("08:00")
|
|
2441
|
+
}).default({
|
|
2442
|
+
start: "22:00",
|
|
2443
|
+
end: "08:00"
|
|
2444
|
+
}),
|
|
2445
|
+
rateLimit: z.number().int().min(1).default(5).describe("Max notifications per minute")
|
|
2446
|
+
}).default({});
|
|
2447
|
+
var SnapshotSettingsSchema = z.object({
|
|
2448
|
+
enabled: z.boolean().default(true),
|
|
2449
|
+
autoCreate: z.boolean().default(true),
|
|
2450
|
+
retentionDays: z.number().int().min(1).default(30)
|
|
2451
|
+
}).default({});
|
|
2452
|
+
var AISettingsSchema = z.object({
|
|
2453
|
+
enabled: z.boolean().default(true),
|
|
2454
|
+
context: z.boolean().default(true).describe("Include code context in AI analysis"),
|
|
2455
|
+
copilot: z.boolean().default(true).describe("Integrate with GitHub Copilot")
|
|
2456
|
+
}).default({});
|
|
2457
|
+
var GuardianPluginsSchema = z.object({
|
|
2458
|
+
secretDetection: z.boolean().default(true),
|
|
2459
|
+
mockReplacement: z.boolean().default(true),
|
|
2460
|
+
phantomDependency: z.boolean().default(true)
|
|
2461
|
+
}).default({});
|
|
2462
|
+
var GuardianThresholdsSchema = z.object({
|
|
2463
|
+
warn: z.number().int().min(0).default(6),
|
|
2464
|
+
block: z.number().int().min(0).default(8)
|
|
2465
|
+
}).default({
|
|
2466
|
+
warn: 6,
|
|
2467
|
+
block: 8
|
|
2468
|
+
});
|
|
2469
|
+
var GuardianSettingsSchema = z.object({
|
|
2470
|
+
enabled: z.boolean().default(true),
|
|
2471
|
+
warnThreshold: z.number().int().min(0).max(100).default(5),
|
|
2472
|
+
blockThreshold: z.number().int().min(0).max(100).default(8),
|
|
2473
|
+
protectionLevel: ProtectionLevelSchema.default("warn"),
|
|
2474
|
+
plugins: GuardianPluginsSchema,
|
|
2475
|
+
thresholds: GuardianThresholdsSchema
|
|
2476
|
+
}).default({});
|
|
2477
|
+
var AutoDecisionSettingsSchema = z.object({
|
|
2478
|
+
riskThreshold: z.number().int().min(0).max(100).default(60).describe("Risk score threshold (0-100) for automatic snapshot creation"),
|
|
2479
|
+
notifyThreshold: z.number().int().min(0).max(100).default(40).describe("Risk score threshold (0-100) for user notifications"),
|
|
2480
|
+
minFilesForBurst: z.number().int().min(1).default(3).describe("Minimum files changed simultaneously to trigger burst detection"),
|
|
2481
|
+
maxSnapshotsPerMinute: z.number().int().min(1).default(4).describe("Maximum snapshots allowed per minute (rate limiting)")
|
|
2482
|
+
}).default({});
|
|
2483
|
+
var MCPSettingsSchema = z.object({
|
|
2484
|
+
performanceBudgets: z.record(z.number().int().min(0)).default({
|
|
2485
|
+
analyze_risk: 200,
|
|
2486
|
+
create_snapshot: 500
|
|
2487
|
+
}).describe("Performance budgets (ms) for MCP operations"),
|
|
2488
|
+
context7: z.object({
|
|
2489
|
+
apiKey: z.string().optional(),
|
|
2490
|
+
apiUrl: z.string().url().default("https://context7.com/api"),
|
|
2491
|
+
cacheTtlSearch: z.number().int().min(0).default(3600),
|
|
2492
|
+
cacheTtlDocs: z.number().int().min(0).default(86400)
|
|
2493
|
+
}).default({}),
|
|
2494
|
+
api: z.object({
|
|
2495
|
+
apiKey: z.string().optional(),
|
|
2496
|
+
baseUrl: z.string().url().default("https://api.snapback.dev")
|
|
2497
|
+
}).default({}),
|
|
2498
|
+
http: z.object({
|
|
2499
|
+
allowedOrigins: z.array(z.string()).default([
|
|
2500
|
+
"*"
|
|
2501
|
+
]),
|
|
2502
|
+
apiUrl: z.string().url().default("http://api:8080")
|
|
2503
|
+
}).default({})
|
|
2504
|
+
}).default({});
|
|
2505
|
+
var SettingsSchema = z.object({
|
|
2506
|
+
defaultProtectionLevel: ProtectionLevelSchema.default("watch"),
|
|
2507
|
+
requireSnapshotMessage: z.boolean().default(true),
|
|
2508
|
+
maxSnapshots: z.number().int().min(1).default(100),
|
|
2509
|
+
aiDetectionEnabled: z.boolean().default(true),
|
|
2510
|
+
autoRestoreOnDetection: z.boolean().default(false),
|
|
2511
|
+
privacy: PrivacySettingsSchema,
|
|
2512
|
+
notifications: NotificationsSettingsSchema,
|
|
2513
|
+
snapshots: SnapshotSettingsSchema,
|
|
2514
|
+
ai: AISettingsSchema,
|
|
2515
|
+
guardian: GuardianSettingsSchema,
|
|
2516
|
+
autoDecision: AutoDecisionSettingsSchema,
|
|
2517
|
+
webBaseUrl: z.string().url().default("https://console.snapback.dev"),
|
|
2518
|
+
apiBaseUrl: z.string().url().optional(),
|
|
2519
|
+
mcp: MCPSettingsSchema
|
|
2520
|
+
}).default({});
|
|
2521
|
+
var PolicyOverrideSchema = z.object({
|
|
2522
|
+
pattern: z.string(),
|
|
2523
|
+
level: ProtectionLevelSchema,
|
|
2524
|
+
ttl: z.number().optional().describe("Expiration timestamp (ms since epoch)")
|
|
2525
|
+
});
|
|
2526
|
+
var PoliciesSchema = z.object({
|
|
2527
|
+
enforceProtectionLevels: z.boolean().default(false),
|
|
2528
|
+
allowOverrides: z.boolean().default(true),
|
|
2529
|
+
overrides: z.array(PolicyOverrideSchema).default([])
|
|
2530
|
+
}).default({});
|
|
2531
|
+
z.object({
|
|
2532
|
+
version: z.literal(2).default(2),
|
|
2533
|
+
protections: z.array(ProtectionRuleSchema).default([]),
|
|
2534
|
+
ignore: IgnorePatternsSchema,
|
|
2535
|
+
engine: EngineConfigSchema.default({}),
|
|
2536
|
+
settings: SettingsSchema,
|
|
2537
|
+
policies: PoliciesSchema,
|
|
2538
|
+
mcp: MCPSettingsSchema.optional()
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2541
|
+
// ../../packages/config/dist/utils/base-url.js
|
|
2542
|
+
function getBaseUrl() {
|
|
2543
|
+
if (process.env.NEXT_PUBLIC_SITE_URL) {
|
|
2544
|
+
return process.env.NEXT_PUBLIC_SITE_URL;
|
|
2545
|
+
}
|
|
2546
|
+
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
|
|
2547
|
+
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
|
2548
|
+
}
|
|
2549
|
+
return `http://localhost:${process.env.PORT ?? 3e3}`;
|
|
2550
|
+
}
|
|
2551
|
+
__name(getBaseUrl, "getBaseUrl");
|
|
2552
|
+
createLogger({
|
|
2553
|
+
name: "feature-flags",
|
|
2554
|
+
level: LogLevel.INFO
|
|
2555
|
+
});
|
|
2556
|
+
new PostHog(process.env.POSTHOG_API_KEY || "default_key", {
|
|
2557
|
+
host: process.env.POSTHOG_HOST || "https://app.posthog.com"
|
|
2558
|
+
});
|
|
2559
|
+
var EntitlementsServiceImpl = class {
|
|
2560
|
+
static {
|
|
2561
|
+
__name(this, "EntitlementsServiceImpl");
|
|
2562
|
+
}
|
|
2563
|
+
cache = /* @__PURE__ */ new Map();
|
|
2564
|
+
CACHE_TTL_MS = 6e4;
|
|
2565
|
+
/**
|
|
2566
|
+
* Fetch complete entitlement set for a user
|
|
2567
|
+
* Queries database for subscription, trial, and Pioneer status with Redis caching
|
|
2568
|
+
*/
|
|
2569
|
+
async getEntitlements(userId) {
|
|
2570
|
+
if (isRedisAvailable()) {
|
|
2571
|
+
const cached = await getCache(`entitlements:${userId}`);
|
|
2572
|
+
if (cached) {
|
|
2573
|
+
return cached;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
const memoryCached = this.cache.get(userId);
|
|
2577
|
+
if (memoryCached && Date.now() - memoryCached.timestamp < this.CACHE_TTL_MS) {
|
|
2578
|
+
return memoryCached.entitlements;
|
|
2579
|
+
}
|
|
2580
|
+
if (!db) {
|
|
2581
|
+
const entitlements = this.createDefaultEntitlements(userId);
|
|
2582
|
+
return entitlements;
|
|
2583
|
+
}
|
|
2584
|
+
try {
|
|
2585
|
+
const [subscription] = await db.select().from(subscriptions).where(eq(subscriptions.userId, userId)).limit(1);
|
|
2586
|
+
const [trial] = await db.select().from(trials).where(and(eq(trials.userId, userId), eq(trials.status, "active"))).limit(1);
|
|
2587
|
+
const [pioneer] = await db.select().from(pioneers).where(eq(pioneers.userId, userId)).limit(1);
|
|
2588
|
+
let tier = subscription?.plan || "free";
|
|
2589
|
+
if (pioneer?.tier === "founding_pioneer" && (tier === "free" || tier === "pro")) {
|
|
2590
|
+
tier = "pro";
|
|
2591
|
+
}
|
|
2592
|
+
const [usageData] = subscription ? await db.select().from(usageLimits).where(eq(usageLimits.subscriptionId, subscription.id)).orderBy(desc(usageLimits.month)).limit(1) : [
|
|
2593
|
+
null
|
|
2594
|
+
];
|
|
2595
|
+
const creditBalance = await this.calculateCreditBalance(userId, tier, subscription);
|
|
2596
|
+
const entitlements = this.buildEntitlements(userId, tier, trial || null, pioneer || null, subscription || null, usageData || null, creditBalance);
|
|
2597
|
+
if (isRedisAvailable()) {
|
|
2598
|
+
await setCache(`entitlements:${userId}`, entitlements, 60);
|
|
2599
|
+
}
|
|
2600
|
+
this.cache.set(userId, {
|
|
2601
|
+
entitlements,
|
|
2602
|
+
timestamp: Date.now()
|
|
2603
|
+
});
|
|
2604
|
+
return entitlements;
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
console.error(`[Entitlements] Error fetching entitlements for user ${userId}:`, error);
|
|
2607
|
+
return this.createDefaultEntitlements(userId);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Check if user has access to a specific feature
|
|
2612
|
+
*/
|
|
2613
|
+
async checkFeatureAccess(userId, feature) {
|
|
2614
|
+
const entitlements = await this.getEntitlements(userId);
|
|
2615
|
+
if (entitlements.features.includes(feature)) {
|
|
2616
|
+
const limit = entitlements.limits.get(feature);
|
|
2617
|
+
if (limit && limit.max !== null && limit.current >= limit.max) {
|
|
2618
|
+
return {
|
|
2619
|
+
granted: false,
|
|
2620
|
+
reason: "usage_limit_reached",
|
|
2621
|
+
limitInfo: limit
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
return {
|
|
2625
|
+
granted: true
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
if (entitlements.trial?.active && entitlements.trial.features.includes(feature)) {
|
|
2629
|
+
return {
|
|
2630
|
+
granted: true
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
if (entitlements.pioneer?.tier === "founding_pioneer") {
|
|
2634
|
+
if (isFeatureAvailableAtTier(feature, "pro")) {
|
|
2635
|
+
return {
|
|
2636
|
+
granted: true
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
const requiredTier = this.getRequiredTierForFeature(feature);
|
|
2641
|
+
return {
|
|
2642
|
+
granted: false,
|
|
2643
|
+
reason: "feature_not_in_plan",
|
|
2644
|
+
requiredTier
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Get usage limits for a specific feature
|
|
2649
|
+
*/
|
|
2650
|
+
async getFeatureLimits(userId, feature) {
|
|
2651
|
+
const entitlements = await this.getEntitlements(userId);
|
|
2652
|
+
return entitlements.limits.get(feature) || null;
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Check if user meets minimum tier requirement
|
|
2656
|
+
*/
|
|
2657
|
+
async checkTierRequirement(userId, requiredTier) {
|
|
2658
|
+
const entitlements = await this.getEntitlements(userId);
|
|
2659
|
+
return this.compareTiers(entitlements.tier, requiredTier) >= 0;
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Invalidate cached entitlements for a user (both Redis and memory)
|
|
2663
|
+
*/
|
|
2664
|
+
async invalidateCache(userId) {
|
|
2665
|
+
this.cache.delete(userId);
|
|
2666
|
+
if (isRedisAvailable()) {
|
|
2667
|
+
await deleteCache(`entitlements:${userId}`);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Build entitlements from database query results
|
|
2672
|
+
*/
|
|
2673
|
+
buildEntitlements(userId, tier, trial, pioneer, subscription, usageData, creditBalance) {
|
|
2674
|
+
const features = getTierFeatures(tier);
|
|
2675
|
+
const trialInfo = trial ? {
|
|
2676
|
+
active: true,
|
|
2677
|
+
endsAt: trial.endsAt,
|
|
2678
|
+
features: trial.features || []
|
|
2679
|
+
} : null;
|
|
2680
|
+
const pioneerInfo = pioneer ? {
|
|
2681
|
+
tier: pioneer.tier,
|
|
2682
|
+
totalPoints: pioneer.totalPoints,
|
|
2683
|
+
pointsToNext: this.calculatePointsToNext(pioneer.tier, pioneer.totalPoints),
|
|
2684
|
+
nextTier: this.getNextTier(pioneer.tier),
|
|
2685
|
+
discountPercent: this.getTierDiscount(pioneer.tier),
|
|
2686
|
+
benefits: this.getTierBenefits(pioneer.tier)
|
|
2687
|
+
} : null;
|
|
2688
|
+
const limits = /* @__PURE__ */ new Map();
|
|
2689
|
+
for (const feature of features) {
|
|
2690
|
+
const maxLimit = getTierLimit(tier, feature);
|
|
2691
|
+
if (maxLimit !== null) {
|
|
2692
|
+
const current = this.getFeatureUsage(feature, usageData);
|
|
2693
|
+
limits.set(feature, {
|
|
2694
|
+
feature,
|
|
2695
|
+
current,
|
|
2696
|
+
max: maxLimit,
|
|
2697
|
+
period: "monthly"
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
const effectiveDate = subscription?.currentPeriodStart || /* @__PURE__ */ new Date();
|
|
2702
|
+
const expiresAt = subscription?.currentPeriodEnd || null;
|
|
2703
|
+
return {
|
|
2704
|
+
userId,
|
|
2705
|
+
tier,
|
|
2706
|
+
features,
|
|
2707
|
+
limits,
|
|
2708
|
+
trial: trialInfo,
|
|
2709
|
+
pioneer: pioneerInfo,
|
|
2710
|
+
credits: creditBalance,
|
|
2711
|
+
effectiveDate,
|
|
2712
|
+
expiresAt,
|
|
2713
|
+
version: 1,
|
|
2714
|
+
reason: "subscription"
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Create default entitlements for a user (graceful degradation fallback)
|
|
2719
|
+
* Used when database is unavailable - returns free tier with no usage data
|
|
2720
|
+
*/
|
|
2721
|
+
createDefaultEntitlements(userId) {
|
|
2722
|
+
const tier = "free";
|
|
2723
|
+
const features = getTierFeatures(tier);
|
|
2724
|
+
const limits = /* @__PURE__ */ new Map();
|
|
2725
|
+
for (const feature of features) {
|
|
2726
|
+
const maxLimit = getTierLimit(tier, feature);
|
|
2727
|
+
if (maxLimit !== null) {
|
|
2728
|
+
limits.set(feature, {
|
|
2729
|
+
feature,
|
|
2730
|
+
current: 0,
|
|
2731
|
+
max: maxLimit,
|
|
2732
|
+
period: "monthly"
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
const defaultCredits = {
|
|
2737
|
+
included: TIER_CREDIT_ALLOWANCES.free,
|
|
2738
|
+
topups: 0,
|
|
2739
|
+
total: TIER_CREDIT_ALLOWANCES.free,
|
|
2740
|
+
overage: 0,
|
|
2741
|
+
softCapReached: false,
|
|
2742
|
+
monthlyAllowance: TIER_CREDIT_ALLOWANCES.free
|
|
2743
|
+
};
|
|
2744
|
+
return {
|
|
2745
|
+
userId,
|
|
2746
|
+
tier,
|
|
2747
|
+
features,
|
|
2748
|
+
limits,
|
|
2749
|
+
trial: null,
|
|
2750
|
+
pioneer: null,
|
|
2751
|
+
credits: defaultCredits,
|
|
2752
|
+
effectiveDate: /* @__PURE__ */ new Date(),
|
|
2753
|
+
expiresAt: null,
|
|
2754
|
+
version: 1,
|
|
2755
|
+
reason: "subscription"
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Calculate credit balance from the credits ledger (pricing_spec_v3.md)
|
|
2760
|
+
* Per spec: Balance = included credits + top-up credits - consumption
|
|
2761
|
+
*/
|
|
2762
|
+
async calculateCreditBalance(userId, tier, subscription) {
|
|
2763
|
+
const monthlyAllowance = TIER_CREDIT_ALLOWANCES[tier] || 0;
|
|
2764
|
+
const now = /* @__PURE__ */ new Date();
|
|
2765
|
+
const billingPeriodStart = subscription?.currentPeriodStart || new Date(now.getFullYear(), now.getMonth(), 1);
|
|
2766
|
+
const billingPeriodEnd = subscription?.currentPeriodEnd || new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
2767
|
+
try {
|
|
2768
|
+
const [includedResult] = await db.select({
|
|
2769
|
+
total: sum(creditsLedger.credits)
|
|
2770
|
+
}).from(creditsLedger).where(and(eq(creditsLedger.userId, userId), eq(creditsLedger.transactionType, "monthly_allowance"), gt(creditsLedger.createdAt, billingPeriodStart), lte(creditsLedger.createdAt, billingPeriodEnd)));
|
|
2771
|
+
const [topupResult] = await db.select({
|
|
2772
|
+
total: sum(creditsLedger.credits)
|
|
2773
|
+
}).from(creditsLedger).where(and(eq(creditsLedger.userId, userId), eq(creditsLedger.transactionType, "top_up")));
|
|
2774
|
+
const [consumptionResult] = await db.select({
|
|
2775
|
+
total: sum(creditsLedger.credits)
|
|
2776
|
+
}).from(creditsLedger).where(and(eq(creditsLedger.userId, userId), eq(creditsLedger.transactionType, "job_consumption")));
|
|
2777
|
+
const included = Number(includedResult?.total || 0);
|
|
2778
|
+
const topups = Number(topupResult?.total || 0);
|
|
2779
|
+
const consumption = Number(consumptionResult?.total || 0);
|
|
2780
|
+
const total = included + topups + consumption;
|
|
2781
|
+
const overage = total < 0 ? Math.abs(total) : 0;
|
|
2782
|
+
const softCapReached = total <= CREDIT_OVERAGE_SOFT_CAP;
|
|
2783
|
+
return {
|
|
2784
|
+
included: Math.max(0, included + consumption),
|
|
2785
|
+
topups,
|
|
2786
|
+
total,
|
|
2787
|
+
overage,
|
|
2788
|
+
softCapReached,
|
|
2789
|
+
monthlyAllowance
|
|
2790
|
+
};
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
console.error(`[Entitlements] Error calculating credit balance for user ${userId}:`, error);
|
|
2793
|
+
return {
|
|
2794
|
+
included: monthlyAllowance,
|
|
2795
|
+
topups: 0,
|
|
2796
|
+
total: monthlyAllowance,
|
|
2797
|
+
overage: 0,
|
|
2798
|
+
softCapReached: false,
|
|
2799
|
+
monthlyAllowance
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Determine required tier for a feature
|
|
2805
|
+
*/
|
|
2806
|
+
getRequiredTierForFeature(feature) {
|
|
2807
|
+
const tiers = [
|
|
2808
|
+
"free",
|
|
2809
|
+
"pro",
|
|
2810
|
+
"team",
|
|
2811
|
+
"enterprise"
|
|
2812
|
+
];
|
|
2813
|
+
for (const tier of tiers) {
|
|
2814
|
+
if (isFeatureAvailableAtTier(feature, tier)) {
|
|
2815
|
+
return tier;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return "enterprise";
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* Calculate points needed to reach next tier
|
|
2822
|
+
*/
|
|
2823
|
+
calculatePointsToNext(currentTier, totalPoints) {
|
|
2824
|
+
const PIONEER_TIER_THRESHOLDS2 = {
|
|
2825
|
+
pioneer: 0,
|
|
2826
|
+
active_pioneer: 1,
|
|
2827
|
+
contributing_pioneer: 2,
|
|
2828
|
+
founding_pioneer: 3
|
|
2829
|
+
};
|
|
2830
|
+
const tierOrder = [
|
|
2831
|
+
"pioneer",
|
|
2832
|
+
"active_pioneer",
|
|
2833
|
+
"contributing_pioneer",
|
|
2834
|
+
"founding_pioneer"
|
|
2835
|
+
];
|
|
2836
|
+
const currentIndex = tierOrder.indexOf(currentTier);
|
|
2837
|
+
if (currentIndex === -1 || currentIndex === tierOrder.length - 1) {
|
|
2838
|
+
return 0;
|
|
2839
|
+
}
|
|
2840
|
+
const nextTier = tierOrder[currentIndex + 1];
|
|
2841
|
+
if (!nextTier) {
|
|
2842
|
+
return 0;
|
|
2843
|
+
}
|
|
2844
|
+
const threshold = PIONEER_TIER_THRESHOLDS2[nextTier];
|
|
2845
|
+
return threshold !== void 0 ? threshold - totalPoints : 0;
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Get next Pioneer tier
|
|
2849
|
+
*/
|
|
2850
|
+
getNextTier(currentTier) {
|
|
2851
|
+
const tierOrder = [
|
|
2852
|
+
"pioneer",
|
|
2853
|
+
"active_pioneer",
|
|
2854
|
+
"contributing_pioneer",
|
|
2855
|
+
"founding_pioneer"
|
|
2856
|
+
];
|
|
2857
|
+
const currentIndex = tierOrder.indexOf(currentTier);
|
|
2858
|
+
if (currentIndex === -1 || currentIndex === tierOrder.length - 1) {
|
|
2859
|
+
return null;
|
|
2860
|
+
}
|
|
2861
|
+
return tierOrder[currentIndex + 1] ?? null;
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Get discount percentage for Pioneer tier
|
|
2865
|
+
*/
|
|
2866
|
+
getTierDiscount(tier) {
|
|
2867
|
+
const discounts = {
|
|
2868
|
+
pioneer: 0,
|
|
2869
|
+
active_pioneer: 50,
|
|
2870
|
+
contributing_pioneer: 75,
|
|
2871
|
+
founding_pioneer: 100
|
|
2872
|
+
};
|
|
2873
|
+
return discounts[tier] || 0;
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Get benefits for Pioneer tier
|
|
2877
|
+
*/
|
|
2878
|
+
getTierBenefits(tier) {
|
|
2879
|
+
const benefits = {
|
|
2880
|
+
pioneer: [
|
|
2881
|
+
"Pioneer badge",
|
|
2882
|
+
"Community access"
|
|
2883
|
+
],
|
|
2884
|
+
active_pioneer: [
|
|
2885
|
+
"Pioneer badge",
|
|
2886
|
+
"Community access",
|
|
2887
|
+
"50% discount on Pro plan"
|
|
2888
|
+
],
|
|
2889
|
+
contributing_pioneer: [
|
|
2890
|
+
"Pioneer badge",
|
|
2891
|
+
"Community access",
|
|
2892
|
+
"75% discount on Pro plan",
|
|
2893
|
+
"Priority support"
|
|
2894
|
+
],
|
|
2895
|
+
founding_pioneer: [
|
|
2896
|
+
"Pioneer badge",
|
|
2897
|
+
"Community access",
|
|
2898
|
+
"Lifetime Pro access",
|
|
2899
|
+
"Priority support",
|
|
2900
|
+
"Founding member recognition"
|
|
2901
|
+
]
|
|
2902
|
+
};
|
|
2903
|
+
return benefits[tier] || [];
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Compare two tiers (returns -1, 0, or 1)
|
|
2907
|
+
*/
|
|
2908
|
+
compareTiers(userTier, requiredTier) {
|
|
2909
|
+
const tierOrder = [
|
|
2910
|
+
"free",
|
|
2911
|
+
"pro",
|
|
2912
|
+
"team",
|
|
2913
|
+
"enterprise"
|
|
2914
|
+
];
|
|
2915
|
+
const userIndex = tierOrder.indexOf(userTier);
|
|
2916
|
+
const requiredIndex = tierOrder.indexOf(requiredTier);
|
|
2917
|
+
return userIndex - requiredIndex;
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Map feature to actual usage from usage_limits table (ENT-001)
|
|
2921
|
+
* Maps feature names to database columns for usage tracking
|
|
2922
|
+
*/
|
|
2923
|
+
getFeatureUsage(feature, usageData) {
|
|
2924
|
+
if (!usageData) {
|
|
2925
|
+
return 0;
|
|
2926
|
+
}
|
|
2927
|
+
switch (feature) {
|
|
2928
|
+
case "cloud_backup":
|
|
2929
|
+
return usageData.snapshotsUsed || 0;
|
|
2930
|
+
case "api_access":
|
|
2931
|
+
return usageData.apiCallsUsed || 0;
|
|
2932
|
+
// Features without usage tracking (binary access)
|
|
2933
|
+
case "advanced_analytics":
|
|
2934
|
+
case "unlimited_workspaces":
|
|
2935
|
+
case "cli_full_features":
|
|
2936
|
+
case "team_dashboard":
|
|
2937
|
+
case "multi_workspace":
|
|
2938
|
+
case "sso_authentication":
|
|
2939
|
+
case "audit_logs":
|
|
2940
|
+
case "priority_support":
|
|
2941
|
+
case "custom_retention":
|
|
2942
|
+
return 0;
|
|
2943
|
+
// No usage tracking for these features
|
|
2944
|
+
default:
|
|
2945
|
+
return 0;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
var entitlementsService = new EntitlementsServiceImpl();
|
|
2950
|
+
function getDb() {
|
|
2951
|
+
if (!db) {
|
|
2952
|
+
throw new Error("Database not initialized. Check DATABASE_URL environment variable.");
|
|
2953
|
+
}
|
|
2954
|
+
return db;
|
|
2955
|
+
}
|
|
2956
|
+
__name(getDb, "getDb");
|
|
2957
|
+
var MCPService = class {
|
|
2958
|
+
static {
|
|
2959
|
+
__name(this, "MCPService");
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Record an observation from the MCP bridge
|
|
2963
|
+
* Uses idempotency key to prevent duplicates
|
|
2964
|
+
*/
|
|
2965
|
+
async recordObservation(input) {
|
|
2966
|
+
try {
|
|
2967
|
+
const database = getDb();
|
|
2968
|
+
if (input.idempotencyKey) {
|
|
2969
|
+
const existing = await database.select({
|
|
2970
|
+
id: mcpObservations.id
|
|
2971
|
+
}).from(mcpObservations).where(eq(mcpObservations.idempotencyKey, input.idempotencyKey)).limit(1);
|
|
2972
|
+
if (existing.length > 0) {
|
|
2973
|
+
return {
|
|
2974
|
+
id: existing[0].id,
|
|
2975
|
+
created: false
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
const data = {
|
|
2980
|
+
workspaceId: input.workspaceId,
|
|
2981
|
+
userId: input.userId,
|
|
2982
|
+
type: input.type,
|
|
2983
|
+
severity: input.severity,
|
|
2984
|
+
message: input.message,
|
|
2985
|
+
context: input.context ?? {},
|
|
2986
|
+
filePath: input.filePath,
|
|
2987
|
+
lineNumber: input.lineNumber,
|
|
2988
|
+
source: input.source ?? "extension",
|
|
2989
|
+
toolName: input.toolName,
|
|
2990
|
+
idempotencyKey: input.idempotencyKey,
|
|
2991
|
+
deviceId: input.deviceId,
|
|
2992
|
+
observedAt: input.observedAt ?? /* @__PURE__ */ new Date(),
|
|
2993
|
+
processed: false
|
|
2994
|
+
};
|
|
2995
|
+
const result = await database.insert(mcpObservations).values(data).returning({
|
|
2996
|
+
id: mcpObservations.id
|
|
2997
|
+
});
|
|
2998
|
+
return {
|
|
2999
|
+
id: result[0].id,
|
|
3000
|
+
created: true
|
|
3001
|
+
};
|
|
3002
|
+
} catch (error) {
|
|
3003
|
+
console.error("[MCPService] Failed to record observation:", error);
|
|
3004
|
+
throw error;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Record a tool invocation
|
|
3009
|
+
* Uses idempotency key to prevent duplicates
|
|
3010
|
+
*/
|
|
3011
|
+
async recordInvocation(input) {
|
|
3012
|
+
try {
|
|
3013
|
+
const database = getDb();
|
|
3014
|
+
if (input.idempotencyKey) {
|
|
3015
|
+
const existing = await database.select({
|
|
3016
|
+
id: mcpToolInvocations.id
|
|
3017
|
+
}).from(mcpToolInvocations).where(eq(mcpToolInvocations.idempotencyKey, input.idempotencyKey)).limit(1);
|
|
3018
|
+
if (existing.length > 0) {
|
|
3019
|
+
return {
|
|
3020
|
+
id: existing[0].id,
|
|
3021
|
+
created: false
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
const data = {
|
|
3026
|
+
workspaceId: input.workspaceId,
|
|
3027
|
+
userId: input.userId,
|
|
3028
|
+
toolName: input.toolName,
|
|
3029
|
+
toolVersion: input.toolVersion,
|
|
3030
|
+
invocationType: input.invocationType,
|
|
3031
|
+
requestPayload: input.requestPayload,
|
|
3032
|
+
responsePayload: input.responsePayload,
|
|
3033
|
+
status: input.status ?? "pending",
|
|
3034
|
+
errorMessage: input.errorMessage,
|
|
3035
|
+
inputTokens: input.inputTokens ?? 0,
|
|
3036
|
+
outputTokens: input.outputTokens ?? 0,
|
|
3037
|
+
idempotencyKey: input.idempotencyKey,
|
|
3038
|
+
source: input.source ?? "extension",
|
|
3039
|
+
sessionId: input.sessionId,
|
|
3040
|
+
durationMs: input.durationMs
|
|
3041
|
+
};
|
|
3042
|
+
const result = await database.insert(mcpToolInvocations).values(data).returning({
|
|
3043
|
+
id: mcpToolInvocations.id
|
|
3044
|
+
});
|
|
3045
|
+
return {
|
|
3046
|
+
id: result[0].id,
|
|
3047
|
+
created: true
|
|
3048
|
+
};
|
|
3049
|
+
} catch (error) {
|
|
3050
|
+
console.error("[MCPService] Failed to record invocation:", error);
|
|
3051
|
+
throw error;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Update sync state for a device
|
|
3056
|
+
* Creates or updates the sync state record
|
|
3057
|
+
*/
|
|
3058
|
+
async updateSyncState(input) {
|
|
3059
|
+
try {
|
|
3060
|
+
const database = getDb();
|
|
3061
|
+
const updateResult = await database.update(extensionSyncState).set({
|
|
3062
|
+
lastSyncAt: input.lastSyncAt ?? /* @__PURE__ */ new Date(),
|
|
3063
|
+
syncVersion: input.syncVersion ?? sql`${extensionSyncState.syncVersion} + 1`,
|
|
3064
|
+
deviceType: input.deviceType,
|
|
3065
|
+
deviceName: input.deviceName,
|
|
3066
|
+
pendingChangesCount: input.pendingChangesCount ?? 0,
|
|
3067
|
+
pendingChanges: input.pendingChanges ?? [],
|
|
3068
|
+
isOnline: input.isOnline ?? true,
|
|
3069
|
+
lastHeartbeatAt: /* @__PURE__ */ new Date(),
|
|
3070
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3071
|
+
}).where(and(eq(extensionSyncState.userId, input.userId), eq(extensionSyncState.workspaceId, input.workspaceId), eq(extensionSyncState.deviceId, input.deviceId))).returning({
|
|
3072
|
+
id: extensionSyncState.id
|
|
3073
|
+
});
|
|
3074
|
+
if (updateResult.length > 0) {
|
|
3075
|
+
return {
|
|
3076
|
+
id: updateResult[0].id,
|
|
3077
|
+
created: false
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
const data = {
|
|
3081
|
+
userId: input.userId,
|
|
3082
|
+
workspaceId: input.workspaceId,
|
|
3083
|
+
deviceId: input.deviceId,
|
|
3084
|
+
deviceType: input.deviceType,
|
|
3085
|
+
deviceName: input.deviceName,
|
|
3086
|
+
lastSyncAt: input.lastSyncAt ?? /* @__PURE__ */ new Date(),
|
|
3087
|
+
syncVersion: input.syncVersion ?? 1,
|
|
3088
|
+
pendingChangesCount: input.pendingChangesCount ?? 0,
|
|
3089
|
+
pendingChanges: input.pendingChanges ?? [],
|
|
3090
|
+
isOnline: input.isOnline ?? true,
|
|
3091
|
+
lastHeartbeatAt: /* @__PURE__ */ new Date()
|
|
3092
|
+
};
|
|
3093
|
+
const result = await database.insert(extensionSyncState).values(data).returning({
|
|
3094
|
+
id: extensionSyncState.id
|
|
3095
|
+
});
|
|
3096
|
+
return {
|
|
3097
|
+
id: result[0].id,
|
|
3098
|
+
created: true
|
|
3099
|
+
};
|
|
3100
|
+
} catch (error) {
|
|
3101
|
+
console.error("[MCPService] Failed to update sync state:", error);
|
|
3102
|
+
throw error;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Query observations with filters
|
|
3107
|
+
*/
|
|
3108
|
+
async queryObservations(input) {
|
|
3109
|
+
const database = getDb();
|
|
3110
|
+
const conditions = [
|
|
3111
|
+
eq(mcpObservations.workspaceId, input.workspaceId)
|
|
3112
|
+
];
|
|
3113
|
+
if (input.userId) {
|
|
3114
|
+
conditions.push(eq(mcpObservations.userId, input.userId));
|
|
3115
|
+
}
|
|
3116
|
+
if (input.processed !== void 0) {
|
|
3117
|
+
conditions.push(eq(mcpObservations.processed, input.processed));
|
|
3118
|
+
}
|
|
3119
|
+
if (input.after) {
|
|
3120
|
+
conditions.push(gte(mcpObservations.createdAt, input.after));
|
|
3121
|
+
}
|
|
3122
|
+
const results = await database.select().from(mcpObservations).where(and(...conditions)).orderBy(sql`${mcpObservations.createdAt} DESC`).limit(input.limit ?? 100);
|
|
3123
|
+
return results;
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Mark observations as processed
|
|
3127
|
+
*/
|
|
3128
|
+
async markObservationsProcessed(observationIds) {
|
|
3129
|
+
if (observationIds.length === 0) return 0;
|
|
3130
|
+
const database = getDb();
|
|
3131
|
+
const result = await database.update(mcpObservations).set({
|
|
3132
|
+
processed: true,
|
|
3133
|
+
processedAt: /* @__PURE__ */ new Date(),
|
|
3134
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3135
|
+
}).where(sql`${mcpObservations.id} IN (${observationIds.join(",")})`);
|
|
3136
|
+
return result.rowCount ?? 0;
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
var instance = null;
|
|
3140
|
+
function getMCPService() {
|
|
3141
|
+
if (!instance) {
|
|
3142
|
+
instance = new MCPService();
|
|
3143
|
+
}
|
|
3144
|
+
return instance;
|
|
3145
|
+
}
|
|
3146
|
+
__name(getMCPService, "getMCPService");
|
|
3147
|
+
var PioneerServiceImpl = class {
|
|
3148
|
+
static {
|
|
3149
|
+
__name(this, "PioneerServiceImpl");
|
|
3150
|
+
}
|
|
3151
|
+
idempotencyCache = /* @__PURE__ */ new Map();
|
|
3152
|
+
IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3153
|
+
eventBus;
|
|
3154
|
+
// In-memory user points storage (stub - will be replaced with database)
|
|
3155
|
+
userPoints = /* @__PURE__ */ new Map();
|
|
3156
|
+
actionHistory = /* @__PURE__ */ new Map();
|
|
3157
|
+
constructor(eventBus) {
|
|
3158
|
+
this.eventBus = eventBus || new EventBus();
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Record a Pioneer action and award points with idempotency guarantee
|
|
3162
|
+
* Uses Redis for distributed idempotency across multiple instances
|
|
3163
|
+
*/
|
|
3164
|
+
async recordAction(userId, action, metadata, idempotencyKey) {
|
|
3165
|
+
const key = idempotencyKey || generateIdempotencyKey(userId, action, metadata);
|
|
3166
|
+
const redisKey = `pioneer:idempotency:${key}`;
|
|
3167
|
+
if (isRedisAvailable()) {
|
|
3168
|
+
const cached = await getCache(redisKey);
|
|
3169
|
+
if (cached) {
|
|
3170
|
+
return {
|
|
3171
|
+
...cached,
|
|
3172
|
+
message: "Action already recorded (idempotent response from Redis)",
|
|
3173
|
+
originalTimestamp: cached.timestamp
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
const memoryCached = this.idempotencyCache.get(key);
|
|
3178
|
+
if (memoryCached && Date.now() - memoryCached.timestamp < this.IDEMPOTENCY_TTL_MS) {
|
|
3179
|
+
return {
|
|
3180
|
+
...memoryCached.result,
|
|
3181
|
+
message: "Action already recorded (idempotent response from memory)",
|
|
3182
|
+
originalTimestamp: memoryCached.result.timestamp
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
if (!db) {
|
|
3186
|
+
return this.recordActionInMemory(userId, action, metadata, key);
|
|
3187
|
+
}
|
|
3188
|
+
try {
|
|
3189
|
+
const [existingAction] = await db.select().from(pioneerActions).where(and(eq(pioneerActions.actionType, action), eq(pioneerActions.metadata, {
|
|
3190
|
+
idempotencyKey: key
|
|
3191
|
+
}))).limit(1);
|
|
3192
|
+
if (existingAction) {
|
|
3193
|
+
const [pioneer2] = await db.select().from(pioneers).where(eq(pioneers.userId, userId)).limit(1);
|
|
3194
|
+
const result2 = {
|
|
3195
|
+
success: true,
|
|
3196
|
+
action,
|
|
3197
|
+
pointsAwarded: 0,
|
|
3198
|
+
newTotalPoints: pioneer2?.totalPoints || 0,
|
|
3199
|
+
tierChanged: false,
|
|
3200
|
+
newTier: null,
|
|
3201
|
+
benefitsUnlocked: [],
|
|
3202
|
+
timestamp: existingAction.createdAt,
|
|
3203
|
+
message: "Action already recorded (idempotent response)",
|
|
3204
|
+
idempotencyKey: key,
|
|
3205
|
+
originalTimestamp: existingAction.createdAt
|
|
3206
|
+
};
|
|
3207
|
+
this.idempotencyCache.set(key, {
|
|
3208
|
+
result: result2,
|
|
3209
|
+
timestamp: Date.now()
|
|
3210
|
+
});
|
|
3211
|
+
return result2;
|
|
3212
|
+
}
|
|
3213
|
+
const eligibilityCheck = await this.checkActionEligibility(userId, action, metadata);
|
|
3214
|
+
if (!eligibilityCheck.eligible) {
|
|
3215
|
+
const [pioneer2] = await db.select().from(pioneers).where(eq(pioneers.userId, userId)).limit(1);
|
|
3216
|
+
return {
|
|
3217
|
+
success: false,
|
|
3218
|
+
action,
|
|
3219
|
+
pointsAwarded: 0,
|
|
3220
|
+
newTotalPoints: pioneer2?.totalPoints || 0,
|
|
3221
|
+
tierChanged: false,
|
|
3222
|
+
newTier: null,
|
|
3223
|
+
benefitsUnlocked: [],
|
|
3224
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3225
|
+
message: eligibilityCheck.reason
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
const [pioneer] = await db.select().from(pioneers).where(eq(pioneers.userId, userId)).limit(1);
|
|
3229
|
+
const pointsToAward = getActionPoints(action);
|
|
3230
|
+
const currentPoints = pioneer?.totalPoints || 0;
|
|
3231
|
+
const newTotalPoints = currentPoints + pointsToAward;
|
|
3232
|
+
const oldTier = calculatePioneerTier(currentPoints);
|
|
3233
|
+
const newTier = calculatePioneerTier(newTotalPoints);
|
|
3234
|
+
const tierChanged = oldTier !== newTier;
|
|
3235
|
+
if (!pioneer) {
|
|
3236
|
+
throw new Error(`Pioneer profile not found for user ${userId}`);
|
|
3237
|
+
}
|
|
3238
|
+
await db.update(pioneers).set({
|
|
3239
|
+
totalPoints: newTotalPoints,
|
|
3240
|
+
tier: newTier,
|
|
3241
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3242
|
+
}).where(eq(pioneers.id, pioneer.id));
|
|
3243
|
+
await db.insert(pioneerActions).values({
|
|
3244
|
+
pioneerId: pioneer.id,
|
|
3245
|
+
actionType: action,
|
|
3246
|
+
points: pointsToAward,
|
|
3247
|
+
verified: true,
|
|
3248
|
+
metadata: {
|
|
3249
|
+
...metadata,
|
|
3250
|
+
idempotencyKey: key
|
|
3251
|
+
},
|
|
3252
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
3253
|
+
});
|
|
3254
|
+
const benefitsUnlocked = [];
|
|
3255
|
+
if (tierChanged) {
|
|
3256
|
+
benefitsUnlocked.push(`Pioneer ${newTier} tier reached`);
|
|
3257
|
+
if (newTier === "founding_pioneer") {
|
|
3258
|
+
benefitsUnlocked.push("Lifetime Pro access unlocked");
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
const result = {
|
|
3262
|
+
success: true,
|
|
3263
|
+
action,
|
|
3264
|
+
pointsAwarded: pointsToAward,
|
|
3265
|
+
newTotalPoints,
|
|
3266
|
+
tierChanged,
|
|
3267
|
+
newTier: tierChanged ? newTier : null,
|
|
3268
|
+
benefitsUnlocked,
|
|
3269
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3270
|
+
idempotencyKey: key
|
|
3271
|
+
};
|
|
3272
|
+
if (isRedisAvailable()) {
|
|
3273
|
+
await setCache(redisKey, result, 86400);
|
|
3274
|
+
}
|
|
3275
|
+
this.idempotencyCache.set(key, {
|
|
3276
|
+
result,
|
|
3277
|
+
timestamp: Date.now()
|
|
3278
|
+
});
|
|
3279
|
+
this.eventBus.emit("pioneer:action_recorded", {
|
|
3280
|
+
userId,
|
|
3281
|
+
action,
|
|
3282
|
+
points: pointsToAward,
|
|
3283
|
+
totalPoints: newTotalPoints,
|
|
3284
|
+
idempotencyKey: key
|
|
3285
|
+
});
|
|
3286
|
+
if (tierChanged) {
|
|
3287
|
+
this.eventBus.emit("pioneer:tier_changed", {
|
|
3288
|
+
userId,
|
|
3289
|
+
oldTier,
|
|
3290
|
+
newTier,
|
|
3291
|
+
totalPoints: newTotalPoints,
|
|
3292
|
+
benefitsUnlocked
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
return result;
|
|
3296
|
+
} catch (error) {
|
|
3297
|
+
console.error(`[Pioneer] Error recording action for user ${userId}:`, error);
|
|
3298
|
+
return this.recordActionInMemory(userId, action, metadata, key);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Check if user has achieved a milestone
|
|
3303
|
+
*/
|
|
3304
|
+
async checkMilestone(userId, milestone) {
|
|
3305
|
+
const currentPoints = this.getUserPoints(userId);
|
|
3306
|
+
const milestones = {
|
|
3307
|
+
first_10_snapshots: {
|
|
3308
|
+
points: 100,
|
|
3309
|
+
description: "Create 10 snapshots",
|
|
3310
|
+
reward: "Explorer badge"
|
|
3311
|
+
},
|
|
3312
|
+
reach_active_pioneer: {
|
|
3313
|
+
points: PIONEER_TIER_THRESHOLDS.active_pioneer,
|
|
3314
|
+
description: "Reach Active Pioneer tier",
|
|
3315
|
+
reward: "50% discount on Pro plan"
|
|
3316
|
+
},
|
|
3317
|
+
reach_founding_pioneer: {
|
|
3318
|
+
points: PIONEER_TIER_THRESHOLDS.founding_pioneer,
|
|
3319
|
+
description: "Reach Founding Pioneer tier",
|
|
3320
|
+
reward: "Lifetime Pro access"
|
|
3321
|
+
}
|
|
3322
|
+
};
|
|
3323
|
+
const milestoneData = milestones[milestone];
|
|
3324
|
+
if (!milestoneData) {
|
|
3325
|
+
return {
|
|
3326
|
+
milestone,
|
|
3327
|
+
achieved: false,
|
|
3328
|
+
progress: 0,
|
|
3329
|
+
description: "Unknown milestone"
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
const achieved = currentPoints >= milestoneData.points;
|
|
3333
|
+
const progress = Math.min(currentPoints / milestoneData.points, 1);
|
|
3334
|
+
return {
|
|
3335
|
+
milestone,
|
|
3336
|
+
achieved,
|
|
3337
|
+
progress,
|
|
3338
|
+
description: milestoneData.description,
|
|
3339
|
+
reward: milestoneData.reward,
|
|
3340
|
+
achievedAt: achieved ? /* @__PURE__ */ new Date() : void 0
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Apply a Pioneer benefit to user's account
|
|
3345
|
+
*/
|
|
3346
|
+
async applyBenefit(_userId, benefit) {
|
|
3347
|
+
return {
|
|
3348
|
+
success: true,
|
|
3349
|
+
benefit,
|
|
3350
|
+
message: `Benefit ${benefit.id} applied successfully`,
|
|
3351
|
+
appliedAt: /* @__PURE__ */ new Date()
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Get user's Pioneer status including points and tier
|
|
3356
|
+
*/
|
|
3357
|
+
async getPioneerStatus(userId) {
|
|
3358
|
+
const totalPoints = this.getUserPoints(userId);
|
|
3359
|
+
if (totalPoints === 0) {
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
const tier = calculatePioneerTier(totalPoints);
|
|
3363
|
+
const tierThresholds = Object.entries(PIONEER_TIER_THRESHOLDS).sort(([, a], [, b]) => a - b);
|
|
3364
|
+
const currentTierIndex = tierThresholds.findIndex(([t]) => t === tier);
|
|
3365
|
+
const nextTierEntry = tierThresholds[currentTierIndex + 1];
|
|
3366
|
+
const nextTier = nextTierEntry ? nextTierEntry[0] : null;
|
|
3367
|
+
const pointsToNext = nextTier ? PIONEER_TIER_THRESHOLDS[nextTier] - totalPoints : 0;
|
|
3368
|
+
const discountPercent = tier === "founding_pioneer" ? 100 : tier === "contributing_pioneer" ? 75 : tier === "active_pioneer" ? 50 : 0;
|
|
3369
|
+
return {
|
|
3370
|
+
tier,
|
|
3371
|
+
totalPoints,
|
|
3372
|
+
pointsToNext,
|
|
3373
|
+
nextTier,
|
|
3374
|
+
discountPercent,
|
|
3375
|
+
benefits: this.getTierBenefits(tier)
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
/**
|
|
3379
|
+
* Get action history for a user
|
|
3380
|
+
*/
|
|
3381
|
+
async getActionHistory(userId, limit = 50) {
|
|
3382
|
+
const history = this.actionHistory.get(userId) || [];
|
|
3383
|
+
return history.slice(-limit).reverse();
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Check if user is eligible for an action
|
|
3387
|
+
*/
|
|
3388
|
+
async checkActionEligibility(userId, action, _metadata) {
|
|
3389
|
+
if (isFirstTimeAction(action)) {
|
|
3390
|
+
const history = this.actionHistory.get(userId) || [];
|
|
3391
|
+
const alreadyDone = history.some((record) => record.action === action);
|
|
3392
|
+
if (alreadyDone) {
|
|
3393
|
+
return {
|
|
3394
|
+
eligible: false,
|
|
3395
|
+
reason: `Action '${action}' not allowed: user has already completed this action`
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
if (action === "daily_snapshot") {
|
|
3400
|
+
const history = this.actionHistory.get(userId) || [];
|
|
3401
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3402
|
+
const doneToday = history.some((record) => {
|
|
3403
|
+
const recordDate = record.recordedAt.toISOString().split("T")[0];
|
|
3404
|
+
return record.action === action && recordDate === today;
|
|
3405
|
+
});
|
|
3406
|
+
if (doneToday) {
|
|
3407
|
+
return {
|
|
3408
|
+
eligible: false,
|
|
3409
|
+
reason: "Daily snapshot already recorded for today"
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
eligible: true
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* Get user's total points (stub - will query database)
|
|
3419
|
+
*/
|
|
3420
|
+
getUserPoints(userId) {
|
|
3421
|
+
return this.userPoints.get(userId) || 0;
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Get benefits available at a tier
|
|
3425
|
+
*/
|
|
3426
|
+
getTierBenefits(tier) {
|
|
3427
|
+
const benefits = {
|
|
3428
|
+
pioneer: [
|
|
3429
|
+
"Pioneer badge",
|
|
3430
|
+
"Community access"
|
|
3431
|
+
],
|
|
3432
|
+
active_pioneer: [
|
|
3433
|
+
"Pioneer badge",
|
|
3434
|
+
"Community access",
|
|
3435
|
+
"50% discount on Pro plan"
|
|
3436
|
+
],
|
|
3437
|
+
contributing_pioneer: [
|
|
3438
|
+
"Pioneer badge",
|
|
3439
|
+
"Community access",
|
|
3440
|
+
"75% discount on Pro plan",
|
|
3441
|
+
"Priority support"
|
|
3442
|
+
],
|
|
3443
|
+
founding_pioneer: [
|
|
3444
|
+
"Pioneer badge",
|
|
3445
|
+
"Community access",
|
|
3446
|
+
"Lifetime Pro access",
|
|
3447
|
+
"Priority support",
|
|
3448
|
+
"Founding member recognition"
|
|
3449
|
+
]
|
|
3450
|
+
};
|
|
3451
|
+
return benefits[tier] || [];
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Fallback: Record action in-memory (for graceful degradation)
|
|
3455
|
+
*/
|
|
3456
|
+
recordActionInMemory(userId, action, metadata, key) {
|
|
3457
|
+
const pointsToAward = getActionPoints(action);
|
|
3458
|
+
const currentPoints = this.getUserPoints(userId);
|
|
3459
|
+
const newTotalPoints = currentPoints + pointsToAward;
|
|
3460
|
+
this.userPoints.set(userId, newTotalPoints);
|
|
3461
|
+
const oldTier = calculatePioneerTier(currentPoints);
|
|
3462
|
+
const newTier = calculatePioneerTier(newTotalPoints);
|
|
3463
|
+
const tierChanged = oldTier !== newTier;
|
|
3464
|
+
const actionRecord = {
|
|
3465
|
+
action,
|
|
3466
|
+
points: pointsToAward,
|
|
3467
|
+
recordedAt: /* @__PURE__ */ new Date(),
|
|
3468
|
+
metadata,
|
|
3469
|
+
idempotencyKey: key
|
|
3470
|
+
};
|
|
3471
|
+
const history = this.actionHistory.get(userId) || [];
|
|
3472
|
+
history.push(actionRecord);
|
|
3473
|
+
this.actionHistory.set(userId, history);
|
|
3474
|
+
const benefitsUnlocked = [];
|
|
3475
|
+
if (tierChanged) {
|
|
3476
|
+
benefitsUnlocked.push(`Pioneer ${newTier} tier reached`);
|
|
3477
|
+
if (newTier === "founding_pioneer") {
|
|
3478
|
+
benefitsUnlocked.push("Lifetime Pro access unlocked");
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
const result = {
|
|
3482
|
+
success: true,
|
|
3483
|
+
action,
|
|
3484
|
+
pointsAwarded: pointsToAward,
|
|
3485
|
+
newTotalPoints,
|
|
3486
|
+
tierChanged,
|
|
3487
|
+
newTier: tierChanged ? newTier : null,
|
|
3488
|
+
benefitsUnlocked,
|
|
3489
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3490
|
+
idempotencyKey: key
|
|
3491
|
+
};
|
|
3492
|
+
this.idempotencyCache.set(key, {
|
|
3493
|
+
result,
|
|
3494
|
+
timestamp: Date.now()
|
|
3495
|
+
});
|
|
3496
|
+
return result;
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
new PioneerServiceImpl();
|
|
3500
|
+
var SagaOrchestratorImpl = class {
|
|
3501
|
+
static {
|
|
3502
|
+
__name(this, "SagaOrchestratorImpl");
|
|
3503
|
+
}
|
|
3504
|
+
persistence;
|
|
3505
|
+
definitions = /* @__PURE__ */ new Map();
|
|
3506
|
+
runningInstances = /* @__PURE__ */ new Map();
|
|
3507
|
+
constructor(persistence) {
|
|
3508
|
+
this.persistence = persistence;
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* Register a saga definition
|
|
3512
|
+
*/
|
|
3513
|
+
registerSaga(definition) {
|
|
3514
|
+
this.definitions.set(definition.sagaType, definition);
|
|
3515
|
+
console.log(`[SagaOrchestrator] Registered saga type: ${definition.sagaType}`);
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* Start a new saga instance
|
|
3519
|
+
*/
|
|
3520
|
+
async start(sagaType, initialContext) {
|
|
3521
|
+
const definition = this.definitions.get(sagaType);
|
|
3522
|
+
if (!definition) {
|
|
3523
|
+
throw new Error(`Saga type not registered: ${sagaType}`);
|
|
3524
|
+
}
|
|
3525
|
+
const sagaId = this.generateSagaId();
|
|
3526
|
+
const instance2 = {
|
|
3527
|
+
sagaId,
|
|
3528
|
+
sagaType,
|
|
3529
|
+
status: "pending",
|
|
3530
|
+
context: initialContext,
|
|
3531
|
+
steps: definition.steps.map((step) => ({
|
|
3532
|
+
stepId: step.stepId,
|
|
3533
|
+
stepName: step.stepName,
|
|
3534
|
+
status: "pending",
|
|
3535
|
+
input: {},
|
|
3536
|
+
output: null,
|
|
3537
|
+
error: null,
|
|
3538
|
+
startedAt: null,
|
|
3539
|
+
completedAt: null,
|
|
3540
|
+
compensatedAt: null
|
|
3541
|
+
})),
|
|
3542
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
3543
|
+
completedAt: null,
|
|
3544
|
+
failedAt: null,
|
|
3545
|
+
error: null,
|
|
3546
|
+
retryCount: 0,
|
|
3547
|
+
maxRetries: definition.maxRetries || 3
|
|
3548
|
+
};
|
|
3549
|
+
await this.persistence.save(instance2);
|
|
3550
|
+
this.runningInstances.set(sagaId, instance2);
|
|
3551
|
+
this.executeAsync(sagaId, definition);
|
|
3552
|
+
return instance2;
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Resume a saga from persisted state
|
|
3556
|
+
*/
|
|
3557
|
+
async resume(sagaId) {
|
|
3558
|
+
const instance2 = await this.persistence.load(sagaId);
|
|
3559
|
+
if (!instance2) {
|
|
3560
|
+
throw new Error(`Saga not found: ${sagaId}`);
|
|
3561
|
+
}
|
|
3562
|
+
const definition = this.definitions.get(instance2.sagaType);
|
|
3563
|
+
if (!definition) {
|
|
3564
|
+
throw new Error(`Saga type not registered: ${instance2.sagaType}`);
|
|
3565
|
+
}
|
|
3566
|
+
this.runningInstances.set(sagaId, instance2);
|
|
3567
|
+
this.executeAsync(sagaId, definition);
|
|
3568
|
+
return instance2;
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Get saga status
|
|
3572
|
+
*/
|
|
3573
|
+
async getStatus(sagaId) {
|
|
3574
|
+
const running = this.runningInstances.get(sagaId);
|
|
3575
|
+
if (running) {
|
|
3576
|
+
return running;
|
|
3577
|
+
}
|
|
3578
|
+
return this.persistence.load(sagaId);
|
|
3579
|
+
}
|
|
3580
|
+
/**
|
|
3581
|
+
* List all sagas
|
|
3582
|
+
*/
|
|
3583
|
+
async listSagas(filter) {
|
|
3584
|
+
return this.persistence.listAll(filter);
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Execute saga steps asynchronously
|
|
3588
|
+
*/
|
|
3589
|
+
async executeAsync(sagaId, definition) {
|
|
3590
|
+
const instance2 = this.runningInstances.get(sagaId);
|
|
3591
|
+
if (!instance2) {
|
|
3592
|
+
console.error(`[SagaOrchestrator] Instance not found: ${sagaId}`);
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
try {
|
|
3596
|
+
instance2.status = "running";
|
|
3597
|
+
await this.persistence.update(sagaId, {
|
|
3598
|
+
status: "running"
|
|
3599
|
+
});
|
|
3600
|
+
for (let i = 0; i < definition.steps.length; i++) {
|
|
3601
|
+
const stepDef = definition.steps[i];
|
|
3602
|
+
const stepExec = instance2.steps[i];
|
|
3603
|
+
if (!stepDef || !stepExec) {
|
|
3604
|
+
throw new Error(`Step definition or execution not found at index ${i}`);
|
|
3605
|
+
}
|
|
3606
|
+
if (stepExec.status === "completed") {
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
const success = await this.executeStep(instance2, stepDef, stepExec);
|
|
3610
|
+
if (!success) {
|
|
3611
|
+
await this.compensate(instance2, definition, i);
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
if (definition.persistenceInterval) {
|
|
3615
|
+
await this.persistence.update(sagaId, {
|
|
3616
|
+
steps: instance2.steps
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
instance2.status = "completed";
|
|
3621
|
+
instance2.completedAt = /* @__PURE__ */ new Date();
|
|
3622
|
+
await this.persistence.update(sagaId, {
|
|
3623
|
+
status: "completed",
|
|
3624
|
+
completedAt: instance2.completedAt
|
|
3625
|
+
});
|
|
3626
|
+
console.log(`[SagaOrchestrator] Saga completed: ${sagaId}`);
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
console.error(`[SagaOrchestrator] Saga execution error: ${sagaId}`, error);
|
|
3629
|
+
instance2.status = "failed";
|
|
3630
|
+
instance2.failedAt = /* @__PURE__ */ new Date();
|
|
3631
|
+
instance2.error = error instanceof Error ? error.message : String(error);
|
|
3632
|
+
await this.persistence.update(sagaId, {
|
|
3633
|
+
status: "failed",
|
|
3634
|
+
failedAt: instance2.failedAt,
|
|
3635
|
+
error: instance2.error
|
|
3636
|
+
});
|
|
3637
|
+
} finally {
|
|
3638
|
+
this.runningInstances.delete(sagaId);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Execute a single saga step
|
|
3643
|
+
*/
|
|
3644
|
+
async executeStep(instance2, stepDef, stepExec) {
|
|
3645
|
+
stepExec.status = "running";
|
|
3646
|
+
stepExec.startedAt = /* @__PURE__ */ new Date();
|
|
3647
|
+
try {
|
|
3648
|
+
const timeoutMs = stepDef.timeout || 3e4;
|
|
3649
|
+
const result = await this.executeWithTimeout(stepDef.execute(stepExec.input, instance2.context), timeoutMs);
|
|
3650
|
+
stepExec.output = result;
|
|
3651
|
+
stepExec.status = "completed";
|
|
3652
|
+
stepExec.completedAt = /* @__PURE__ */ new Date();
|
|
3653
|
+
console.log(`[SagaOrchestrator] Step completed: ${stepDef.stepId}`);
|
|
3654
|
+
return true;
|
|
3655
|
+
} catch (error) {
|
|
3656
|
+
stepExec.error = error instanceof Error ? error.message : String(error);
|
|
3657
|
+
stepExec.status = "failed";
|
|
3658
|
+
console.error(`[SagaOrchestrator] Step failed: ${stepDef.stepId}`, error);
|
|
3659
|
+
if (stepDef.retryable && instance2.retryCount < instance2.maxRetries) {
|
|
3660
|
+
instance2.retryCount++;
|
|
3661
|
+
console.log(`[SagaOrchestrator] Retrying step ${stepDef.stepId} (${instance2.retryCount}/${instance2.maxRetries})`);
|
|
3662
|
+
stepExec.status = "pending";
|
|
3663
|
+
stepExec.error = null;
|
|
3664
|
+
const delayMs = Math.min(1e3 * 2 ** instance2.retryCount, 3e4);
|
|
3665
|
+
await this.sleep(delayMs);
|
|
3666
|
+
return this.executeStep(instance2, stepDef, stepExec);
|
|
3667
|
+
}
|
|
3668
|
+
return false;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Compensate (rollback) completed steps
|
|
3673
|
+
*/
|
|
3674
|
+
async compensate(instance2, definition, failedStepIndex) {
|
|
3675
|
+
instance2.status = "compensating";
|
|
3676
|
+
await this.persistence.update(instance2.sagaId, {
|
|
3677
|
+
status: "compensating"
|
|
3678
|
+
});
|
|
3679
|
+
console.log(`[SagaOrchestrator] Starting compensation for saga: ${instance2.sagaId}`);
|
|
3680
|
+
for (let i = failedStepIndex - 1; i >= 0; i--) {
|
|
3681
|
+
const stepDef = definition.steps[i];
|
|
3682
|
+
const stepExec = instance2.steps[i];
|
|
3683
|
+
if (!stepDef || !stepExec) {
|
|
3684
|
+
console.warn(`[SagaOrchestrator] Step not found at index ${i} during compensation`);
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
if (stepExec.status !== "completed") {
|
|
3688
|
+
continue;
|
|
3689
|
+
}
|
|
3690
|
+
if (!stepDef.compensate) {
|
|
3691
|
+
console.log(`[SagaOrchestrator] No compensation for step: ${stepDef.stepId}`);
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
try {
|
|
3695
|
+
await stepDef.compensate(stepExec.input, stepExec.output, instance2.context);
|
|
3696
|
+
stepExec.status = "compensated";
|
|
3697
|
+
stepExec.compensatedAt = /* @__PURE__ */ new Date();
|
|
3698
|
+
console.log(`[SagaOrchestrator] Compensated step: ${stepDef.stepId}`);
|
|
3699
|
+
} catch (error) {
|
|
3700
|
+
console.error(`[SagaOrchestrator] Compensation failed for step: ${stepDef.stepId}`, error);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
instance2.status = "compensated";
|
|
3704
|
+
instance2.failedAt = /* @__PURE__ */ new Date();
|
|
3705
|
+
await this.persistence.update(instance2.sagaId, {
|
|
3706
|
+
status: "compensated",
|
|
3707
|
+
failedAt: instance2.failedAt,
|
|
3708
|
+
steps: instance2.steps
|
|
3709
|
+
});
|
|
3710
|
+
console.log(`[SagaOrchestrator] Compensation completed for saga: ${instance2.sagaId}`);
|
|
3711
|
+
}
|
|
3712
|
+
// Helper methods
|
|
3713
|
+
generateSagaId() {
|
|
3714
|
+
return `saga_${randomBytes(16).toString("hex")}`;
|
|
3715
|
+
}
|
|
3716
|
+
async executeWithTimeout(promise, timeoutMs) {
|
|
3717
|
+
return Promise.race([
|
|
3718
|
+
promise,
|
|
3719
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error("Step execution timeout")), timeoutMs))
|
|
3720
|
+
]);
|
|
3721
|
+
}
|
|
3722
|
+
sleep(ms) {
|
|
3723
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3724
|
+
}
|
|
3725
|
+
};
|
|
3726
|
+
var SagaPersistenceImpl = class {
|
|
3727
|
+
static {
|
|
3728
|
+
__name(this, "SagaPersistenceImpl");
|
|
3729
|
+
}
|
|
3730
|
+
// In-memory fallback storage
|
|
3731
|
+
inMemoryStore = /* @__PURE__ */ new Map();
|
|
3732
|
+
/**
|
|
3733
|
+
* Save a new saga instance to database
|
|
3734
|
+
*/
|
|
3735
|
+
async save(saga) {
|
|
3736
|
+
if (!db) {
|
|
3737
|
+
this.inMemoryStore.set(saga.sagaId, saga);
|
|
3738
|
+
console.log(`[SagaPersistence] Saved saga ${saga.sagaId} to in-memory store (DB unavailable)`);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
try {
|
|
3742
|
+
await db.insert(sagas).values({
|
|
3743
|
+
sagaId: saga.sagaId,
|
|
3744
|
+
sagaType: saga.sagaType,
|
|
3745
|
+
status: saga.status,
|
|
3746
|
+
context: saga.context,
|
|
3747
|
+
steps: saga.steps.map((step) => ({
|
|
3748
|
+
stepId: step.stepId,
|
|
3749
|
+
stepName: step.stepName,
|
|
3750
|
+
status: step.status,
|
|
3751
|
+
input: step.input,
|
|
3752
|
+
output: step.output,
|
|
3753
|
+
error: step.error,
|
|
3754
|
+
startedAt: step.startedAt?.toISOString() || null,
|
|
3755
|
+
completedAt: step.completedAt?.toISOString() || null,
|
|
3756
|
+
compensatedAt: step.compensatedAt?.toISOString() || null
|
|
3757
|
+
})),
|
|
3758
|
+
error: saga.error,
|
|
3759
|
+
retryCount: String(saga.retryCount),
|
|
3760
|
+
maxRetries: String(saga.maxRetries),
|
|
3761
|
+
startedAt: saga.startedAt,
|
|
3762
|
+
completedAt: saga.completedAt,
|
|
3763
|
+
failedAt: saga.failedAt
|
|
3764
|
+
});
|
|
3765
|
+
console.log(`[SagaPersistence] Saved saga ${saga.sagaId} to database`);
|
|
3766
|
+
} catch (error) {
|
|
3767
|
+
console.error(`[SagaPersistence] Failed to save saga ${saga.sagaId}:`, error);
|
|
3768
|
+
this.inMemoryStore.set(saga.sagaId, saga);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Load a saga instance from database
|
|
3773
|
+
*/
|
|
3774
|
+
async load(sagaId) {
|
|
3775
|
+
if (!db) {
|
|
3776
|
+
const saga = this.inMemoryStore.get(sagaId);
|
|
3777
|
+
if (saga) {
|
|
3778
|
+
console.log(`[SagaPersistence] Loaded saga ${sagaId} from in-memory store (DB unavailable)`);
|
|
3779
|
+
}
|
|
3780
|
+
return saga || null;
|
|
3781
|
+
}
|
|
3782
|
+
try {
|
|
3783
|
+
const [row] = await db.select().from(sagas).where(eq(sagas.sagaId, sagaId)).limit(1);
|
|
3784
|
+
if (!row) {
|
|
3785
|
+
return this.inMemoryStore.get(sagaId) || null;
|
|
3786
|
+
}
|
|
3787
|
+
const instance2 = {
|
|
3788
|
+
sagaId: row.sagaId,
|
|
3789
|
+
sagaType: row.sagaType,
|
|
3790
|
+
status: row.status,
|
|
3791
|
+
context: row.context,
|
|
3792
|
+
steps: row.steps.map((step) => ({
|
|
3793
|
+
stepId: step.stepId,
|
|
3794
|
+
stepName: step.stepName,
|
|
3795
|
+
status: step.status,
|
|
3796
|
+
input: step.input,
|
|
3797
|
+
output: step.output,
|
|
3798
|
+
error: step.error,
|
|
3799
|
+
startedAt: step.startedAt ? new Date(step.startedAt) : null,
|
|
3800
|
+
completedAt: step.completedAt ? new Date(step.completedAt) : null,
|
|
3801
|
+
compensatedAt: step.compensatedAt ? new Date(step.compensatedAt) : null
|
|
3802
|
+
})),
|
|
3803
|
+
startedAt: row.startedAt,
|
|
3804
|
+
completedAt: row.completedAt,
|
|
3805
|
+
failedAt: row.failedAt,
|
|
3806
|
+
error: row.error,
|
|
3807
|
+
retryCount: Number.parseInt(row.retryCount, 10),
|
|
3808
|
+
maxRetries: Number.parseInt(row.maxRetries, 10)
|
|
3809
|
+
};
|
|
3810
|
+
console.log(`[SagaPersistence] Loaded saga ${sagaId} from database`);
|
|
3811
|
+
return instance2;
|
|
3812
|
+
} catch (error) {
|
|
3813
|
+
console.error(`[SagaPersistence] Failed to load saga ${sagaId}:`, error);
|
|
3814
|
+
return this.inMemoryStore.get(sagaId) || null;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Update saga instance fields
|
|
3819
|
+
*/
|
|
3820
|
+
async update(sagaId, updates) {
|
|
3821
|
+
if (!db) {
|
|
3822
|
+
const existing = this.inMemoryStore.get(sagaId);
|
|
3823
|
+
if (existing) {
|
|
3824
|
+
this.inMemoryStore.set(sagaId, {
|
|
3825
|
+
...existing,
|
|
3826
|
+
...updates
|
|
3827
|
+
});
|
|
3828
|
+
console.log(`[SagaPersistence] Updated saga ${sagaId} in in-memory store (DB unavailable)`);
|
|
3829
|
+
}
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
try {
|
|
3833
|
+
const updateData = {};
|
|
3834
|
+
if (updates.status) {
|
|
3835
|
+
updateData.status = updates.status;
|
|
3836
|
+
}
|
|
3837
|
+
if (updates.error !== void 0) {
|
|
3838
|
+
updateData.error = updates.error;
|
|
3839
|
+
}
|
|
3840
|
+
if (updates.completedAt !== void 0) {
|
|
3841
|
+
updateData.completedAt = updates.completedAt;
|
|
3842
|
+
}
|
|
3843
|
+
if (updates.failedAt !== void 0) {
|
|
3844
|
+
updateData.failedAt = updates.failedAt;
|
|
3845
|
+
}
|
|
3846
|
+
if (updates.retryCount !== void 0) {
|
|
3847
|
+
updateData.retryCount = String(updates.retryCount);
|
|
3848
|
+
}
|
|
3849
|
+
if (updates.context) {
|
|
3850
|
+
updateData.context = updates.context;
|
|
3851
|
+
}
|
|
3852
|
+
if (updates.steps) {
|
|
3853
|
+
updateData.steps = updates.steps.map((step) => ({
|
|
3854
|
+
stepId: step.stepId,
|
|
3855
|
+
stepName: step.stepName,
|
|
3856
|
+
status: step.status,
|
|
3857
|
+
input: step.input,
|
|
3858
|
+
output: step.output,
|
|
3859
|
+
error: step.error,
|
|
3860
|
+
startedAt: step.startedAt?.toISOString() || null,
|
|
3861
|
+
completedAt: step.completedAt?.toISOString() || null,
|
|
3862
|
+
compensatedAt: step.compensatedAt?.toISOString() || null
|
|
3863
|
+
}));
|
|
3864
|
+
}
|
|
3865
|
+
updateData.updatedAt = /* @__PURE__ */ new Date();
|
|
3866
|
+
await db.update(sagas).set(updateData).where(eq(sagas.sagaId, sagaId));
|
|
3867
|
+
console.log(`[SagaPersistence] Updated saga ${sagaId} in database`);
|
|
3868
|
+
} catch (error) {
|
|
3869
|
+
console.error(`[SagaPersistence] Failed to update saga ${sagaId}:`, error);
|
|
3870
|
+
const existing = this.inMemoryStore.get(sagaId);
|
|
3871
|
+
if (existing) {
|
|
3872
|
+
this.inMemoryStore.set(sagaId, {
|
|
3873
|
+
...existing,
|
|
3874
|
+
...updates
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* List all sagas with optional filtering
|
|
3881
|
+
*/
|
|
3882
|
+
async listAll(filter) {
|
|
3883
|
+
if (!db) {
|
|
3884
|
+
let results = Array.from(this.inMemoryStore.values());
|
|
3885
|
+
if (filter?.sagaType) {
|
|
3886
|
+
results = results.filter((s) => s.sagaType === filter.sagaType);
|
|
3887
|
+
}
|
|
3888
|
+
if (filter?.status) {
|
|
3889
|
+
results = results.filter((s) => filter.status?.includes(s.status));
|
|
3890
|
+
}
|
|
3891
|
+
if (filter?.startedAfter) {
|
|
3892
|
+
const startedAfter = filter.startedAfter;
|
|
3893
|
+
results = results.filter((s) => s.startedAt >= startedAfter);
|
|
3894
|
+
}
|
|
3895
|
+
if (filter?.startedBefore) {
|
|
3896
|
+
const startedBefore = filter.startedBefore;
|
|
3897
|
+
results = results.filter((s) => s.startedAt <= startedBefore);
|
|
3898
|
+
}
|
|
3899
|
+
if (filter?.limit) {
|
|
3900
|
+
results = results.slice(0, filter.limit);
|
|
3901
|
+
}
|
|
3902
|
+
console.log(`[SagaPersistence] Listed ${results.length} sagas from in-memory store (DB unavailable)`);
|
|
3903
|
+
return results;
|
|
3904
|
+
}
|
|
3905
|
+
try {
|
|
3906
|
+
const conditions = [];
|
|
3907
|
+
if (filter?.sagaType) {
|
|
3908
|
+
conditions.push(eq(sagas.sagaType, filter.sagaType));
|
|
3909
|
+
}
|
|
3910
|
+
if (filter?.status && filter.status.length > 0) {
|
|
3911
|
+
conditions.push(inArray(sagas.status, filter.status));
|
|
3912
|
+
}
|
|
3913
|
+
if (filter?.startedAfter) {
|
|
3914
|
+
conditions.push(gte(sagas.startedAt, filter.startedAfter));
|
|
3915
|
+
}
|
|
3916
|
+
if (filter?.startedBefore) {
|
|
3917
|
+
conditions.push(lte(sagas.startedAt, filter.startedBefore));
|
|
3918
|
+
}
|
|
3919
|
+
let query = db.select().from(sagas);
|
|
3920
|
+
if (conditions.length > 0) {
|
|
3921
|
+
query = query.where(and(...conditions));
|
|
3922
|
+
}
|
|
3923
|
+
query = query.orderBy(desc(sagas.startedAt));
|
|
3924
|
+
if (filter?.limit) {
|
|
3925
|
+
query = query.limit(filter.limit);
|
|
3926
|
+
}
|
|
3927
|
+
const rows = await query;
|
|
3928
|
+
const instances = rows.map((row) => ({
|
|
3929
|
+
sagaId: row.sagaId,
|
|
3930
|
+
sagaType: row.sagaType,
|
|
3931
|
+
status: row.status,
|
|
3932
|
+
context: row.context,
|
|
3933
|
+
steps: row.steps.map((step) => ({
|
|
3934
|
+
stepId: step.stepId,
|
|
3935
|
+
stepName: step.stepName,
|
|
3936
|
+
status: step.status,
|
|
3937
|
+
input: step.input,
|
|
3938
|
+
output: step.output,
|
|
3939
|
+
error: step.error,
|
|
3940
|
+
startedAt: step.startedAt ? new Date(step.startedAt) : null,
|
|
3941
|
+
completedAt: step.completedAt ? new Date(step.completedAt) : null,
|
|
3942
|
+
compensatedAt: step.compensatedAt ? new Date(step.compensatedAt) : null
|
|
3943
|
+
})),
|
|
3944
|
+
startedAt: row.startedAt,
|
|
3945
|
+
completedAt: row.completedAt,
|
|
3946
|
+
failedAt: row.failedAt,
|
|
3947
|
+
error: row.error,
|
|
3948
|
+
retryCount: Number.parseInt(row.retryCount, 10),
|
|
3949
|
+
maxRetries: Number.parseInt(row.maxRetries, 10)
|
|
3950
|
+
}));
|
|
3951
|
+
console.log(`[SagaPersistence] Listed ${instances.length} sagas from database`);
|
|
3952
|
+
return instances;
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
console.error("[SagaPersistence] Failed to list sagas:", error);
|
|
3955
|
+
return Array.from(this.inMemoryStore.values());
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* Delete a saga instance from storage
|
|
3960
|
+
*/
|
|
3961
|
+
async delete(sagaId) {
|
|
3962
|
+
if (!db) {
|
|
3963
|
+
this.inMemoryStore.delete(sagaId);
|
|
3964
|
+
console.log(`[SagaPersistence] Deleted saga ${sagaId} from in-memory store (DB unavailable)`);
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
try {
|
|
3968
|
+
await db.delete(sagas).where(eq(sagas.sagaId, sagaId));
|
|
3969
|
+
this.inMemoryStore.delete(sagaId);
|
|
3970
|
+
console.log(`[SagaPersistence] Deleted saga ${sagaId} from database`);
|
|
3971
|
+
} catch (error) {
|
|
3972
|
+
console.error(`[SagaPersistence] Failed to delete saga ${sagaId}:`, error);
|
|
3973
|
+
this.inMemoryStore.delete(sagaId);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3977
|
+
var sagaPersistence = new SagaPersistenceImpl();
|
|
3978
|
+
async function updateSubscriptionStep(input, context) {
|
|
3979
|
+
const priceIdMap = {
|
|
3980
|
+
pro: process.env.STRIPE_PRO_MONTHLY_PRICE_ID,
|
|
3981
|
+
team: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID,
|
|
3982
|
+
enterprise: process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID
|
|
3983
|
+
};
|
|
3984
|
+
const newPriceId = priceIdMap[input.toTier];
|
|
3985
|
+
if (!newPriceId) {
|
|
3986
|
+
throw new Error(`No price ID configured for tier: ${input.toTier}`);
|
|
3987
|
+
}
|
|
3988
|
+
if (!input.subscriptionId) {
|
|
3989
|
+
throw new Error("Subscription ID is required for tier upgrade");
|
|
3990
|
+
}
|
|
3991
|
+
const stripeClient = context.stripeClient;
|
|
3992
|
+
if (!stripeClient) {
|
|
3993
|
+
throw new Error("Stripe client not available in context");
|
|
3994
|
+
}
|
|
3995
|
+
const currentSubscription = await stripeClient.subscriptions.retrieve(input.subscriptionId);
|
|
3996
|
+
const currentPriceId = currentSubscription.items.data[0]?.price?.id;
|
|
3997
|
+
context.previousPriceId = currentPriceId;
|
|
3998
|
+
await stripeClient.subscriptions.update(input.subscriptionId, {
|
|
3999
|
+
items: [
|
|
4000
|
+
{
|
|
4001
|
+
id: currentSubscription.items.data[0].id,
|
|
4002
|
+
price: newPriceId
|
|
4003
|
+
}
|
|
4004
|
+
],
|
|
4005
|
+
proration_behavior: "create_prorations"
|
|
4006
|
+
});
|
|
4007
|
+
const effectiveDate = /* @__PURE__ */ new Date();
|
|
4008
|
+
const prorationAmount = null;
|
|
4009
|
+
console.log(`[TierUpgradeSaga] Updated subscription ${input.subscriptionId} to ${input.toTier} tier (price: ${newPriceId})`);
|
|
4010
|
+
return {
|
|
4011
|
+
subscriptionId: input.subscriptionId,
|
|
4012
|
+
priceId: newPriceId,
|
|
4013
|
+
effectiveDate,
|
|
4014
|
+
prorationAmount
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
__name(updateSubscriptionStep, "updateSubscriptionStep");
|
|
4018
|
+
async function compensateUpdateSubscription(_input, output, context) {
|
|
4019
|
+
if (!output?.subscriptionId || !context.previousPriceId) {
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
try {
|
|
4023
|
+
const stripeClient = context.stripeClient;
|
|
4024
|
+
if (!stripeClient) {
|
|
4025
|
+
console.warn("[TierUpgradeSaga] Stripe client not available, skipping compensation");
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
const currentSubscription = await stripeClient.subscriptions.retrieve(output.subscriptionId);
|
|
4029
|
+
await stripeClient.subscriptions.update(output.subscriptionId, {
|
|
4030
|
+
items: [
|
|
4031
|
+
{
|
|
4032
|
+
id: currentSubscription.items.data[0].id,
|
|
4033
|
+
price: context.previousPriceId
|
|
4034
|
+
}
|
|
4035
|
+
],
|
|
4036
|
+
proration_behavior: "none"
|
|
4037
|
+
});
|
|
4038
|
+
console.log(`[TierUpgradeSaga] Compensated: Reverted subscription ${output.subscriptionId} to price ${context.previousPriceId}`);
|
|
4039
|
+
} catch (error) {
|
|
4040
|
+
console.error("[TierUpgradeSaga] Failed to compensate updateSubscription:", error);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
__name(compensateUpdateSubscription, "compensateUpdateSubscription");
|
|
4044
|
+
async function updateUserTierStep(input, context) {
|
|
4045
|
+
if (!db) {
|
|
4046
|
+
throw new Error("Database not available");
|
|
4047
|
+
}
|
|
4048
|
+
const currentEntitlements = await entitlementsService.getEntitlements(input.userId);
|
|
4049
|
+
const previousTier = currentEntitlements.tier;
|
|
4050
|
+
context.previousTier = previousTier;
|
|
4051
|
+
await db.update(user).set({
|
|
4052
|
+
subscriptionTier: input.toTier
|
|
4053
|
+
}).where(eq(user.id, input.userId));
|
|
4054
|
+
await db.update(subscriptions).set({
|
|
4055
|
+
plan: input.toTier
|
|
4056
|
+
}).where(eq(subscriptions.userId, input.userId));
|
|
4057
|
+
console.log(`[TierUpgradeSaga] Updated user ${input.userId} tier from ${previousTier} to ${input.toTier}`);
|
|
4058
|
+
return {
|
|
4059
|
+
previousTier,
|
|
4060
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
__name(updateUserTierStep, "updateUserTierStep");
|
|
4064
|
+
async function compensateUpdateUserTier(input, output, context) {
|
|
4065
|
+
if (!output?.previousTier || !db) {
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
try {
|
|
4069
|
+
const previousTier = context.previousTier;
|
|
4070
|
+
await db.update(user).set({
|
|
4071
|
+
subscriptionTier: previousTier
|
|
4072
|
+
}).where(eq(user.id, input.userId));
|
|
4073
|
+
await db.update(subscriptions).set({
|
|
4074
|
+
plan: previousTier
|
|
4075
|
+
}).where(eq(subscriptions.userId, input.userId));
|
|
4076
|
+
console.log(`[TierUpgradeSaga] Compensated: Reverted user ${input.userId} tier to ${previousTier}`);
|
|
4077
|
+
} catch (error) {
|
|
4078
|
+
console.error("[TierUpgradeSaga] Failed to compensate updateUserTier:", error);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
__name(compensateUpdateUserTier, "compensateUpdateUserTier");
|
|
4082
|
+
async function updateUpgradeEntitlementsStep(input, _context) {
|
|
4083
|
+
const currentEntitlements = await entitlementsService.getEntitlements(input.userId);
|
|
4084
|
+
const previousVersion = 1;
|
|
4085
|
+
await entitlementsService.invalidateCache(input.userId);
|
|
4086
|
+
const newEntitlements = await entitlementsService.getEntitlements(input.userId);
|
|
4087
|
+
const addedFeatures = newEntitlements.features.filter((f) => !currentEntitlements.features.includes(f));
|
|
4088
|
+
console.log(`[TierUpgradeSaga] Updated entitlements for user ${input.userId}: +${addedFeatures.length} features`);
|
|
4089
|
+
return {
|
|
4090
|
+
entitlements: newEntitlements,
|
|
4091
|
+
previousVersion,
|
|
4092
|
+
addedFeatures
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
__name(updateUpgradeEntitlementsStep, "updateUpgradeEntitlementsStep");
|
|
4096
|
+
async function compensateUpdateUpgradeEntitlements(input, _output, _context) {
|
|
4097
|
+
try {
|
|
4098
|
+
await entitlementsService.invalidateCache(input.userId);
|
|
4099
|
+
console.log(`[TierUpgradeSaga] Compensated: Invalidated entitlements for user ${input.userId}`);
|
|
4100
|
+
} catch (error) {
|
|
4101
|
+
console.error("[TierUpgradeSaga] Failed to compensate updateUpgradeEntitlements:", error);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
__name(compensateUpdateUpgradeEntitlements, "compensateUpdateUpgradeEntitlements");
|
|
4105
|
+
async function sendUpgradeConfirmationStep(input, context) {
|
|
4106
|
+
const emailService = context.emailService;
|
|
4107
|
+
if (!emailService) {
|
|
4108
|
+
const skippedJobId = `skipped_${nanoid()}`;
|
|
4109
|
+
console.warn(`[TierUpgradeSaga] EmailService unavailable, skipping email for user ${input.userId}`);
|
|
4110
|
+
return {
|
|
4111
|
+
emailJobId: skippedJobId,
|
|
4112
|
+
scheduledAt: /* @__PURE__ */ new Date()
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
const emailJobData = {
|
|
4116
|
+
id: nanoid(),
|
|
4117
|
+
userId: input.userId,
|
|
4118
|
+
recipientEmail: input.userEmail,
|
|
4119
|
+
template: "tier_upgraded",
|
|
4120
|
+
templateVersion: 1,
|
|
4121
|
+
variant: null,
|
|
4122
|
+
templateData: {
|
|
4123
|
+
fromTier: input.fromTier,
|
|
4124
|
+
toTier: input.toTier,
|
|
4125
|
+
newFeatures: input.newFeatures,
|
|
4126
|
+
effectiveDate: input.effectiveDate.toISOString()
|
|
4127
|
+
},
|
|
4128
|
+
sendAt: /* @__PURE__ */ new Date(),
|
|
4129
|
+
priority: "medium"
|
|
4130
|
+
};
|
|
4131
|
+
const emailJobId = await emailService.queueEmail(emailJobData);
|
|
4132
|
+
const scheduledAt = /* @__PURE__ */ new Date();
|
|
4133
|
+
console.log(`[TierUpgradeSaga] Scheduled upgrade confirmation email ${emailJobId} for user ${input.userId}`);
|
|
4134
|
+
return {
|
|
4135
|
+
emailJobId,
|
|
4136
|
+
scheduledAt
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
__name(sendUpgradeConfirmationStep, "sendUpgradeConfirmationStep");
|
|
4140
|
+
async function compensateSendUpgradeConfirmation(_input, output, context) {
|
|
4141
|
+
if (!output?.emailJobId) {
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
if (output.emailJobId.startsWith("skipped_")) {
|
|
4145
|
+
console.log("[TierUpgradeSaga] Compensated: Email was skipped, no cancellation needed");
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
const emailService = context.emailService;
|
|
4149
|
+
if (!emailService) {
|
|
4150
|
+
console.warn(`[TierUpgradeSaga] EmailService unavailable, cannot cancel email job ${output.emailJobId}`);
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
try {
|
|
4154
|
+
await emailService.cancelJob(output.emailJobId);
|
|
4155
|
+
console.log(`[TierUpgradeSaga] Compensated: Cancelled email job ${output.emailJobId}`);
|
|
4156
|
+
} catch (error) {
|
|
4157
|
+
console.error("[TierUpgradeSaga] Failed to compensate sendUpgradeConfirmation:", error);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
__name(compensateSendUpgradeConfirmation, "compensateSendUpgradeConfirmation");
|
|
4161
|
+
async function emitTierUpgradedStep(input, context) {
|
|
4162
|
+
const eventBus = context.eventBus;
|
|
4163
|
+
const eventId = `event_${nanoid()}`;
|
|
4164
|
+
const timestamp3 = /* @__PURE__ */ new Date();
|
|
4165
|
+
const sagaId = context.sagaId ?? `saga_${nanoid()}`;
|
|
4166
|
+
if (eventBus) {
|
|
4167
|
+
eventBus.emit("tier:upgraded", {
|
|
4168
|
+
userId: input.userId,
|
|
4169
|
+
fromTier: input.fromTier,
|
|
4170
|
+
toTier: input.toTier,
|
|
4171
|
+
subscriptionId: input.subscriptionId,
|
|
4172
|
+
effectiveDate: input.effectiveDate,
|
|
4173
|
+
sagaId
|
|
4174
|
+
});
|
|
4175
|
+
console.log(`[TierUpgradeSaga] Emitted tier_upgraded event ${eventId}: ${input.fromTier} \u2192 ${input.toTier}`);
|
|
4176
|
+
} else {
|
|
4177
|
+
console.warn(`[TierUpgradeSaga] EventBus unavailable, logging event instead: ${input.fromTier} \u2192 ${input.toTier}`);
|
|
4178
|
+
}
|
|
4179
|
+
return {
|
|
4180
|
+
eventId,
|
|
4181
|
+
timestamp: timestamp3
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
__name(emitTierUpgradedStep, "emitTierUpgradedStep");
|
|
4185
|
+
function getTierUpgradeSaga() {
|
|
4186
|
+
return {
|
|
4187
|
+
...TIER_UPGRADE_SAGA,
|
|
4188
|
+
steps: [
|
|
4189
|
+
{
|
|
4190
|
+
stepId: "update_subscription",
|
|
4191
|
+
stepName: "Update Subscription in Payment Provider",
|
|
4192
|
+
execute: updateSubscriptionStep,
|
|
4193
|
+
compensate: compensateUpdateSubscription,
|
|
4194
|
+
retryable: true,
|
|
4195
|
+
timeout: 3e4
|
|
4196
|
+
},
|
|
4197
|
+
{
|
|
4198
|
+
stepId: "update_user_tier",
|
|
4199
|
+
stepName: "Update User Tier in Database",
|
|
4200
|
+
execute: updateUserTierStep,
|
|
4201
|
+
compensate: compensateUpdateUserTier,
|
|
4202
|
+
retryable: true,
|
|
4203
|
+
timeout: 5e3
|
|
4204
|
+
},
|
|
4205
|
+
{
|
|
4206
|
+
stepId: "update_entitlements",
|
|
4207
|
+
stepName: "Update Entitlements with New Tier Features",
|
|
4208
|
+
execute: updateUpgradeEntitlementsStep,
|
|
4209
|
+
compensate: compensateUpdateUpgradeEntitlements,
|
|
4210
|
+
retryable: true,
|
|
4211
|
+
timeout: 5e3
|
|
4212
|
+
},
|
|
4213
|
+
{
|
|
4214
|
+
stepId: "send_confirmation",
|
|
4215
|
+
stepName: "Send Upgrade Confirmation Email",
|
|
4216
|
+
execute: sendUpgradeConfirmationStep,
|
|
4217
|
+
compensate: compensateSendUpgradeConfirmation,
|
|
4218
|
+
retryable: true,
|
|
4219
|
+
timeout: 1e4
|
|
4220
|
+
},
|
|
4221
|
+
{
|
|
4222
|
+
stepId: "emit_event",
|
|
4223
|
+
stepName: "Emit Tier Upgraded Event",
|
|
4224
|
+
execute: emitTierUpgradedStep,
|
|
4225
|
+
// No compensation needed for events (idempotent)
|
|
4226
|
+
retryable: false,
|
|
4227
|
+
timeout: 3e3
|
|
4228
|
+
}
|
|
4229
|
+
]
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
__name(getTierUpgradeSaga, "getTierUpgradeSaga");
|
|
4233
|
+
function createTierUpgradeSagaWithDeps(deps) {
|
|
4234
|
+
const sagaDef = getTierUpgradeSaga();
|
|
4235
|
+
const originalSteps = sagaDef.steps;
|
|
4236
|
+
return {
|
|
4237
|
+
...sagaDef,
|
|
4238
|
+
steps: originalSteps.map((step) => {
|
|
4239
|
+
const enhanceContext = /* @__PURE__ */ __name((context) => ({
|
|
4240
|
+
...context,
|
|
4241
|
+
emailService: deps.emailService,
|
|
4242
|
+
eventBus: deps.eventBus
|
|
4243
|
+
}), "enhanceContext");
|
|
4244
|
+
return {
|
|
4245
|
+
...step,
|
|
4246
|
+
execute: /* @__PURE__ */ __name(async (input, context) => {
|
|
4247
|
+
return step.execute(input, enhanceContext(context));
|
|
4248
|
+
}, "execute"),
|
|
4249
|
+
// Also wrap compensate to ensure dependencies are available during rollback
|
|
4250
|
+
compensate: step.compensate ? async (input, output, context) => {
|
|
4251
|
+
return step.compensate(input, output, enhanceContext(context));
|
|
4252
|
+
} : void 0
|
|
4253
|
+
};
|
|
4254
|
+
})
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
__name(createTierUpgradeSagaWithDeps, "createTierUpgradeSagaWithDeps");
|
|
4258
|
+
|
|
4259
|
+
export { AccountSchema, AiChatSchema, AttributionServiceImpl, ENABLE_CAPTCHA, ENABLE_ENHANCED_2FA, ENABLE_MULTI_SESSION, ENABLE_SSO, EntitlementsServiceImpl, InvitationSchema, MCPService, MemberSchema, OrganizationSchema, OrganizationUpdateSchema, PasskeySchema, PioneerServiceImpl, PurchaseInsertSchema, PurchaseSchema, PurchaseUpdateSchema, SagaOrchestratorImpl, SessionSchema, SnapshotStoreDb, TelemetrySinkDb, UserSchema, UserUpdateSchema, VerificationSchema, anonymizeEmail, anonymizeUserData, anonymizeUserId, appendFalsePositivePatterns, calculateDecayedWeight, cleanupExpiredData, clearCapabilityCache, closeTestDb, config, countAllOrganizations, countAllUsers, createPurchase, createTestUser, createTierUpgradeSagaWithDeps, createUser, createUserAccount, databaseService, deletePurchaseBySubscriptionId, deleteUserApiKeys, deleteUserData, exportUserData, extensionLinkTokens, extensionSessions, findSimilarPatterns, generateOrganizationSlug, getAccountById, getBaseUrl, getCacheMetrics, getCapabilities, getCapabilityAuditHistory, getInvitationById, getMCPService, getOrganizationById, getOrganizationBySlug, getOrganizationMembership, getOrganizationWithPurchasesAndMembersCount, getOrganizations, getOrganizationsWithMembers, getPendingInvitationByEmail, getPurchaseById, getPurchaseBySubscriptionId, getPurchasesByOrganizationId, getPurchasesByUserId, getTestDb, getUserByEmail, getUserById, getUserPrivacyPreferences, getUsers, getVectorStats, getWorkspaceLinkById, getWorkspaceLinksByUserId, handleTierDowngrade, handleTierUpgrade, healthCheck, incrementDetectionsAnalyzed, insertPatternWithEmbedding, invalidateCapabilityCache, isPgvectorEnabled, linkWorkspace, logAnonymizedEvent, logCapabilityAudit, mergeSignalIntoPattern, recordFalsePositiveSignal, resetCacheMetrics, resetCapabilities, resolveTierByWorkspaceId, sagaPersistence, sanitizeForLogging, searchSimilarPatterns, shouldRetainData, signalToPattern, testInTransaction, truncateAllTables, unlinkAllWorkspacesForUser, unlinkWorkspace, updateCapabilities, updateOrganization, updatePatternEmbedding, updatePurchase, updateUser, updateWorkspaceTier };
|