@uptimizr/collector-server 1.1.1 → 2.0.1

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 (61) hide show
  1. package/AGENTS.md +193 -0
  2. package/README.md +112 -8
  3. package/dist/__tests__/support/registryRequests.d.ts +47 -0
  4. package/dist/__tests__/support/registryRequests.d.ts.map +1 -0
  5. package/dist/__tests__/support/registryRequests.js +93 -0
  6. package/dist/__tests__/support/registryRequests.js.map +1 -0
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +34 -3
  9. package/dist/app.js.map +1 -1
  10. package/dist/audit.d.ts +22 -0
  11. package/dist/audit.d.ts.map +1 -0
  12. package/dist/audit.js +96 -0
  13. package/dist/audit.js.map +1 -0
  14. package/dist/auth.d.ts +64 -0
  15. package/dist/auth.d.ts.map +1 -0
  16. package/dist/auth.js +71 -0
  17. package/dist/auth.js.map +1 -0
  18. package/dist/cli.js +279 -0
  19. package/dist/cli.js.map +1 -1
  20. package/dist/cliStore.d.ts +20 -2
  21. package/dist/cliStore.d.ts.map +1 -1
  22. package/dist/cliStore.js +20 -8
  23. package/dist/cliStore.js.map +1 -1
  24. package/dist/clickhouseStore.d.ts.map +1 -1
  25. package/dist/clickhouseStore.js +7 -1
  26. package/dist/clickhouseStore.js.map +1 -1
  27. package/dist/config.d.ts +13 -0
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +2 -0
  30. package/dist/config.js.map +1 -1
  31. package/dist/duckdbStore.d.ts.map +1 -1
  32. package/dist/duckdbStore.js +7 -1
  33. package/dist/duckdbStore.js.map +1 -1
  34. package/dist/liveToken.d.ts +14 -5
  35. package/dist/liveToken.d.ts.map +1 -1
  36. package/dist/liveToken.js +32 -6
  37. package/dist/liveToken.js.map +1 -1
  38. package/dist/memoryStore.d.ts +8 -3
  39. package/dist/memoryStore.d.ts.map +1 -1
  40. package/dist/memoryStore.js +57 -2
  41. package/dist/memoryStore.js.map +1 -1
  42. package/dist/mssqlStore.d.ts.map +1 -1
  43. package/dist/mssqlStore.js +7 -1
  44. package/dist/mssqlStore.js.map +1 -1
  45. package/dist/postgresStore.d.ts.map +1 -1
  46. package/dist/postgresStore.js +7 -1
  47. package/dist/postgresStore.js.map +1 -1
  48. package/dist/routes/live.d.ts.map +1 -1
  49. package/dist/routes/live.js +31 -26
  50. package/dist/routes/live.js.map +1 -1
  51. package/dist/routes/meta.d.ts +67 -0
  52. package/dist/routes/meta.d.ts.map +1 -0
  53. package/dist/routes/meta.js +436 -0
  54. package/dist/routes/meta.js.map +1 -0
  55. package/dist/routes/query.d.ts.map +1 -1
  56. package/dist/routes/query.js +805 -108
  57. package/dist/routes/query.js.map +1 -1
  58. package/dist/store.d.ts +32 -5
  59. package/dist/store.d.ts.map +1 -1
  60. package/llms.txt +38 -0
  61. package/package.json +11 -7
