@iann29/rastro 0.1.0-alpha.8 → 0.1.0

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 (105) hide show
  1. package/README.md +274 -75
  2. package/agent/integration.md +223 -65
  3. package/agent/manifest.json +34 -13
  4. package/agent/manifest.schema.json +53 -11
  5. package/dist/client/federation.d.ts +196 -11
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +160 -6
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/heat.d.ts +37 -0
  10. package/dist/client/heat.d.ts.map +1 -0
  11. package/dist/client/heat.js +43 -0
  12. package/dist/client/heat.js.map +1 -0
  13. package/dist/client/index.d.ts +448 -105
  14. package/dist/client/index.d.ts.map +1 -1
  15. package/dist/client/index.js +207 -17
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/component/_generated/api.d.ts +6 -0
  18. package/dist/component/_generated/api.d.ts.map +1 -1
  19. package/dist/component/_generated/api.js.map +1 -1
  20. package/dist/component/_generated/component.d.ts +53 -16
  21. package/dist/component/_generated/component.d.ts.map +1 -1
  22. package/dist/component/constants.d.ts +5 -1
  23. package/dist/component/constants.d.ts.map +1 -1
  24. package/dist/component/constants.js +8 -1
  25. package/dist/component/constants.js.map +1 -1
  26. package/dist/component/coverage.d.ts +9 -0
  27. package/dist/component/coverage.d.ts.map +1 -1
  28. package/dist/component/coverage.js +36 -6
  29. package/dist/component/coverage.js.map +1 -1
  30. package/dist/component/eventStore.d.ts +1 -1
  31. package/dist/component/geo.d.ts +2 -0
  32. package/dist/component/geo.d.ts.map +1 -1
  33. package/dist/component/geo.js +56 -19
  34. package/dist/component/geo.js.map +1 -1
  35. package/dist/component/http.d.ts.map +1 -1
  36. package/dist/component/http.js +49 -27
  37. package/dist/component/http.js.map +1 -1
  38. package/dist/component/ingest.d.ts +1 -1
  39. package/dist/component/ingest.d.ts.map +1 -1
  40. package/dist/component/ingest.js +252 -305
  41. package/dist/component/ingest.js.map +1 -1
  42. package/dist/component/localTime.d.ts +25 -0
  43. package/dist/component/localTime.d.ts.map +1 -0
  44. package/dist/component/localTime.js +126 -0
  45. package/dist/component/localTime.js.map +1 -0
  46. package/dist/component/reports.d.ts +46 -15
  47. package/dist/component/reports.d.ts.map +1 -1
  48. package/dist/component/reports.js +193 -38
  49. package/dist/component/reports.js.map +1 -1
  50. package/dist/component/retention.js +26 -8
  51. package/dist/component/retention.js.map +1 -1
  52. package/dist/component/rollupStore.d.ts +320 -0
  53. package/dist/component/rollupStore.d.ts.map +1 -0
  54. package/dist/component/rollupStore.js +596 -0
  55. package/dist/component/rollupStore.js.map +1 -0
  56. package/dist/component/rollups.d.ts +20 -0
  57. package/dist/component/rollups.d.ts.map +1 -0
  58. package/dist/component/rollups.js +73 -0
  59. package/dist/component/rollups.js.map +1 -0
  60. package/dist/component/sanitize.d.ts +5 -0
  61. package/dist/component/sanitize.d.ts.map +1 -1
  62. package/dist/component/sanitize.js +15 -0
  63. package/dist/component/sanitize.js.map +1 -1
  64. package/dist/component/schema.d.ts +349 -15
  65. package/dist/component/schema.js +25 -0
  66. package/dist/component/schema.js.map +1 -1
  67. package/dist/component/sites.d.ts +12 -0
  68. package/dist/component/sites.d.ts.map +1 -1
  69. package/dist/component/sites.js +30 -0
  70. package/dist/component/sites.js.map +1 -1
  71. package/dist/component/validators.d.ts +41 -20
  72. package/dist/component/validators.d.ts.map +1 -1
  73. package/dist/component/validators.js +24 -3
  74. package/dist/component/validators.js.map +1 -1
  75. package/dist/tracker/generated.d.ts +7 -4
  76. package/dist/tracker/generated.d.ts.map +1 -1
  77. package/dist/tracker/generated.js +7 -4
  78. package/dist/tracker/generated.js.map +1 -1
  79. package/dist/tracker/tracker.d.ts +1 -1
  80. package/dist/tracker/tracker.d.ts.map +1 -1
  81. package/dist/tracker/tracker.js +24 -2
  82. package/dist/tracker/tracker.js.map +1 -1
  83. package/dist/tracker.min.js +1 -1
  84. package/docs/federation-setup.md +104 -27
  85. package/docs/federation.md +168 -31
  86. package/docs/upgrading.md +191 -16
  87. package/llms.txt +9 -7
  88. package/package.json +2 -1
  89. package/src/component/_generated/api.ts +6 -0
  90. package/src/component/_generated/component.ts +50 -12
  91. package/src/component/constants.ts +8 -1
  92. package/src/component/coverage.ts +46 -6
  93. package/src/component/geo.ts +82 -28
  94. package/src/component/http.ts +62 -26
  95. package/src/component/ingest.ts +378 -440
  96. package/src/component/localTime.ts +167 -0
  97. package/src/component/reports.ts +261 -39
  98. package/src/component/retention.ts +25 -7
  99. package/src/component/rollupStore.ts +799 -0
  100. package/src/component/rollups.ts +82 -0
  101. package/src/component/sanitize.ts +14 -0
  102. package/src/component/schema.ts +35 -0
  103. package/src/component/sites.ts +32 -0
  104. package/src/component/validators.ts +28 -1
  105. package/src/tracker/generated.ts +12 -0
