@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,137 @@
1
+ /**
2
+ * Authorization for per-project API routes.
3
+ *
4
+ * Closes the open-data hole: every `/api/sites/{siteId}/*` and stealth
5
+ * `/api/p/{siteId}/*` endpoint must belong to the authenticated user. Applied
6
+ * as a single global middleware (see router.use) so no handler is missed.
7
+ *
8
+ * Account-first by default (#114): enforcement is ON unless
9
+ * ANALYTICS_REQUIRE_AUTH=false (kiosk/legacy single-tenant installs).
10
+ * The public tracker script (`/script`, `/script.js`) is always exempt — site
11
+ * visitors load it without a session.
12
+ */
13
+ import { getSessionFromRequest } from './auth'
14
+ import { handleValidateApiKey } from './api-keys'
15
+ import { getMembership, addMembership, roleAtLeast, type Role } from '../lib/membership'
16
+ import { dynamodb, TABLE_NAME, unmarshall } from '../lib/dynamodb'
17
+ import { jsonResponse } from '../utils/response'
18
+
19
+ const SITE_PATH = /^\/api\/(?:sites|p)\/([^/]+)\//
20
+ const PUBLIC_SUFFIX = /\/(?:script|script\.js)$/
21
+
22
+ /**
23
+ * Endpoints a share token may read (#152): aggregate reports only, canonical
24
+ * AND stealth names. Deliberately excludes sessions/flow (visitor-level),
25
+ * goals, and every management surface (api-keys/team/webhooks/...).
26
+ */
27
+ const SHARE_READ_PATH = new RegExp(`/(?:${[
28
+ // canonical
29
+ 'stats', 'timeseries', 'realtime', 'pages', 'referrers', 'devices',
30
+ 'browsers', 'os', 'countries', 'regions', 'cities', 'campaigns', 'events',
31
+ 'entry-exit', 'comparison', 'insights',
32
+ // stealth aliases (resources/stores/dashboard.ts STEALTH_MAP)
33
+ 'summary', 'series', 'pulse', 'content', 'sources', 'clients', 'agents',
34
+ 'platform', 'geo', 'area', 'locale', 'promo', 'actions', 'endpoints',
35
+ 'diff', 'intel',
36
+ ].join('|')})(?:\\?|$)`)
37
+
38
+ /** Whether the SITES record for `siteId` names `userId` as its owner. */
39
+ async function ownsSite(userId: string, siteId: string): Promise<boolean> {
40
+ try {
41
+ const res = await dynamodb.getItem({
42
+ TableName: TABLE_NAME,
43
+ Key: { pk: { S: 'SITES' }, sk: { S: `SITE#${siteId}` } },
44
+ }) as { Item?: Record<string, any> }
45
+ return !!res.Item && unmarshall(res.Item).ownerId === userId
46
+ }
47
+ catch {
48
+ return false
49
+ }
50
+ }
51
+
52
+ /**
53
+ * The project id a path is scoped to, or null if the path is not a guarded
54
+ * per-project route (or is a public exemption). Pure — unit tested.
55
+ */
56
+ export function guardedSiteId(path: string): string | null {
57
+ if (PUBLIC_SUFFIX.test(path)) return null
58
+ const m = path.match(SITE_PATH)
59
+ return m ? m[1] : null
60
+ }
61
+
62
+ /** Whether enforcement is active (default ON; opt out with ANALYTICS_REQUIRE_AUTH=false). */
63
+ export function authRequired(): boolean {
64
+ return process.env.ANALYTICS_REQUIRE_AUTH !== 'false'
65
+ }
66
+
67
+ /**
68
+ * Minimum role for a request: reads need viewer, member management needs admin,
69
+ * other writes need editor. Pure — unit tested.
70
+ */
71
+ export function requiredRole(method: string, path: string): Role {
72
+ if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') return 'viewer'
73
+ // Member management AND credential rotation are admin actions (#129) —
74
+ // an editor must not be able to rotate the site's API keys.
75
+ if (/\/(?:team|members|api-keys|tokens)(?:\/|$)/.test(path)) return 'admin'
76
+ return 'editor'
77
+ }
78
+
79
+ /**
80
+ * Returns a 401/403 Response to block the request, or null to allow it.
81
+ */
82
+ export async function siteAuthGuard(req: Request): Promise<Response | null> {
83
+ if (!authRequired()) return null
84
+
85
+ const path = new URL(req.url).pathname
86
+ const siteId = guardedSiteId(path)
87
+ if (!siteId) return null
88
+
89
+ // Programmatic read API (#154): a valid `read` API key scoped to this site
90
+ // authorizes read-only access without a session. Only for read methods — a
91
+ // read key can never mutate; writes still require a session with editor/admin.
92
+ const method = (req as { method?: string }).method || 'GET'
93
+ const presentsKey = !!(req.headers.get('X-Analytics-Token') || req.headers.get('Authorization'))
94
+ if (presentsKey && (method === 'GET' || method === 'HEAD' || method === 'OPTIONS')) {
95
+ const key = await handleValidateApiKey(req, 'read')
96
+ if (key.valid && key.siteId === siteId)
97
+ return null
98
+ }
99
+
100
+ // Shared-dashboard token (#152): grants anonymous READ access to the
101
+ // aggregate report endpoints only — never sessions, settings, keys, or any
102
+ // write. Token via header (the share page) or ?share= (simple embeds);
103
+ // password-protected links also require the matching ?share_pw=.
104
+ if (method === 'GET' || method === 'HEAD') {
105
+ const url = new URL(req.url)
106
+ const shareToken = req.headers.get('X-Share-Token') || url.searchParams.get('share') || ''
107
+ if (shareToken && SHARE_READ_PATH.test(path)) {
108
+ const { resolveShareToken } = await import('./sharing')
109
+ const link = await resolveShareToken(shareToken)
110
+ if (link && link.siteId === siteId) {
111
+ const pw = req.headers.get('X-Share-Password') || url.searchParams.get('share_pw') || ''
112
+ if (!link.password || link.password === pw)
113
+ return null
114
+ }
115
+ }
116
+ }
117
+
118
+ const session = await getSessionFromRequest(req)
119
+ if (!session) return jsonResponse({ error: 'Authentication required' }, 401)
120
+
121
+ // Fall back to the site record's ownerId when no membership row exists, so
122
+ // sites that predate the membership model (legacy installs, tracker
123
+ // auto-created sites later claimed, ADMIN_EMAIL backfill) remain openable by
124
+ // their owner (#114 regression). Self-healing: backfill the membership.
125
+ let role = await getMembership(session.userId, siteId)
126
+ if (!role && await ownsSite(session.userId, siteId)) {
127
+ await addMembership(session.userId, siteId, 'owner').catch(() => {})
128
+ role = 'owner'
129
+ }
130
+ if (!role) return jsonResponse({ error: 'You do not have access to this project' }, 403)
131
+
132
+ if (!roleAtLeast(role, requiredRole((req as { method?: string }).method || 'GET', path))) {
133
+ return jsonResponse({ error: 'Insufficient permissions for this action' }, 403)
134
+ }
135
+
136
+ return null
137
+ }