@ucptools/validator 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/auth/config.d.ts +20 -0
  2. package/dist/auth/config.d.ts.map +1 -0
  3. package/dist/auth/config.js +114 -0
  4. package/dist/auth/config.js.map +1 -0
  5. package/dist/auth/index.d.ts +5 -0
  6. package/dist/auth/index.d.ts.map +1 -0
  7. package/dist/auth/index.js +17 -0
  8. package/dist/auth/index.js.map +1 -0
  9. package/dist/auth/middleware.d.ts +45 -0
  10. package/dist/auth/middleware.d.ts.map +1 -0
  11. package/dist/auth/middleware.js +170 -0
  12. package/dist/auth/middleware.js.map +1 -0
  13. package/dist/auth/service.d.ts +80 -0
  14. package/dist/auth/service.d.ts.map +1 -0
  15. package/dist/auth/service.js +298 -0
  16. package/dist/auth/service.js.map +1 -0
  17. package/dist/cli/index.js +96 -0
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/mock-server.d.ts +20 -0
  20. package/dist/cli/mock-server.d.ts.map +1 -0
  21. package/dist/cli/mock-server.js +261 -0
  22. package/dist/cli/mock-server.js.map +1 -0
  23. package/dist/db/index.d.ts +8 -2
  24. package/dist/db/index.d.ts.map +1 -1
  25. package/dist/db/index.js +22 -5
  26. package/dist/db/index.js.map +1 -1
  27. package/dist/db/schema.d.ts +3570 -128
  28. package/dist/db/schema.d.ts.map +1 -1
  29. package/dist/db/schema.js +377 -17
  30. package/dist/db/schema.js.map +1 -1
  31. package/dist/db/utils.d.ts +252 -0
  32. package/dist/db/utils.d.ts.map +1 -0
  33. package/dist/db/utils.js +295 -0
  34. package/dist/db/utils.js.map +1 -0
  35. package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -1
  36. package/dist/feed-analyzer/feed-analyzer.js +218 -4
  37. package/dist/feed-analyzer/feed-analyzer.js.map +1 -1
  38. package/dist/feed-analyzer/types.d.ts +82 -1
  39. package/dist/feed-analyzer/types.d.ts.map +1 -1
  40. package/dist/feed-analyzer/types.js +13 -0
  41. package/dist/feed-analyzer/types.js.map +1 -1
  42. package/dist/generator/profile-builder.d.ts.map +1 -1
  43. package/dist/generator/profile-builder.js +158 -115
  44. package/dist/generator/profile-builder.js.map +1 -1
  45. package/dist/lib/analytics.d.ts +349 -0
  46. package/dist/lib/analytics.d.ts.map +1 -0
  47. package/dist/lib/analytics.js +198 -0
  48. package/dist/lib/analytics.js.map +1 -0
  49. package/dist/security/security-scanner.d.ts.map +1 -1
  50. package/dist/security/security-scanner.js +130 -2
  51. package/dist/security/security-scanner.js.map +1 -1
  52. package/dist/security/types.d.ts +32 -0
  53. package/dist/security/types.d.ts.map +1 -1
  54. package/dist/security/types.js.map +1 -1
  55. package/dist/services/analytics.d.ts +114 -0
  56. package/dist/services/analytics.d.ts.map +1 -0
  57. package/dist/services/analytics.js +862 -0
  58. package/dist/services/analytics.js.map +1 -0
  59. package/dist/services/badge.d.ts +31 -0
  60. package/dist/services/badge.d.ts.map +1 -0
  61. package/dist/services/badge.js +152 -0
  62. package/dist/services/badge.js.map +1 -0
  63. package/dist/services/cron.d.ts +127 -0
  64. package/dist/services/cron.d.ts.map +1 -0
  65. package/dist/services/cron.js +693 -0
  66. package/dist/services/cron.js.map +1 -0
  67. package/dist/services/directory.d.ts +2 -0
  68. package/dist/services/directory.d.ts.map +1 -1
  69. package/dist/services/directory.js +45 -27
  70. package/dist/services/directory.js.map +1 -1
  71. package/dist/services/email.d.ts +127 -0
  72. package/dist/services/email.d.ts.map +1 -0
  73. package/dist/services/email.js +876 -0
  74. package/dist/services/email.js.map +1 -0
  75. package/dist/services/hosted-profiles.d.ts +77 -0
  76. package/dist/services/hosted-profiles.d.ts.map +1 -0
  77. package/dist/services/hosted-profiles.js +433 -0
  78. package/dist/services/hosted-profiles.js.map +1 -0
  79. package/dist/services/latency.d.ts +67 -0
  80. package/dist/services/latency.d.ts.map +1 -0
  81. package/dist/services/latency.js +274 -0
  82. package/dist/services/latency.js.map +1 -0
  83. package/dist/services/manifest-compliance.d.ts +64 -0
  84. package/dist/services/manifest-compliance.d.ts.map +1 -0
  85. package/dist/services/manifest-compliance.js +271 -0
  86. package/dist/services/manifest-compliance.js.map +1 -0
  87. package/dist/services/monitoring-diff.d.ts +31 -0
  88. package/dist/services/monitoring-diff.d.ts.map +1 -0
  89. package/dist/services/monitoring-diff.js +189 -0
  90. package/dist/services/monitoring-diff.js.map +1 -0
  91. package/dist/services/notifications.d.ts +46 -0
  92. package/dist/services/notifications.d.ts.map +1 -0
  93. package/dist/services/notifications.js +88 -0
  94. package/dist/services/notifications.js.map +1 -0
  95. package/dist/services/posthog.d.ts +43 -0
  96. package/dist/services/posthog.d.ts.map +1 -0
  97. package/dist/services/posthog.js +110 -0
  98. package/dist/services/posthog.js.map +1 -0
  99. package/dist/services/stripe.d.ts +93 -0
  100. package/dist/services/stripe.d.ts.map +1 -0
  101. package/dist/services/stripe.js +490 -0
  102. package/dist/services/stripe.js.map +1 -0
  103. package/dist/services/validation-history.d.ts +99 -0
  104. package/dist/services/validation-history.d.ts.map +1 -0
  105. package/dist/services/validation-history.js +344 -0
  106. package/dist/services/validation-history.js.map +1 -0
  107. package/dist/services/validation-logging.d.ts +103 -0
  108. package/dist/services/validation-logging.d.ts.map +1 -0
  109. package/dist/services/validation-logging.js +210 -0
  110. package/dist/services/validation-logging.js.map +1 -0
  111. package/dist/services/validation.d.ts +119 -0
  112. package/dist/services/validation.d.ts.map +1 -0
  113. package/dist/services/validation.js +1185 -0
  114. package/dist/services/validation.js.map +1 -0
  115. package/dist/simulator/agent-simulator.d.ts.map +1 -1
  116. package/dist/simulator/agent-simulator.js +229 -9
  117. package/dist/simulator/agent-simulator.js.map +1 -1
  118. package/dist/simulator/types.d.ts +26 -0
  119. package/dist/simulator/types.d.ts.map +1 -1
  120. package/dist/simulator/types.js.map +1 -1
  121. package/dist/types/acp-validation.d.ts +87 -0
  122. package/dist/types/acp-validation.d.ts.map +1 -0
  123. package/dist/types/acp-validation.js +40 -0
  124. package/dist/types/acp-validation.js.map +1 -0
  125. package/dist/types/analytics.d.ts +182 -0
  126. package/dist/types/analytics.d.ts.map +1 -0
  127. package/dist/types/analytics.js +7 -0
  128. package/dist/types/analytics.js.map +1 -0
  129. package/dist/types/generator.d.ts +4 -0
  130. package/dist/types/generator.d.ts.map +1 -1
  131. package/dist/types/ucp-profile.d.ts +32 -2
  132. package/dist/types/ucp-profile.d.ts.map +1 -1
  133. package/dist/types/ucp-profile.js +31 -1
  134. package/dist/types/ucp-profile.js.map +1 -1
  135. package/dist/types/validation.d.ts +14 -0
  136. package/dist/types/validation.d.ts.map +1 -1
  137. package/dist/types/validation.js +19 -0
  138. package/dist/types/validation.js.map +1 -1
  139. package/dist/validator/acp/index.d.ts +31 -0
  140. package/dist/validator/acp/index.d.ts.map +1 -0
  141. package/dist/validator/acp/index.js +574 -0
  142. package/dist/validator/acp/index.js.map +1 -0
  143. package/dist/validator/network-validator.d.ts.map +1 -1
  144. package/dist/validator/network-validator.js +23 -13
  145. package/dist/validator/network-validator.js.map +1 -1
  146. package/dist/validator/rules-validator.d.ts +8 -0
  147. package/dist/validator/rules-validator.d.ts.map +1 -1
  148. package/dist/validator/rules-validator.js +159 -43
  149. package/dist/validator/rules-validator.js.map +1 -1
  150. package/dist/validator/structural-validator.d.ts.map +1 -1
  151. package/dist/validator/structural-validator.js +283 -53
  152. package/dist/validator/structural-validator.js.map +1 -1
  153. package/dist/validator/utils.d.ts +62 -0
  154. package/dist/validator/utils.d.ts.map +1 -0
  155. package/dist/validator/utils.js +151 -0
  156. package/dist/validator/utils.js.map +1 -0
  157. package/package.json +45 -12
  158. package/.claude/settings.local.json +0 -60
  159. package/.vercel/README.txt +0 -11
  160. package/.vercel/project.json +0 -1
  161. package/publish-output.txt +0 -0
  162. package/tsconfig.json +0 -20
