@snapback/cli 1.1.14 → 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.
Files changed (67) hide show
  1. package/README.md +70 -9
  2. package/dist/SkippedTestDetector-AXTMWWHC.js +5 -0
  3. package/dist/SkippedTestDetector-QLSQV7K7.js +5 -0
  4. package/dist/analysis-6WTBZJH3.js +6 -0
  5. package/dist/analysis-C472LUGW.js +2475 -0
  6. package/dist/auth-HFJRXXG2.js +1446 -0
  7. package/dist/auto-provision-organization-SF6XM7X4.js +161 -0
  8. package/dist/chunk-23G5VYA3.js +4259 -0
  9. package/dist/{chunk-QAKFE3NE.js → chunk-4YTE4JEW.js} +3 -4
  10. package/dist/chunk-5EOPYJ4Y.js +12 -0
  11. package/dist/{chunk-OJNDAPC2.js → chunk-5SQA44V7.js} +1086 -19
  12. package/dist/{chunk-BW7RALUZ.js → chunk-7ADPL4Q3.js} +11 -4
  13. package/dist/chunk-CBGOC6RV.js +293 -0
  14. package/dist/chunk-DNEADD2G.js +3499 -0
  15. package/dist/{chunk-Q5XZ3DCB.js → chunk-DPWFZNMY.js} +122 -15
  16. package/dist/chunk-GQ73B37K.js +314 -0
  17. package/dist/chunk-HR34NJP7.js +6133 -0
  18. package/dist/chunk-ICKSHS3A.js +2264 -0
  19. package/dist/{chunk-2TOJVUVJ.js → chunk-OI2HNNT6.js} +285 -33
  20. package/dist/chunk-PL4HF4M2.js +593 -0
  21. package/dist/chunk-WS36HDEU.js +3735 -0
  22. package/dist/chunk-XYU5FFE3.js +111 -0
  23. package/dist/chunk-ZBQDE6WJ.js +108 -0
  24. package/dist/client-WIO6W447.js +8 -0
  25. package/dist/dist-E7E2T3DQ.js +9 -0
  26. package/dist/dist-TEWNOZYS.js +5 -0
  27. package/dist/dist-YZBJAYEJ.js +12 -0
  28. package/dist/index.js +63852 -39806
  29. package/dist/local-service-adapter-3JHN6G4O.js +6 -0
  30. package/dist/pioneer-oauth-hook-V2JKEXM7.js +12 -0
  31. package/dist/{secure-credentials-A4QHHOE2.js → secure-credentials-UEPG7GWW.js} +3 -4
  32. package/dist/snapback-dir-MG7DTRMF.js +6 -0
  33. package/package.json +8 -41
  34. package/scripts/postinstall.mjs +2 -3
  35. package/dist/SkippedTestDetector-B3JZUE5G.js +0 -5
  36. package/dist/SkippedTestDetector-B3JZUE5G.js.map +0 -1
  37. package/dist/analysis-C6XVLBAL.js +0 -6
  38. package/dist/analysis-C6XVLBAL.js.map +0 -1
  39. package/dist/chunk-2TOJVUVJ.js.map +0 -1
  40. package/dist/chunk-5EQLSU5B.js +0 -385
  41. package/dist/chunk-5EQLSU5B.js.map +0 -1
  42. package/dist/chunk-6MR2TINI.js +0 -27
  43. package/dist/chunk-6MR2TINI.js.map +0 -1
  44. package/dist/chunk-A3TUM7U4.js +0 -13002
  45. package/dist/chunk-A3TUM7U4.js.map +0 -1
  46. package/dist/chunk-BW7RALUZ.js.map +0 -1
  47. package/dist/chunk-LEXNOXPV.js +0 -21621
  48. package/dist/chunk-LEXNOXPV.js.map +0 -1
  49. package/dist/chunk-OJNDAPC2.js.map +0 -1
  50. package/dist/chunk-Q5XZ3DCB.js.map +0 -1
  51. package/dist/chunk-QAKFE3NE.js.map +0 -1
  52. package/dist/chunk-QLCHTUT5.js +0 -1067
  53. package/dist/chunk-QLCHTUT5.js.map +0 -1
  54. package/dist/dist-D2SHOZMS.js +0 -8
  55. package/dist/dist-D2SHOZMS.js.map +0 -1
  56. package/dist/dist-L76VXYJ5.js +0 -5
  57. package/dist/dist-L76VXYJ5.js.map +0 -1
  58. package/dist/dist-RPM72FHJ.js +0 -5
  59. package/dist/dist-RPM72FHJ.js.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/learning-pruner-YSZSOOOC.js +0 -7
  62. package/dist/learning-pruner-YSZSOOOC.js.map +0 -1
  63. package/dist/secure-credentials-A4QHHOE2.js.map +0 -1
  64. package/dist/snapback-dir-6QUSO6Y3.js +0 -6
  65. package/dist/snapback-dir-6QUSO6Y3.js.map +0 -1
  66. package/dist/storage-H366UNAR.js +0 -6
  67. package/dist/storage-H366UNAR.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 };