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