@stacksjs/ts-analytics 0.1.6

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 (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. package/src/version.ts +7 -0
@@ -0,0 +1,71 @@
1
+ // ============================================================================
2
+ // Stacks Model Type Stub
3
+ // ============================================================================
4
+ // Local type definition for Stacks model compatibility
5
+ // This allows analytics models to work without the @stacksjs/types package
6
+
7
+ /**
8
+ * Attribute validation configuration
9
+ */
10
+ export interface AttributeValidation {
11
+ rule: string
12
+ min?: number
13
+ max?: number
14
+ message?: string | Record<string, string>
15
+ }
16
+
17
+ /**
18
+ * Model attribute definition
19
+ */
20
+ export interface ModelAttributeDefinition {
21
+ required?: boolean
22
+ unique?: boolean
23
+ fillable?: boolean
24
+ default?: unknown
25
+ validation?: AttributeValidation
26
+ cast?: string
27
+ comment?: string
28
+ }
29
+
30
+ /**
31
+ * Model traits configuration
32
+ */
33
+ export interface ModelTraits {
34
+ useUuid?: boolean
35
+ useTtl?: boolean
36
+ useTimestamps?: boolean
37
+ useSoftDeletes?: boolean
38
+ }
39
+
40
+ type KeyFunction = (_item: any) => string
41
+
42
+ /**
43
+ * DynamoDB key configuration for single-table design
44
+ */
45
+ export interface DynamoDBKeyConfig {
46
+ pk?: KeyFunction
47
+ sk?: KeyFunction
48
+ gsi1pk?: KeyFunction
49
+ gsi1sk?: KeyFunction
50
+ gsi2pk?: KeyFunction
51
+ gsi2sk?: KeyFunction
52
+ gsi3pk?: KeyFunction
53
+ gsi3sk?: KeyFunction
54
+ }
55
+
56
+ /**
57
+ * Stacks Model definition interface
58
+ * Compatible with @stacksjs/types Model
59
+ */
60
+ export interface Model {
61
+ name: string
62
+ table?: string
63
+ primaryKey?: string | string[]
64
+ autoIncrement?: boolean
65
+ traits?: ModelTraits
66
+ belongsTo?: string[]
67
+ hasMany?: string[]
68
+ hasOne?: string[]
69
+ attributes: Record<string, ModelAttributeDefinition>
70
+ dynamodb?: DynamoDBKeyConfig
71
+ }
package/src/router.ts ADDED
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Router configuration using bun-router
3
+ *
4
+ * This file defines all API routes for the analytics service.
5
+ * Note: bun-router uses {param} syntax for route parameters, not :param
6
+ */
7
+
8
+ import { Router } from '@stacksjs/bun-router'
9
+ import { preflightResponse, credentialedCorsHeaders, allowedCorsOrigins } from './utils/response'
10
+
11
+ // Import handlers
12
+ import * as stats from './handlers/stats'
13
+ import * as goals from './handlers/goals'
14
+ import * as sessions from './handlers/sessions'
15
+ import * as heatmaps from './handlers/heatmaps'
16
+ import { withReadCache } from './handlers/lib/read-cache'
17
+ import * as performance from './handlers/performance'
18
+ import * as funnels from './handlers/funnels'
19
+ import * as annotations from './handlers/annotations'
20
+ import * as experiments from './handlers/experiments'
21
+ import * as alerts from './handlers/alerts'
22
+ import * as apiKeys from './handlers/api-keys'
23
+ import * as uptime from './handlers/uptime'
24
+ import * as webhooks from './handlers/webhooks'
25
+ import * as team from './handlers/team'
26
+ import * as data from './handlers/data'
27
+ import * as sharing from './handlers/sharing'
28
+ import * as collect from './handlers/collect'
29
+ import * as misc from './handlers/misc'
30
+ import * as auth from './handlers/auth'
31
+ import * as authz from './handlers/authz'
32
+ import * as oauth from './handlers/oauth'
33
+
34
+ /**
35
+ * Stealth API path mapping
36
+ * These use innocuous names to bypass content blockers
37
+ * /api/p/ = "project" (instead of /api/sites/)
38
+ */
39
+ const _STEALTH_PATHS = {
40
+ // Analytics data
41
+ stats: 'summary',
42
+ realtime: 'pulse',
43
+ pages: 'content',
44
+ referrers: 'sources',
45
+ devices: 'clients',
46
+ browsers: 'agents',
47
+ os: 'platform',
48
+ countries: 'geo',
49
+ regions: 'area',
50
+ cities: 'locale',
51
+ timeseries: 'series',
52
+ events: 'actions',
53
+ campaigns: 'promo',
54
+ comparison: 'diff',
55
+ // User behavior
56
+ sessions: 'visits',
57
+ flow: 'journey',
58
+ 'entry-exit': 'endpoints',
59
+ live: 'now',
60
+ // Heatmaps
61
+ heatmap: 'touch',
62
+ // Errors
63
+ // Performance
64
+ vitals: 'metrics',
65
+ 'vitals-trends': 'metrics-trends',
66
+ 'performance-budgets': 'budgets',
67
+ // Goals
68
+ goals: 'targets',
69
+ // Funnels
70
+ funnels: 'pipelines',
71
+ // Other
72
+ annotations: 'notes',
73
+ experiments: 'tests',
74
+ alerts: 'notifications',
75
+ 'email-reports': 'scheduled',
76
+ 'api-keys': 'tokens',
77
+ uptime: 'monitors',
78
+ webhooks: 'hooks',
79
+ team: 'members',
80
+ export: 'download',
81
+ retention: 'storage',
82
+ gdpr: 'privacy',
83
+ insights: 'intel',
84
+ revenue: 'income',
85
+ share: 'link',
86
+ }
87
+
88
+ /**
89
+ * Create and configure the router with all routes
90
+ */
91
+ export async function createRouter(): Promise<Router> {
92
+ // Register background jobs on EVERY entrypoint (#168): the Lambda handler
93
+ // imports the router directly and never ran server/index.ts, so
94
+ // POST /api/jobs/tick iterated an empty jobs array in production — rollups,
95
+ // alerts, digests, and webhooks never ran. bootstrapJobs() is idempotent.
96
+ const { bootstrapJobs } = await import('./jobs')
97
+ bootstrapJobs()
98
+
99
+ const router = new Router({
100
+ // Automatically preserve siteId across all dashboard navigation
101
+ queryPreservation: {
102
+ enabled: true,
103
+ preserve: ['siteId'],
104
+ exclude: ['_t', '_cache', 'callback'],
105
+ routes: ['/dashboard', '/dashboard/*'],
106
+ },
107
+ })
108
+
109
+ // Disable auto file-based routing (all routes are registered explicitly)
110
+ router._fileRoutesInitialized = true
111
+
112
+ // Split-domain credentialed CORS (#118): when CORS_ORIGINS allowlists the
113
+ // dashboard origin, /api/* responses echo it with Allow-Credentials (the
114
+ // wildcard is forbidden for credentialed fetches), OPTIONS preflights are
115
+ // answered, and state-changing /api/* requests from any OTHER browser
116
+ // origin are rejected (CSRF gate — SameSite=None cookies would otherwise
117
+ // ride along; non-browser clients send no Origin and are unaffected).
118
+ await router.use(async (req: any, next: () => Promise<Response>) => {
119
+ const path = new URL(req.url).pathname
120
+ if (!path.startsWith('/api/'))
121
+ return next()
122
+ const cors = credentialedCorsHeaders(req)
123
+ const origin = req.headers.get('origin')
124
+ if (req.method === 'OPTIONS' && cors) {
125
+ return new Response(null, {
126
+ status: 204,
127
+ headers: {
128
+ ...cors,
129
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
130
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Analytics-Token, X-Share-Token, X-Share-Password',
131
+ 'Access-Control-Max-Age': '86400',
132
+ },
133
+ })
134
+ }
135
+ if (!cors && origin && allowedCorsOrigins().length > 0
136
+ && req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
137
+ return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
138
+ }
139
+ const res = await next()
140
+ if (!cors)
141
+ return res
142
+ const headers = new Headers(res.headers)
143
+ headers.set('Access-Control-Allow-Origin', cors['Access-Control-Allow-Origin'])
144
+ headers.set('Access-Control-Allow-Credentials', 'true')
145
+ headers.append('Vary', 'Origin')
146
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
147
+ })
148
+
149
+ // Global authorization: gate every /api/sites/{id}/* + /api/p/{id}/* route on
150
+ // project membership (no-op unless ANALYTICS_REQUIRE_AUTH=true). See handlers/authz.
151
+ await router.use(async (req: any, next: () => Promise<Response>) => {
152
+ const denied = await authz.siteAuthGuard(req)
153
+ return denied || next()
154
+ })
155
+
156
+ // Catch-all preflight (#118): bun-router answers unmatched OPTIONS with a
157
+ // built-in wildcard BEFORE middleware runs, which breaks credentialed
158
+ // preflights. An explicit '*' OPTIONS route routes them through normal
159
+ // dispatch: allowlisted /api/* origins get the credentialed grant (via the
160
+ // middleware above), other /api/* origins get a bare 204 (browser fails
161
+ // the preflight), and public paths keep the origin-echo behavior.
162
+ await router.options('*', (req: any) => {
163
+ const path = new URL(req.url).pathname
164
+ if (path.startsWith('/api/'))
165
+ return new Response(null, { status: 204 })
166
+ return preflightResponse(req)
167
+ })
168
+
169
+ // Health check
170
+ await router.get('/health', misc.handleHealth)
171
+
172
+ // Favicon (return empty to prevent 404)
173
+ await router.get('/favicon.ico', () => new Response(null, { status: 204 }))
174
+
175
+ // Auth routes (API only — login page served by stx frontend)
176
+ await router.post('/login', auth.handleLogin)
177
+ await router.post('/logout', auth.handleLogout)
178
+ await router.post('/signup', auth.handleSignupForm)
179
+ await router.post('/forgot', auth.handleForgotForm)
180
+ await router.post('/reset', auth.handleResetForm)
181
+ await router.post('/api/auth/signup', auth.handleSignup)
182
+ await router.post('/api/auth/login', auth.handleAuthLogin)
183
+ await router.get('/api/auth/me', auth.handleMe)
184
+ await router.post('/api/auth/logout', auth.handleApiLogout)
185
+ await router.get('/api/auth/verify', auth.handleVerifyEmail)
186
+ await router.post('/api/auth/verify/resend', auth.handleResendVerification)
187
+ await router.post('/api/auth/forgot', auth.handleForgotPassword)
188
+ await router.post('/api/auth/reset', auth.handleResetPassword)
189
+ await router.post('/api/auth/logout-all', auth.handleLogoutAll)
190
+ await router.put('/api/auth/profile', auth.handleUpdateProfile)
191
+ await router.post('/api/auth/password', auth.handleChangePassword)
192
+ await router.post('/api/auth/email', auth.handleChangeEmail)
193
+ await router.get('/api/auth/oauth/{provider}', (req: any) => oauth.handleOAuthStart(req, req.params.provider))
194
+ await router.get('/api/auth/oauth/{provider}/callback', (req: any) => oauth.handleOAuthCallback(req, req.params.provider))
195
+
196
+ // Scheduled-jobs tick — for an external cron (Lambda schedule / EventBridge).
197
+ // Gated by ANALYTICS_JOBS_SECRET; disabled (403) when no secret is set.
198
+ await router.post('/api/jobs/tick', async (req: any) => {
199
+ const secret = process.env.ANALYTICS_JOBS_SECRET
200
+ if (!secret || req.headers.get('x-jobs-secret') !== secret) {
201
+ return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' } })
202
+ }
203
+ const result = await import('./lib/scheduler').then(s => s.runDueJobs())
204
+ return new Response(JSON.stringify(result), { status: 200, headers: { 'Content-Type': 'application/json' } })
205
+ })
206
+
207
+ // Collection endpoints. Cross-domain installs preflight every beacon (JSON
208
+ // content type; the error SDK also sends X-Analytics-Token), so each public
209
+ // collect endpoint answers OPTIONS (#86).
210
+ await router.post('/collect', collect.handleCollect)
211
+ await router.post('/t', collect.handleCollect)
212
+ for (const path of ['/collect', '/t', '/p']) {
213
+ await router.options(path, (req: any) => preflightResponse(req))
214
+ }
215
+
216
+ // Sites list and creation — account-scoped (#114). With enforcement on
217
+ // (the default), no session → 401; the all-sites fallback only survives in
218
+ // explicit ANALYTICS_REQUIRE_AUTH=false installs.
219
+ await router.get('/api/sites', async (req: any) => {
220
+ const session = await auth.getSessionFromRequest(req)
221
+ if (!session && authz.authRequired()) {
222
+ return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
223
+ }
224
+ return misc.handleGetSites(req, session?.userId)
225
+ })
226
+ await router.post('/api/sites', async (req: any) => {
227
+ const session = await auth.getSessionFromRequest(req)
228
+ if (!session && authz.authRequired()) {
229
+ return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
230
+ }
231
+ return misc.handleCreateSite(req, session?.userId)
232
+ })
233
+ await router.delete('/api/sites/{siteId}', async (req: any) => {
234
+ const session = await auth.getSessionFromRequest(req)
235
+ return misc.handleDeleteSite(req, req.params.siteId, session?.userId)
236
+ })
237
+
238
+ // Share link validation
239
+ await router.get('/api/share/{token}', (req: any) => sharing.handleGetSharedDashboard(req, req.params.token))
240
+
241
+ // Site-specific API routes
242
+ // Stats & Analytics
243
+ await router.get('/api/sites/{siteId}/stats', (req: any) => withReadCache(req, 'stats', 30_000, () => stats.handleGetStats(req, req.params.siteId)))
244
+ await router.get('/api/sites/{siteId}/realtime', (req: any) => stats.handleGetRealtime(req, req.params.siteId))
245
+ await router.get('/api/sites/{siteId}/ingest-counters', (req: any) => misc.handleGetIngestCounters(req, req.params.siteId))
246
+ await router.get('/api/sites/{siteId}/pages', (req: any) => withReadCache(req, 'pages', 60_000, () => stats.handleGetPages(req, req.params.siteId)))
247
+ await router.get('/api/sites/{siteId}/referrers', (req: any) => withReadCache(req, 'referrers', 60_000, () => stats.handleGetReferrers(req, req.params.siteId)))
248
+ await router.get('/api/sites/{siteId}/screen-sizes', (req: any) => withReadCache(req, 'screen-sizes', 60_000, () => stats.handleGetScreenSizes(req, req.params.siteId)))
249
+ await router.get('/api/sites/{siteId}/devices', (req: any) => withReadCache(req, 'devices', 60_000, () => stats.handleGetDevices(req, req.params.siteId)))
250
+ await router.get('/api/sites/{siteId}/browsers', (req: any) => withReadCache(req, 'browsers', 60_000, () => stats.handleGetBrowsers(req, req.params.siteId)))
251
+ await router.get('/api/sites/{siteId}/os', (req: any) => withReadCache(req, 'os', 60_000, () => stats.handleGetOS(req, req.params.siteId)))
252
+ await router.get('/api/sites/{siteId}/countries', (req: any) => withReadCache(req, 'countries', 60_000, () => stats.handleGetCountries(req, req.params.siteId)))
253
+ await router.get('/api/sites/{siteId}/regions', (req: any) => withReadCache(req, 'regions', 60_000, () => stats.handleGetRegions(req, req.params.siteId)))
254
+ await router.get('/api/sites/{siteId}/cities', (req: any) => withReadCache(req, 'cities', 60_000, () => stats.handleGetCities(req, req.params.siteId)))
255
+ await router.get('/api/sites/{siteId}/timeseries', (req: any) => withReadCache(req, 'timeseries', 60_000, () => stats.handleGetTimeSeries(req, req.params.siteId)))
256
+ await router.get('/api/sites/{siteId}/events', (req: any) => withReadCache(req, 'events', 60_000, () => stats.handleGetEvents(req, req.params.siteId)))
257
+ await router.get('/api/sites/{siteId}/event-properties', (req: any) => withReadCache(req, 'event-properties', 60_000, () => stats.handleGetEventProperties(req, req.params.siteId)))
258
+ await router.get('/api/sites/{siteId}/clicks', (req: any) => withReadCache(req, 'clicks', 60_000, () => stats.handleGetClicks(req, req.params.siteId)))
259
+ await router.get('/api/sites/{siteId}/engagement', (req: any) => withReadCache(req, 'engagement', 60_000, () => stats.handleGetEngagement(req, req.params.siteId)))
260
+ await router.get('/api/sites/{siteId}/campaigns', (req: any) => withReadCache(req, 'campaigns', 60_000, () => stats.handleGetCampaigns(req, req.params.siteId)))
261
+ await router.get('/api/sites/{siteId}/comparison', (req: any) => withReadCache(req, 'comparison', 60_000, () => stats.handleGetComparison(req, req.params.siteId)))
262
+
263
+ // Goals
264
+ await router.get('/api/sites/{siteId}/goals/stats', (req: any) => withReadCache(req, 'goals-stats', 60_000, () => goals.handleGetGoalStats(req, req.params.siteId)))
265
+ await router.get('/api/sites/{siteId}/goals', (req: any) => goals.handleGetGoals(req, req.params.siteId))
266
+ await router.post('/api/sites/{siteId}/goals', (req: any) => goals.handleCreateGoal(req, req.params.siteId))
267
+ await router.put('/api/sites/{siteId}/goals/{goalId}', (req: any) => goals.handleUpdateGoal(req, req.params.siteId, req.params.goalId))
268
+ await router.delete('/api/sites/{siteId}/goals/{goalId}', (req: any) => goals.handleDeleteGoal(req, req.params.siteId, req.params.goalId))
269
+
270
+ // Sessions
271
+ await router.get('/api/sites/{siteId}/sessions', (req: any) => sessions.handleGetSessions(req, req.params.siteId))
272
+ await router.get('/api/sites/{siteId}/sessions/{sessionId}', (req: any) => sessions.handleGetSessionDetail(req, req.params.siteId, req.params.sessionId))
273
+ await router.get('/api/sites/{siteId}/flow', (req: any) => withReadCache(req, 'flow', 60_000, () => sessions.handleGetUserFlow(req, req.params.siteId)))
274
+ await router.get('/api/sites/{siteId}/entry-exit', (req: any) => withReadCache(req, 'entry-exit', 60_000, () => sessions.handleGetEntryExitPages(req, req.params.siteId)))
275
+ await router.get('/api/sites/{siteId}/live', (req: any) => sessions.handleGetLiveView(req, req.params.siteId))
276
+
277
+ // Heatmaps
278
+ await router.get('/api/sites/{siteId}/heatmap/clicks', (req: any) => heatmaps.handleGetHeatmapClicks(req, req.params.siteId))
279
+ await router.get('/api/sites/{siteId}/heatmap/scroll', (req: any) => heatmaps.handleGetHeatmapScroll(req, req.params.siteId))
280
+ await router.get('/api/sites/{siteId}/heatmap/pages', (req: any) => heatmaps.handleGetHeatmapPages(req, req.params.siteId))
281
+
282
+ // Performance & Vitals
283
+ await router.get('/api/sites/{siteId}/vitals', (req: any) => performance.handleGetVitals(req, req.params.siteId))
284
+ await router.get('/api/sites/{siteId}/vitals-trends', (req: any) => performance.handleGetVitalsTrends(req, req.params.siteId))
285
+ await router.get('/api/sites/{siteId}/performance-budgets', (req: any) => performance.handleGetPerformanceBudgets(req, req.params.siteId))
286
+ await router.get('/api/sites/{siteId}/performance-budgets/check', (req: any) => performance.handleCheckPerformanceBudgets(req, req.params.siteId))
287
+ await router.post('/api/sites/{siteId}/performance-budgets', (req: any) => performance.handleCreatePerformanceBudget(req, req.params.siteId))
288
+ await router.delete('/api/sites/{siteId}/performance-budgets/{budgetId}', (req: any) => performance.handleDeletePerformanceBudget(req, req.params.siteId, req.params.budgetId))
289
+
290
+ // Funnels
291
+ await router.get('/api/sites/{siteId}/funnels', (req: any) => funnels.handleGetFunnels(req, req.params.siteId))
292
+ await router.get('/api/sites/{siteId}/funnels/{funnelId}', (req: any) => funnels.handleGetFunnelAnalysis(req, req.params.siteId, req.params.funnelId))
293
+ await router.post('/api/sites/{siteId}/funnels', (req: any) => funnels.handleCreateFunnel(req, req.params.siteId))
294
+ await router.delete('/api/sites/{siteId}/funnels/{funnelId}', (req: any) => funnels.handleDeleteFunnel(req, req.params.siteId, req.params.funnelId))
295
+
296
+ // Annotations
297
+ await router.get('/api/sites/{siteId}/annotations', (req: any) => annotations.handleGetAnnotations(req, req.params.siteId))
298
+ await router.post('/api/sites/{siteId}/annotations', (req: any) => annotations.handleCreateAnnotation(req, req.params.siteId))
299
+ await router.delete('/api/sites/{siteId}/annotations/{annotationId}', (req: any) => annotations.handleDeleteAnnotation(req, req.params.siteId, req.params.annotationId))
300
+
301
+ // Experiments
302
+ await router.get('/api/sites/{siteId}/experiments', (req: any) => experiments.handleGetExperiments(req, req.params.siteId))
303
+ await router.post('/api/sites/{siteId}/experiments', (req: any) => experiments.handleCreateExperiment(req, req.params.siteId))
304
+ await router.post('/api/sites/{siteId}/experiments/event', (req: any) => experiments.handleRecordExperimentEvent(req, req.params.siteId))
305
+
306
+ // Alerts
307
+ await router.get('/api/sites/{siteId}/alerts', (req: any) => alerts.handleGetAlerts(req, req.params.siteId))
308
+ await router.post('/api/sites/{siteId}/alerts', (req: any) => alerts.handleCreateAlert(req, req.params.siteId))
309
+ await router.delete('/api/sites/{siteId}/alerts/{alertId}', (req: any) => alerts.handleDeleteAlert(req, req.params.siteId, req.params.alertId))
310
+
311
+ // Email Reports
312
+ await router.get('/api/sites/{siteId}/email-reports', (req: any) => alerts.handleGetEmailReports(req, req.params.siteId))
313
+ await router.post('/api/sites/{siteId}/email-reports', (req: any) => alerts.handleCreateEmailReport(req, req.params.siteId))
314
+ await router.delete('/api/sites/{siteId}/email-reports/{reportId}', (req: any) => alerts.handleDeleteEmailReport(req, req.params.siteId, req.params.reportId))
315
+
316
+ // API Keys
317
+ await router.get('/api/sites/{siteId}/api-keys', (req: any) => apiKeys.handleGetApiKey(req, req.params.siteId))
318
+ await router.post('/api/sites/{siteId}/api-keys/regenerate', (req: any) => apiKeys.handleRegenerateApiKey(req, req.params.siteId))
319
+
320
+ // Uptime Monitoring
321
+ await router.get('/api/sites/{siteId}/uptime', (req: any) => uptime.handleGetUptimeMonitors(req, req.params.siteId))
322
+ await router.get('/api/sites/{siteId}/uptime/{monitorId}/history', (req: any) => uptime.handleGetUptimeHistory(req, req.params.siteId, req.params.monitorId))
323
+ await router.post('/api/sites/{siteId}/uptime', (req: any) => uptime.handleCreateUptimeMonitor(req, req.params.siteId))
324
+ await router.delete('/api/sites/{siteId}/uptime/{monitorId}', (req: any) => uptime.handleDeleteUptimeMonitor(req, req.params.siteId, req.params.monitorId))
325
+
326
+ // Webhooks
327
+ await router.get('/api/sites/{siteId}/webhooks', (req: any) => webhooks.handleGetWebhooks(req, req.params.siteId))
328
+ await router.post('/api/sites/{siteId}/webhooks', (req: any) => webhooks.handleCreateWebhook(req, req.params.siteId))
329
+ await router.delete('/api/sites/{siteId}/webhooks/{webhookId}', (req: any) => webhooks.handleDeleteWebhook(req, req.params.siteId, req.params.webhookId))
330
+
331
+ // Team Management
332
+ await router.get('/api/sites/{siteId}/team', (req: any) => team.handleGetTeamMembers(req, req.params.siteId))
333
+ await router.post('/api/sites/{siteId}/team', (req: any) => team.handleInviteTeamMember(req, req.params.siteId))
334
+ await router.delete('/api/sites/{siteId}/team/{memberId}', (req: any) => team.handleRemoveTeamMember(req, req.params.siteId, req.params.memberId))
335
+
336
+ // Data Export & Retention
337
+ await router.post('/api/sites/{siteId}/import/ga4-api', (req: any) => data.handleGa4ApiImport(req, req.params.siteId))
338
+ await router.post('/api/sites/{siteId}/import/ga', (req: any) => data.handleGaImport(req, req.params.siteId))
339
+ await router.get('/api/sites/{siteId}/export', (req: any) => data.handleExport(req, req.params.siteId))
340
+ await router.get('/api/sites/{siteId}/retention', (req: any) => data.handleGetRetentionSettings(req, req.params.siteId))
341
+ await router.put('/api/sites/{siteId}/retention', (req: any) => data.handleUpdateRetentionSettings(req, req.params.siteId))
342
+
343
+ // GDPR
344
+ await router.get('/api/sites/{siteId}/gdpr/export', (req: any) => data.handleGdprExport(req, req.params.siteId))
345
+ await router.post('/api/sites/{siteId}/gdpr/delete', (req: any) => data.handleGdprDelete(req, req.params.siteId))
346
+
347
+ // Insights
348
+ await router.get('/api/sites/{siteId}/insights', (req: any) => withReadCache(req, 'insights', 120_000, () => data.handleGetInsights(req, req.params.siteId)))
349
+
350
+ // Revenue
351
+ await router.get('/api/sites/{siteId}/revenue', (req: any) => misc.handleGetRevenue(req, req.params.siteId))
352
+ await router.get('/api/sites/{siteId}/verify-install', (req: any) => misc.handleVerifyInstall(req, req.params.siteId))
353
+
354
+ // Share Links
355
+ await router.post('/api/sites/{siteId}/share', (req: any) => sharing.handleCreateShareLink(req, req.params.siteId))
356
+ await router.get('/api/sites/{siteId}/share', (req: any) => sharing.handleListShareLinks(req, req.params.siteId))
357
+ await router.delete('/api/sites/{siteId}/share/{token}', (req: any) => sharing.handleRevokeShareLink(req, req.params.siteId, req.params.token))
358
+
359
+ // ============================================
360
+ // STEALTH ROUTES - Bypass content blockers
361
+ // Uses /api/p/ ("project") with innocuous names
362
+ // ============================================
363
+
364
+ // Collection endpoints (stealth)
365
+ await router.post('/t', collect.handleCollect) // Already defined above, but /t is short
366
+ await router.post('/p', collect.handleCollect) // Even shorter alias
367
+
368
+ // Sites list (stealth) — same account scoping as /api/sites (#114)
369
+ await router.get('/api/projects', async (req: any) => {
370
+ const session = await auth.getSessionFromRequest(req)
371
+ if (!session && authz.authRequired()) {
372
+ return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
373
+ }
374
+ return misc.handleGetSites(req, session?.userId)
375
+ })
376
+ await router.post('/api/projects', async (req: any) => {
377
+ const session = await auth.getSessionFromRequest(req)
378
+ if (!session && authz.authRequired()) {
379
+ return new Response(JSON.stringify({ error: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } })
380
+ }
381
+ return misc.handleCreateSite(req, session?.userId)
382
+ })
383
+
384
+ // Share link validation (stealth)
385
+ await router.get('/api/link/{token}', (req: any) => sharing.handleGetSharedDashboard(req, req.params.token))
386
+
387
+ // Site-specific stealth routes using /api/p/ prefix
388
+ // Stats & Analytics (stealth)
389
+ await router.get('/api/p/{siteId}/summary', (req: any) => withReadCache(req, 'stats', 30_000, () => stats.handleGetStats(req, req.params.siteId)))
390
+ await router.get('/api/p/{siteId}/pulse', (req: any) => stats.handleGetRealtime(req, req.params.siteId))
391
+ await router.get('/api/p/{siteId}/content', (req: any) => stats.handleGetPages(req, req.params.siteId))
392
+ await router.get('/api/p/{siteId}/sources', (req: any) => stats.handleGetReferrers(req, req.params.siteId))
393
+ await router.get('/api/p/{siteId}/displays', (req: any) => withReadCache(req, 'screen-sizes', 60_000, () => stats.handleGetScreenSizes(req, req.params.siteId)))
394
+ await router.get('/api/p/{siteId}/clients', (req: any) => stats.handleGetDevices(req, req.params.siteId))
395
+ await router.get('/api/p/{siteId}/agents', (req: any) => stats.handleGetBrowsers(req, req.params.siteId))
396
+ await router.get('/api/p/{siteId}/platform', (req: any) => stats.handleGetOS(req, req.params.siteId))
397
+ await router.get('/api/p/{siteId}/geo', (req: any) => stats.handleGetCountries(req, req.params.siteId))
398
+ await router.get('/api/p/{siteId}/area', (req: any) => stats.handleGetRegions(req, req.params.siteId))
399
+ await router.get('/api/p/{siteId}/locale', (req: any) => stats.handleGetCities(req, req.params.siteId))
400
+ await router.get('/api/p/{siteId}/series', (req: any) => withReadCache(req, 'timeseries', 60_000, () => stats.handleGetTimeSeries(req, req.params.siteId)))
401
+ await router.get('/api/p/{siteId}/actions', (req: any) => stats.handleGetEvents(req, req.params.siteId))
402
+ await router.get('/api/p/{siteId}/traits', (req: any) => stats.handleGetEventProperties(req, req.params.siteId))
403
+ await router.get('/api/p/{siteId}/links', (req: any) => stats.handleGetClicks(req, req.params.siteId))
404
+ await router.get('/api/p/{siteId}/dwell', (req: any) => stats.handleGetEngagement(req, req.params.siteId))
405
+ await router.get('/api/p/{siteId}/promo', (req: any) => stats.handleGetCampaigns(req, req.params.siteId))
406
+ await router.get('/api/p/{siteId}/diff', (req: any) => stats.handleGetComparison(req, req.params.siteId))
407
+
408
+ // Goals (stealth)
409
+ await router.get('/api/p/{siteId}/targets/data', (req: any) => goals.handleGetGoalStats(req, req.params.siteId))
410
+ await router.get('/api/p/{siteId}/targets', (req: any) => goals.handleGetGoals(req, req.params.siteId))
411
+ await router.post('/api/p/{siteId}/targets', (req: any) => goals.handleCreateGoal(req, req.params.siteId))
412
+ await router.put('/api/p/{siteId}/targets/{goalId}', (req: any) => goals.handleUpdateGoal(req, req.params.siteId, req.params.goalId))
413
+ await router.delete('/api/p/{siteId}/targets/{goalId}', (req: any) => goals.handleDeleteGoal(req, req.params.siteId, req.params.goalId))
414
+
415
+ // Sessions (stealth)
416
+ await router.get('/api/p/{siteId}/visits', (req: any) => sessions.handleGetSessions(req, req.params.siteId))
417
+ await router.get('/api/p/{siteId}/visits/{sessionId}', (req: any) => sessions.handleGetSessionDetail(req, req.params.siteId, req.params.sessionId))
418
+ await router.get('/api/p/{siteId}/journey', (req: any) => sessions.handleGetUserFlow(req, req.params.siteId))
419
+ await router.get('/api/p/{siteId}/endpoints', (req: any) => sessions.handleGetEntryExitPages(req, req.params.siteId))
420
+ await router.get('/api/p/{siteId}/now', (req: any) => sessions.handleGetLiveView(req, req.params.siteId))
421
+
422
+ // Heatmaps (stealth)
423
+ await router.get('/api/p/{siteId}/touch/clicks', (req: any) => heatmaps.handleGetHeatmapClicks(req, req.params.siteId))
424
+ await router.get('/api/p/{siteId}/touch/scroll', (req: any) => heatmaps.handleGetHeatmapScroll(req, req.params.siteId))
425
+ await router.get('/api/p/{siteId}/touch/list', (req: any) => heatmaps.handleGetHeatmapPages(req, req.params.siteId))
426
+
427
+ // Errors (stealth)
428
+
429
+ // Performance & Vitals (stealth)
430
+ await router.get('/api/p/{siteId}/metrics', (req: any) => performance.handleGetVitals(req, req.params.siteId))
431
+ await router.get('/api/p/{siteId}/metrics-trends', (req: any) => performance.handleGetVitalsTrends(req, req.params.siteId))
432
+ await router.get('/api/p/{siteId}/budgets', (req: any) => performance.handleGetPerformanceBudgets(req, req.params.siteId))
433
+ await router.get('/api/p/{siteId}/budgets/check', (req: any) => performance.handleCheckPerformanceBudgets(req, req.params.siteId))
434
+ await router.post('/api/p/{siteId}/budgets', (req: any) => performance.handleCreatePerformanceBudget(req, req.params.siteId))
435
+ await router.delete('/api/p/{siteId}/budgets/{budgetId}', (req: any) => performance.handleDeletePerformanceBudget(req, req.params.siteId, req.params.budgetId))
436
+
437
+ // Funnels (stealth)
438
+ await router.get('/api/p/{siteId}/pipelines', (req: any) => funnels.handleGetFunnels(req, req.params.siteId))
439
+ await router.get('/api/p/{siteId}/pipelines/{funnelId}', (req: any) => funnels.handleGetFunnelAnalysis(req, req.params.siteId, req.params.funnelId))
440
+ await router.post('/api/p/{siteId}/pipelines', (req: any) => funnels.handleCreateFunnel(req, req.params.siteId))
441
+ await router.delete('/api/p/{siteId}/pipelines/{funnelId}', (req: any) => funnels.handleDeleteFunnel(req, req.params.siteId, req.params.funnelId))
442
+
443
+ // Annotations (stealth)
444
+ await router.get('/api/p/{siteId}/notes', (req: any) => annotations.handleGetAnnotations(req, req.params.siteId))
445
+ await router.post('/api/p/{siteId}/notes', (req: any) => annotations.handleCreateAnnotation(req, req.params.siteId))
446
+ await router.delete('/api/p/{siteId}/notes/{annotationId}', (req: any) => annotations.handleDeleteAnnotation(req, req.params.siteId, req.params.annotationId))
447
+
448
+ // Experiments (stealth)
449
+ await router.get('/api/p/{siteId}/tests', (req: any) => experiments.handleGetExperiments(req, req.params.siteId))
450
+ await router.post('/api/p/{siteId}/tests', (req: any) => experiments.handleCreateExperiment(req, req.params.siteId))
451
+ await router.post('/api/p/{siteId}/tests/record', (req: any) => experiments.handleRecordExperimentEvent(req, req.params.siteId))
452
+
453
+ // Alerts (stealth)
454
+ await router.get('/api/p/{siteId}/notifications', (req: any) => alerts.handleGetAlerts(req, req.params.siteId))
455
+ await router.post('/api/p/{siteId}/notifications', (req: any) => alerts.handleCreateAlert(req, req.params.siteId))
456
+ await router.delete('/api/p/{siteId}/notifications/{alertId}', (req: any) => alerts.handleDeleteAlert(req, req.params.siteId, req.params.alertId))
457
+
458
+ // Email Reports (stealth)
459
+ await router.get('/api/p/{siteId}/scheduled', (req: any) => alerts.handleGetEmailReports(req, req.params.siteId))
460
+ await router.post('/api/p/{siteId}/scheduled', (req: any) => alerts.handleCreateEmailReport(req, req.params.siteId))
461
+ await router.delete('/api/p/{siteId}/scheduled/{reportId}', (req: any) => alerts.handleDeleteEmailReport(req, req.params.siteId, req.params.reportId))
462
+
463
+ // API Keys (stealth)
464
+ await router.get('/api/p/{siteId}/tokens', (req: any) => apiKeys.handleGetApiKey(req, req.params.siteId))
465
+ await router.post('/api/p/{siteId}/tokens/regenerate', (req: any) => apiKeys.handleRegenerateApiKey(req, req.params.siteId))
466
+
467
+ // Uptime Monitoring (stealth)
468
+ await router.get('/api/p/{siteId}/monitors', (req: any) => uptime.handleGetUptimeMonitors(req, req.params.siteId))
469
+ await router.get('/api/p/{siteId}/monitors/{monitorId}/history', (req: any) => uptime.handleGetUptimeHistory(req, req.params.siteId, req.params.monitorId))
470
+ await router.post('/api/p/{siteId}/monitors', (req: any) => uptime.handleCreateUptimeMonitor(req, req.params.siteId))
471
+ await router.delete('/api/p/{siteId}/monitors/{monitorId}', (req: any) => uptime.handleDeleteUptimeMonitor(req, req.params.siteId, req.params.monitorId))
472
+
473
+ // Webhooks (stealth)
474
+ await router.get('/api/p/{siteId}/hooks', (req: any) => webhooks.handleGetWebhooks(req, req.params.siteId))
475
+ await router.post('/api/p/{siteId}/hooks', (req: any) => webhooks.handleCreateWebhook(req, req.params.siteId))
476
+ await router.delete('/api/p/{siteId}/hooks/{webhookId}', (req: any) => webhooks.handleDeleteWebhook(req, req.params.siteId, req.params.webhookId))
477
+
478
+ // Team Management (stealth)
479
+ await router.get('/api/p/{siteId}/members', (req: any) => team.handleGetTeamMembers(req, req.params.siteId))
480
+ await router.post('/api/p/{siteId}/members', (req: any) => team.handleInviteTeamMember(req, req.params.siteId))
481
+ await router.delete('/api/p/{siteId}/members/{memberId}', (req: any) => team.handleRemoveTeamMember(req, req.params.siteId, req.params.memberId))
482
+
483
+ // Data Export & Retention (stealth)
484
+ await router.get('/api/p/{siteId}/download', (req: any) => data.handleExport(req, req.params.siteId))
485
+ await router.get('/api/p/{siteId}/storage', (req: any) => data.handleGetRetentionSettings(req, req.params.siteId))
486
+ await router.put('/api/p/{siteId}/storage', (req: any) => data.handleUpdateRetentionSettings(req, req.params.siteId))
487
+
488
+ // GDPR (stealth)
489
+ await router.get('/api/p/{siteId}/privacy/download', (req: any) => data.handleGdprExport(req, req.params.siteId))
490
+ await router.post('/api/p/{siteId}/privacy/remove', (req: any) => data.handleGdprDelete(req, req.params.siteId))
491
+
492
+ // Insights (stealth)
493
+ await router.get('/api/p/{siteId}/intel', (req: any) => data.handleGetInsights(req, req.params.siteId))
494
+
495
+ // Revenue (stealth)
496
+ await router.get('/api/p/{siteId}/income', (req: any) => misc.handleGetRevenue(req, req.params.siteId))
497
+
498
+ // Share Links (stealth)
499
+ await router.post('/api/p/{siteId}/link', (req: any) => sharing.handleCreateShareLink(req, req.params.siteId))
500
+ await router.get('/api/p/{siteId}/link', (req: any) => sharing.handleListShareLinks(req, req.params.siteId))
501
+ await router.delete('/api/p/{siteId}/link/{token}', (req: any) => sharing.handleRevokeShareLink(req, req.params.siteId, req.params.token))
502
+
503
+ // Tracking script (generates embeddable JS for site owners)
504
+ await router.get('/api/sites/{siteId}/script', (req: any) => {
505
+ return import('./handlers/views').then(v => v.handleScript(req))
506
+ })
507
+ // Raw-JS variant for the `<script src=".../script.js">` one-liner install.
508
+ await router.get('/api/sites/{siteId}/script.js', (req: any) => {
509
+ return import('./handlers/views').then(v => v.handleScript(req))
510
+ })
511
+ // Shared analytics tracker (Fathom-style): one cacheable file for all sites;
512
+ // the site comes from the tag's data-site attribute, the endpoint from origin.
513
+ await router.get('/script.js', (req: any) => {
514
+ return import('./handlers/views').then(v => v.handleSharedScript(req))
515
+ })
516
+
517
+ return router
518
+ }
519
+
520
+ // Export a singleton router instance (using top-level await)
521
+ export const router: Router = await createRouter()