@@ -0,0 +1,693 @@
1
+ "use strict";
2
+ /**
3
+ * Cron Service
4
+ *
5
+ * Handles scheduled tasks like weekly domain validation.
6
+ * Designed to be triggered by Vercel Cron, external scheduler, or manual API call.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.getDomainsNeedingValidation = getDomainsNeedingValidation;
10
+ exports.runWeeklyValidation = runWeeklyValidation;
11
+ exports.getCronStats = getCronStats;
12
+ exports.runTrialEmailSequence = runTrialEmailSequence;
13
+ exports.runHostedProfileMaintenance = runHostedProfileMaintenance;
14
+ exports.runExpireTrials = runExpireTrials;
15
+ const index_js_1 = require("../db/index.js");
16
+ const schema_js_1 = require("../db/schema.js");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const validation_history_js_1 = require("./validation-history.js");
19
+ const email_js_1 = require("./email.js");
20
+ const monitoring_diff_js_1 = require("./monitoring-diff.js");
21
+ const notifications_js_1 = require("./notifications.js");
22
+ const hosted_profiles_js_1 = require("./hosted-profiles.js");
23
+ const posthog_js_1 = require("./posthog.js");
24
+ /**
25
+ * Get all domains that need validation based on their monitoring frequency.
26
+ * - Must be active
27
+ * - User must have active or trialing subscription
28
+ * - Filters by frequency: daily → 24h threshold, weekly → 7d threshold
29
+ * - Never-checked domains are always included
30
+ */
31
+ async function getDomainsNeedingValidation() {
32
+ const db = (0, index_js_1.getDb)();
33
+ // Get all active domains with active/trialing subscriptions
34
+ const domains = await db
35
+ .select({
36
+ id: schema_js_1.monitoredDomains.id,
37
+ domain: schema_js_1.monitoredDomains.domain,
38
+ userId: schema_js_1.monitoredDomains.userId,
39
+ userEmail: schema_js_1.users.email,
40
+ userName: schema_js_1.users.name,
41
+ lastCheckedAt: schema_js_1.monitoredDomains.lastCheckedAt,
42
+ lastScore: schema_js_1.monitoredDomains.lastScore,
43
+ lastGrade: schema_js_1.monitoredDomains.lastGrade,
44
+ alertOnScoreChange: schema_js_1.monitoredDomains.alertOnScoreChange,
45
+ alertOnGradeChange: schema_js_1.monitoredDomains.alertOnGradeChange,
46
+ alertThreshold: schema_js_1.monitoredDomains.alertThreshold,
47
+ monitoringFrequency: schema_js_1.monitoredDomains.monitoringFrequency,
48
+ webhookUrl: schema_js_1.monitoredDomains.webhookUrl,
49
+ webhookEnabled: schema_js_1.monitoredDomains.webhookEnabled,
50
+ })
51
+ .from(schema_js_1.monitoredDomains)
52
+ .innerJoin(schema_js_1.users, (0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.userId, schema_js_1.users.id))
53
+ .innerJoin(schema_js_1.subscriptions, (0, drizzle_orm_1.eq)(schema_js_1.users.id, schema_js_1.subscriptions.userId))
54
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.isActive, true), (0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_js_1.subscriptions.status, 'active'), (0, drizzle_orm_1.eq)(schema_js_1.subscriptions.status, 'trialing'), (0, drizzle_orm_1.eq)(schema_js_1.subscriptions.status, 'expired'))));
55
+ // Filter in JS by frequency threshold
56
+ const now = Date.now();
57
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
58
+ const SEVEN_DAYS_MS = 7 * ONE_DAY_MS;
59
+ return domains.filter(d => {
60
+ // Never checked → always include
61
+ if (!d.lastCheckedAt)
62
+ return true;
63
+ const elapsed = now - new Date(d.lastCheckedAt).getTime();
64
+ const frequency = d.monitoringFrequency || 'weekly';
65
+ if (frequency === 'daily') {
66
+ return elapsed >= ONE_DAY_MS;
67
+ }
68
+ // weekly (default)
69
+ return elapsed >= SEVEN_DAYS_MS;
70
+ });
71
+ }
72
+ /**
73
+ * Run domain validation for all eligible domains (frequency-aware).
74
+ * Includes diff analysis, severity classification, and webhook dispatch.
75
+ *
76
+ * @param maxDomains - Maximum number of domains to process (for rate limiting)
77
+ * @param delayBetweenMs - Delay between validations in milliseconds
78
+ */
79
+ async function runWeeklyValidation(maxDomains = 100, delayBetweenMs = 1000) {
80
+ const startTime = Date.now();
81
+ const result = {
82
+ success: true,
83
+ domainsProcessed: 0,
84
+ domainsValidated: 0,
85
+ domainsFailed: 0,
86
+ alertsSent: 0,
87
+ alertsFailed: 0,
88
+ webhooksSent: 0,
89
+ errors: [],
90
+ durationMs: 0,
91
+ };
92
+ const dashboardBaseUrl = process.env.DASHBOARD_URL || 'https://ucptools.dev/dashboard';
93
+ try {
94
+ const domains = await getDomainsNeedingValidation();
95
+ const toProcess = domains.slice(0, maxDomains);
96
+ console.log(`[Cron] Found ${domains.length} domains needing validation, processing ${toProcess.length}`);
97
+ for (const domainInfo of toProcess) {
98
+ result.domainsProcessed++;
99
+ try {
100
+ console.log(`[Cron] Validating domain: ${domainInfo.domain}`);
101
+ // Run validation and store history
102
+ const { result: validationResult, historyEntry } = await (0, validation_history_js_1.validateAndStoreHistory)(domainInfo.id, domainInfo.domain);
103
+ result.domainsValidated++;
104
+ const newScore = validationResult.ai_readiness.score;
105
+ const newGrade = validationResult.ai_readiness.grade;
106
+ console.log(`[Cron] Successfully validated: ${domainInfo.domain} (score: ${newScore})`);
107
+ // ── Diff analysis ──────────────────────────────────────────
108
+ let diff = null;
109
+ const previousEntry = await (0, validation_history_js_1.getPreviousValidation)(domainInfo.id, historyEntry?.id);
110
+ if (previousEntry?.resultJson) {
111
+ try {
112
+ const previousResult = JSON.parse(previousEntry.resultJson);
113
+ diff = (0, monitoring_diff_js_1.diffValidationResults)(previousResult, validationResult, domainInfo.alertThreshold ?? 10);
114
+ // Store severity + changes summary on the history entry
115
+ if (historyEntry) {
116
+ await (0, validation_history_js_1.updateHistorySeverity)(historyEntry.id, diff.severity, JSON.stringify({ text: diff.summary, changes: diff.changes }));
117
+ }
118
+ // Update lastSeverity on the monitored domain
119
+ const db = (0, index_js_1.getDb)();
120
+ await db
121
+ .update(schema_js_1.monitoredDomains)
122
+ .set({ lastSeverity: diff.severity, updatedAt: new Date() })
123
+ .where((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.id, domainInfo.id));
124
+ console.log(`[Cron] Diff for ${domainInfo.domain}: severity=${diff.severity}, changes=${diff.changes.length}`);
125
+ }
126
+ catch (diffError) {
127
+ console.error(`[Cron] Diff error for ${domainInfo.domain}:`, diffError);
128
+ }
129
+ }
130
+ // ── Severity-based alerting ────────────────────────────────
131
+ const severity = diff?.severity;
132
+ const dashboardUrl = `${dashboardBaseUrl}/domains/${domainInfo.id}`;
133
+ const shouldEmail = severity === 'broken'
134
+ || (severity === 'degraded' && (domainInfo.alertOnScoreChange || domainInfo.alertOnGradeChange))
135
+ || (!diff && domainInfo.alertOnScoreChange && domainInfo.lastScore !== null && Math.abs(newScore - domainInfo.lastScore) >= (domainInfo.alertThreshold ?? 10));
136
+ if (shouldEmail) {
137
+ try {
138
+ let emailResult;
139
+ // Use severity-aware template when we have diff data
140
+ if (diff && (severity === 'broken' || severity === 'degraded' || severity === 'changed')) {
141
+ emailResult = await (0, email_js_1.sendSeverityAlert)({
142
+ to: {
143
+ email: domainInfo.userEmail,
144
+ name: domainInfo.userName || undefined,
145
+ },
146
+ domain: domainInfo.domain,
147
+ severity,
148
+ oldScore: domainInfo.lastScore,
149
+ newScore,
150
+ oldGrade: domainInfo.lastGrade,
151
+ newGrade,
152
+ summary: diff.summary,
153
+ changes: diff.changes.map(c => ({
154
+ field: c.field,
155
+ type: c.type,
156
+ oldValue: c.oldValue,
157
+ newValue: c.newValue,
158
+ })),
159
+ dashboardUrl,
160
+ });
161
+ }
162
+ else {
163
+ // Fallback to basic score change alert
164
+ emailResult = await (0, email_js_1.sendScoreChangeAlert)({
165
+ to: {
166
+ email: domainInfo.userEmail,
167
+ name: domainInfo.userName || undefined,
168
+ },
169
+ domain: domainInfo.domain,
170
+ oldScore: domainInfo.lastScore ?? 0,
171
+ newScore,
172
+ oldGrade: domainInfo.lastGrade || 'F',
173
+ newGrade,
174
+ dashboardUrl,
175
+ });
176
+ }
177
+ if (emailResult.success) {
178
+ result.alertsSent++;
179
+ console.log(`[Cron] Alert sent to ${domainInfo.userEmail} (severity: ${severity || 'score-change'})`);
180
+ }
181
+ else {
182
+ result.alertsFailed++;
183
+ console.error(`[Cron] Failed to send alert: ${emailResult.error}`);
184
+ }
185
+ }
186
+ catch (emailError) {
187
+ result.alertsFailed++;
188
+ console.error(`[Cron] Email error:`, emailError);
189
+ }
190
+ }
191
+ // ── Webhook dispatch ───────────────────────────────────────
192
+ if (diff && diff.severity !== 'stable' && domainInfo.webhookEnabled && domainInfo.webhookUrl) {
193
+ const eventMap = {
194
+ broken: 'profile_broken',
195
+ degraded: 'profile_degraded',
196
+ changed: 'profile_changed',
197
+ };
198
+ const payload = {
199
+ event: eventMap[diff.severity] || 'profile_changed',
200
+ domain: domainInfo.domain,
201
+ severity: diff.severity,
202
+ oldScore: domainInfo.lastScore,
203
+ newScore,
204
+ oldGrade: domainInfo.lastGrade,
205
+ newGrade,
206
+ changes: diff.changes.map(c => ({
207
+ field: c.field,
208
+ type: c.type,
209
+ oldValue: c.oldValue,
210
+ newValue: c.newValue,
211
+ })),
212
+ summary: diff.summary,
213
+ timestamp: new Date().toISOString(),
214
+ dashboardUrl,
215
+ };
216
+ const sent = await (0, notifications_js_1.sendDomainWebhook)(domainInfo.webhookUrl, payload);
217
+ if (sent)
218
+ result.webhooksSent++;
219
+ }
220
+ }
221
+ catch (error) {
222
+ result.domainsFailed++;
223
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
224
+ result.errors.push({ domain: domainInfo.domain, error: errorMessage });
225
+ console.error(`[Cron] Failed to validate ${domainInfo.domain}:`, errorMessage);
226
+ }
227
+ // Add delay between validations to avoid overwhelming the system
228
+ if (delayBetweenMs > 0 && result.domainsProcessed < toProcess.length) {
229
+ await sleep(delayBetweenMs);
230
+ }
231
+ }
232
+ result.success = result.domainsFailed === 0;
233
+ }
234
+ catch (error) {
235
+ result.success = false;
236
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
237
+ result.errors.push({ domain: 'system', error: errorMessage });
238
+ console.error('[Cron] Domain validation failed:', errorMessage);
239
+ }
240
+ result.durationMs = Date.now() - startTime;
241
+ console.log(`[Cron] Completed: ${result.domainsValidated}/${result.domainsProcessed} validated, ${result.alertsSent} alerts, ${result.webhooksSent} webhooks in ${result.durationMs}ms`);
242
+ return result;
243
+ }
244
+ /**
245
+ * Get cron status/stats
246
+ */
247
+ async function getCronStats() {
248
+ const domains = await getDomainsNeedingValidation();
249
+ // Next run is typically handled by external scheduler
250
+ // For Vercel Cron, this would be configured in vercel.json
251
+ const nextRunAt = new Date();
252
+ nextRunAt.setDate(nextRunAt.getDate() + 7);
253
+ nextRunAt.setHours(0, 0, 0, 0);
254
+ return {
255
+ pendingDomains: domains.length,
256
+ lastRunAt: null, // Would need separate tracking for this
257
+ nextRunAt,
258
+ };
259
+ }
260
+ function sleep(ms) {
261
+ return new Promise(resolve => setTimeout(resolve, ms));
262
+ }
263
+ /**
264
+ * Send trial sequence emails to users
265
+ *
266
+ * - Day 3: Value reminder + Feature highlight
267
+ * - Day 6: Trial ending + Conversion CTA
268
+ *
269
+ * This should be run daily via cron job.
270
+ */
271
+ async function runTrialEmailSequence() {
272
+ const startTime = Date.now();
273
+ const result = {
274
+ success: true,
275
+ day1Sent: 0,
276
+ day1Failed: 0,
277
+ day2Sent: 0,
278
+ day2Failed: 0,
279
+ day3Sent: 0,
280
+ day3Failed: 0,
281
+ day6Sent: 0,
282
+ day6Failed: 0,
283
+ errors: [],
284
+ durationMs: 0,
285
+ };
286
+ const dashboardBaseUrl = process.env.DASHBOARD_URL || 'https://ucptools.dev/dashboard';
287
+ try {
288
+ const db = (0, index_js_1.getDb)();
289
+ // Get trialing users with their trial start dates
290
+ const trialingUsers = await db
291
+ .select({
292
+ userId: schema_js_1.users.id,
293
+ email: schema_js_1.users.email,
294
+ name: schema_js_1.users.name,
295
+ trialStartedAt: schema_js_1.subscriptions.createdAt,
296
+ currentPeriodEnd: schema_js_1.subscriptions.currentPeriodEnd,
297
+ })
298
+ .from(schema_js_1.subscriptions)
299
+ .innerJoin(schema_js_1.users, (0, drizzle_orm_1.eq)(schema_js_1.subscriptions.userId, schema_js_1.users.id))
300
+ .where((0, drizzle_orm_1.eq)(schema_js_1.subscriptions.status, 'trialing'));
301
+ console.log(`[Cron] Found ${trialingUsers.length} users in trial`);
302
+ const now = new Date();
303
+ for (const user of trialingUsers) {
304
+ if (!user.trialStartedAt)
305
+ continue;
306
+ const trialStart = new Date(user.trialStartedAt);
307
+ const daysSinceTrialStart = Math.floor((now.getTime() - trialStart.getTime()) / (1000 * 60 * 60 * 24));
308
+ // Get user's domain count for personalization
309
+ const domainCount = await db
310
+ .select({ count: schema_js_1.monitoredDomains.id })
311
+ .from(schema_js_1.monitoredDomains)
312
+ .where((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.userId, user.userId))
313
+ .then((rows) => rows.length);
314
+ // Day 1 email - Quick Start Guide
315
+ if (daysSinceTrialStart === 1) {
316
+ try {
317
+ console.log(`[Cron] Sending Day 1 email to ${user.email}`);
318
+ const emailResult = await (0, email_js_1.sendTrialDay1Email)({
319
+ to: {
320
+ email: user.email,
321
+ name: user.name || undefined,
322
+ },
323
+ dashboardUrl: dashboardBaseUrl,
324
+ });
325
+ (0, posthog_js_1.trackEmailSent)({ userId: user.userId, email: user.email, emailType: 'trial_day1', success: emailResult.success });
326
+ if (emailResult.success) {
327
+ result.day1Sent++;
328
+ console.log(`[Cron] Day 1 email sent to ${user.email}`);
329
+ }
330
+ else {
331
+ result.day1Failed++;
332
+ result.errors.push({
333
+ userId: user.userId,
334
+ email: user.email,
335
+ error: emailResult.error || 'Unknown error',
336
+ });
337
+ }
338
+ }
339
+ catch (error) {
340
+ result.day1Failed++;
341
+ result.errors.push({
342
+ userId: user.userId,
343
+ email: user.email,
344
+ error: error instanceof Error ? error.message : 'Unknown error',
345
+ });
346
+ }
347
+ }
348
+ // Day 2 email - Personalized Domain Report
349
+ if (daysSinceTrialStart === 2) {
350
+ try {
351
+ console.log(`[Cron] Sending Day 2 email to ${user.email}`);
352
+ // Get the user's first domain for personalization
353
+ const userDomains = await db
354
+ .select({
355
+ id: schema_js_1.monitoredDomains.id,
356
+ domain: schema_js_1.monitoredDomains.domain,
357
+ lastScore: schema_js_1.monitoredDomains.lastScore,
358
+ lastGrade: schema_js_1.monitoredDomains.lastGrade,
359
+ })
360
+ .from(schema_js_1.monitoredDomains)
361
+ .where((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.userId, user.userId))
362
+ .limit(1);
363
+ const firstDomain = userDomains[0];
364
+ let topIssues = [];
365
+ // Get latest validation result for top issues
366
+ if (firstDomain) {
367
+ const latestHistory = await db
368
+ .select({ resultJson: schema_js_1.validationHistory.resultJson })
369
+ .from(schema_js_1.validationHistory)
370
+ .where((0, drizzle_orm_1.eq)(schema_js_1.validationHistory.domainId, firstDomain.id))
371
+ .orderBy((0, drizzle_orm_1.desc)(schema_js_1.validationHistory.validatedAt))
372
+ .limit(1);
373
+ if (latestHistory[0]?.resultJson) {
374
+ try {
375
+ const lastResult = JSON.parse(latestHistory[0].resultJson);
376
+ const issues = lastResult?.issues || lastResult?.findings || [];
377
+ topIssues = issues
378
+ .filter((i) => i.severity === 'error' || i.severity === 'warn')
379
+ .slice(0, 5)
380
+ .map((i) => ({ message: i.message, severity: i.severity }));
381
+ }
382
+ catch {
383
+ // Ignore JSON parse errors
384
+ }
385
+ }
386
+ }
387
+ const emailResult = await (0, email_js_1.sendTrialDay2Email)({
388
+ to: {
389
+ email: user.email,
390
+ name: user.name || undefined,
391
+ },
392
+ dashboardUrl: dashboardBaseUrl,
393
+ domain: firstDomain?.domain,
394
+ score: firstDomain?.lastScore ?? undefined,
395
+ grade: firstDomain?.lastGrade ?? undefined,
396
+ topIssues,
397
+ });
398
+ (0, posthog_js_1.trackEmailSent)({ userId: user.userId, email: user.email, emailType: 'trial_day2', success: emailResult.success });
399
+ if (emailResult.success) {
400
+ result.day2Sent++;
401
+ console.log(`[Cron] Day 2 email sent to ${user.email}`);
402
+ }
403
+ else {
404
+ result.day2Failed++;
405
+ result.errors.push({
406
+ userId: user.userId,
407
+ email: user.email,
408
+ error: emailResult.error || 'Unknown error',
409
+ });
410
+ }
411
+ }
412
+ catch (error) {
413
+ result.day2Failed++;
414
+ result.errors.push({
415
+ userId: user.userId,
416
+ email: user.email,
417
+ error: error instanceof Error ? error.message : 'Unknown error',
418
+ });
419
+ }
420
+ }
421
+ // Day 3 email (send on day 3)
422
+ if (daysSinceTrialStart === 3) {
423
+ try {
424
+ console.log(`[Cron] Sending Day 3 email to ${user.email}`);
425
+ const emailResult = await (0, email_js_1.sendTrialDay3Email)({
426
+ to: {
427
+ email: user.email,
428
+ name: user.name || undefined,
429
+ },
430
+ dashboardUrl: dashboardBaseUrl,
431
+ domainsAdded: domainCount,
432
+ });
433
+ (0, posthog_js_1.trackEmailSent)({ userId: user.userId, email: user.email, emailType: 'trial_day3', success: emailResult.success });
434
+ if (emailResult.success) {
435
+ result.day3Sent++;
436
+ console.log(`[Cron] Day 3 email sent to ${user.email}`);
437
+ }
438
+ else {
439
+ result.day3Failed++;
440
+ result.errors.push({
441
+ userId: user.userId,
442
+ email: user.email,
443
+ error: emailResult.error || 'Unknown error',
444
+ });
445
+ }
446
+ }
447
+ catch (error) {
448
+ result.day3Failed++;
449
+ result.errors.push({
450
+ userId: user.userId,
451
+ email: user.email,
452
+ error: error instanceof Error ? error.message : 'Unknown error',
453
+ });
454
+ }
455
+ }
456
+ // Day 6 email (send on day 6)
457
+ if (daysSinceTrialStart === 6) {
458
+ try {
459
+ console.log(`[Cron] Sending Day 6 email to ${user.email}`);
460
+ // Get validation count for this user
461
+ const validationCount = await db
462
+ .select({ count: schema_js_1.monitoredDomains.id })
463
+ .from(schema_js_1.monitoredDomains)
464
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.userId, user.userId),
465
+ // Has been validated at least once
466
+ (0, drizzle_orm_1.lt)(schema_js_1.monitoredDomains.lastCheckedAt, now)))
467
+ .then((rows) => rows.length);
468
+ const emailResult = await (0, email_js_1.sendTrialDay6Email)({
469
+ to: {
470
+ email: user.email,
471
+ name: user.name || undefined,
472
+ },
473
+ dashboardUrl: dashboardBaseUrl,
474
+ domainsAdded: domainCount,
475
+ validationsRun: validationCount,
476
+ });
477
+ (0, posthog_js_1.trackEmailSent)({ userId: user.userId, email: user.email, emailType: 'trial_day6', success: emailResult.success });
478
+ if (emailResult.success) {
479
+ result.day6Sent++;
480
+ console.log(`[Cron] Day 6 email sent to ${user.email}`);
481
+ }
482
+ else {
483
+ result.day6Failed++;
484
+ result.errors.push({
485
+ userId: user.userId,
486
+ email: user.email,
487
+ error: emailResult.error || 'Unknown error',
488
+ });
489
+ }
490
+ }
491
+ catch (error) {
492
+ result.day6Failed++;
493
+ result.errors.push({
494
+ userId: user.userId,
495
+ email: user.email,
496
+ error: error instanceof Error ? error.message : 'Unknown error',
497
+ });
498
+ }
499
+ }
500
+ // Small delay between emails to avoid rate limits
501
+ await sleep(100);
502
+ }
503
+ result.success =
504
+ result.day1Failed === 0 && result.day2Failed === 0 && result.day3Failed === 0 && result.day6Failed === 0;
505
+ }
506
+ catch (error) {
507
+ result.success = false;
508
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
509
+ result.errors.push({ userId: 'system', email: '', error: errorMessage });
510
+ console.error('[Cron] Trial email sequence failed:', errorMessage);
511
+ }
512
+ result.durationMs = Date.now() - startTime;
513
+ console.log(`[Cron] Trial emails completed: Day 1: ${result.day1Sent}, Day 2: ${result.day2Sent}, Day 3: ${result.day3Sent}, Day 6: ${result.day6Sent} sent in ${result.durationMs}ms`);
514
+ return result;
515
+ }
516
+ /**
517
+ * Run validation on active hosted profiles that haven't been validated in 7+ days.
518
+ * Also checks pending DNS verifications.
519
+ */
520
+ async function runHostedProfileMaintenance(maxProfiles = 100, delayBetweenMs = 500) {
521
+ const startTime = Date.now();
522
+ const result = {
523
+ success: true,
524
+ profilesValidated: 0,
525
+ profilesFailed: 0,
526
+ verificationsChecked: 0,
527
+ verificationsConfirmed: 0,
528
+ errors: [],
529
+ durationMs: 0,
530
+ };
531
+ try {
532
+ // 1. Re-validate active profiles needing validation
533
+ const profilesToValidate = await (0, hosted_profiles_js_1.getProfilesNeedingValidation)(maxProfiles);
534
+ console.log(`[Cron] Found ${profilesToValidate.length} hosted profiles needing validation`);
535
+ for (const profile of profilesToValidate) {
536
+ try {
537
+ await (0, hosted_profiles_js_1.validateHostedProfile)(profile.id);
538
+ result.profilesValidated++;
539
+ }
540
+ catch (error) {
541
+ result.profilesFailed++;
542
+ result.errors.push({
543
+ profileId: profile.id,
544
+ error: error instanceof Error ? error.message : 'Unknown error',
545
+ });
546
+ }
547
+ if (delayBetweenMs > 0) {
548
+ await sleep(delayBetweenMs);
549
+ }
550
+ }
551
+ // 2. Check pending verifications
552
+ const pendingVerifications = await (0, hosted_profiles_js_1.getProfilesPendingVerification)(50);
553
+ console.log(`[Cron] Found ${pendingVerifications.length} profiles pending DNS verification`);
554
+ for (const profile of pendingVerifications) {
555
+ result.verificationsChecked++;
556
+ try {
557
+ const check = await (0, hosted_profiles_js_1.checkDnsVerification)(profile.id);
558
+ if (check.verified) {
559
+ result.verificationsConfirmed++;
560
+ console.log(`[Cron] DNS verified for ${profile.domain}`);
561
+ }
562
+ }
563
+ catch (error) {
564
+ result.errors.push({
565
+ profileId: profile.id,
566
+ error: error instanceof Error ? error.message : 'DNS check failed',
567
+ });
568
+ }
569
+ if (delayBetweenMs > 0) {
570
+ await sleep(delayBetweenMs);
571
+ }
572
+ }
573
+ result.success = result.profilesFailed === 0;
574
+ }
575
+ catch (error) {
576
+ result.success = false;
577
+ result.errors.push({
578
+ profileId: 'system',
579
+ error: error instanceof Error ? error.message : 'Unknown error',
580
+ });
581
+ console.error('[Cron] Hosted profile maintenance failed:', error);
582
+ }
583
+ result.durationMs = Date.now() - startTime;
584
+ console.log(`[Cron] Hosted profile maintenance: ${result.profilesValidated} validated, ${result.verificationsConfirmed}/${result.verificationsChecked} DNS verified in ${result.durationMs}ms`);
585
+ return result;
586
+ }
587
+ /**
588
+ * Expire trials that have passed their currentPeriodEnd date.
589
+ *
590
+ * Downgrades users from trialing/starter → canceled/free.
591
+ * Sends a "trial expired" email with upgrade CTA.
592
+ *
593
+ * Should be run daily via cron job (same schedule as trial-emails).
594
+ */
595
+ async function runExpireTrials() {
596
+ const startTime = Date.now();
597
+ const result = {
598
+ success: true,
599
+ expired: 0,
600
+ emailsSent: 0,
601
+ emailsFailed: 0,
602
+ errors: [],
603
+ durationMs: 0,
604
+ };
605
+ const dashboardBaseUrl = process.env.DASHBOARD_URL || 'https://ucptools.dev/dashboard';
606
+ try {
607
+ const db = (0, index_js_1.getDb)();
608
+ const now = new Date();
609
+ // Find all trialing subscriptions where currentPeriodEnd has passed
610
+ const expiredTrials = await db
611
+ .select({
612
+ userId: schema_js_1.subscriptions.userId,
613
+ currentPeriodEnd: schema_js_1.subscriptions.currentPeriodEnd,
614
+ userEmail: schema_js_1.users.email,
615
+ userName: schema_js_1.users.name,
616
+ })
617
+ .from(schema_js_1.subscriptions)
618
+ .innerJoin(schema_js_1.users, (0, drizzle_orm_1.eq)(schema_js_1.subscriptions.userId, schema_js_1.users.id))
619
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.subscriptions.status, 'trialing'), (0, drizzle_orm_1.lt)(schema_js_1.subscriptions.currentPeriodEnd, now)));
620
+ console.log(`[Cron] Found ${expiredTrials.length} expired trials to downgrade`);
621
+ for (const trial of expiredTrials) {
622
+ try {
623
+ // Expire trial — keep tier so monitoring continues, set status to expired
624
+ await db
625
+ .update(schema_js_1.subscriptions)
626
+ .set({
627
+ status: 'expired',
628
+ updatedAt: new Date(),
629
+ })
630
+ .where((0, drizzle_orm_1.eq)(schema_js_1.subscriptions.userId, trial.userId));
631
+ result.expired++;
632
+ console.log(`[Cron] Expired trial for user ${trial.userId} (${trial.userEmail})`);
633
+ // Get domain count for email personalization
634
+ const domainCount = await db
635
+ .select({ id: schema_js_1.monitoredDomains.id })
636
+ .from(schema_js_1.monitoredDomains)
637
+ .where((0, drizzle_orm_1.eq)(schema_js_1.monitoredDomains.userId, trial.userId))
638
+ .then((rows) => rows.length);
639
+ // Send trial expired email
640
+ try {
641
+ const emailResult = await (0, email_js_1.sendTrialExpiredEmail)({
642
+ to: {
643
+ email: trial.userEmail,
644
+ name: trial.userName || undefined,
645
+ },
646
+ dashboardUrl: dashboardBaseUrl,
647
+ domainsAdded: domainCount,
648
+ });
649
+ (0, posthog_js_1.trackEmailSent)({ userId: trial.userId, email: trial.userEmail, emailType: 'trial_expired', success: emailResult.success });
650
+ if (emailResult.success) {
651
+ result.emailsSent++;
652
+ console.log(`[Cron] Trial expired email sent to ${trial.userEmail}`);
653
+ }
654
+ else {
655
+ result.emailsFailed++;
656
+ result.errors.push({
657
+ userId: trial.userId,
658
+ error: `Email failed: ${emailResult.error}`,
659
+ });
660
+ }
661
+ }
662
+ catch (emailError) {
663
+ result.emailsFailed++;
664
+ result.errors.push({
665
+ userId: trial.userId,
666
+ error: `Email error: ${emailError instanceof Error ? emailError.message : 'Unknown'}`,
667
+ });
668
+ }
669
+ }
670
+ catch (error) {
671
+ result.errors.push({
672
+ userId: trial.userId,
673
+ error: error instanceof Error ? error.message : 'Unknown error',
674
+ });
675
+ console.error(`[Cron] Failed to expire trial for ${trial.userId}:`, error);
676
+ }
677
+ await sleep(100);
678
+ }
679
+ result.success = result.errors.length === 0;
680
+ }
681
+ catch (error) {
682
+ result.success = false;
683
+ result.errors.push({
684
+ userId: 'system',
685
+ error: error instanceof Error ? error.message : 'Unknown error',
686
+ });
687
+ console.error('[Cron] Trial expiration failed:', error);
688
+ }
689
+ result.durationMs = Date.now() - startTime;
690
+ console.log(`[Cron] Trial expiration completed: ${result.expired} expired, ${result.emailsSent} emails sent in ${result.durationMs}ms`);
691
+ return result;
692
+ }
693
+ //# sourceMappingURL=cron.js.map