package/AGENTS.md ADDED
@@ -0,0 +1,193 @@
1
+ # AGENTS.md — @uptimizr/collector-server
2
+
3
+ > Packaged agent guide. For the human reference see [README.md](./README.md); for design
4
+ > rationale see the project ADRs at https://github.com/RaananW/Uptimizr/tree/main/docs/adr.
5
+
6
+ ## What this package is
7
+
8
+ The public-facing **ingestion + query API** (Fastify) for the OSS data collector, plus the
9
+ `uptimizr` CLI that self-hosts it. All client input is untrusted and validated against
10
+ `@uptimizr/schema` at the boundary. Route handlers stay thin; storage lives behind a swappable
11
+ `CollectorStore` (ADR 0005).
12
+
13
+ It is the **single gateway** to the data: the dashboard, `@uptimizr/mcp`, the in-browser assistant
14
+ and your own agents all read through this HTTP API, never the database.
15
+
16
+ ## Run it
17
+
18
+ ```bash
19
+ # 1. One-time setup: generate a visitor-hash secret, create + migrate the store,
20
+ # mint a first project + API key, write a local .env.
21
+ npx -p @uptimizr/collector-server uptimizr init "My Project"
22
+
23
+ # 2. Start the ingestion + query API (reads the generated .env; 0.0.0.0:4318).
24
+ npx -p @uptimizr/collector-server uptimizr serve
25
+ ```
26
+
27
+ `init` prints a **`projectId`** (public — give it to your client SDK along with this server's URL)
28
+ and a one-time **API key** (secret — `x-api-key` for the query routes).
29
+
30
+ ### CLI (ADR 0029)
31
+
32
+ | Command | What it does |
33
+ | ------------------------------ | -------------------------------------------------------------------- |
34
+ | `uptimizr init [name]` | Secret + store + migrations + first project/key + `.env`. |
35
+ | `uptimizr serve` | Run the ingestion + query API. The default when no command is given. |
36
+ | `uptimizr new-project <name>` | Mint an additional project + API key. |
37
+ | `uptimizr new-key <projectId>` | Mint an additional key on an existing project (see the flags below). |
38
+ | `uptimizr migrate` | Apply store migrations. |
39
+ | `uptimizr regions set <scene>` | Replace a scene's named regions from `--file <regions.json>`. |
40
+ | `uptimizr regions get <scene>` | Print a scene's named regions as JSON. |
41
+ | `uptimizr help` | Usage. |
42
+
43
+ `new-key` flags: `--capabilities <list>` (comma-separated; default `query`), `--label <name>`,
44
+ and the paired `--rate-limit-max <n>` / `--rate-limit-window-ms <ms>`:
45
+
46
+ ```bash
47
+ uptimizr new-key <projectId> --capabilities query,annotate \
48
+ --label "weekly-report-agent" --rate-limit-max 120 --rate-limit-window-ms 60000
49
+ ```
50
+
51
+ `regions` commands take `--project <projectId>` (or `UPTIMIZR_PROJECT_ID`). They talk to the store
52
+ **directly**, so they need no API key — an operator command, unlike the HTTP equivalent.
53
+
54
+ **Every command targets the store selected by `COLLECTOR_STORE`**, read through the same
55
+ connection variables `serve` uses — so export the store + its settings before `init` and the
56
+ project you mint is the one the running collector resolves.
57
+
58
+ Installed as a dependency, the package exposes the `uptimizr` CLI plus the legacy
59
+ `uptimizr-collector` bin (equivalent to `uptimizr serve`).
60
+
61
+ ## API keys and capabilities (ADR 0051 §7)
62
+
63
+ A key carries a **set of capabilities**, not a single role. Keys default to `query`.
64
+
65
+ | Capability | Unlocks |
66
+ | ----------- | --------------------------------------------------------------------------------------------------------- |
67
+ | `query` | The aggregate analytics API, the scene registry, the live token exchange, and `GET /api/v1/audit`. |
68
+ | `query:raw` | Raw per-session streams: `GET /api/v1/sessions/:id/events` and `GET /api/v1/live/sessions/:id`. |
69
+ | `annotate` | The project **metadata** write path (annotations, glossary, saved analyses, panel specs). Never events. |
70
+ | `ingest` | Reserved for server-side write paths. Public ingestion is keyless, so issued keys are normally read keys. |
71
+
72
+ The raw endpoints are gated **twice**: the collector must run with `ENABLE_RAW_SESSION_RETENTION`
73
+ **and** the key must hold `query:raw` — otherwise `403`. Retention alone is not enough.
74
+
75
+ `--rate-limit-max` / `--rate-limit-window-ms` give a key its own request budget, bucketed on the
76
+ **key id** rather than the client IP; keys without one fall back to `COLLECTOR_RATE_LIMIT_*`.
77
+ Ingestion keeps its separate `COLLECTOR_INGEST_RATE_LIMIT_*` budget.
78
+
79
+ ## Endpoints an agent should know
80
+
81
+ - **`GET /api/v1/openapi.json`** — **unauthenticated** OpenAPI 3.1 for the whole read API,
82
+ generated from the `@uptimizr/metrics` registry (ADR 0051) and this server's route table. One
83
+ path per endpoint, the real validating schema per parameter, a response schema per metric, and
84
+ the semantics OpenAPI cannot express as `x-uptimizr-*` extensions (result `grain`, per-column
85
+ `units`, `caveats`, `interpretation`, source capture channels, row `limits`). **Start here**
86
+ rather than guessing routes; generate a typed client with
87
+ `npx openapi-typescript <collector>/api/v1/openapi.json -o collector.d.ts`.
88
+ - **`GET /api/v1/whoami`** — the calling key's `projectId`, `keyId`, `capabilities`, `label` and
89
+ effective `rateLimit` (plus `rateLimitSource`: `"key"` or `"default"`). It reports the key's
90
+ **id**, never the key. Call it first and register only the tools your capabilities permit,
91
+ instead of probing for `403`s.
92
+ - **`GET /api/v1/audit`** (`since` / `until` / `limit`, any `query` key) — the agent audit log.
93
+ - `POST /api/v1/collect` — batched ingestion. **Keyless by design** (it runs in untrusted
94
+ browsers, where a key is not a secret); protected by schema validation, payload bounds and rate
95
+ limits instead. The server sets the cookieless `visitorId = hash(ip + ua + dailySalt)`; the raw
96
+ IP is never stored (ADR 0003).
97
+ - The read API: sessions, heatmaps (`pointer`, `camera`, `position`, `world`, `gaze`, `mesh-uv`,
98
+ `click-rays`, `flow`, `perf`, `errors`), mesh/interaction insights, performance and diagnostics,
99
+ scene/path/funnel analytics, scene representations and regions, and the live SSE endpoints.
100
+ - `GET /health` — liveness probe, unauthenticated.
101
+
102
+ ### Result envelopes: `format=full | table | summary` (ADR 0051 §2)
103
+
104
+ Every aggregate endpoint accepts `format`. It **filters nothing** — it picks the result envelope:
105
+
106
+ - `full` (default) — the bare rows, unchanged. What the dashboard uses.
107
+ - `table` — adds a `meta` envelope: metric, range, applied filters, sample size, row count,
108
+ `truncated`, limits.
109
+ - `summary` — a bounded digest: ranked top rows, a first/last/min/max/trend series, or merged
110
+ spatial clusters, with shares, the metric's caveats and a templated `reading` sentence, capped at
111
+ the registry's `maxSummaryRows`. **This is what makes a 500-bin heatmap affordable for an LLM** —
112
+ prefer it over `full` when feeding a model.
113
+
114
+ ### Filters
115
+
116
+ Common query params: `since`, `until` (epoch ms), `bins`, `limit`, `scene`, `session`,
117
+ `cameraMode`, `source`, and spatial `cellSize` / **`region`** where supported. `region=<id>` drills
118
+ a spatial query into one named place from the scene registry — declare regions with
119
+ `uptimizr regions set`, `PUT /api/v1/scenes/:sceneId/regions` (an `annotate` key), or
120
+ `registerRegions` in `@uptimizr/sdk-core`.
121
+
122
+ ### Live endpoints
123
+
124
+ `POST /api/v1/live/token` exchanges a query key for a short-lived SSE token; the key's capability
125
+ set is carried **inside the signed token**, so `GET /api/v1/live/sessions/:id` can enforce
126
+ `query:raw` even though `EventSource` cannot send headers. `GET /api/v1/live/presence` and
127
+ `/live/stream` use the same `?token=...`.
128
+
129
+ ## Storage (`COLLECTOR_STORE`)
130
+
131
+ | Value | Store |
132
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
133
+ | `duckdb` **(default)** | Single-file OSS store: events **and** metadata in one file at `DUCKDB_PATH` (default `./data/uptimizr.duckdb`). No external service. **Single-writer** — one collector process per file; back up by copying the file. |
134
+ | `memory` | Dependency-free in-memory store for local dev / E2E only (`COLLECTOR_MEMORY_PROJECT_ID` / `COLLECTOR_MEMORY_API_KEY`). |
135
+ | `postgres` | `@uptimizr/db-postgres` — `POSTGRES_URL` / `DATABASE_URL`, `POSTGRES_SCHEMA`, `POSTGRES_POOL_MAX`. Multi-writer. |
136
+ | `mssql` | `@uptimizr/db-mssql` — `MSSQL_URL` (or `MSSQL_SERVER` / `MSSQL_PORT` / `MSSQL_DATABASE` / `MSSQL_USER` / `MSSQL_PASSWORD`). SQL Server 2022+ / Azure SQL. Multi-writer. |
137
+ | `clickhouse` | `@uptimizr/db-clickhouse` — `CLICKHOUSE_URL` / `CLICKHOUSE_DATABASE` / `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD`. Concurrent writers, high-volume ingestion. |
138
+
139
+ All four return **identical analytics** (the cross-engine parity suite). Aggregations are computed
140
+ at **query time** in v1 — no materialized views.
141
+
142
+ ## Other configuration
143
+
144
+ - Server / browser access: `COLLECTOR_HOST` (`0.0.0.0`), `COLLECTOR_PORT` (`4318`),
145
+ `COLLECTOR_CORS_ORIGINS`, `COLLECTOR_TRUST_PROXY`, `COLLECTOR_BODY_LIMIT`.
146
+ - Privacy / replay / live: **`VISITOR_HASH_SECRET` (required — the server fails fast without it)**,
147
+ `ENABLE_RAW_SESSION_RETENTION`, `LIVE_TOKEN_SECRET`, `LIVE_TOKEN_TTL_MS`, `LIVE_WINDOW_MS`,
148
+ `LIVE_MAX_CONNECTIONS`, `LIVE_PRESENCE_INTERVAL_MS`.
149
+ - Rate limits: `COLLECTOR_RATE_LIMIT_MAX`, `COLLECTOR_RATE_LIMIT_WINDOW_MS`,
150
+ `COLLECTOR_INGEST_RATE_LIMIT_MAX`, `COLLECTOR_INGEST_RATE_LIMIT_WINDOW_MS`.
151
+ - Agent audit: **`AUDIT_RETENTION_DAYS`** (default `30`; `0` = keep forever),
152
+ `AUDIT_DASHBOARD_REQUESTS` (default off — requests carrying `x-uptimizr-client: dashboard` are
153
+ skipped as a volume filter, **not** a security boundary).
154
+ - All-in-one dashboard: `COLLECTOR_DASHBOARD_DIR` (point it at a static dashboard export and one
155
+ process serves ingestion, queries and the UI), `COLLECTOR_CSP` (`strict` or `off`).
156
+
157
+ ## Rules for agents
158
+
159
+ - **Validate at the boundary.** Every request body and querystring is parsed against
160
+ `@uptimizr/schema` / the metric registry. Never trust client input, never skip validation.
161
+ - **Keep handlers thin** (ADR 0005). Aggregation logic belongs in `@uptimizr/db`; the semantics of
162
+ a metric belong in `@uptimizr/metrics`. A new read endpoint is a **registry entry**, not a
163
+ hand-written route — `registryRoutes.test.ts` asserts the endpoint exists and that its
164
+ querystring keys equal the metric's `filters`, and `queryResponseSchemas.test.ts` asserts every
165
+ endpoint's rows parse against the metric's `row` schema on a seeded store, an empty one, and the
166
+ in-memory store.
167
+ - **Never weaken the raw-data gate.** `ENABLE_RAW_SESSION_RETENTION` **and** `query:raw`, both,
168
+ for `/sessions/:id/events` and `/live/sessions/:id` (ADR 0003).
169
+ - **Never log secrets or raw IPs.** The audit log's subject is the key **id**; `params` drops
170
+ credential-shaped fields and is capped at 512 bytes. Audit writes happen after the response is
171
+ flushed, so they can never block or fail a request. Refusals are recorded too.
172
+ - **Ingestion stays keyless.** Do not "fix" it by requiring a key — a key shipped to an untrusted
173
+ browser is not a secret. Harden with validation, payload bounds, rate limits and CORS instead.
174
+ - Events live once in `@uptimizr/schema`. Do not redefine an event shape here.
175
+ - Aggregations are query-time in v1; do not add materialized views without an ADR.
176
+
177
+ ## Develop
178
+
179
+ ```bash
180
+ pnpm --filter @uptimizr/collector-server dev # tsx watch
181
+ pnpm --filter @uptimizr/collector-server test # vitest (app.inject() + a fake store)
182
+ pnpm --filter @uptimizr/collector-server build
183
+ ```
184
+
185
+ Tests run against a fake `CollectorStore` with `app.inject()` — no live database required.
186
+
187
+ ## More
188
+
189
+ - Package reference: [README.md](./README.md)
190
+ - HTTP API reference:
191
+ https://github.com/RaananW/Uptimizr/blob/main/docs/integration.md#4-http-api
192
+ - Query API guide: https://uptimizr.com/docs/api/query/
193
+ - Deploy guide: https://uptimizr.com/docs/deploy/collector/
package/README.md CHANGED
@@ -25,7 +25,10 @@ npx -p @uptimizr/collector-server uptimizr serve
25
25
  and this server's URL (the **`endpoint`**) to your client SDK (e.g.
26
26
  `@uptimizr/babylon`); use the **API key** (`x-api-key`) for the query routes /
