lytx 0.3.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 (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
package/cli/setup.ts ADDED
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env bun
2
+ //
3
+ // Lytx Kit – Interactive Setup
4
+ //
5
+ // Generates alchemy.run.ts and .env for deploying Lytx on your own
6
+ // Cloudflare account. Run with:
7
+ //
8
+ // bun run cli/setup.ts
9
+ // bun run cli/setup.ts --non-interactive (use all defaults)
10
+ //
11
+
12
+ import { existsSync, writeFileSync, readFileSync } from "fs";
13
+ import { resolve } from "path";
14
+ import { createInterface } from "readline";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Defaults — match what the reference alchemy.run.ts uses
18
+ // ---------------------------------------------------------------------------
19
+
20
+ interface SetupConfig {
21
+ appName: string;
22
+ workerName: string;
23
+ domains: string[];
24
+ apiWorkerEnabled: boolean;
25
+ apiWorkerName: string;
26
+ apiWorkerDomain: string;
27
+ apiWorkerPort: number;
28
+ d1Name: string;
29
+ kvEvents: string;
30
+ kvConfig: string;
31
+ kvSessions: string;
32
+ queueName: string;
33
+ doName: string;
34
+ adoptMode: boolean;
35
+ queueBatchSize: number;
36
+ queueMaxConcurrency: number;
37
+ queueMaxRetries: number;
38
+ queueMaxWaitTimeMs: number;
39
+ queueRetryDelay: number;
40
+ }
41
+
42
+ const DEFAULTS: SetupConfig = {
43
+ appName: "lytx",
44
+ workerName: "lytx-app",
45
+ domains: [],
46
+ apiWorkerEnabled: false,
47
+ apiWorkerName: "lytx-api",
48
+ apiWorkerDomain: "",
49
+ apiWorkerPort: 8788,
50
+ d1Name: "lytx-core-db",
51
+ kvEvents: "LYTX_EVENTS",
52
+ kvConfig: "lytx_config",
53
+ kvSessions: "lytx_sessions",
54
+ queueName: "site-events-queue",
55
+ doName: "site-durable-object",
56
+ adoptMode: false,
57
+ queueBatchSize: 100,
58
+ queueMaxConcurrency: 4,
59
+ queueMaxRetries: 3,
60
+ queueMaxWaitTimeMs: 20_000,
61
+ queueRetryDelay: 30,
62
+ };
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Prompt helper
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
69
+
70
+ function ask(question: string, fallback: string): Promise<string> {
71
+ const label = fallback ? ` (${fallback})` : "";
72
+ return new Promise((resolve) => {
73
+ rl.question(`${question}${label}: `, (answer) => {
74
+ resolve(answer.trim() || fallback);
75
+ });
76
+ });
77
+ }
78
+
79
+ async function askBool(question: string, fallback: boolean): Promise<boolean> {
80
+ const hint = fallback ? "Y/n" : "y/N";
81
+ const answer = await ask(`${question} [${hint}]`, "");
82
+ if (!answer) return fallback;
83
+ return answer.toLowerCase().startsWith("y");
84
+ }
85
+
86
+ async function askNumber(question: string, fallback: number): Promise<number> {
87
+ const raw = await ask(question, String(fallback));
88
+ const parsed = Number(raw);
89
+ return Number.isFinite(parsed) ? parsed : fallback;
90
+ }
91
+
92
+ async function askList(question: string, hint: string): Promise<string[]> {
93
+ const raw = await ask(`${question} (${hint})`, "");
94
+ if (!raw) return [];
95
+ return raw
96
+ .split(",")
97
+ .map((s) => s.trim())
98
+ .filter(Boolean);
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Interactive prompts
103
+ // ---------------------------------------------------------------------------
104
+
105
+ async function promptConfig(): Promise<SetupConfig> {
106
+ const config = { ...DEFAULTS };
107
+
108
+ console.log("\n╔══════════════════════════════════════════╗");
109
+ console.log("║ Lytx Kit – Setup Wizard ║");
110
+ console.log("╚══════════════════════════════════════════╝\n");
111
+
112
+ // -- Core identity --
113
+ console.log("── Project ────────────────────────────────\n");
114
+ config.appName = await ask("Alchemy app name", config.appName);
115
+ config.workerName = await ask("Main worker name", config.workerName);
116
+
117
+ // -- Domains --
118
+ console.log("\n── Domains ────────────────────────────────\n");
119
+ console.log(" Add custom domains Cloudflare will route to this worker.");
120
+ console.log(" Leave blank for workers.dev only (you can add domains later).\n");
121
+ config.domains = await askList("Custom domains", "comma-separated, e.g. analytics.example.com");
122
+
123
+ // -- API Worker --
124
+ console.log("\n── API Worker (optional) ──────────────────\n");
125
+ console.log(" A separate Worker that exposes the Lytx REST API on its own");
126
+ console.log(" domain/subdomain. Skip this if you only need the main app.\n");
127
+ config.apiWorkerEnabled = await askBool("Deploy a separate API worker?", config.apiWorkerEnabled);
128
+ if (config.apiWorkerEnabled) {
129
+ config.apiWorkerName = await ask(" API worker name", config.apiWorkerName);
130
+ config.apiWorkerDomain = await ask(" API worker domain", "api.example.com");
131
+ config.apiWorkerPort = await askNumber(" Local dev port", config.apiWorkerPort);
132
+ }
133
+
134
+ // -- Cloudflare resources --
135
+ console.log("\n── Cloudflare Resources ───────────────────\n");
136
+ config.d1Name = await ask("D1 database name", config.d1Name);
137
+ config.kvEvents = await ask("KV namespace – events", config.kvEvents);
138
+ config.kvConfig = await ask("KV namespace – config", config.kvConfig);
139
+ config.kvSessions = await ask("KV namespace – sessions", config.kvSessions);
140
+ config.queueName = await ask("Queue name", config.queueName);
141
+ config.doName = await ask("Durable Object namespace", config.doName);
142
+
143
+ // -- Adopt mode --
144
+ console.log("\n── Resource Mode ─────────────────────────\n");
145
+ console.log(" Adopt mode tells Alchemy to adopt existing Cloudflare");
146
+ console.log(" resources instead of creating new ones. Turn this on if");
147
+ console.log(" you already created resources via wrangler or the dashboard.\n");
148
+ config.adoptMode = await askBool("Enable adopt mode?", config.adoptMode);
149
+
150
+ // -- Queue tuning --
151
+ console.log("\n── Queue Settings ─────────────────────────\n");
152
+ const tuneQueue = await askBool("Customize queue settings?", false);
153
+ if (tuneQueue) {
154
+ config.queueBatchSize = await askNumber(" Batch size", config.queueBatchSize);
155
+ config.queueMaxConcurrency = await askNumber(" Max concurrency", config.queueMaxConcurrency);
156
+ config.queueMaxRetries = await askNumber(" Max retries", config.queueMaxRetries);
157
+ config.queueMaxWaitTimeMs = await askNumber(" Max wait time (ms)", config.queueMaxWaitTimeMs);
158
+ config.queueRetryDelay = await askNumber(" Retry delay (s)", config.queueRetryDelay);
159
+ }
160
+
161
+ return config;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Code generation
166
+ // ---------------------------------------------------------------------------
167
+
168
+ function generateAlchemyRunTs(c: SetupConfig): string {
169
+ const adopt = c.adoptMode ? "true" : "false";
170
+
171
+ const domainsBlock =
172
+ c.domains.length > 0
173
+ ? c.domains
174
+ .map(
175
+ (d) =>
176
+ ` {\n adopt: ${adopt},\n domainName: "${d}",\n }`,
177
+ )
178
+ .join(",\n")
179
+ : "";
180
+
181
+ const domainsProperty = domainsBlock
182
+ ? `\n domains: [\n${domainsBlock},\n ],`
183
+ : "";
184
+
185
+ const apiWorkerBlock = c.apiWorkerEnabled
186
+ ? `
187
+ await Worker("${c.apiWorkerName}", {
188
+ entrypoint: "./endpoints/api_worker.tsx",
189
+ dev: {
190
+ port: ${c.apiWorkerPort},
191
+ },
192
+ url: false,
193
+ adopt: ${adopt},${
194
+ c.apiWorkerDomain
195
+ ? `\n domains: [{\n adopt: ${adopt},\n domainName: "${c.apiWorkerDomain}",\n }],`
196
+ : ""
197
+ }
198
+ bindings: {
199
+ STORAGE:
200
+ app.local && localDurableHost
201
+ ? localDurableHost.bindings.SITE_DURABLE_OBJECT
202
+ : worker.bindings.SITE_DURABLE_OBJECT,
203
+ lytx_core_db: lytxCoreDb,
204
+ ENVIRONMENT: process.env.ENVIRONMENT ?? "development",
205
+ },
206
+ });
207
+ `
208
+ : "";
209
+
210
+ return `import type { SiteDurableObject } from "./db/durable/siteDurableObject";
211
+ import alchemy from "alchemy";
212
+ import {
213
+ D1Database,
214
+ KVNamespace,
215
+ DurableObjectNamespace,
216
+ Redwood,
217
+ Queue,
218
+ Worker,
219
+ } from "alchemy/cloudflare";
220
+
221
+ const app = await alchemy("${c.appName}");
222
+ if (app.local && app.stage !== "dev") {
223
+ throw new Error(\`Refusing local run on non-dev stage: \${app.stage}\`);
224
+ }
225
+
226
+ const adoptMode = ${adopt};
227
+
228
+ const siteDurableObject = DurableObjectNamespace<SiteDurableObject>("${c.doName}", {
229
+ className: "SiteDurableObject",
230
+ sqlite: true,
231
+ });
232
+
233
+ const lytxKv = await KVNamespace("${c.kvEvents}", {
234
+ adopt: adoptMode,
235
+ delete: false,
236
+ });
237
+
238
+ const lytx_config = await KVNamespace("${c.kvConfig}", {
239
+ adopt: adoptMode,
240
+ delete: false,
241
+ });
242
+
243
+ const siteEventsQueue = await Queue("${c.queueName}", {
244
+ name: "${c.queueName}",
245
+ adopt: adoptMode,
246
+ delete: false,
247
+ });
248
+
249
+ const lytx_sessions = await KVNamespace("${c.kvSessions}", {
250
+ adopt: adoptMode,
251
+ delete: false,
252
+ });
253
+
254
+ const lytxCoreDb = await D1Database("${c.d1Name}", {
255
+ name: "${c.d1Name}",
256
+ migrationsDir: "./db/d1/migrations",
257
+ adopt: adoptMode,
258
+ delete: false,
259
+ });
260
+
261
+ const localDurableHost = app.local
262
+ ? await Worker("${c.workerName}-do-host", {
263
+ entrypoint: "./endpoints/site_do_worker.ts",
264
+ bindings: {
265
+ SITE_DURABLE_OBJECT: siteDurableObject,
266
+ lytx_core_db: lytxCoreDb,
267
+ ENVIRONMENT: process.env.ENVIRONMENT ?? "development",
268
+ },
269
+ })
270
+ : undefined;
271
+
272
+ export const worker = await Redwood("${c.workerName}", {
273
+ adopt: ${adopt},
274
+ url: false,
275
+ noBundle: false,${domainsProperty}
276
+ wrangler: {
277
+ main: "src/worker.tsx",
278
+ transform: (spec) => ({
279
+ ...spec,
280
+ compatibility_flags: ["nodejs_compat"],
281
+ }),
282
+ },
283
+ eventSources: [
284
+ {
285
+ queue: siteEventsQueue,
286
+ settings: {
287
+ batchSize: ${c.queueBatchSize},
288
+ maxConcurrency: ${c.queueMaxConcurrency},
289
+ maxRetries: ${c.queueMaxRetries},
290
+ maxWaitTimeMs: ${c.queueMaxWaitTimeMs},
291
+ retryDelay: ${c.queueRetryDelay},
292
+ },
293
+ },
294
+ ],
295
+ bindings: {
296
+ SITE_DURABLE_OBJECT: siteDurableObject,
297
+ LYTX_EVENTS: lytxKv,
298
+ lytx_config: lytx_config,
299
+ lytx_sessions: lytx_sessions,
300
+ lytx_core_db: lytxCoreDb,
301
+ SITE_EVENTS_QUEUE: siteEventsQueue,
302
+ LYTX_DOMAIN: process.env.LYTX_DOMAIN || "localhost:5173",
303
+ EMAIL_FROM: process.env.EMAIL_FROM || "noreply@example.com",
304
+ BETTER_AUTH_SECRET: alchemy.secret(process.env.BETTER_AUTH_SECRET),
305
+ GITHUB_CLIENT_SECRET: alchemy.secret(process.env.GITHUB_CLIENT_SECRET),
306
+ GOOGLE_CLIENT_SECRET: alchemy.secret(process.env.GOOGLE_CLIENT_SECRET),
307
+ RESEND_API_KEY: alchemy.secret(process.env.RESEND_API_KEY),
308
+ ENCRYPTION_KEY: alchemy.secret(process.env.ENCRYPTION_KEY),
309
+ AI_API_KEY: alchemy.secret(process.env.AI_API_KEY),
310
+ SEED_DATA_SECRET: alchemy.secret(process.env.SEED_DATA_SECRET),
311
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
312
+ GITHUB_CLIENT_ID: alchemy.secret(process.env.GITHUB_CLIENT_ID),
313
+ GOOGLE_CLIENT_ID: alchemy.secret(process.env.GOOGLE_CLIENT_ID),
314
+ REPORT_BUILDER: process.env.REPORT_BUILDER || "false",
315
+ ASK_AI: process.env.ASK_AI || "true",
316
+ ENVIRONMENT: process.env.ENVIRONMENT ?? "development",
317
+ AI_BASE_URL: process.env.AI_BASE_URL ?? "",
318
+ AI_MODEL: process.env.AI_MODEL ?? "",
319
+ AI_DAILY_TOKEN_LIMIT: process.env.AI_DAILY_TOKEN_LIMIT ?? "",
320
+ },
321
+ });
322
+ ${apiWorkerBlock}
323
+ await app.finalize();
324
+ `;
325
+ }
326
+
327
+ function generateEnvFile(c: SetupConfig): string {
328
+ const domain =
329
+ c.domains.length > 0 ? c.domains[0] : "localhost:5173";
330
+
331
+ return `# ── Lytx – Generated by \`bun run cli/setup.ts\` ──
332
+
333
+ # Domain that appears in emails and auth callbacks
334
+ LYTX_DOMAIN=${domain}
335
+
336
+ # Auth (required)
337
+ BETTER_AUTH_SECRET=change-me-${crypto.randomUUID().slice(0, 8)}
338
+ BETTER_AUTH_URL=http://localhost:5173
339
+ ENCRYPTION_KEY=change-me-${crypto.randomUUID().slice(0, 8)}
340
+
341
+ # Auth providers (optional – fill in to enable)
342
+ GITHUB_CLIENT_ID=
343
+ GITHUB_CLIENT_SECRET=
344
+ GOOGLE_CLIENT_ID=
345
+ GOOGLE_CLIENT_SECRET=
346
+
347
+ # Email via Resend (optional)
348
+ RESEND_API_KEY=
349
+ EMAIL_FROM=noreply@yourdomain.com
350
+
351
+ # AI features (optional)
352
+ AI_API_KEY=
353
+ AI_BASE_URL=
354
+ AI_MODEL=
355
+ AI_DAILY_TOKEN_LIMIT=
356
+ REPORT_BUILDER=false
357
+ ASK_AI=true
358
+
359
+ # Misc
360
+ SEED_DATA_SECRET=change-me-${crypto.randomUUID().slice(0, 8)}
361
+ ENVIRONMENT=development
362
+ `;
363
+ }
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // File writing with backup
367
+ // ---------------------------------------------------------------------------
368
+
369
+ function safeWrite(filePath: string, content: string, label: string): void {
370
+ const fullPath = resolve(filePath);
371
+ if (existsSync(fullPath)) {
372
+ const backupPath = `${fullPath}.bak`;
373
+ const existing = readFileSync(fullPath, "utf-8");
374
+ writeFileSync(backupPath, existing, "utf-8");
375
+ console.log(` ⚠ Backed up existing ${label} → ${filePath}.bak`);
376
+ }
377
+ writeFileSync(fullPath, content, "utf-8");
378
+ console.log(` ✓ Wrote ${label} → ${filePath}`);
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Config file (lytx.config.json) for re-running setup
383
+ // ---------------------------------------------------------------------------
384
+
385
+ const CONFIG_PATH = "lytx.config.json";
386
+
387
+ function saveSetupConfig(c: SetupConfig): void {
388
+ writeFileSync(CONFIG_PATH, JSON.stringify(c, null, 2) + "\n", "utf-8");
389
+ console.log(` ✓ Saved setup config → ${CONFIG_PATH}`);
390
+ }
391
+
392
+ function loadSetupConfig(): SetupConfig | null {
393
+ if (!existsSync(CONFIG_PATH)) return null;
394
+ try {
395
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as SetupConfig;
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Main
403
+ // ---------------------------------------------------------------------------
404
+
405
+ async function main() {
406
+ const args = process.argv.slice(2);
407
+ const nonInteractive = args.includes("--non-interactive") || args.includes("-y");
408
+ const regenOnly = args.includes("--regen");
409
+
410
+ let config: SetupConfig;
411
+
412
+ if (regenOnly) {
413
+ const saved = loadSetupConfig();
414
+ if (!saved) {
415
+ console.error("No lytx.config.json found. Run setup interactively first.");
416
+ process.exit(1);
417
+ }
418
+ config = saved;
419
+ console.log("\nRegenerating from saved lytx.config.json...\n");
420
+ } else if (nonInteractive) {
421
+ const saved = loadSetupConfig();
422
+ config = saved ?? { ...DEFAULTS };
423
+ console.log("\nUsing defaults (non-interactive mode)...\n");
424
+ } else {
425
+ config = await promptConfig();
426
+ }
427
+
428
+ rl.close();
429
+
430
+ console.log("\n── Writing files ─────────────────────────\n");
431
+
432
+ safeWrite("alchemy.run.ts", generateAlchemyRunTs(config), "alchemy.run.ts");
433
+
434
+ if (!existsSync(".env")) {
435
+ safeWrite(".env", generateEnvFile(config), ".env");
436
+ } else {
437
+ console.log(" · .env already exists, skipping (won't overwrite secrets)");
438
+ }
439
+
440
+ saveSetupConfig(config);
441
+
442
+ console.log(`
443
+ ── Done! ──────────────────────────────────
444
+
445
+ Next steps:
446
+
447
+ 1. Review and fill in secrets in .env
448
+ 2. Install dependencies: bun install
449
+ 3. Run locally: bun run dev
450
+ 4. Deploy to Cloudflare: bun run deploy
451
+
452
+ Re-run this wizard anytime:
453
+ bun run cli/setup.ts
454
+
455
+ Regenerate files from saved config:
456
+ bun run cli/setup.ts --regen
457
+ `);
458
+ }
459
+
460
+ main().catch((err) => {
461
+ console.error(err);
462
+ process.exit(1);
463
+ });
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Migration Validation CLI Tool
5
+ *
6
+ * This script validates data integrity after migrating from original databases
7
+ * to site-specific durable objects.
8
+ *
9
+ * Usage:
10
+ * npx tsx cli/validate-migration.ts --site-id=123
11
+ * npx tsx cli/validate-migration.ts --all-sites
12
+ * npx tsx cli/validate-migration.ts --site-id=123 --strict
13
+ */
14
+
15
+ import { env } from 'cloudflare:workers';
16
+ import {
17
+ validateSiteMigration,
18
+ generateValidationReport,
19
+ type ValidationConfig
20
+ } from '@/utilities/dataValidation';
21
+ import type { SiteEventInput } from '@/session/siteSchema';
22
+
23
+ // Mock data fetching functions - replace with actual database queries
24
+ async function getOriginalSiteEvents(siteId: number): Promise<SiteEventInput[]> {
25
+ // TODO: Implement actual database query to fetch original events
26
+ // This would query postgres/singlestore for the site's events
27
+ console.log(`Fetching original events for site ${siteId}...`);
28
+
29
+ // For now, return empty array - replace with actual implementation
30
+ return [];
31
+ }
32
+
33
+ async function getOriginalEventCount(siteId: number): Promise<number> {
34
+ // TODO: Implement actual database query to count original events
35
+ console.log(`Counting original events for site ${siteId}...`);
36
+
37
+ // For now, return 0 - replace with actual implementation
38
+ return 0;
39
+ }
40
+
41
+ async function getAllSiteIds(): Promise<number[]> {
42
+ // TODO: Implement actual database query to get all site IDs
43
+ console.log('Fetching all site IDs...');
44
+
45
+ // For now, return empty array - replace with actual implementation
46
+ return [];
47
+ }
48
+
49
+ /**
50
+ * Validate a single site's migration
51
+ */
52
+ async function validateSite(
53
+ siteId: number,
54
+ config: ValidationConfig,
55
+ env: Env
56
+ ): Promise<boolean> {
57
+ console.log(`\n=== Validating Site ${siteId} ===`);
58
+
59
+ try {
60
+ // Fetch original data
61
+ const originalEvents = await getOriginalSiteEvents(siteId);
62
+ const originalCount = await getOriginalEventCount(siteId);
63
+
64
+ if (originalCount === 0) {
65
+ console.log(`Site ${siteId} has no events to validate.`);
66
+ return true;
67
+ }
68
+
69
+ // Run validation
70
+ const result = await validateSiteMigration(
71
+ siteId,
72
+ originalEvents,
73
+ originalCount,
74
+ env,
75
+ config
76
+ );
77
+
78
+ // Generate and display report
79
+ const report = generateValidationReport(result, siteId);
80
+ console.log(report);
81
+
82
+ // Return success status
83
+ return result.isValid;
84
+
85
+ } catch (error) {
86
+ console.error(`Error validating site ${siteId}:`, error);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Main validation function
93
+ */
94
+ async function main() {
95
+ const args = process.argv.slice(2);
96
+
97
+ // Parse command line arguments
98
+ let siteId: number | null = null;
99
+ let validateAllSites = false;
100
+ let strictMode = false;
101
+
102
+ for (const arg of args) {
103
+ if (arg.startsWith('--site-id=')) {
104
+ siteId = parseInt(arg.split('=')[1], 10);
105
+ } else if (arg === '--all-sites') {
106
+ validateAllSites = true;
107
+ } else if (arg === '--strict') {
108
+ strictMode = true;
109
+ } else if (arg === '--help') {
110
+ console.log(`
111
+ Migration Validation Tool
112
+
113
+ Usage:
114
+ npx tsx cli/validate-migration.ts --site-id=123 # Validate specific site
115
+ npx tsx cli/validate-migration.ts --all-sites # Validate all sites
116
+ npx tsx cli/validate-migration.ts --strict # Use strict validation mode
117
+
118
+ Options:
119
+ --site-id=N Validate specific site by ID
120
+ --all-sites Validate all sites
121
+ --strict Enable strict mode (warnings become errors)
122
+ --help Show this help message
123
+ `);
124
+ process.exit(0);
125
+ }
126
+ }
127
+
128
+ // Validation configuration
129
+ const config: ValidationConfig = {
130
+ strictMode,
131
+ allowEmptyFields: ['bot_data', 'custom_data', 'query_params', 'rid', 'postal', 'region', 'city', 'country'],
132
+ maxStringLength: 2000,
133
+ dateRange: {
134
+ minDate: new Date('2020-01-01'),
135
+ maxDate: new Date(Date.now() + 24 * 60 * 60 * 1000)
136
+ }
137
+ };
138
+
139
+ console.log('Starting migration validation...');
140
+ console.log(`Strict mode: ${strictMode ? 'ON' : 'OFF'}`);
141
+
142
+ let allPassed = true;
143
+ let totalSites = 0;
144
+ let passedSites = 0;
145
+
146
+ try {
147
+ if (siteId) {
148
+ // Validate single site
149
+ totalSites = 1;
150
+ const passed = await validateSite(siteId, config, env as unknown as Env);
151
+ if (passed) passedSites = 1;
152
+ allPassed = passed;
153
+
154
+ } else if (validateAllSites) {
155
+ // Validate all sites
156
+ const siteIds = await getAllSiteIds();
157
+ totalSites = siteIds.length;
158
+
159
+ console.log(`Found ${siteIds.length} sites to validate.`);
160
+
161
+ for (const id of siteIds) {
162
+ const passed = await validateSite(id, config, env as unknown as Env);
163
+ if (passed) {
164
+ passedSites++;
165
+ } else {
166
+ allPassed = false;
167
+ }
168
+ }
169
+
170
+ } else {
171
+ console.error('Error: Must specify either --site-id=N or --all-sites');
172
+ console.log('Use --help for usage information.');
173
+ process.exit(1);
174
+ }
175
+
176
+ // Final summary
177
+ console.log(`\n=== Validation Summary ===`);
178
+ console.log(`Total sites validated: ${totalSites}`);
179
+ console.log(`Sites passed: ${passedSites}`);
180
+ console.log(`Sites failed: ${totalSites - passedSites}`);
181
+ console.log(`Overall status: ${allPassed ? 'PASSED' : 'FAILED'}`);
182
+
183
+ // Exit with appropriate code
184
+ process.exit(allPassed ? 0 : 1);
185
+
186
+ } catch (error) {
187
+ console.error('Fatal error during validation:', error);
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ // Run the script
193
+ if (require.main === module) {
194
+ main().catch(error => {
195
+ console.error('Unhandled error:', error);
196
+ process.exit(1);
197
+ });
198
+ }
199
+
200
+ export { validateSite, main };
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "lytx-migration-worker",
3
+ "main": "./cli/migration-worker.ts",
4
+ "compatibility_date": "2024-01-01",
5
+ "compatibility_flags": ["nodejs_compat"],
6
+
7
+ "durable_objects": {
8
+ "bindings": [
9
+ {
10
+ "name": "SITE_DURABLE_OBJECT",
11
+ "class_name": "SiteDurableObject",
12
+ "script_name": "lytx-core"
13
+ }
14
+ ]
15
+ },
16
+
17
+ "d1_databases": [
18
+ {
19
+ "binding": "lytx_core_db",
20
+ "database_name": "lytx_core_db",
21
+ "database_id": "your-d1-database-id"
22
+ }
23
+ ],
24
+
25
+ "vars": {
26
+ "ENVIRONMENT": "migration"
27
+ }
28
+ }