@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.
- package/README.md +274 -75
- package/agent/integration.md +223 -65
- package/agent/manifest.json +34 -13
- package/agent/manifest.schema.json +53 -11
- package/dist/client/federation.d.ts +196 -11
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +160 -6
- package/dist/client/federation.js.map +1 -1
- package/dist/client/heat.d.ts +37 -0
- package/dist/client/heat.d.ts.map +1 -0
- package/dist/client/heat.js +43 -0
- package/dist/client/heat.js.map +1 -0
- package/dist/client/index.d.ts +448 -105
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +207 -17
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +6 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +53 -16
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +5 -1
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +8 -1
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +9 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +36 -6
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/eventStore.d.ts +1 -1
- package/dist/component/geo.d.ts +2 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +56 -19
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +49 -27
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +252 -305
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/localTime.d.ts +25 -0
- package/dist/component/localTime.d.ts.map +1 -0
- package/dist/component/localTime.js +126 -0
- package/dist/component/localTime.js.map +1 -0
- package/dist/component/reports.d.ts +46 -15
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +193 -38
- package/dist/component/reports.js.map +1 -1
- package/dist/component/retention.js +26 -8
- package/dist/component/retention.js.map +1 -1
- package/dist/component/rollupStore.d.ts +320 -0
- package/dist/component/rollupStore.d.ts.map +1 -0
- package/dist/component/rollupStore.js +596 -0
- package/dist/component/rollupStore.js.map +1 -0
- package/dist/component/rollups.d.ts +20 -0
- package/dist/component/rollups.d.ts.map +1 -0
- package/dist/component/rollups.js +73 -0
- package/dist/component/rollups.js.map +1 -0
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +15 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +349 -15
- package/dist/component/schema.js +25 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts +12 -0
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +30 -0
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +41 -20
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +24 -3
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +7 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +7 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +1 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +24 -2
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/federation-setup.md +104 -27
- package/docs/federation.md +168 -31
- package/docs/upgrading.md +191 -16
- package/llms.txt +9 -7
- package/package.json +2 -1
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +50 -12
- package/src/component/constants.ts +8 -1
- package/src/component/coverage.ts +46 -6
- package/src/component/geo.ts +82 -28
- package/src/component/http.ts +62 -26
- package/src/component/ingest.ts +378 -440
- package/src/component/localTime.ts +167 -0
- package/src/component/reports.ts +261 -39
- package/src/component/retention.ts +25 -7
- package/src/component/rollupStore.ts +799 -0
- package/src/component/rollups.ts +82 -0
- package/src/component/sanitize.ts +14 -0
- package/src/component/schema.ts +35 -0
- package/src/component/sites.ts +32 -0
- package/src/component/validators.ts +28 -1
- package/src/tracker/generated.ts +12 -0
package/dist/tracker/tracker.js
CHANGED
|
@@ -32,7 +32,18 @@
|
|
|
32
32
|
sessionId = createId();
|
|
33
33
|
affiliate = referral;
|
|
34
34
|
}
|
|
35
|
-
|
|
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", () =>
|
|
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":";
|
|
1
|
+
{"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/tracker/tracker.ts"],"names":[],"mappings":";AA0CA,CAAC,GAAG,EAAE;IACJ,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAyC,CAAC;IAClE,MAAM,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC;IACpC,MAAM,QAAQ,GACZ,MAAM,EAAE,OAAO,CAAC,QAAQ;QACxB,CAAC,MAAM,EAAE,GAAG,IAAI,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ;QAAE,OAAO;IAEjC,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjD,gFAAgF;IAChF,IAAI,GAAiC,CAAC;IACtC,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;QACtE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,IAAI,KAAK;YAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC;IAChD,CAAC;IACD,IAAI,SAAiB,CAAC;IACtB,IAAI,SAA6B,CAAC;IAClC,IAAI,CAAC;QACH,SAAS,GAAG,cAAc,CAAC,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC5C,cAAc,CAAC,EAAE,GAAG,SAAS,CAAC;QAC9B,SAAS,GAAG,QAAQ,IAAI,cAAc,CAAC,EAAE,CAAC;QAC1C,IAAI,SAAS;YAAE,cAAc,CAAC,EAAE,GAAG,SAAS,CAAC;QAC7C,GAAG,KAAK,cAAc,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QAC3D,IAAI,GAAG;YAAE,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,SAAS,GAAG,QAAQ,EAAE,CAAC;QACvB,SAAS,GAAG,QAAQ,CAAC;IACvB,CAAC;IAED,kEAAkE;IAClE,sEAAsE;IACtE,wEAAwE;IACxE,8DAA8D;IAC9D,IAAI,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;IACvC,IAAI,CAAC;QACH,SAAS,KAAK,YAAY,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;IACrE,CAAC;IACD,SAAS,KAAK,SAAS,CAAC;IACxB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,UAAqD,CAAC;IAC1D,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ;QAChC,CAAC,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM;QACnC,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,KAAK,GAAG,CAAC,MAAgB,EAAE,EAAE;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,OAAO;QAC3B,0EAA0E;QAC1E,yEAAyE;QACzE,MAAM,SAAS,GAAG,GAAG,EAAE,CACrB,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACrE,IAAI,IAAI,GAAG,SAAS,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAG,CAAC,CAAC;YAC7B,IAAI,GAAG,SAAS,EAAE,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;YACrD,KAAK,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CACnE,GAAG,EAAE;gBACH,mEAAmE;YACrE,CAAC,CACF,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,MAAM;YAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,CACX,IAAqB,EACrB,IAAa,EACb,KAA4B,EAC5B,EAAE;QACF,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC;YACT,OAAO,EAAE,GAAG,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,QAAQ,EAAE;YAC7D,SAAS;YACT,SAAS;YACT,IAAI;YACJ,IAAI;YACJ,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;YACrC,QAAQ;YACR,SAAS;YACT,QAAQ,EAAE,QAAQ,EAAE;YACpB,aAAa,EAAE,SAAS;YACxB,GAAG,KAAK;SACT,CAAC,CAAC;QACH,YAAY,CAAC,UAAU,CAAC,CAAC;QACzB,UAAU,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC,CAAC;IAEF,IAAI,QAA4B,CAAC;IACjC,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAC/B,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO;QAC9B,QAAQ,GAAG,IAAI,CAAC;QAChB,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;IAC1D,CAAC,CAAC;IACD,MAAuB,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;QAC/D,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;QAC3D,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,IAAI,CACF,QAAQ,EACR,GAAG,KAAK,IAAI,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAClC,UAAU,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,MAAM;gBACtD,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,EAAE,UAAU,EAAE,CACnB,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;IAEF,QAAQ,CAAC,gBAAgB,CACvB,OAAO,EACP,CAAC,KAAK,EAAE,EAAE;QACR,MAAM,OAAO,GAAI,KAAK,CAAC,MAAyB,EAAE,OAAO,CACvD,8BAA8B,CAC/B,CAAC;QACF,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC;QACzC,MAAM,IAAI,GAAI,OAA6B,CAAC,IAAI,CAAC;QACjD,MAAM,GAAG,GAAG,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAoB,CAAC;QACvD,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE,QAAkB,CAAC,CAAC;QACtD,MAAM,QAAQ,GAAG,GAAG,IAAI,GAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,CAAC;QACxD,wEAAwE;QACxE,qEAAqE;QACrE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE;YAC5D,MAAM,EAAE,CACN,OAAO,CAAC,OAAO,CAAC,WAAW;gBAC3B,OAAO,CAAC,SAAS;gBACjB,OAAO,CAAC,OAAO,CAChB,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;YACd,IAAI,EAAE,GAAG;gBACP,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;gBAC/D,CAAC,CAAC,SAAS;SACd,CAAC,CAAC;IACL,CAAC,EACD,IAAI,CACL,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,MAAoC,EAAE,EAAE;QAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC/B,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,GAAG,IAAI;YACjC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC5B,QAAQ,EAAE,CAAC;QACb,CAAC,CAAC;IACJ,CAAC,CAAC;IACF,WAAW,CAAC,WAAW,CAAC,CAAC;IACzB,WAAW,CAAC,cAAc,CAAC,CAAC;IAC5B,gBAAgB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvC,gBAAgB,CAAC,UAAU,EAAE,GAAG,EAAE;QAChC,IAAI,CAAC,OAAO,CAAC,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,CAAC;IACd,CAAC,CAAC,CAAC;IACH,gBAAgB,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;QACrC,0EAA0E;QAC1E,qEAAqE;QACrE,IAAK,KAA6B,CAAC,SAAS,EAAE,CAAC;YAC7C,IAAI,CAAC,WAAW,CAAC,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,CAAC;QACd,CAAC;IACH,CAAC,CAAC,CAAC;IACH,WAAW,CAAC,GAAG,EAAE;QACf,IAAI,CAAC,WAAW,CAAC,CAAC;QAClB,KAAK,EAAE,CAAC;IACV,CAAC,EAAE,MAAM,CAAC,CAAC;IACX,QAAQ,EAAE,CAAC;AACb,CAAC,CAAC,EAAE,CAAC"}
|
package/dist/tracker.min.js
CHANGED
|
@@ -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}
|
|
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()})();
|
package/docs/federation-setup.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
356
|
-
|
|
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
|
|
384
|
-
|
|
|
385
|
-
| Manifest not found
|
|
386
|
-
| `FEDERATION_UNAUTHENTICATED`
|
|
387
|
-
| `FEDERATION_ISSUER_MISMATCH`
|
|
388
|
-
| JWT audience failure
|
|
389
|
-
| `FEDERATION_CONNECTION_DENIED`
|
|
390
|
-
| `FEDERATION_SITE_DENIED`
|
|
391
|
-
|
|
|
392
|
-
| `
|
|
393
|
-
|
|
|
394
|
-
|
|
|
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.
|
package/docs/federation.md
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
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
|
|
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`,
|
|
71
|
-
|
|
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
|
|
86
|
-
`FEDERATED_ANALYTICS_CLAIMS
|
|
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
|
|
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
|
|
206
|
-
|
|
|
207
|
-
| `FEDERATION_UNAUTHENTICATED`
|
|
208
|
-
| `FEDERATION_ISSUER_MISMATCH`
|
|
209
|
-
| `FEDERATION_INVALID_CLAIMS`
|
|
210
|
-
| `FEDERATION_CONNECTION_DENIED`
|
|
211
|
-
| `FEDERATION_INVALID_SCOPE`
|
|
212
|
-
| `FEDERATION_SITE_DENIED`
|
|
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,
|
|
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
|
-
|
|
249
|
-
|
|
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
|