27
27
  dashboard. Mint more projects later with
28
- `npx -p @uptimizr/collector-server uptimizr new-project "<name>"`.
28
+ `npx -p @uptimizr/collector-server uptimizr new-project "<name>"`, or add a key
29
+ to an existing project with
30
+ `npx -p @uptimizr/collector-server uptimizr new-key <projectId> [--capabilities …] [--label …]`
31
+ — see [API keys and capabilities](#api-keys-and-capabilities).
29
32
 
30
33
  `init`, `new-project` and `migrate` target the store selected by
31
34
  `COLLECTOR_STORE`, read through the same connection variables `serve` uses — so
@@ -41,6 +44,34 @@ npx -p @uptimizr/collector-server uptimizr init "My Project" # schema + first
41
44
  npx -p @uptimizr/collector-server uptimizr serve
42
45
  ```
43
46
 
47
+ ### Naming places in a scene: `uptimizr regions`
48
+
49
+ A scene can carry **regions** — named, labelled world-space boxes ("the
50
+ entrance", "the checkout counter") that give spatial results a vocabulary and let
51
+ any spatial query be drilled into a place with `?region=<id>`. Declare them from
52
+ a JSON file straight against the store, without a running collector:
53
+
54
+ ```bash
55
+ cat > regions.json <<'JSON'
56
+ [
57
+ { "id": "entrance", "label": "Entrance", "bounds": [-5, 0, -5, 5, 3, 0] },
58
+ { "id": "counter", "label": "Checkout counter", "bounds": [-1, 0, 1, 1, 2, 3] }
59
+ ]
60
+ JSON
61
+
62
+ npx -p @uptimizr/collector-server uptimizr regions set lobby --file regions.json --project "$PROJECT_ID"
63
+ npx -p @uptimizr/collector-server uptimizr regions get lobby --project "$PROJECT_ID"
64
+ ```
65
+
66
+ The file is either a bare array or the `{ "regions": [...] }` envelope the HTTP
67
+ endpoint takes, so one file works with both. `--project` may be replaced by
68
+ `UPTIMIZR_PROJECT_ID`. The write **replaces** the scene's whole set, so leaving a
69
+ region out removes it and `[]` clears them. The CLI talks to the store directly,
70
+ so it needs no API key. Over HTTP the same thing is
71
+ `PUT /api/v1/scenes/:sceneId/regions` (see the integration guide), which takes
72
+ an `annotate`-capable key, and from a client build `registerRegions` in
73
+ `@uptimizr/sdk-core`.
74
+
44
75
  ### All-in-one: serve the dashboard too
45
76
 
46
77
  The collector can also serve a pre-built static dashboard from its own origin, so
@@ -92,6 +123,18 @@ file; back up by copying the file).
92
123
 
93
124
  ## Endpoints
94
125
 
126
+ ### Self-description
127
+
128
+ - `GET /api/v1/openapi.json` — an **OpenAPI 3.1** document for the whole read API,
129
+ generated from the semantic metric registry (ADR 0051) and this server's own
130
+ route table: one path per endpoint, every parameter carrying the schema that
131
+ actually validates it, and a response schema per metric. The semantics OpenAPI
132
+ cannot express ride along as `x-uptimizr-*` extensions — the result `grain`,
133
+ per-column `units`, `caveats`, `interpretation`, the capture channels that feed
134
+ the metric, and its row `limits`. **Unauthenticated**: it is documentation and
135
+ contains no project data. Generate a typed client with
136
+ `npx openapi-typescript <collector>/api/v1/openapi.json -o collector.d.ts`.
137
+
95
138
  ### Ingestion
96
139
 
97
140
  - `POST /api/v1/collect` — accepts a batched `collectRequest`. Validates → rejects
@@ -127,20 +170,38 @@ the project the API key resolves to.
127
170
  `/xr/sources`, `/xr/abandonment`, `/xr/locomotion`.
128
171
  - Scene representations: `PUT /api/v1/scenes/:sceneId/representation`,
129
172
  `GET /api/v1/scenes/:sceneId/representation`.
130
- - `GET /api/v1/sessions/:id/events` — ordered replay timeline, **gated by**
131
- `ENABLE_RAW_SESSION_RETENTION` (returns `403` when disabled); supports buffered
132
- JSON or NDJSON streaming (`Accept: application/x-ndjson` / `?format=ndjson`).
173
+ - `GET /api/v1/sessions/:id/events` — ordered replay timeline. Raw per-session
174
+ data, so it is **gated twice**: `ENABLE_RAW_SESSION_RETENTION` must be on
175
+ **and** the key must hold `query:raw` (`403` otherwise). Supports buffered JSON
176
+ or NDJSON streaming (`Accept: application/x-ndjson` / `?format=ndjson`).
177
+ - Key identity + audit: `GET /api/v1/whoami` (the calling key's project, key id,
178
+ capabilities, label and effective rate limit) and `GET /api/v1/audit`
179
+ (`since`/`until`/`limit`) — see
180
+ [API keys and capabilities](#api-keys-and-capabilities).
133
181
 
134
182
  Live endpoints:
135
183
 
136
- - `POST /api/v1/live/token` — exchange a query API key for a short-lived live token.
184
+ - `POST /api/v1/live/token` — exchange a query API key for a short-lived live
185
+ token. The key's capability set is carried inside the signed token, so the
186
+ per-session follow can enforce `query:raw` without a header `EventSource`
187
+ cannot send.
137
188
  - `GET /api/v1/live/presence`, `/live/stream`, `/live/sessions/:id` — SSE streams
138
- authenticated with `?token=...`; per-session live follow is also gated by raw
139
- retention.
189
+ authenticated with `?token=...`; the per-session follow is gated by raw
190
+ retention **and** `query:raw`, exactly like the replay timeline.
140
191
 
141
192
  Common query params include `since`, `until` (epoch ms), `bins`, `limit`, `scene`,
142
193
  `session`, `cameraMode`, `source`, and spatial `cellSize` / `region` where supported.
143
194
 
195
+ Every aggregate endpoint also accepts `format=full | table | summary` (ADR 0051 §2).
196
+ It filters nothing — it picks the result envelope. `full` is the default and returns
197
+ the bare rows unchanged (what the dashboard uses); `table` adds a `meta` envelope
198
+ (metric, range, applied filters, sample size, row count, `truncated`, limits); and
199
+ `summary` returns a bounded digest — ranked top rows, a first/last/min/max/trend
200
+ series, or merged spatial clusters, with shares, the metric's caveats and a
201
+ templated `reading` sentence — capped at the registry's `maxSummaryRows`, which is
202
+ what makes a 500-bin heatmap affordable for an LLM. See
203
+ [Result formats](https://uptimizr.com/docs/api/query/#result-formats).
204
+
144
205
  - `GET /health` — liveness probe.
145
206
 
146
207
  ## Security
@@ -158,6 +219,46 @@ if `VISITOR_HASH_SECRET` is missing.
158
219
  | `POST /api/v1/live/token` | `x-api-key` | Exchanges a project query key for a short-lived SSE token. |
159
220
  | Live SSE routes (`/api/v1/live/*` `GET`s) | `?token=...` | Browser `EventSource` cannot attach custom headers, so live streams use short-lived bearer tokens. |
160
221
  | `GET /health` | None | Liveness probe. |
222
+ | `GET /api/v1/openapi.json` | None | API documentation, not data — a client needs it before it has a key. Rate-limited like every other route. |
223
+
224
+ ### API keys and capabilities
225
+
226
+ A key carries a **set of capabilities** (ADR 0051 §7), not a single role:
227
+
228
+ | Capability | Grants |
229
+ | ----------- | --------------------------------------------------------------------------------------------------------- |
230
+ | `query` | The aggregate analytics API, the scene registry, the live token exchange and `GET /api/v1/audit`. |
231
+ | `query:raw` | Raw per-session streams: `GET /api/v1/sessions/:id/events` and `GET /api/v1/live/sessions/:id`. |
232
+ | `annotate` | The project **metadata** write path (annotations, glossary, saved analyses, panel specs). Never events. |
233
+ | `ingest` | Reserved for server-side write paths. Public ingestion is keyless, so issued keys are normally read keys. |
234
+
235
+ Keys default to `query`, including those from `uptimizr init` / `uptimizr new-project`:
236
+
237
+ ```bash
238
+ uptimizr new-key <projectId> --capabilities query,annotate \
239
+ --label "weekly-report-agent" --rate-limit-max 120 --rate-limit-window-ms 60000
240
+ ```
241
+
242
+ > **Breaking change.** `query:raw` is new, and the raw per-session endpoints now require **both**
243
+ > `ENABLE_RAW_SESSION_RETENTION` **and** `query:raw` — previously retention alone was enough for
244
+ > any `query` key. Existing keys keep working for every aggregate endpoint; a key that drives
245
+ > session replay or live-follow must be re-minted with `--capabilities query,query:raw`.
246
+
247
+ `--rate-limit-max` / `--rate-limit-window-ms` give a key its own request budget, bucketed on the
248
+ key id rather than the client IP; keys without one fall back to `COLLECTOR_RATE_LIMIT_*`.
249
+ Ingestion keeps its separate `COLLECTOR_INGEST_RATE_LIMIT_*` budget.
250
+
251
+ ### Agent audit log
252
+
253
+ Every authenticated request made with a key that is not the dashboard's own session is recorded
254
+ (`keyId`, `surface`, route pattern, bounded+redacted `params`, `rowCount`, `durationMs`,
255
+ `status`), readable at `GET /api/v1/audit` with any `query` key. Refusals are recorded too. A key
256
+ never appears in a row — the subject is the key's **id** — and `params` drops credential-shaped
257
+ fields and is capped at 512 bytes. Writes happen after the response is flushed, so the audit log
258
+ can never block or fail a request. "The dashboard's own session" is a request carrying
259
+ `x-uptimizr-client: dashboard` (a volume filter, not a security boundary — set
260
+ `AUDIT_DASHBOARD_REQUESTS=1` to record everything). Rows expire after `AUDIT_RETENTION_DAYS`
261
+ (default `30`; `0` keeps them forever).
161
262
 
162
263
  ### Threat model for keyless ingestion
163
264
 
@@ -188,7 +289,10 @@ Environment-driven (see [`.env.example`](../../../.env.example)):
188
289
  `ENABLE_RAW_SESSION_RETENTION`, `LIVE_TOKEN_SECRET`, `LIVE_TOKEN_TTL_MS`,
189
290
  `LIVE_WINDOW_MS`, `LIVE_MAX_CONNECTIONS`, `LIVE_PRESENCE_INTERVAL_MS`.
190
291
  - Rate limits: `COLLECTOR_RATE_LIMIT_MAX`, `COLLECTOR_RATE_LIMIT_WINDOW_MS`,
191
- `COLLECTOR_INGEST_RATE_LIMIT_MAX`, `COLLECTOR_INGEST_RATE_LIMIT_WINDOW_MS`.
292
+ `COLLECTOR_INGEST_RATE_LIMIT_MAX`, `COLLECTOR_INGEST_RATE_LIMIT_WINDOW_MS`
293
+ (a key's own budget overrides the first pair).
294
+ - Agent audit: `AUDIT_RETENTION_DAYS` (default `30`, `0` = keep forever),
295
+ `AUDIT_DASHBOARD_REQUESTS` (default off).
192
296
  - All-in-one dashboard: `COLLECTOR_DASHBOARD_DIR` (optional; see
193
297
  [above](#all-in-one-serve-the-dashboard-too)), `COLLECTOR_CSP` (`strict` or `off`).
194
298
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared fixtures for the suites that sweep **every** registry endpoint
3
+ * (`queryResponseSchemas.test.ts`, `resultFormat.test.ts`).
4
+ *
5
+ * Both need the same three things: a collector config, a scene proxy so the
6
+ * `scene_representation` resource has something to return, and the rule for
7
+ * turning a registry `endpoint.path` into a request URL. Keeping them here means
8
+ * a new endpoint that needs a required parameter is taught about once.
9
+ *
10
+ * Not a test file — the filename has no `.test.` segment, so Vitest does not
11
+ * collect it.
12
+ */
13
+ import type { MetricDefinition } from "@uptimizr/metrics";
14
+ import type { CollectorConfig } from "../../config.js";
15
+ /** A collector config with every gate open and every secret a test secret. */
16
+ export declare const TEST_CONFIG: CollectorConfig;
17
+ /** Fixture scene proxy, so the `scene_representation` resource has a hit. */
18
+ export declare const TEST_PROXY: {
19
+ version: 1;
20
+ sceneId: string;
21
+ kind: "aabb";
22
+ bounds: [number, number, number, number, number, number];
23
+ upAxis: "y";
24
+ unitScale: number;
25
+ meshes: {
26
+ name: string;
27
+ aabb: [number, number, number, number, number, number];
28
+ }[];
29
+ meshCount: number;
30
+ contentHash: string;
31
+ capturedAt: number;
32
+ };
33
+ /** Path params every registry endpoint that declares one can be satisfied with. */
34
+ export declare const PATH_PARAM_VALUES: Readonly<Record<string, string>>;
35
+ /**
36
+ * Query parameters an endpoint needs beyond the shared range. Only the genuinely
37
+ * required ones: `mesh` for the per-mesh UV heatmap and `steps` for the funnel.
38
+ */
39
+ export declare const REQUIRED_QUERY: Readonly<Record<string, Record<string, string>>>;
40
+ /** The two resource reads, which legitimately 404 when nothing is registered. */
41
+ export declare const RESOURCE_METRICS: ReadonlySet<string>;
42
+ /**
43
+ * Fill a registry path's `:params` and append the query string for a request.
44
+ * `extra` adds (or overrides) query parameters — `format`, for instance.
45
+ */
46
+ export declare function requestUrl(metric: MetricDefinition, extra?: Readonly<Record<string, string>>): string;
47
+ //# sourceMappingURL=registryRequests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registryRequests.d.ts","sourceRoot":"","sources":["../../../src/__tests__/support/registryRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,8EAA8E;AAC9E,eAAO,MAAM,WAAW,EAAE,eAqBzB,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,UAAU;;;;YAIW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;cAM5C,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;CAMrF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAK3E,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAG/C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,gBAAgB,EACxB,KAAK,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAC3C,MAAM,CAYR"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared fixtures for the suites that sweep **every** registry endpoint
3
+ * (`queryResponseSchemas.test.ts`, `resultFormat.test.ts`).
4
+ *
5
+ * Both need the same three things: a collector config, a scene proxy so the
6
+ * `scene_representation` resource has something to return, and the rule for
7
+ * turning a registry `endpoint.path` into a request URL. Keeping them here means
8
+ * a new endpoint that needs a required parameter is taught about once.
9
+ *
10
+ * Not a test file — the filename has no `.test.` segment, so Vitest does not
11
+ * collect it.
12
+ */
13
+ import { PARITY_RANGE } from "@uptimizr/db";
14
+ /** A collector config with every gate open and every secret a test secret. */
15
+ export const TEST_CONFIG = {
16
+ host: "127.0.0.1",
17
+ port: 0,
18
+ corsOrigins: [],
19
+ visitorHashSecret: "test-secret",
20
+ enableRawSessionRetention: false,
21
+ liveWindowMs: 30_000,
22
+ liveTokenSecret: "test-live-secret",
23
+ liveTokenSecretIsDedicated: true,
24
+ liveTokenTtlMs: 900_000,
25
+ liveMaxConnections: 200,
26
+ livePresenceIntervalMs: 2_000,
27
+ rateLimitMax: 1000,
28
+ rateLimitWindowMs: 60_000,
29
+ ingestRateLimitMax: 1000,
30
+ ingestRateLimitWindowMs: 60_000,
31
+ trustProxy: false,
32
+ bodyLimit: 1_048_576,
33
+ cspMode: "strict",
34
+ auditRetentionDays: 30,
35
+ auditDashboardRequests: false,
36
+ };
37
+ /** Fixture scene proxy, so the `scene_representation` resource has a hit. */
38
+ export const TEST_PROXY = {
39
+ version: 1,
40
+ sceneId: "lobby",
41
+ kind: "aabb",
42
+ bounds: [-2, 0, -2, 2, 3, 2],
43
+ upAxis: "y",
44
+ unitScale: 1,
45
+ meshes: [
46
+ {
47
+ name: "floor",
48
+ aabb: [-2, 0, -2, 2, 0.1, 2],
49
+ },
50
+ ],
51
+ meshCount: 1,
52
+ contentHash: "abc123",
53
+ capturedAt: 1_750_000_000_000,
54
+ };
55
+ /** Path params every registry endpoint that declares one can be satisfied with. */
56
+ export const PATH_PARAM_VALUES = {
57
+ ":sessionId": "s1",
58
+ ":id": "s1",
59
+ ":sceneId": "lobby",
60
+ };
61
+ /**
62
+ * Query parameters an endpoint needs beyond the shared range. Only the genuinely
63
+ * required ones: `mesh` for the per-mesh UV heatmap and `steps` for the funnel.
64
+ */
65
+ export const REQUIRED_QUERY = {
66
+ "/api/v1/heatmaps/mesh-uv": { mesh: "box" },
67
+ "/api/v1/funnel": {
68
+ steps: JSON.stringify([{ type: "session_start" }, { type: "pointer_click" }]),
69
+ },
70
+ };
71
+ /** The two resource reads, which legitimately 404 when nothing is registered. */
72
+ export const RESOURCE_METRICS = new Set([
73
+ "session_meta",
74
+ "scene_representation",
75
+ ]);
76
+ /**
77
+ * Fill a registry path's `:params` and append the query string for a request.
78
+ * `extra` adds (or overrides) query parameters — `format`, for instance.
79
+ */
80
+ export function requestUrl(metric, extra = {}) {
81
+ let path = metric.endpoint.path;
82
+ for (const [token, value] of Object.entries(PATH_PARAM_VALUES)) {
83
+ path = path.replace(token, value);
84
+ }
85
+ const params = new URLSearchParams({
86
+ since: String(PARITY_RANGE.since),
87
+ until: String(PARITY_RANGE.until),
88
+ ...(REQUIRED_QUERY[metric.endpoint.path] ?? {}),
89
+ ...extra,
90
+ });
91
+ return `${path}?${params.toString()}`;
92
+ }
93
+ //# sourceMappingURL=registryRequests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registryRequests.js","sourceRoot":"","sources":["../../../src/__tests__/support/registryRequests.ts"],"names":[],"mappings":"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"}
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAIA,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAMxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAM3D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA0BD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAuE3E"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAIA,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAMxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAS3D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA0BD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAuG3E"}
package/dist/app.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import cors from "@fastify/cors";
2
2
  import helmet from "@fastify/helmet";
