@iann29/rastro 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (179) hide show
  1. package/README.md +693 -69
  2. package/agent/integration.md +801 -0
  3. package/agent/manifest.json +205 -0
  4. package/agent/manifest.schema.json +444 -0
  5. package/dist/client/federation.d.ts +381 -0
  6. package/dist/client/federation.d.ts.map +1 -0
  7. package/dist/client/federation.js +274 -0
  8. package/dist/client/federation.js.map +1 -0
  9. package/dist/client/index.d.ts +2673 -15
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +565 -33
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +22 -0
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/api.js.map +1 -1
  16. package/dist/component/_generated/component.d.ts +315 -4
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/_generated/server.d.ts +4 -0
  19. package/dist/component/_generated/server.d.ts.map +1 -1
  20. package/dist/component/_generated/server.js.map +1 -1
  21. package/dist/component/affiliates.d.ts.map +1 -1
  22. package/dist/component/affiliates.js +6 -2
  23. package/dist/component/affiliates.js.map +1 -1
  24. package/dist/component/cardinality.d.ts +12 -0
  25. package/dist/component/cardinality.d.ts.map +1 -0
  26. package/dist/component/cardinality.js +94 -0
  27. package/dist/component/cardinality.js.map +1 -0
  28. package/dist/component/constants.d.ts +28 -1
  29. package/dist/component/constants.d.ts.map +1 -1
  30. package/dist/component/constants.js +44 -1
  31. package/dist/component/constants.js.map +1 -1
  32. package/dist/component/convex.config.d.ts +6 -1
  33. package/dist/component/convex.config.js +9 -1
  34. package/dist/component/convex.config.js.map +1 -1
  35. package/dist/component/coverage.d.ts +27 -0
  36. package/dist/component/coverage.d.ts.map +1 -0
  37. package/dist/component/coverage.js +56 -0
  38. package/dist/component/coverage.js.map +1 -0
  39. package/dist/component/diagnostics.d.ts +9 -0
  40. package/dist/component/diagnostics.d.ts.map +1 -0
  41. package/dist/component/diagnostics.js +47 -0
  42. package/dist/component/diagnostics.js.map +1 -0
  43. package/dist/component/errors.d.ts +1 -1
  44. package/dist/component/errors.d.ts.map +1 -1
  45. package/dist/component/errors.js.map +1 -1
  46. package/dist/component/eventStore.d.ts +21 -9
  47. package/dist/component/eventStore.d.ts.map +1 -1
  48. package/dist/component/eventStore.js +142 -152
  49. package/dist/component/eventStore.js.map +1 -1
  50. package/dist/component/funnels.d.ts.map +1 -1
  51. package/dist/component/funnels.js +5 -3
  52. package/dist/component/funnels.js.map +1 -1
  53. package/dist/component/geo.d.ts +73 -0
  54. package/dist/component/geo.d.ts.map +1 -0
  55. package/dist/component/geo.js +648 -0
  56. package/dist/component/geo.js.map +1 -0
  57. package/dist/component/goals.d.ts.map +1 -1
  58. package/dist/component/goals.js +8 -5
  59. package/dist/component/goals.js.map +1 -1
  60. package/dist/component/guards.d.ts.map +1 -1
  61. package/dist/component/guards.js.map +1 -1
  62. package/dist/component/http.d.ts.map +1 -1
  63. package/dist/component/http.js +281 -62
  64. package/dist/component/http.js.map +1 -1
  65. package/dist/component/identity.d.ts +13 -0
  66. package/dist/component/identity.d.ts.map +1 -0
  67. package/dist/component/identity.js +58 -0
  68. package/dist/component/identity.js.map +1 -0
  69. package/dist/component/ingest.d.ts +3 -1
  70. package/dist/component/ingest.d.ts.map +1 -1
  71. package/dist/component/ingest.js +498 -95
  72. package/dist/component/ingest.js.map +1 -1
  73. package/dist/component/live.d.ts.map +1 -1
  74. package/dist/component/live.js +7 -6
  75. package/dist/component/live.js.map +1 -1
  76. package/dist/component/localTime.d.ts +25 -0
  77. package/dist/component/localTime.d.ts.map +1 -0
  78. package/dist/component/localTime.js +126 -0
  79. package/dist/component/localTime.js.map +1 -0
  80. package/dist/component/reports.d.ts +266 -10
  81. package/dist/component/reports.d.ts.map +1 -1
  82. package/dist/component/reports.js +1243 -137
  83. package/dist/component/reports.js.map +1 -1
  84. package/dist/component/retention.d.ts +75 -1
  85. package/dist/component/retention.d.ts.map +1 -1
  86. package/dist/component/retention.js +561 -38
  87. package/dist/component/retention.js.map +1 -1
  88. package/dist/component/rollupStore.d.ts +320 -0
  89. package/dist/component/rollupStore.d.ts.map +1 -0
  90. package/dist/component/rollupStore.js +596 -0
  91. package/dist/component/rollupStore.js.map +1 -0
  92. package/dist/component/rollups.d.ts +20 -0
  93. package/dist/component/rollups.d.ts.map +1 -0
  94. package/dist/component/rollups.js +73 -0
  95. package/dist/component/rollups.js.map +1 -0
  96. package/dist/component/sanitize.d.ts +24 -1
  97. package/dist/component/sanitize.d.ts.map +1 -1
  98. package/dist/component/sanitize.js +96 -16
  99. package/dist/component/sanitize.js.map +1 -1
  100. package/dist/component/schema.d.ts +687 -65
  101. package/dist/component/schema.js +187 -20
  102. package/dist/component/schema.js.map +1 -1
  103. package/dist/component/sites.d.ts +12 -0
  104. package/dist/component/sites.d.ts.map +1 -1
  105. package/dist/component/sites.js +41 -7
  106. package/dist/component/sites.js.map +1 -1
  107. package/dist/component/useragent.d.ts +9 -0
  108. package/dist/component/useragent.d.ts.map +1 -0
  109. package/dist/component/useragent.js +152 -0
  110. package/dist/component/useragent.js.map +1 -0
  111. package/dist/component/validators.d.ts +124 -69
  112. package/dist/component/validators.d.ts.map +1 -1
  113. package/dist/component/validators.js +35 -14
  114. package/dist/component/validators.js.map +1 -1
  115. package/dist/component/visitors.d.ts +19 -0
  116. package/dist/component/visitors.d.ts.map +1 -0
  117. package/dist/component/visitors.js +86 -0
  118. package/dist/component/visitors.js.map +1 -0
  119. package/dist/component/vitals.d.ts +41 -0
  120. package/dist/component/vitals.d.ts.map +1 -0
  121. package/dist/component/vitals.js +115 -0
  122. package/dist/component/vitals.js.map +1 -0
  123. package/dist/react/index.d.ts.map +1 -1
  124. package/dist/react/index.js.map +1 -1
  125. package/dist/tracker/generated.d.ts +11 -4
  126. package/dist/tracker/generated.d.ts.map +1 -1
  127. package/dist/tracker/generated.js +11 -4
  128. package/dist/tracker/generated.js.map +1 -1
  129. package/dist/tracker/tracker.d.ts +1 -1
  130. package/dist/tracker/tracker.d.ts.map +1 -1
  131. package/dist/tracker/tracker.js +57 -20
  132. package/dist/tracker/tracker.js.map +1 -1
  133. package/dist/tracker/vitals.d.ts +10 -0
  134. package/dist/tracker/vitals.d.ts.map +1 -0
  135. package/dist/tracker/vitals.js +140 -0
  136. package/dist/tracker/vitals.js.map +1 -0
  137. package/dist/tracker.min.js +1 -1
  138. package/dist/vitals.min.js +1 -0
  139. package/docs/benchmarks/2026-08-20-realistic.md +76 -76
  140. package/docs/benchmarks/2026-08-21-formal-certification.md +353 -0
  141. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  142. package/docs/federation-setup.md +464 -0
  143. package/docs/federation.md +352 -0
  144. package/docs/upgrading.md +344 -0
  145. package/llms.txt +72 -0
  146. package/package.json +55 -11
  147. package/scripts/benchmark-ingest.mjs +175 -73
  148. package/scripts/generate-federation-keys.mjs +20 -0
  149. package/src/component/_generated/api.ts +22 -0
  150. package/src/component/_generated/component.ts +381 -4
  151. package/src/component/_generated/server.ts +4 -0
  152. package/src/component/affiliates.ts +20 -5
  153. package/src/component/cardinality.ts +117 -0
  154. package/src/component/constants.ts +44 -1
  155. package/src/component/convex.config.ts +11 -1
  156. package/src/component/coverage.ts +71 -0
  157. package/src/component/diagnostics.ts +65 -0
  158. package/src/component/errors.ts +2 -1
  159. package/src/component/eventStore.ts +217 -193
  160. package/src/component/funnels.ts +19 -16
  161. package/src/component/geo.ts +835 -0
  162. package/src/component/goals.ts +29 -21
  163. package/src/component/guards.ts +3 -1
  164. package/src/component/http.ts +404 -70
  165. package/src/component/identity.ts +74 -0
  166. package/src/component/ingest.ts +894 -188
  167. package/src/component/live.ts +13 -7
  168. package/src/component/localTime.ts +167 -0
  169. package/src/component/reports.ts +1872 -197
  170. package/src/component/retention.ts +788 -96
  171. package/src/component/rollupStore.ts +799 -0
  172. package/src/component/rollups.ts +82 -0
  173. package/src/component/sanitize.ts +144 -29
  174. package/src/component/schema.ts +217 -21
  175. package/src/component/sites.ts +59 -12
  176. package/src/component/useragent.ts +171 -0
  177. package/src/component/validators.ts +49 -14
  178. package/src/component/visitors.ts +116 -0
  179. package/src/component/vitals.ts +146 -0
