@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.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- 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
|
+
}
|