3
- import rateLimit from "@fastify/rate-limit";
3
+ import rateLimit, { normalizeIP } from "@fastify/rate-limit";
4
4
  import fastifyStatic from "@fastify/static";
5
5
  import Fastify, {} from "fastify";
6
6
  import { serializerCompiler, validatorCompiler, } from "fastify-type-provider-zod";
7
7
  import { createLiveBus } from "./liveBus.js";
8
+ import { attachApiKey } from "./auth.js";
9
+ import { registerAuditHooks, startAuditRetention } from "./audit.js";
8
10
  import { buildDashboardCsp } from "./csp.js";
9
11
  import { collectRoutes } from "./routes/collect.js";
10
12
  import { liveRoutes } from "./routes/live.js";
13
+ import { collectRouteSchemas, metaRoutes } from "./routes/meta.js";
11
14
  import { queryRoutes } from "./routes/query.js";
12
15
  /**
13
16
  * Strip the live-SSE `?token=` from a logged URL. The token is a short-lived
@@ -74,14 +77,42 @@ export async function buildApp(deps) {
74
77
  // satisfies the preflight that sendBeacon forces.
75
78
  credentials: true,
76
79
  });
80
+ // Resolve `x-api-key` once, before the rate limiter runs, so (a) a key with
81
+ // its own budget is throttled per key rather than per client IP and (b) the
82
+ // handlers and the audit hook reuse one metadata lookup per request (#309).
83
+ // Instance-level `onRequest` hooks run before the route-level hook the
84
+ // rate-limit plugin installs, so registration order here is load-bearing.
85
+ app.decorateRequest("resolvedKey", null);
86
+ app.decorateRequest("auditRowCount", null);
87
+ app.addHook("onRequest", async (request) => {
88
+ await attachApiKey(request, store);
89
+ });
77
90
  await app.register(rateLimit, {
78
- max: config.rateLimitMax,
79
- timeWindow: config.rateLimitWindowMs,
91
+ // A key carrying its own `rate_limit_max` / `rate_limit_window_ms` is
92
+ // bucketed on the key id with those values; everything else (including
93
+ // keyless ingest) keeps the global per-client-IP budget.
94
+ max: (request) => request.resolvedKey?.rateLimit?.max ?? config.rateLimitMax,
95
+ timeWindow: (request) => request.resolvedKey?.rateLimit?.windowMs ?? config.rateLimitWindowMs,
96
+ // `normalizeIP` is exactly what the plugin's own default key generator uses,
97
+ // so requests without a per-key budget keep their existing IPv6-aware bucket.
98
+ keyGenerator: (request) => request.resolvedKey?.rateLimit ? `key:${request.resolvedKey.keyId}` : normalizeIP(request.ip),
80
99
  });
100
+ // Audit every authenticated, non-dashboard request (ADR 0051 §7). Registered
101
+ // after the rate limiter so a throttled request is still recorded.
102
+ registerAuditHooks(app, store, config);
103
+ const stopAuditRetention = startAuditRetention(app, store, config);
104
+ app.addHook("onClose", async () => stopAuditRetention());
105
+ // Record every route's Zod schemas as they are registered, so the generated
106
+ // OpenAPI document describes each parameter with the *same* schema that
107
+ // validates the request (ADR 0051 §1). The hook must be installed before the
108
+ // route plugins below; the array it fills is complete once `app.ready()` has
109
+ // resolved, which is always before `metaRoutes` serves its first request.
110
+ const routeSchemas = collectRouteSchemas(app);
81
111
  app.get("/health", async () => ({ status: "ok" }));
82
112
  await app.register(collectRoutes, { store, config, liveBus });
83
113
  await app.register(liveRoutes, { store, config, liveBus });
84
114
  await app.register(queryRoutes, { store, config });
115
+ await app.register(metaRoutes, { routeSchemas });
85
116
  // All-in-one: serve a pre-built static dashboard from `dashboardDir`. The API
86
117
  // routes above (`/health`, `/api/v1/*`) are matched first; everything else
87
118
  // falls through to the static files. Unmatched GET navigations (the SPA deep
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { CollectorConfig } from "./config.js";
3
+ import type { CollectorStore } from "./store.js";
4
+ /**
5
+ * Register the audit hooks on `app`:
6
+ *
7
+ * - `preSerialization` captures the row count when a handler returned an array
8
+ * (streamed/hijacked responses simply have none);
9
+ * - `onResponse` writes the row for authenticated, non-dashboard requests.
10
+ */
11
+ export declare function registerAuditHooks(app: FastifyInstance, store: CollectorStore, config: CollectorConfig): void;
12
+ /**
13
+ * Start the retention sweep: delete audit rows older than
14
+ * `config.auditRetentionDays` now, then every {@link PRUNE_INTERVAL_MS}. The
15
+ * delete is idempotent, so a restart loop or several collector instances
16
+ * sharing one database cost nothing. Returns a stop function; the timer is
17
+ * `unref`'d so it never keeps the process alive.
18
+ *
19
+ * `auditRetentionDays === 0` disables the sweep (keep rows indefinitely).
20
+ */
21
+ export declare function startAuditRetention(app: FastifyInstance, store: CollectorStore, config: CollectorConfig): () => void;
22
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAwBjD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,eAAe,GACtB,IAAI,CAoCN;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,eAAe,GACtB,MAAM,IAAI,CAkBZ"}