@@ -32,7 +32,18 @@
32
32
  sessionId = createId();
33
33
  affiliate = referral;
34
34
  }
35
- const visitorId = script.dataset.visitor || sessionId;
35
+ // The identity, in order: the host's signed-in id, the persistent
36
+ // pseudonymous id (one random id per browser and site origin, kept in
37
+ // localStorage), and, when storage is blocked, the per-tab session id —
38
+ // which the server then counts through its salted daily hash.
39
+ let visitorId = script.dataset.visitor;
40
+ try {
41
+ visitorId ||= localStorage._rv ||= createId();
42
+ }
43
+ catch {
44
+ // Blocked storage never breaks the page; the session still counts.
45
+ }
46
+ visitorId ||= sessionId;
36
47
  let sequence = 0;
37
48
  let flushTimer;
38
49
  const queue = [];
@@ -124,7 +135,18 @@
124
135
  wrapHistory("pushState");
125
136
  wrapHistory("replaceState");
126
137
  addEventListener("popstate", pageview);
127
- addEventListener("pagehide", () => flush(true));
138
+ addEventListener("pagehide", () => {
139
+ emit("leave");
140
+ flush(true);
141
+ });
142
+ addEventListener("pageshow", (event) => {
143
+ // Back from the bfcache: the leave above already shortened this session's
144
+ // live presence, so revive it immediately instead of waiting a beat.
145
+ if (event.persisted) {
146
+ emit("heartbeat");
147
+ flush(true);
148
+ }
149
+ });
128
150
  setInterval(() => {
129
151
  emit("heartbeat");
130
152
  flush();
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/tracker/tracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/tracker/tracker.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- (()=>{const e=document.currentScript,t=e?.dataset.site,s=e?.dataset.endpoint||e?.src&&new URL("events",e.src).href;if(!t||!s)return;const o=()=>crypto.randomUUID?.()||Math.random()+"",n=new URLSearchParams(location.search),r=n.get("ref")?.slice(0,64);let a,i,c;for(const e of["source","medium","campaign","term","content"]){const t=n.get("utm_"+e)?.slice(0,64);t&&((a??={})["utm_"+e]=t)}try{i=sessionStorage._r||o(),sessionStorage._r=i,c=r||sessionStorage._a,c&&(sessionStorage._a=c),a||=sessionStorage._u&&JSON.parse(sessionStorage._u),a&&(sessionStorage._u=JSON.stringify(a))}catch{i=o(),c=r}const d=e.dataset.visitor||i;let l,p=0;const g=[],u=document.referrer?new URL(document.referrer).origin:void 0,h=e=>{const o=g.splice(0,50);if(!o.length)return;const n=()=>new Blob([JSON.stringify({siteId:t,events:o,sentAt:Date.now()})]);let r=n();for(;r.size>64e3&&o.length>1;)g.unshift(o.pop()),r=n();e&&navigator.sendBeacon(s,r)||fetch(s,{method:"POST",body:r,keepalive:1}).catch(()=>{}),g.length&&h(e)},m=(e,t,s)=>{const o=Date.now();g.push({eventId:`${i}.${o.toString(36)}.${p}`,sessionId:i,visitorId:d,type:e,name:t,path:location.pathname.slice(0,256),referrer:u,timestamp:o,sequence:p++,affiliateSlug:c,...s}),clearTimeout(l),l=setTimeout(h,1200)};let f;const v=()=>{const e=location.pathname;e!==f&&(f=e,m("pageview",void 0,a&&{properties:a}))};window.rastro=(e,t,s)=>{if("context"===e)return{sessionId:i,visitorId:d};"event"===e&&m("custom",`${t||"event"}`.slice(0,80),s&&JSON.stringify(s).length>2e4?void 0:{properties:s})},document.addEventListener("click",e=>{const t=e.target?.closest("a,button,[data-rastro-event]");if(!t)return;const s=t.dataset.rastroEvent,o=t.href,n=o&&new URL(o),r=/^https?:$/.test(n?.protocol),a=r&&n.origin!==location.origin;m(s?"custom":a?"outbound":"click",s,{target:(t.dataset.rastroLabel||t.innerText||t.tagName).slice(0,64),href:r?((a?n.origin:"")+n.pathname).slice(0,256):void 0})},1);const S=e=>{const t=history[e];history[e]=(...e)=>{t.apply(history,e),v()}};S("pushState"),S("replaceState"),addEventListener("popstate",v),addEventListener("pagehide",()=>h(1)),setInterval(()=>{m("heartbeat"),h()},2e4),v()})();
1
+ (()=>{const e=document.currentScript,t=e?.dataset.site,s=e?.dataset.endpoint||e?.src&&new URL("events",e.src).href;if(!t||!s)return;const o=()=>crypto.randomUUID?.()||Math.random()+"",n=new URLSearchParams(location.search),r=n.get("ref")?.slice(0,64);let a,i,c;for(const e of["source","medium","campaign","term","content"]){const t=n.get("utm_"+e)?.slice(0,64);t&&((a??={})["utm_"+e]=t)}try{i=sessionStorage._r||o(),sessionStorage._r=i,c=r||sessionStorage._a,c&&(sessionStorage._a=c),a||=sessionStorage._u&&JSON.parse(sessionStorage._u),a&&(sessionStorage._u=JSON.stringify(a))}catch{i=o(),c=r}let d=e.dataset.visitor;try{d||=localStorage._rv||=o()}catch{}d||=i;let l,p=0;const g=[],h=document.referrer?new URL(document.referrer).origin:void 0,u=e=>{const o=g.splice(0,50);if(!o.length)return;const n=()=>new Blob([JSON.stringify({siteId:t,events:o,sentAt:Date.now()})]);let r=n();for(;r.size>64e3&&o.length>1;)g.unshift(o.pop()),r=n();e&&navigator.sendBeacon(s,r)||fetch(s,{method:"POST",body:r,keepalive:1}).catch(()=>{}),g.length&&u(e)},m=(e,t,s)=>{const o=Date.now();g.push({eventId:`${i}.${o.toString(36)}.${p}`,sessionId:i,visitorId:d,type:e,name:t,path:location.pathname.slice(0,256),referrer:h,timestamp:o,sequence:p++,affiliateSlug:c,...s}),clearTimeout(l),l=setTimeout(u,1200)};let v;const f=()=>{const e=location.pathname;e!==v&&(v=e,m("pageview",void 0,a&&{properties:a}))};window.rastro=(e,t,s)=>{if("context"===e)return{sessionId:i,visitorId:d};"event"===e&&m("custom",`${t||"event"}`.slice(0,80),s&&JSON.stringify(s).length>2e4?void 0:{properties:s})},document.addEventListener("click",e=>{const t=e.target?.closest("a,button,[data-rastro-event]");if(!t)return;const s=t.dataset.rastroEvent,o=t.href,n=o&&new URL(o),r=/^https?:$/.test(n?.protocol),a=r&&n.origin!==location.origin;m(s?"custom":a?"outbound":"click",s,{target:(t.dataset.rastroLabel||t.innerText||t.tagName).slice(0,64),href:r?((a?n.origin:"")+n.pathname).slice(0,256):void 0})},1);const S=e=>{const t=history[e];history[e]=(...e)=>{t.apply(history,e),f()}};S("pushState"),S("replaceState"),addEventListener("popstate",f),addEventListener("pagehide",()=>{m("leave"),u(1)}),addEventListener("pageshow",e=>{e.persisted&&(m("heartbeat"),u(1))}),setInterval(()=>{m("heartbeat"),u()},2e4),f()})();
@@ -10,7 +10,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
10
10
  checkout. Discover the current tag without changing the host:
11
11
 
12
12
  ```bash
13
- npm view @iann29/rastro@alpha version
13
+ npm view @iann29/rastro version
14
14
  ```
15
15
 
16
16
  After the operator approves that exact version, execute the complete static
@@ -37,7 +37,10 @@ Do not configure federation until the customer deployment already has:
37
37
  - at least one site with the correct allowed browser domains;
38
38
  - a tracker using the deployment's HTTP-actions URL;
39
39
  - an accepted `POST /rastro/events` request;
40
- - a protected local report returning that site's telemetry.
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;
41
44
  - an audit through authorized local reports of retained routes, visitor/session
42
45
  IDs, live location, campaigns, affiliates, conversions, and custom properties,
43
46
  proving that no unexpected personal data, credentials, secrets, or form values
@@ -95,19 +98,24 @@ rastroFederationGrants: defineTable({
95
98
  connectionId: v.string(),
96
99
  organizationId: v.string(),
97
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; "analytics:public" lets them publish a
104
+ // read-only public link of those sites.
105
+ permissions: v.optional(v.array(v.string())),
98
106
  revokedAt: v.optional(v.number()),
99
107
  createdAt: v.number(),
100
108
  updatedAt: v.number(),
101
109
  }).index("by_connectionId", ["connectionId"]),
102
110
  ```
103
111
 
104
- The grant, not the JWT, owns the allowed `siteIds`. Add operator-only
105
- provisioning functions:
112
+ The grant, not the JWT, owns the allowed `siteIds` and the permissions the host
113
+ honors. Add operator-only provisioning functions:
106
114
 
107
115
  ```ts
108
116
  // convex/rastroFederationAdmin.ts
109
117
  import { ConvexError, v } from "convex/values";
110
- import { Rastro } from "@iann29/rastro";
118
+ import { federatedAnalyticsPermissionValidator, Rastro } from "@iann29/rastro";
111
119
  import { components } from "./_generated/api";
112
120
  import { internalMutation } from "./_generated/server";
113
121
 
@@ -119,10 +127,12 @@ export const provisionConnection = internalMutation({
119
127
  organizationId: v.string(),
120
128
  ownerId: v.string(),
121
129
  siteIds: v.array(v.string()),
130
+ permissions: v.optional(v.array(federatedAnalyticsPermissionValidator)),
122
131
  },
123
132
  returns: v.null(),
124
133
  handler: async (ctx, args) => {
125
134
  const siteIds = [...new Set(args.siteIds)];
135
+ const permissions = [...new Set(args.permissions ?? [])];
126
136
  if (
127
137
  args.connectionId.length === 0 ||
128
138
  args.connectionId.length > 128 ||
@@ -153,6 +163,7 @@ export const provisionConnection = internalMutation({
153
163
  connectionId: args.connectionId,
154
164
  organizationId: args.organizationId,
155
165
  siteIds,
166
+ permissions: permissions.length ? permissions : undefined,
156
167
  revokedAt: undefined,
157
168
  updatedAt: now,
158
169
  };
@@ -198,6 +209,22 @@ only with deployment administrator credentials. A host that needs self-service
198
209
  pairing may wrap equivalent logic in its own authenticated owner/admin mutation;
199
210
  never accept an owner or user ID from an untrusted caller.
200
211
 
212
+ `permissions` is the opt-in for the configure scope. Leave it out for a
213
+ read-only connection: the dashboard then shows every configuration form but ends
214
+ each one in the code the host runs itself. Pass `["analytics:configure"]` to let
215
+ the dashboard organization's owners and admins create and change goals, funnels,
216
+ affiliates, the site's name, domains and timezone, and the retention policy for
217
+ the granted sites; members of the organization stay read only whatever the grant
218
+ says, because the control plane never puts the configure claim in their tokens.
219
+
220
+ Add `"analytics:public"` to let the organization's owners and admins create
221
+ public links: a page at `https://www.amagerastro.com/#/p/<slug>` that anyone
222
+ opens without a sign-in and that shows only the overview — without revenue,
223
+ conversions, goals, campaigns, or custom events — and the live visitors —
224
+ without visitor identities — of the sites a link names. The dashboard offers the
225
+ form only after a verification records the permission, and removing it from the
226
+ grant silences every link at its next token.
227
+
201
228
  ## 5. Trust the federation issuer
202
229
 
203
230
  The displayed issuer must exactly equal the independently verified production
@@ -231,10 +258,16 @@ Insert only these two properties into the pilot project's existing
231
258
  changing any other `defineApp` option. Do not add a second Rastro mount:
232
259
 
233
260
  ```ts
234
- RASTRO_FEDERATION_ISSUER: v.string(),
235
- RASTRO_FEDERATION_AUDIENCE: v.string(),
261
+ RASTRO_FEDERATION_ISSUER: v.optional(v.string()),
262
+ RASTRO_FEDERATION_AUDIENCE: v.optional(v.string()),
236
263
  ```
237
264
 
265
+ Declare them optional, as the [example host](../example/convex/convex.config.ts)
266
+ does. A required `v.string()` makes the push fail on a deployment where the
267
+ values are not set yet — including every deployment that pushes the code before
268
+ pairing. Set both values first and push afterwards: `auth.config.ts` reads them
269
+ at deploy time, so values set after a push take effect only on the next push.
270
+
238
271
  Insert this conditional spread into the existing `providers` array; never
239
272
  replace the array or its current entries:
240
273
 
@@ -258,7 +291,7 @@ const audience = process.env.RASTRO_FEDERATION_AUDIENCE;
258
291
  An exact issuer mismatch is rejected even when the JWT signature is otherwise
259
292
  valid. Convex discovers keys from `<issuer>/.well-known/openid-configuration`.
260
293
 
261
- ## 6. Expose the read-only host module
294
+ ## 6. Expose the host module
262
295
 
263
296
  Create the canonical module with this exact filename:
264
297
 
@@ -267,12 +300,21 @@ Create the canonical module with this exact filename:
267
300
  import {
268
301
  exposeFederatedAnalyticsApi,
269
302
  type FederatedAnalyticsConnection,
303
+ type FederatedAnalyticsPermission,
270
304
  } from "@iann29/rastro";
271
305
  import { components } from "./_generated/api";
272
306
  import { env, type QueryCtx } from "./_generated/server";
273
307
 
308
+ /** The production Amage Rastro OIDC issuer; override with RASTRO_FEDERATION_ISSUER. */
309
+ export const DEFAULT_FEDERATION_ISSUER =
310
+ "https://site.api.amagerastro.com/federation";
311
+
312
+ export const federationIssuer = (
313
+ env.RASTRO_FEDERATION_ISSUER ?? DEFAULT_FEDERATION_ISSUER
314
+ ).replace(/\/$/, "");
315
+
274
316
  const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
275
- issuer: env.RASTRO_FEDERATION_ISSUER.replace(/\/$/, ""),
317
+ issuer: federationIssuer,
276
318
  resolveConnection: async (ctx, identity) => {
277
319
  const db = ctx.db as unknown as QueryCtx["db"];
278
320
  const grant = await db
@@ -286,6 +328,7 @@ const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
286
328
  connectionId: grant.connectionId,
287
329
  organizationId: grant.organizationId,
288
330
  siteIds: grant.siteIds,
331
+ permissions: grant.permissions as FederatedAnalyticsPermission[],
289
332
  revokedAt: grant.revokedAt,
290
333
  } satisfies FederatedAnalyticsConnection;
291
334
  },
@@ -307,11 +350,33 @@ export const {
307
350
  vitalsReport,
308
351
  siteMap,
309
352
  dataCoverage,
353
+ siteSettings,
354
+ updateSite,
355
+ listGoals,
356
+ upsertGoal,
357
+ removeGoal,
358
+ listFunnels,
359
+ upsertFunnel,
360
+ removeFunnel,
361
+ listAffiliates,
362
+ upsertAffiliate,
363
+ removeAffiliate,
364
+ retentionStatus,
365
+ setRetentionPolicy,
366
+ disableRetentionPolicy,
310
367
  } = federated;
311
368
  ```
312
369
 
313
- The manifest is public static metadata. Every other function is read-only and
314
- requires both a valid JWT and a matching non-revoked local grant.
370
+ The manifest is public static metadata. Every report function is read-only and
371
+ requires both a valid JWT and a matching non-revoked local grant. The functions
372
+ from `siteSettings` down are the configure scope: they also require
373
+ `analytics:configure` on the token and in the grant, and fail with
374
+ `FEDERATION_CONFIGURE_FORBIDDEN` otherwise. A host that keeps every grant
375
+ read-only may omit them from the export; the dashboard never calls them for a
376
+ read-only connection. The issuer falls back to the production control plane so
377
+ the module compiles and pushes before the environment variable exists; tokens
378
+ are still rejected until `auth.config.ts` trusts the same issuer, which requires
379
+ both variables to be set and the deployment pushed afterwards.
315
380
 
316
381
  ## 7. Deploy and provision
317
382
 
@@ -339,7 +404,10 @@ synapse convex --dev run rastroFederationAdmin:provisionConnection \
339
404
  ```
340
405
 
341
406
  Use the corresponding explicit production target only after rehearsing DEV. The
342
- command is idempotent for one `connectionId` and replaces its site scope.
407
+ command is idempotent for one `connectionId` and replaces its site scope and
408
+ permissions. To opt into the configure scope, add
409
+ `"permissions":["analytics:configure"]` to the same JSON; run the command again
410
+ without it to return the connection to read only.
343
411
 
344
412
  ## 8. Verify in order
345
413
 
@@ -351,9 +419,15 @@ command is idempotent for one `connectionId` and replaces its site scope.
351
419
  6. Trigger one real browser pageview and confirm Ao vivo updates reactively.
352
420
  7. Check Metas/Afiliados with a complete UTC-day range when those features are
353
421
  configured.
354
-
355
- The central verification signs a short-lived RS256 token, calls authenticated
356
- `connectionStatus` and `listSites`, and refuses incompatible manifests or
422
+ 8. Confirm the connection card in **Conexões** reads "Somente leitura" or
423
+ "Leitura e configuração" as the grant intends. When it reads "Leitura e
424
+ configuração", create a goal from **Metas** as an owner or admin and confirm
425
+ it appears in the host's `listGoals`; then sign in as a member and confirm
426
+ the same form only offers the code.
427
+
428
+ The central verification signs a short-lived RS256 token with the verifying
429
+ admin's permissions, calls authenticated `connectionStatus` and `listSites`,
430
+ records the permissions the host honors, and refuses incompatible manifests or
357
431
  inconsistent site counts.
358
432
 
359
433
  ## 9. Revoke safely
@@ -380,18 +454,21 @@ the local step, an existing token remains usable until its ten-minute expiry.
380
454
 
381
455
  ## Troubleshooting
382
456
 
383
- | Symptom or code | Check |
384
- | ------------------------------ | ----------------------------------------------------------------------------- |
385
- | Manifest not found | File must be `convex/rastroFederation.ts`; deploy/codegen must have completed |
386
- | `FEDERATION_UNAUTHENTICATED` | Federation provider missing, token absent, or deployment not redeployed |
387
- | `FEDERATION_ISSUER_MISMATCH` | `auth.config.ts` domain and helper issuer differ, often by a trailing slash |
388
- | JWT audience failure | `RASTRO_FEDERATION_AUDIENCE` must equal the functions URL, not the HTTP URL |
389
- | `FEDERATION_CONNECTION_DENIED` | Grant missing, organization mismatched, empty, malformed, or revoked |
390
- | `FEDERATION_SITE_DENIED` | Dashboard requested a site outside the local grant |
391
- | Tracker 404 | Tracker uses the functions URL instead of the HTTP-actions URL |
392
- | `ORIGIN_NOT_ALLOWED` | Browser host is absent from the site's exact/wildcard domains |
393
- | `REPORT_INCOMPLETE` | Use complete UTC buckets or reduce a range that exceeds a bounded read budget |
394
- | `UNTRUSTED_DEPLOYMENT_HOST` | Ask the control-plane operator to allow the exact custom functions origin |
457
+ | Symptom or code | Check |
458
+ | -------------------------------- | ----------------------------------------------------------------------------- |
459
+ | Manifest not found | File must be `convex/rastroFederation.ts`; deploy/codegen must have completed |
460
+ | `FEDERATION_UNAUTHENTICATED` | Federation provider missing, token absent, or deployment not redeployed |
461
+ | `FEDERATION_ISSUER_MISMATCH` | `auth.config.ts` domain and helper issuer differ, often by a trailing slash |
462
+ | JWT audience failure | `RASTRO_FEDERATION_AUDIENCE` must equal the functions URL, not the HTTP URL |
463
+ | `FEDERATION_CONNECTION_DENIED` | Grant missing, organization mismatched, empty, malformed, or revoked |
464
+ | `FEDERATION_SITE_DENIED` | Dashboard requested a site outside the local grant |
465
+ | `FEDERATION_CONFIGURE_FORBIDDEN` | Grant lacks `analytics:configure`, or the viewer is not an owner or admin |
466
+ | `FEDERATION_PUBLIC_FORBIDDEN` | Grant lacks `analytics:public`, or a public link asked beyond its surface |
467
+ | Missing function on configure | Host upgraded the package but did not re-export the configure functions |
468
+ | Tracker 404 | Tracker uses the functions URL instead of the HTTP-actions URL |
469
+ | `ORIGIN_NOT_ALLOWED` | Browser host is absent from the site's exact/wildcard domains |
470
+ | `REPORT_INCOMPLETE` | Use complete UTC buckets or reduce a range that exceeds a bounded read budget |
471
+ | `UNTRUSTED_DEPLOYMENT_HOST` | Ask the control-plane operator to allow the exact custom functions origin |
395
472
 
396
473
  Do not fix authorization errors by weakening the resolver, copying site IDs into
397
474
  JWT claims, or exposing a permissive report API.
@@ -31,7 +31,10 @@ v1 contract:
31
31
  The host module must be `convex/rastroFederation.ts`, so its public references
32
32
  match the function names advertised by the manifest. The generated `manifest`
33
33
  query is public because it contains only static protocol metadata. It does not
34
- resolve a connection or expose customer data.
34
+ resolve a connection or expose customer data. Compare a served manifest with
35
+ `agent/manifest.json`'s `connector` object structurally, never as serialized
36
+ text: the two are the same value with different key order, and a byte-for-byte
37
+ comparison fails on a correct host.
35
38
 
36
39
  The exact generated surface is:
37
40
 
@@ -50,9 +53,23 @@ The exact generated surface is:
50
53
  - `dataCoverage`
51
54
  - `vitalsReport` (optional capability `vitals`, hosts from `alpha.6`)
52
55
  - `siteMap` (optional capability `siteMap`, hosts from `alpha.8`)
53
-
54
- There are no ingestion, configuration-write, retention-write, raw-site, or site
55
- mutation functions in this surface. The read-only goal, funnel, and affiliate
56
+ - the configure scope (optional capability `configure`, hosts from `alpha.11`):
57
+ `siteSettings`, `updateSite`, `listGoals`, `upsertGoal`, `removeGoal`,
58
+ `listFunnels`, `upsertFunnel`, `removeFunnel`, `listAffiliates`,
59
+ `upsertAffiliate`, `removeAffiliate`, `retentionStatus`, `setRetentionPolicy`,
60
+ and `disableRetentionPolicy`
61
+
62
+ Hosts from `alpha.11` also advertise the optional capability `localDays`: their
63
+ `overview` answers a daily range that covers whole calendar days in the site's
64
+ timezone from buckets kept in that zone, and `listSites` says per site which
65
+ zone that is and since when the days are complete. Dashboards must not require
66
+ the capability.
67
+
68
+ There are no ingestion, owner-wide enumeration, or arbitrary host functions in
69
+ this surface, and no write runs with a read-only token: every function of the
70
+ configure scope requires the `analytics:configure` permission on the token
71
+ **and** on the host's local grant, as described under
72
+ [Configure scope](#configure-scope). The read-only goal, funnel, and affiliate
56
73
  reports include their bounded definitions, and `listConversions` returns only
57
74
  the trusted server-side conversion ledger. Browser `conversion` telemetry
58
75
  remains untrusted journey data and never enters that ledger. Journey events
@@ -62,13 +79,87 @@ host `AnalyticsAuthorizer`.
62
79
 
63
80
  `connectionStatus` authenticates and resolves the authoritative connection on
64
81
  every call. Its response is deliberately redacted to status, protocol version,
65
- capabilities, and site count. It does not return connection IDs, organization
66
- IDs, identity claims, or site IDs.
82
+ capabilities, the effective permissions, and site count. It does not return
83
+ connection IDs, organization IDs, identity claims, or site IDs.
67
84
 
68
85
  `listSites` takes no arguments. It loads sites only from the `siteIds` in the
69
86
  validated local connection and returns `FederatedSiteSummary` values containing
70
- only `siteId`, `name`, `currency`, `timezone`, and `cookieless`. In particular,
71
- it never returns `ownerId`, `domains`, or `networkId`.
87
+ only `siteId`, `name`, `currency`, `timezone`, `cookieless`, and (hosts from
88
+ `alpha.11`) `localDays` `{ timezone, since }` when the host keeps the site's
89
+ calendar days, `null` otherwise. In particular, it never returns `ownerId`,
90
+ `domains`, or `networkId`.
91
+
92
+ ## Configure scope
93
+
94
+ Hosts from `alpha.11` advertise the optional `configure` capability. Behind it,
95
+ the dashboard's organization owners and admins can manage goals, funnels,
96
+ affiliates, site settings, and the retention policy of the granted sites without
97
+ a host deploy. The scope is off unless the host opts in:
98
+
99
+ - The control plane mints `rastro_permissions: ["analytics:read"]` for every
100
+ member and adds `"analytics:configure"` only for members whose organization
101
+ role is `owner` or `admin`.
102
+ - The host's local grant lists the permissions it honors. A grant without
103
+ `permissions`, or without `analytics:configure` in it, is read only whatever
104
+ the token claims.
105
+ - A configure function runs only when both agree; otherwise it fails with
106
+ `FEDERATION_CONFIGURE_FORBIDDEN` before the site scope is examined, so a
107
+ read-only token learns nothing about the grant by probing. `connectionStatus`
108
+ reports the effective permissions, and the dashboard renders forms only when
109
+ they include the configure scope; otherwise every form ends in the code the
110
+ host runs itself.
111
+
112
+ The scope is bounded to configuration: `upsertGoal`, `removeGoal`,
113
+ `upsertFunnel`, `removeFunnel`, `upsertAffiliate`, and `removeAffiliate` mirror
114
+ the `Rastro` class, `listGoals`, `listFunnels`, and `listAffiliates` return the
115
+ definitions, `setRetentionPolicy` and `disableRetentionPolicy` manage the
116
+ recurring cleanup that `retentionStatus` reports, and `updateSite` changes only
117
+ `name`, `domains`, and `timezone` — the currency is fixed by the ledger and
118
+ `cookieless`, `networkId`, and `ownerId` stay outside the protocol.
119
+ `siteSettings` is the one place the surface shows a site's `domains`, because a
120
+ connection allowed to change them must see them; `listSites` stays redacted for
121
+ every reader. Nothing in the scope ingests telemetry or enumerates sites beyond
122
+ the grant. The constants `FEDERATED_ANALYTICS_CONFIGURE_PERMISSION`,
123
+ `FEDERATED_ANALYTICS_PERMISSIONS`, `FEDERATED_ANALYTICS_CONFIGURE_FUNCTIONS`,
124
+ the `federatedAnalyticsPermissionValidator`, and `effectiveFederatedPermissions`
125
+ are exported for hosts and connector clients.
126
+
127
+ ## Public links
128
+
129
+ Hosts from `0.1.0` honor a third permission, `analytics:public`, which the
130
+ control plane claims for a **public link**: a read-only page at
131
+ `https://www.amagerastro.com/#/p/<slug>` that anyone holding the address opens
132
+ without a sign-in. Its token carries `rastro_permissions: ["analytics:public"]`
133
+ — never `analytics:read` — and `rastro_site_ids`, the sites the link was created
134
+ for. The host decides twice:
135
+
136
+ - A grant that does not list `analytics:public` refuses the token whole with
137
+ `FEDERATION_PUBLIC_FORBIDDEN`, `connectionStatus` included, so an organization
138
+ cannot publish a host that never agreed. A host from before the permission
139
+ fails the token with `FEDERATION_INVALID_CLAIMS`, which is the safe failure.
140
+ - A public token runs only `manifest`, `connectionStatus`, `listSites`,
141
+ `overview`, `liveVisitors`, and `dataCoverage`
142
+ (`FEDERATED_ANALYTICS_PUBLIC_FUNCTIONS`); every other function, sessions and
143
+ journeys first, fails with `FEDERATION_PUBLIC_FORBIDDEN` before the site scope
144
+ is examined. `overview` answers it with traffic alone — `conversions` and
145
+ `revenueCents` are zero on the totals, the series, and every item, and
146
+ `topCampaigns`, `topMediums`, `topEvents`, and `topGoals` are empty — and
147
+ `liveVisitors` replaces `visitorId` with the session id, since the host may
148
+ have linked the visitor to its own signed-in user id, and drops the intent
149
+ fields (`entryPath`, `returning`, `utmCampaign`, `affiliateSlug`,
150
+ `conversionCount`, `funnel`).
151
+ - `rastro_site_ids` narrows every token kind, a reader's included: the scope is
152
+ the grant's sites the claim also names, `listSites` returns only those, and a
153
+ claim naming none of them is denied with `FEDERATION_SITE_DENIED`.
154
+
155
+ `connectionStatus` reports `permissions: ["analytics:public"]` for such a token.
156
+ The control plane creates links only for organization owners and admins, only on
157
+ a connection whose last verification recorded the host's permission, and revokes
158
+ them without a host deploy; the host's grant remains the authority, so removing
159
+ `analytics:public` from it silences every link at the next token. The permission
160
+ ladder — public below read below configure — is exported as
161
+ `federatedPermissionSatisfies`, with `FEDERATED_ANALYTICS_PUBLIC_PERMISSION`,
162
+ `FEDERATED_ANALYTICS_PUBLIC_FUNCTIONS`, and `isPublicFederatedAccess` alongside.
72
163
 
73
164
  ## Authentication contract
74
165
 
@@ -76,14 +167,17 @@ The customer host must configure Convex authentication to trust the Amage Rastro
76
167
  OIDC issuer. Convex validates the JWT signature, issuer, audience, and expiry
77
168
  before any host function runs. The helper then requires these custom claims:
78
169
 
79
- | Claim | Value |
80
- | ------------------------ | -------------------------------- |
81
- | `rastro_connection_id` | The paired deployment connection |
82
- | `rastro_organization_id` | The Amage Rastro organization |
83
- | `rastro_permissions` | Must include `analytics:read` |
170
+ | Claim | Value |
171
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
172
+ | `rastro_connection_id` | The paired deployment connection |
173
+ | `rastro_organization_id` | The Amage Rastro organization |
174
+ | `rastro_permissions` | Must include `analytics:read`; owners and admins add `analytics:configure`. A public link's token carries `analytics:public` alone |
175
+ | `rastro_site_ids` | Optional: the sites the token may read, a subset of the grant; a public link's token names the sites it was created for |
84
176
 
85
- Claim names and the permission constant are exported as
86
- `FEDERATED_ANALYTICS_CLAIMS` and `FEDERATED_ANALYTICS_READ_PERMISSION`.
177
+ Claim names and the permission constants are exported as
178
+ `FEDERATED_ANALYTICS_CLAIMS`, `FEDERATED_ANALYTICS_READ_PERMISSION`,
179
+ `FEDERATED_ANALYTICS_CONFIGURE_PERMISSION`, and
180
+ `FEDERATED_ANALYTICS_PUBLIC_PERMISSION`.
87
181
 
88
182
  The audience is the customer's normalized Convex deployment URL, for example
89
183
  `https://product-123.convex.cloud`. It is stable across connections to that
@@ -131,6 +225,9 @@ export default defineSchema({
131
225
  connectionId: v.string(),
132
226
  organizationId: v.string(),
133
227
  siteIds: v.array(v.string()),
228
+ // Absent means read only; "analytics:configure" opts into the scope and
229
+ // "analytics:public" lets the organization's public links answer.
230
+ permissions: v.optional(v.array(v.string())),
134
231
  createdAt: v.number(),
135
232
  updatedAt: v.number(),
136
233
  revokedAt: v.optional(v.number()),
@@ -140,7 +237,8 @@ export default defineSchema({
140
237
 
141
238
  Pairing and revocation mutations belong to the host application and must use its
142
239
  existing administrator authorization. The Rastro component cannot inspect host
143
- authentication state.
240
+ authentication state. The grant's `permissions`, like its `siteIds`, are the
241
+ host's decision: a token cannot grant itself the configure scope.
144
242
 
145
243
  ## Host API
146
244
 
@@ -149,12 +247,19 @@ authentication state.
149
247
  import {
150
248
  exposeFederatedAnalyticsApi,
151
249
  type FederatedAnalyticsConnection,
250
+ type FederatedAnalyticsPermission,
152
251
  } from "@iann29/rastro";
153
252
  import { components } from "./_generated/api";
154
253
  import { env, type QueryCtx } from "./_generated/server";
155
254
 
255
+ // RASTRO_FEDERATION_ISSUER is declared optional in convex.config.ts so a push
256
+ // never depends on it; the production issuer is the default.
257
+ const issuer = (
258
+ env.RASTRO_FEDERATION_ISSUER ?? "https://site.api.amagerastro.com/federation"
259
+ ).replace(/\/$/, "");
260
+
156
261
  const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
157
- issuer: env.RASTRO_FEDERATION_ISSUER.replace(/\/$/, ""),
262
+ issuer,
158
263
  resolveConnection: async (ctx, identity) => {
159
264
  const db = ctx.db as unknown as QueryCtx["db"];
160
265
  const grant = await db
@@ -169,6 +274,7 @@ const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
169
274
  connectionId: grant.connectionId,
170
275
  organizationId: grant.organizationId,
171
276
  siteIds: grant.siteIds,
277
+ permissions: grant.permissions as FederatedAnalyticsPermission[],
172
278
  revokedAt: grant.revokedAt,
173
279
  } satisfies FederatedAnalyticsConnection;
174
280
  },
@@ -190,40 +296,65 @@ export const {
190
296
  vitalsReport,
191
297
  siteMap,
192
298
  dataCoverage,
299
+ siteSettings,
300
+ updateSite,
301
+ listGoals,
302
+ upsertGoal,
303
+ removeGoal,
304
+ listFunnels,
305
+ upsertFunnel,
306
+ removeFunnel,
307
+ listAffiliates,
308
+ upsertAffiliate,
309
+ removeAffiliate,
310
+ retentionStatus,
311
+ setRetentionPolicy,
312
+ disableRetentionPolicy,
193
313
  } = federated;
194
314
  ```
195
315
 
196
316
  The resolver receives the authenticated `UserIdentity` as well as the parsed
197
317
  connection and organization IDs, so a host may enforce additional local rules.
198
318
  It must use an indexed, authoritative host lookup rather than token-provided
199
- site IDs.
319
+ site IDs. A host that never lists `analytics:configure` in a grant may leave the
320
+ configure functions out of the export; the manifest still advertises them, and
321
+ the dashboard never calls them for a read-only connection.
200
322
 
201
323
  ## Stable errors
202
324
 
203
325
  `FEDERATED_ANALYTICS_ERROR_CODES` exports all protocol v1 authorization codes:
204
326
 
205
- | Code | Meaning |
206
- | ------------------------------ | -------------------------------------------- |
207
- | `FEDERATION_UNAUTHENTICATED` | No authenticated identity |
208
- | `FEDERATION_ISSUER_MISMATCH` | Identity came from another issuer |
209
- | `FEDERATION_INVALID_CLAIMS` | Required bounded claims or permission absent |
210
- | `FEDERATION_CONNECTION_DENIED` | Connection missing, revoked, or malformed |
211
- | `FEDERATION_INVALID_SCOPE` | Requested site list violates public limits |
212
- | `FEDERATION_SITE_DENIED` | Requested site is outside the connection |
327
+ | Code | Meaning |
328
+ | -------------------------------- | ------------------------------------------------------------------- |
329
+ | `FEDERATION_UNAUTHENTICATED` | No authenticated identity |
330
+ | `FEDERATION_ISSUER_MISMATCH` | Identity came from another issuer |
331
+ | `FEDERATION_INVALID_CLAIMS` | Required bounded claims or permission absent |
332
+ | `FEDERATION_CONNECTION_DENIED` | Connection missing, revoked, or malformed |
333
+ | `FEDERATION_INVALID_SCOPE` | Requested site list violates public limits |
334
+ | `FEDERATION_SITE_DENIED` | Requested site is outside the connection |
335
+ | `FEDERATION_CONFIGURE_FORBIDDEN` | Token or grant lacks `analytics:configure` |
336
+ | `FEDERATION_PUBLIC_FORBIDDEN` | Public token outside its surface, or grant lacks `analytics:public` |
213
337
 
214
338
  Consumers should branch on `ConvexError.data.code`, not human-readable messages.
215
339
 
216
340
  ## Enforced invariants
217
341
 
218
342
  - The identity issuer must match exactly.
219
- - The token must include the read permission.
343
+ - The token must include the read permission, or the public permission alone.
344
+ - A configure function runs only when the token and the grant both carry
345
+ `analytics:configure`; a grant listing an unknown permission is denied whole.
346
+ - A public token runs only the public surface, and only when the grant lists
347
+ `analytics:public`; `rastro_site_ids` narrows every token to the granted sites
348
+ it names.
220
349
  - Report queries must request one through ten unique, bounded site IDs.
221
350
  - The resolved connection ID and organization must match the token.
222
351
  - Revoked, missing, malformed, empty, or duplicate-site connections are denied.
223
352
  - The resolved connection may authorize at most 10 unique site IDs.
224
353
  - Every report site must exist in the local connection scope.
225
354
  - Site discovery trusts only the resolved connection, never caller arguments.
226
- - Owner-wide enumeration, raw site configuration, and every write are absent.
355
+ - Owner-wide enumeration, ingestion, and arbitrary host functions are absent;
356
+ the only writes are the configure scope's, and `listSites` never returns
357
+ `ownerId`, `domains`, or `networkId`.
227
358
  - `dataCoverage` is read-only and uses the same connection-scoped site
228
359
  authorization.
229
360
 
@@ -244,9 +375,15 @@ either overview dataset.
244
375
 
245
376
  The production control plane at `https://www.amagerastro.com` and its reference
246
377
  implementation under `control-plane/` provide Better Auth sessions,
247
- organization-scoped connections, OIDC discovery, JWKS, ten-minute RS256 tokens,
248
- manifest verification, redacted site discovery, audit records, revocation of new
249
- token issuance, and dynamic browser subscriptions to active deployments.
378
+ organization-scoped connections, OIDC discovery, JWKS, ten-minute RS256 tokens
379
+ whose permissions follow the member's organization role, manifest verification,
380
+ redacted site discovery, audit records, revocation of new token issuance, and
381
+ dynamic browser subscriptions to active deployments. Verification records the
382
+ permissions the host honors for an admin token, so **Conexões** shows whether a
383
+ connection is read only, reads and configures, and whether it lets the
384
+ organization publish public links; those links mint `analytics:public` tokens
385
+ for anyone holding the address, without a sign-in and under their own rate
386
+ limits.
250
387
 
251
388
  Do not assume that a dist-tag or source checkout contains this protocol. Inspect
252
389
  an operator-approved exact npm registry artifact outside the host, without