@@ -0,0 +1,464 @@
1
+ # Federated dashboard setup
2
+
3
+ This runbook connects an existing Convex or Synapse project to the Amage Rastro
4
+ control plane without moving telemetry out of that project. Read
5
+ [`federation.md`](federation.md) for the protocol and threat model.
6
+
7
+ ## Package eligibility requirement
8
+
9
+ Do not infer federation availability from this website, a dist-tag, or a source
10
+ checkout. Discover the current tag without changing the host:
11
+
12
+ ```bash
13
+ npm view @iann29/rastro@alpha version
14
+ ```
15
+
16
+ After the operator approves that exact version, execute the complete static
17
+ tarball, export-map, AST, and TypeScript consumer probe in the **Package
18
+ eligibility gate** of the canonical
19
+ [`agent/integration.md`](https://www.amagerastro.com/agent/integration.md). Do
20
+ not replace it with an `import()` test, which executes package code, or a text
21
+ search, which can accept comments and broken re-exports.
22
+
23
+ If verification fails, stop and request an eligible release. If it passes,
24
+ record that exact version but do not install it yet. Detect the currently
25
+ installed version with `npm ls @iann29/rastro --depth=0`, then follow
26
+ [`upgrading.md`](upgrading.md): snapshot/rehearse maintained deployments and
27
+ complete every required migration bridge. Only then install with
28
+ `npm install --ignore-scripts --save-exact @iann29/rastro@EXACT_VERSION` and
29
+ commit the lockfile. Never fetch a mutable branch or install a source tarball
30
+ supplied by this website.
31
+
32
+ ## 1. Verify local analytics first
33
+
34
+ Do not configure federation until the customer deployment already has:
35
+
36
+ - the Rastro component mounted under `/rastro/`;
37
+ - at least one site with the correct allowed browser domains;
38
+ - a tracker using the deployment's HTTP-actions URL;
39
+ - an accepted `POST /rastro/events` request;
40
+ - a protected local report returning that site's telemetry — `liveVisitors` or
41
+ `sessionJourney` reflect an accepted request immediately, while `listSessions`
42
+ and the bucketed `overview` can trail it by seconds, so an empty result from
43
+ those two right after the first request is not a failure;
44
+ - an audit through authorized local reports of retained routes, visitor/session
45
+ IDs, live location, campaigns, affiliates, conversions, and custom properties,
46
+ proving that no unexpected personal data, credentials, secrets, or form values
47
+ will become visible to the intended dashboard organization members and that
48
+ retention is acceptable.
49
+
50
+ The README quickstart covers those steps. Federation only exposes existing
51
+ reports; it does not install the tracker or create sites.
52
+
53
+ ## 2. Record both deployment URLs
54
+
55
+ The connection form requires two different base URLs with no path, query, hash,
56
+ username, or password:
57
+
58
+ | Field | Used for | Convex Cloud example | Synapse example |
59
+ | -------------- | --------------------------------- | ------------------------------ | -------------------------------------- |
60
+ | URL de funções | Reactive queries and JWT audience | `https://product.convex.cloud` | `https://product.synapse.example` |
61
+ | URL HTTP | Tracker and event ingestion | `https://product.convex.site` | `https://product.site.synapse.example` |
62
+
63
+ Custom domains are valid when they route to the corresponding Convex surface.
64
+ The control-plane operator must add each custom functions origin to
65
+ `RASTRO_FEDERATION_ALLOWED_DEPLOYMENT_ORIGINS` as an exact, comma-separated
66
+ HTTPS origin before it can be registered. Do not derive one URL by appending
67
+ paths to the other or derive the allowlist from browser input. If the dashboard
68
+ returns `UNTRUSTED_DEPLOYMENT_HOST`, stop and ask the Amage Rastro operator who
69
+ issued the organization access to add the exact functions origin. Provide only
70
+ that public HTTPS origin, never credentials; retry after the operator confirms
71
+ the allowlist update.
72
+
73
+ ## 3. Create the pending connection
74
+
75
+ In the central dashboard:
76
+
77
+ 1. Sign in and select the intended organization.
78
+ 2. Open **Conexões**.
79
+ 3. Enter a recognizable name, the functions URL, and the HTTP URL.
80
+ 4. Create the connection, but do not verify it yet.
81
+ 5. Copy the displayed issuer, audience, connection ID, and organization ID.
82
+
83
+ The audience must exactly equal the normalized functions URL. The connection
84
+ remains pending until its local grant is installed and verification succeeds.
85
+
86
+ ## 4. Add the authoritative local grant
87
+
88
+ Before editing, inventory every existing schema table/index, component mount/env
89
+ declaration, and authentication provider. After editing, prove each remains in
90
+ the diff and typechecks. The snippets in this runbook are insertion fragments,
91
+ not replacement files.
92
+
93
+ Insert this table property into the pilot project's existing
94
+ `defineSchema({ ... })` object:
95
+
96
+ ```ts
97
+ rastroFederationGrants: defineTable({
98
+ connectionId: v.string(),
99
+ organizationId: v.string(),
100
+ siteIds: v.array(v.string()),
101
+ // Absent means read only. ["analytics:configure"] lets the dashboard's
102
+ // owners and admins manage goals, funnels, affiliates, site settings and
103
+ // retention for the granted sites.
104
+ permissions: v.optional(v.array(v.string())),
105
+ revokedAt: v.optional(v.number()),
106
+ createdAt: v.number(),
107
+ updatedAt: v.number(),
108
+ }).index("by_connectionId", ["connectionId"]),
109
+ ```
110
+
111
+ The grant, not the JWT, owns the allowed `siteIds` and the permissions the host
112
+ honors. Add operator-only provisioning functions:
113
+
114
+ ```ts
115
+ // convex/rastroFederationAdmin.ts
116
+ import { ConvexError, v } from "convex/values";
117
+ import { federatedAnalyticsPermissionValidator, Rastro } from "@iann29/rastro";
118
+ import { components } from "./_generated/api";
119
+ import { internalMutation } from "./_generated/server";
120
+
121
+ const rastro = new Rastro(components.rastroAnalytics);
122
+
123
+ export const provisionConnection = internalMutation({
124
+ args: {
125
+ connectionId: v.string(),
126
+ organizationId: v.string(),
127
+ ownerId: v.string(),
128
+ siteIds: v.array(v.string()),
129
+ permissions: v.optional(v.array(federatedAnalyticsPermissionValidator)),
130
+ },
131
+ returns: v.null(),
132
+ handler: async (ctx, args) => {
133
+ const siteIds = [...new Set(args.siteIds)];
134
+ const permissions = [...new Set(args.permissions ?? [])];
135
+ if (
136
+ args.connectionId.length === 0 ||
137
+ args.connectionId.length > 128 ||
138
+ args.organizationId.length === 0 ||
139
+ args.organizationId.length > 128 ||
140
+ siteIds.length === 0 ||
141
+ siteIds.length > 10 ||
142
+ siteIds.length !== args.siteIds.length
143
+ ) {
144
+ throw new ConvexError({ code: "INVALID_FEDERATION_GRANT" });
145
+ }
146
+
147
+ for (const siteId of siteIds) {
148
+ const site = await rastro.getSite(ctx, siteId);
149
+ if (!site || site.ownerId !== args.ownerId) {
150
+ throw new ConvexError({ code: "FEDERATION_SITE_NOT_OWNED" });
151
+ }
152
+ }
153
+
154
+ const existing = await ctx.db
155
+ .query("rastroFederationGrants")
156
+ .withIndex("by_connectionId", (query) =>
157
+ query.eq("connectionId", args.connectionId),
158
+ )
159
+ .unique();
160
+ const now = Date.now();
161
+ const grant = {
162
+ connectionId: args.connectionId,
163
+ organizationId: args.organizationId,
164
+ siteIds,
165
+ permissions: permissions.length ? permissions : undefined,
166
+ revokedAt: undefined,
167
+ updatedAt: now,
168
+ };
169
+ if (existing) {
170
+ await ctx.db.patch("rastroFederationGrants", existing._id, grant);
171
+ } else {
172
+ await ctx.db.insert("rastroFederationGrants", {
173
+ ...grant,
174
+ createdAt: now,
175
+ });
176
+ }
177
+ return null;
178
+ },
179
+ });
180
+
181
+ export const revokeConnection = internalMutation({
182
+ args: { connectionId: v.string() },
183
+ returns: v.null(),
184
+ handler: async (ctx, args) => {
185
+ const grant = await ctx.db
186
+ .query("rastroFederationGrants")
187
+ .withIndex("by_connectionId", (query) =>
188
+ query.eq("connectionId", args.connectionId),
189
+ )
190
+ .unique();
191
+ if (!grant) {
192
+ throw new ConvexError({ code: "FEDERATION_GRANT_NOT_FOUND" });
193
+ }
194
+ if (grant.revokedAt === undefined) {
195
+ const now = Date.now();
196
+ await ctx.db.patch("rastroFederationGrants", grant._id, {
197
+ revokedAt: now,
198
+ updatedAt: now,
199
+ });
200
+ }
201
+ return null;
202
+ },
203
+ });
204
+ ```
205
+
206
+ These mutations are `internalMutation`s so a browser cannot call them. Run them
207
+ only with deployment administrator credentials. A host that needs self-service
208
+ pairing may wrap equivalent logic in its own authenticated owner/admin mutation;
209
+ never accept an owner or user ID from an untrusted caller.
210
+
211
+ `permissions` is the opt-in for the configure scope. Leave it out for a
212
+ read-only connection: the dashboard then shows every configuration form but ends
213
+ each one in the code the host runs itself. Pass `["analytics:configure"]` to let
214
+ the dashboard organization's owners and admins create and change goals, funnels,
215
+ affiliates, the site's name, domains and timezone, and the retention policy for
216
+ the granted sites; members of the organization stay read only whatever the grant
217
+ says, because the control plane never puts the configure claim in their tokens.
218
+
219
+ ## 5. Trust the federation issuer
220
+
221
+ The displayed issuer must exactly equal the independently verified production
222
+ manifest issuer, `https://site.api.amagerastro.com/federation`; abort on any
223
+ mismatch. Derive the audience locally from the normalized functions URL and
224
+ compare it with the displayed audience. For Convex Cloud DEV:
225
+
226
+ ```bash
227
+ npx convex env set --deployment dev RASTRO_FEDERATION_ISSUER \
228
+ 'https://site.api.amagerastro.com/federation'
229
+ npx convex env set --deployment dev RASTRO_FEDERATION_AUDIENCE \
230
+ 'https://product.convex.cloud'
231
+ ```
232
+
233
+ Use `--deployment prod` only with fresh production consent. For an existing
234
+ Synapse-managed deployment, target its live Convex environment explicitly:
235
+
236
+ ```bash
237
+ synapse convex --dev env set \
238
+ RASTRO_FEDERATION_ISSUER 'https://site.api.amagerastro.com/federation'
239
+ synapse convex --dev env set \
240
+ RASTRO_FEDERATION_AUDIENCE 'https://product.synapse.example'
241
+ ```
242
+
243
+ Use `synapse env set NAME=value --for=dev` only when defining project defaults
244
+ before a new deployment is created; defaults do not update an existing live
245
+ deployment.
246
+
247
+ Insert only these two properties into the pilot project's existing
248
+ `defineApp({ env: { ... } })` object. If no `env` object exists, add it without
249
+ changing any other `defineApp` option. Do not add a second Rastro mount:
250
+
251
+ ```ts
252
+ RASTRO_FEDERATION_ISSUER: v.optional(v.string()),
253
+ RASTRO_FEDERATION_AUDIENCE: v.optional(v.string()),
254
+ ```
255
+
256
+ Declare them optional, as the [example host](../example/convex/convex.config.ts)
257
+ does. A required `v.string()` makes the push fail on a deployment where the
258
+ values are not set yet — including every deployment that pushes the code before
259
+ pairing. Set both values first and push afterwards: `auth.config.ts` reads them
260
+ at deploy time, so values set after a push take effect only on the next push.
261
+
262
+ Insert this conditional spread into the existing `providers` array; never
263
+ replace the array or its current entries:
264
+
265
+ ```ts
266
+ // convex/auth.config.ts
267
+ import type { AuthConfig } from "convex/server";
268
+
269
+ const issuer = process.env.RASTRO_FEDERATION_ISSUER;
270
+ const audience = process.env.RASTRO_FEDERATION_AUDIENCE;
271
+
272
+ ...(issuer && audience
273
+ ? [
274
+ {
275
+ domain: issuer.replace(/\/$/, ""),
276
+ applicationID: audience.replace(/\/$/, ""),
277
+ },
278
+ ]
279
+ : []),
280
+ ```
281
+
282
+ An exact issuer mismatch is rejected even when the JWT signature is otherwise
283
+ valid. Convex discovers keys from `<issuer>/.well-known/openid-configuration`.
284
+
285
+ ## 6. Expose the host module
286
+
287
+ Create the canonical module with this exact filename:
288
+
289
+ ```ts
290
+ // convex/rastroFederation.ts
291
+ import {
292
+ exposeFederatedAnalyticsApi,
293
+ type FederatedAnalyticsConnection,
294
+ type FederatedAnalyticsPermission,
295
+ } from "@iann29/rastro";
296
+ import { components } from "./_generated/api";
297
+ import { env, type QueryCtx } from "./_generated/server";
298
+
299
+ /** The production Amage Rastro OIDC issuer; override with RASTRO_FEDERATION_ISSUER. */
300
+ export const DEFAULT_FEDERATION_ISSUER =
301
+ "https://site.api.amagerastro.com/federation";
302
+
303
+ export const federationIssuer = (
304
+ env.RASTRO_FEDERATION_ISSUER ?? DEFAULT_FEDERATION_ISSUER
305
+ ).replace(/\/$/, "");
306
+
307
+ const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
308
+ issuer: federationIssuer,
309
+ resolveConnection: async (ctx, identity) => {
310
+ const db = ctx.db as unknown as QueryCtx["db"];
311
+ const grant = await db
312
+ .query("rastroFederationGrants")
313
+ .withIndex("by_connectionId", (query) =>
314
+ query.eq("connectionId", identity.connectionId),
315
+ )
316
+ .unique();
317
+ if (!grant || grant.organizationId !== identity.organizationId) return null;
318
+ return {
319
+ connectionId: grant.connectionId,
320
+ organizationId: grant.organizationId,
321
+ siteIds: grant.siteIds,
322
+ permissions: grant.permissions as FederatedAnalyticsPermission[],
323
+ revokedAt: grant.revokedAt,
324
+ } satisfies FederatedAnalyticsConnection;
325
+ },
326
+ });
327
+
328
+ export const {
329
+ manifest,
330
+ connectionStatus,
331
+ listSites,
332
+ overview,
333
+ liveVisitors,
334
+ listSessions,
335
+ sessionJourney,
336
+ listConversions,
337
+ visitorJourney,
338
+ goalsReport,
339
+ funnelsReport,
340
+ affiliatesReport,
341
+ vitalsReport,
342
+ siteMap,
343
+ dataCoverage,
344
+ siteSettings,
345
+ updateSite,
346
+ listGoals,
347
+ upsertGoal,
348
+ removeGoal,
349
+ listFunnels,
350
+ upsertFunnel,
351
+ removeFunnel,
352
+ listAffiliates,
353
+ upsertAffiliate,
354
+ removeAffiliate,
355
+ retentionStatus,
356
+ setRetentionPolicy,
357
+ disableRetentionPolicy,
358
+ } = federated;
359
+ ```
360
+
361
+ The manifest is public static metadata. Every report function is read-only and
362
+ requires both a valid JWT and a matching non-revoked local grant. The functions
363
+ from `siteSettings` down are the configure scope: they also require
364
+ `analytics:configure` on the token and in the grant, and fail with
365
+ `FEDERATION_CONFIGURE_FORBIDDEN` otherwise. A host that keeps every grant
366
+ read-only may omit them from the export; the dashboard never calls them for a
367
+ read-only connection. The issuer falls back to the production control plane so
368
+ the module compiles and pushes before the environment variable exists; tokens
369
+ are still rejected until `auth.config.ts` trusts the same issuer, which requires
370
+ both variables to be set and the deployment pushed afterwards.
371
+
372
+ ## 7. Deploy and provision
373
+
374
+ Push the customer host changes first:
375
+
376
+ ```bash
377
+ # Convex Cloud DEV, one push
378
+ npx convex dev --once
379
+
380
+ # Synapse DEV
381
+ synapse dev --once
382
+ ```
383
+
384
+ Then provision the exact site scope. Replace every placeholder with values
385
+ copied from the pending connection and the host's existing site records:
386
+
387
+ ```bash
388
+ # Convex Cloud DEV
389
+ npx convex run --deployment dev rastroFederationAdmin:provisionConnection \
390
+ '{"connectionId":"CONNECTION_ID","organizationId":"ORGANIZATION_ID","ownerId":"OPAQUE_HOST_OWNER_ID","siteIds":["SITE_ID"]}'
391
+
392
+ # Synapse DEV
393
+ synapse convex --dev run rastroFederationAdmin:provisionConnection \
394
+ '{"connectionId":"CONNECTION_ID","organizationId":"ORGANIZATION_ID","ownerId":"OPAQUE_HOST_OWNER_ID","siteIds":["SITE_ID"]}'
395
+ ```
396
+
397
+ Use the corresponding explicit production target only after rehearsing DEV. The
398
+ command is idempotent for one `connectionId` and replaces its site scope and
399
+ permissions. To opt into the configure scope, add
400
+ `"permissions":["analytics:configure"]` to the same JSON; run the command again
401
+ without it to return the connection to read only.
402
+
403
+ ## 8. Verify in order
404
+
405
+ 1. Run `rastroFederation:manifest` without auth and confirm protocol version 1.
406
+ 2. Return to **Conexões** in the central dashboard.
407
+ 3. Click **Verificar conexão**.
408
+ 4. Confirm the connection becomes active and reports the expected site count.
409
+ 5. Open Visão geral and confirm the deployment selector names the new host.
410
+ 6. Trigger one real browser pageview and confirm Ao vivo updates reactively.
411
+ 7. Check Metas/Afiliados with a complete UTC-day range when those features are
412
+ configured.
413
+ 8. Confirm the connection card in **Conexões** reads "Somente leitura" or
414
+ "Leitura e configuração" as the grant intends. When it reads "Leitura e
415
+ configuração", create a goal from **Metas** as an owner or admin and confirm
416
+ it appears in the host's `listGoals`; then sign in as a member and confirm
417
+ the same form only offers the code.
418
+
419
+ The central verification signs a short-lived RS256 token with the verifying
420
+ admin's permissions, calls authenticated `connectionStatus` and `listSites`,
421
+ records the permissions the host honors, and refuses incompatible manifests or
422
+ inconsistent site counts.
423
+
424
+ ## 9. Revoke safely
425
+
426
+ For immediate denial, revoke the local grant first:
427
+
428
+ ```bash
429
+ npx convex run --deployment dev rastroFederationAdmin:revokeConnection \
430
+ '{"connectionId":"CONNECTION_ID"}'
431
+
432
+ # Synapse DEV
433
+ synapse convex --dev run rastroFederationAdmin:revokeConnection \
434
+ '{"connectionId":"CONNECTION_ID"}'
435
+ ```
436
+
437
+ For production, repeat the rehearsed command with the platform's explicit
438
+ production target: `npx convex run --deployment prod ...` or
439
+ `synapse convex --prod run ...`. Never infer the target from the current shell
440
+ state.
441
+
442
+ Then revoke the connection in the central dashboard. Central revocation stops
443
+ new tokens; local revocation rejects already-issued tokens immediately. Without
444
+ the local step, an existing token remains usable until its ten-minute expiry.
445
+
446
+ ## Troubleshooting
447
+
448
+ | Symptom or code | Check |
449
+ | -------------------------------- | ----------------------------------------------------------------------------- |
450
+ | Manifest not found | File must be `convex/rastroFederation.ts`; deploy/codegen must have completed |
451
+ | `FEDERATION_UNAUTHENTICATED` | Federation provider missing, token absent, or deployment not redeployed |
452
+ | `FEDERATION_ISSUER_MISMATCH` | `auth.config.ts` domain and helper issuer differ, often by a trailing slash |
453
+ | JWT audience failure | `RASTRO_FEDERATION_AUDIENCE` must equal the functions URL, not the HTTP URL |
454
+ | `FEDERATION_CONNECTION_DENIED` | Grant missing, organization mismatched, empty, malformed, or revoked |
455
+ | `FEDERATION_SITE_DENIED` | Dashboard requested a site outside the local grant |
456
+ | `FEDERATION_CONFIGURE_FORBIDDEN` | Grant lacks `analytics:configure`, or the viewer is not an owner or admin |
457
+ | Missing function on configure | Host upgraded the package but did not re-export the configure functions |
458
+ | Tracker 404 | Tracker uses the functions URL instead of the HTTP-actions URL |
459
+ | `ORIGIN_NOT_ALLOWED` | Browser host is absent from the site's exact/wildcard domains |
460
+ | `REPORT_INCOMPLETE` | Use complete UTC buckets or reduce a range that exceeds a bounded read budget |
461
+ | `UNTRUSTED_DEPLOYMENT_HOST` | Ask the control-plane operator to allow the exact custom functions origin |
462
+
463
+ Do not fix authorization errors by weakening the resolver, copying site IDs into
464
+ JWT claims, or exposing a permissive report API.