@iann29/rastro 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +325 -63
- package/agent/integration.md +651 -0
- package/agent/manifest.json +183 -0
- package/agent/manifest.schema.json +405 -0
- package/dist/client/federation.d.ts +205 -0
- package/dist/client/federation.d.ts.map +1 -0
- package/dist/client/federation.js +179 -0
- package/dist/client/federation.js.map +1 -0
- package/dist/client/index.d.ts +1600 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +210 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +9 -5
- 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 +161 -1
- 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/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 +10 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +10 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +7 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +9 -4
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/coverage.d.ts +18 -0
- package/dist/component/coverage.d.ts.map +1 -0
- package/dist/component/coverage.js +19 -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 +11 -9
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/eventStore.js +55 -227
- package/dist/component/eventStore.js.map +1 -1
- package/dist/component/geo.d.ts +71 -0
- package/dist/component/geo.d.ts.map +1 -0
- package/dist/component/geo.js +610 -0
- package/dist/component/geo.js.map +1 -0
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +168 -17
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +272 -32
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +6 -3
- package/dist/component/live.js.map +1 -1
- package/dist/component/reports.d.ts +117 -8
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +641 -79
- 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 +517 -54
- package/dist/component/retention.js.map +1 -1
- package/dist/component/sanitize.d.ts +4 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +9 -4
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +202 -53
- package/dist/component/schema.js +113 -16
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +5 -1
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +29 -51
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +11 -12
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +4 -4
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +4 -4
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.js +7 -6
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/benchmarks/2026-08-20-realistic.md +5 -5
- package/docs/benchmarks/2026-08-21-formal-certification.md +333 -0
- package/docs/federation-setup.md +395 -0
- package/docs/federation.md +258 -0
- package/docs/upgrading.md +130 -0
- package/llms.txt +65 -0
- package/package.json +29 -7
- package/scripts/benchmark-ingest.mjs +81 -32
- package/scripts/generate-federation-keys.mjs +20 -0
- package/src/component/_generated/api.ts +9 -5
- package/src/component/_generated/component.ts +224 -1
- package/src/component/_generated/server.ts +4 -0
- package/src/component/cardinality.ts +116 -0
- package/src/component/constants.ts +10 -0
- package/src/component/convex.config.ts +11 -5
- package/src/component/coverage.ts +25 -0
- package/src/component/diagnostics.ts +64 -0
- package/src/component/errors.ts +2 -1
- package/src/component/eventStore.ts +78 -280
- package/src/component/geo.ts +779 -0
- package/src/component/http.ts +259 -22
- package/src/component/ingest.ts +436 -27
- package/src/component/live.ts +8 -3
- package/src/component/reports.ts +895 -87
- package/src/component/retention.ts +624 -88
- package/src/component/sanitize.ts +15 -4
- package/src/component/schema.ts +125 -16
- package/src/component/sites.ts +5 -1
- package/src/component/validators.ts +12 -13
- package/src/test.ts +0 -2
- package/dist/component/migrations.d.ts +0 -17
- package/dist/component/migrations.d.ts.map +0 -1
- package/dist/component/migrations.js +0 -44
- package/dist/component/migrations.js.map +0 -1
- package/src/component/migrations.ts +0 -52
|
@@ -0,0 +1,395 @@
|
|
|
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.
|
|
41
|
+
- an audit through authorized local reports of retained routes, visitor/session
|
|
42
|
+
IDs, live location, campaigns, affiliates, conversions, and custom properties,
|
|
43
|
+
proving that no unexpected personal data, credentials, secrets, or form values
|
|
44
|
+
will become visible to the intended dashboard organization members and that
|
|
45
|
+
retention is acceptable.
|
|
46
|
+
|
|
47
|
+
The README quickstart covers those steps. Federation only exposes existing
|
|
48
|
+
reports; it does not install the tracker or create sites.
|
|
49
|
+
|
|
50
|
+
## 2. Record both deployment URLs
|
|
51
|
+
|
|
52
|
+
The connection form requires two different base URLs with no path, query, hash,
|
|
53
|
+
username, or password:
|
|
54
|
+
|
|
55
|
+
| Field | Used for | Convex Cloud example | Synapse example |
|
|
56
|
+
| -------------- | --------------------------------- | ------------------------------ | -------------------------------------- |
|
|
57
|
+
| URL de funções | Reactive queries and JWT audience | `https://product.convex.cloud` | `https://product.synapse.example` |
|
|
58
|
+
| URL HTTP | Tracker and event ingestion | `https://product.convex.site` | `https://product.site.synapse.example` |
|
|
59
|
+
|
|
60
|
+
Custom domains are valid when they route to the corresponding Convex surface.
|
|
61
|
+
The control-plane operator must add each custom functions origin to
|
|
62
|
+
`RASTRO_FEDERATION_ALLOWED_DEPLOYMENT_ORIGINS` as an exact, comma-separated
|
|
63
|
+
HTTPS origin before it can be registered. Do not derive one URL by appending
|
|
64
|
+
paths to the other or derive the allowlist from browser input. If the dashboard
|
|
65
|
+
returns `UNTRUSTED_DEPLOYMENT_HOST`, stop and ask the Amage Rastro operator who
|
|
66
|
+
issued the organization access to add the exact functions origin. Provide only
|
|
67
|
+
that public HTTPS origin, never credentials; retry after the operator confirms
|
|
68
|
+
the allowlist update.
|
|
69
|
+
|
|
70
|
+
## 3. Create the pending connection
|
|
71
|
+
|
|
72
|
+
In the central dashboard:
|
|
73
|
+
|
|
74
|
+
1. Sign in and select the intended organization.
|
|
75
|
+
2. Open **Conexões**.
|
|
76
|
+
3. Enter a recognizable name, the functions URL, and the HTTP URL.
|
|
77
|
+
4. Create the connection, but do not verify it yet.
|
|
78
|
+
5. Copy the displayed issuer, audience, connection ID, and organization ID.
|
|
79
|
+
|
|
80
|
+
The audience must exactly equal the normalized functions URL. The connection
|
|
81
|
+
remains pending until its local grant is installed and verification succeeds.
|
|
82
|
+
|
|
83
|
+
## 4. Add the authoritative local grant
|
|
84
|
+
|
|
85
|
+
Before editing, inventory every existing schema table/index, component mount/env
|
|
86
|
+
declaration, and authentication provider. After editing, prove each remains in
|
|
87
|
+
the diff and typechecks. The snippets in this runbook are insertion fragments,
|
|
88
|
+
not replacement files.
|
|
89
|
+
|
|
90
|
+
Insert this table property into the pilot project's existing
|
|
91
|
+
`defineSchema({ ... })` object:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
rastroFederationGrants: defineTable({
|
|
95
|
+
connectionId: v.string(),
|
|
96
|
+
organizationId: v.string(),
|
|
97
|
+
siteIds: v.array(v.string()),
|
|
98
|
+
revokedAt: v.optional(v.number()),
|
|
99
|
+
createdAt: v.number(),
|
|
100
|
+
updatedAt: v.number(),
|
|
101
|
+
}).index("by_connectionId", ["connectionId"]),
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The grant, not the JWT, owns the allowed `siteIds`. Add operator-only
|
|
105
|
+
provisioning functions:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// convex/rastroFederationAdmin.ts
|
|
109
|
+
import { ConvexError, v } from "convex/values";
|
|
110
|
+
import { Rastro } from "@iann29/rastro";
|
|
111
|
+
import { components } from "./_generated/api";
|
|
112
|
+
import { internalMutation } from "./_generated/server";
|
|
113
|
+
|
|
114
|
+
const rastro = new Rastro(components.rastroAnalytics);
|
|
115
|
+
|
|
116
|
+
export const provisionConnection = internalMutation({
|
|
117
|
+
args: {
|
|
118
|
+
connectionId: v.string(),
|
|
119
|
+
organizationId: v.string(),
|
|
120
|
+
ownerId: v.string(),
|
|
121
|
+
siteIds: v.array(v.string()),
|
|
122
|
+
},
|
|
123
|
+
returns: v.null(),
|
|
124
|
+
handler: async (ctx, args) => {
|
|
125
|
+
const siteIds = [...new Set(args.siteIds)];
|
|
126
|
+
if (
|
|
127
|
+
args.connectionId.length === 0 ||
|
|
128
|
+
args.connectionId.length > 128 ||
|
|
129
|
+
args.organizationId.length === 0 ||
|
|
130
|
+
args.organizationId.length > 128 ||
|
|
131
|
+
siteIds.length === 0 ||
|
|
132
|
+
siteIds.length > 10 ||
|
|
133
|
+
siteIds.length !== args.siteIds.length
|
|
134
|
+
) {
|
|
135
|
+
throw new ConvexError({ code: "INVALID_FEDERATION_GRANT" });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const siteId of siteIds) {
|
|
139
|
+
const site = await rastro.getSite(ctx, siteId);
|
|
140
|
+
if (!site || site.ownerId !== args.ownerId) {
|
|
141
|
+
throw new ConvexError({ code: "FEDERATION_SITE_NOT_OWNED" });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const existing = await ctx.db
|
|
146
|
+
.query("rastroFederationGrants")
|
|
147
|
+
.withIndex("by_connectionId", (query) =>
|
|
148
|
+
query.eq("connectionId", args.connectionId),
|
|
149
|
+
)
|
|
150
|
+
.unique();
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const grant = {
|
|
153
|
+
connectionId: args.connectionId,
|
|
154
|
+
organizationId: args.organizationId,
|
|
155
|
+
siteIds,
|
|
156
|
+
revokedAt: undefined,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
};
|
|
159
|
+
if (existing) {
|
|
160
|
+
await ctx.db.patch("rastroFederationGrants", existing._id, grant);
|
|
161
|
+
} else {
|
|
162
|
+
await ctx.db.insert("rastroFederationGrants", {
|
|
163
|
+
...grant,
|
|
164
|
+
createdAt: now,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const revokeConnection = internalMutation({
|
|
172
|
+
args: { connectionId: v.string() },
|
|
173
|
+
returns: v.null(),
|
|
174
|
+
handler: async (ctx, args) => {
|
|
175
|
+
const grant = await ctx.db
|
|
176
|
+
.query("rastroFederationGrants")
|
|
177
|
+
.withIndex("by_connectionId", (query) =>
|
|
178
|
+
query.eq("connectionId", args.connectionId),
|
|
179
|
+
)
|
|
180
|
+
.unique();
|
|
181
|
+
if (!grant) {
|
|
182
|
+
throw new ConvexError({ code: "FEDERATION_GRANT_NOT_FOUND" });
|
|
183
|
+
}
|
|
184
|
+
if (grant.revokedAt === undefined) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
await ctx.db.patch("rastroFederationGrants", grant._id, {
|
|
187
|
+
revokedAt: now,
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
These mutations are `internalMutation`s so a browser cannot call them. Run them
|
|
197
|
+
only with deployment administrator credentials. A host that needs self-service
|
|
198
|
+
pairing may wrap equivalent logic in its own authenticated owner/admin mutation;
|
|
199
|
+
never accept an owner or user ID from an untrusted caller.
|
|
200
|
+
|
|
201
|
+
## 5. Trust the federation issuer
|
|
202
|
+
|
|
203
|
+
The displayed issuer must exactly equal the independently verified production
|
|
204
|
+
manifest issuer, `https://site.api.amagerastro.com/federation`; abort on any
|
|
205
|
+
mismatch. Derive the audience locally from the normalized functions URL and
|
|
206
|
+
compare it with the displayed audience. For Convex Cloud DEV:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npx convex env set --deployment dev RASTRO_FEDERATION_ISSUER \
|
|
210
|
+
'https://site.api.amagerastro.com/federation'
|
|
211
|
+
npx convex env set --deployment dev RASTRO_FEDERATION_AUDIENCE \
|
|
212
|
+
'https://product.convex.cloud'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Use `--deployment prod` only with fresh production consent. For an existing
|
|
216
|
+
Synapse-managed deployment, target its live Convex environment explicitly:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
synapse convex --dev env set \
|
|
220
|
+
RASTRO_FEDERATION_ISSUER 'https://site.api.amagerastro.com/federation'
|
|
221
|
+
synapse convex --dev env set \
|
|
222
|
+
RASTRO_FEDERATION_AUDIENCE 'https://product.synapse.example'
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Use `synapse env set NAME=value --for=dev` only when defining project defaults
|
|
226
|
+
before a new deployment is created; defaults do not update an existing live
|
|
227
|
+
deployment.
|
|
228
|
+
|
|
229
|
+
Insert only these two properties into the pilot project's existing
|
|
230
|
+
`defineApp({ env: { ... } })` object. If no `env` object exists, add it without
|
|
231
|
+
changing any other `defineApp` option. Do not add a second Rastro mount:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
RASTRO_FEDERATION_ISSUER: v.string(),
|
|
235
|
+
RASTRO_FEDERATION_AUDIENCE: v.string(),
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Insert this conditional spread into the existing `providers` array; never
|
|
239
|
+
replace the array or its current entries:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// convex/auth.config.ts
|
|
243
|
+
import type { AuthConfig } from "convex/server";
|
|
244
|
+
|
|
245
|
+
const issuer = process.env.RASTRO_FEDERATION_ISSUER;
|
|
246
|
+
const audience = process.env.RASTRO_FEDERATION_AUDIENCE;
|
|
247
|
+
|
|
248
|
+
...(issuer && audience
|
|
249
|
+
? [
|
|
250
|
+
{
|
|
251
|
+
domain: issuer.replace(/\/$/, ""),
|
|
252
|
+
applicationID: audience.replace(/\/$/, ""),
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: []),
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
An exact issuer mismatch is rejected even when the JWT signature is otherwise
|
|
259
|
+
valid. Convex discovers keys from `<issuer>/.well-known/openid-configuration`.
|
|
260
|
+
|
|
261
|
+
## 6. Expose the read-only host module
|
|
262
|
+
|
|
263
|
+
Create the canonical module with this exact filename:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// convex/rastroFederation.ts
|
|
267
|
+
import {
|
|
268
|
+
exposeFederatedAnalyticsApi,
|
|
269
|
+
type FederatedAnalyticsConnection,
|
|
270
|
+
} from "@iann29/rastro";
|
|
271
|
+
import { components } from "./_generated/api";
|
|
272
|
+
import { env, type QueryCtx } from "./_generated/server";
|
|
273
|
+
|
|
274
|
+
const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
|
|
275
|
+
issuer: env.RASTRO_FEDERATION_ISSUER.replace(/\/$/, ""),
|
|
276
|
+
resolveConnection: async (ctx, identity) => {
|
|
277
|
+
const db = ctx.db as unknown as QueryCtx["db"];
|
|
278
|
+
const grant = await db
|
|
279
|
+
.query("rastroFederationGrants")
|
|
280
|
+
.withIndex("by_connectionId", (query) =>
|
|
281
|
+
query.eq("connectionId", identity.connectionId),
|
|
282
|
+
)
|
|
283
|
+
.unique();
|
|
284
|
+
if (!grant || grant.organizationId !== identity.organizationId) return null;
|
|
285
|
+
return {
|
|
286
|
+
connectionId: grant.connectionId,
|
|
287
|
+
organizationId: grant.organizationId,
|
|
288
|
+
siteIds: grant.siteIds,
|
|
289
|
+
revokedAt: grant.revokedAt,
|
|
290
|
+
} satisfies FederatedAnalyticsConnection;
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export const {
|
|
295
|
+
manifest,
|
|
296
|
+
connectionStatus,
|
|
297
|
+
listSites,
|
|
298
|
+
overview,
|
|
299
|
+
liveVisitors,
|
|
300
|
+
listSessions,
|
|
301
|
+
sessionJourney,
|
|
302
|
+
listConversions,
|
|
303
|
+
visitorJourney,
|
|
304
|
+
goalsReport,
|
|
305
|
+
funnelsReport,
|
|
306
|
+
affiliatesReport,
|
|
307
|
+
dataCoverage,
|
|
308
|
+
} = federated;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The manifest is public static metadata. Every other function is read-only and
|
|
312
|
+
requires both a valid JWT and a matching non-revoked local grant.
|
|
313
|
+
|
|
314
|
+
## 7. Deploy and provision
|
|
315
|
+
|
|
316
|
+
Push the customer host changes first:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Convex Cloud DEV, one push
|
|
320
|
+
npx convex dev --once
|
|
321
|
+
|
|
322
|
+
# Synapse DEV
|
|
323
|
+
synapse dev --once
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Then provision the exact site scope. Replace every placeholder with values
|
|
327
|
+
copied from the pending connection and the host's existing site records:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Convex Cloud DEV
|
|
331
|
+
npx convex run --deployment dev rastroFederationAdmin:provisionConnection \
|
|
332
|
+
'{"connectionId":"CONNECTION_ID","organizationId":"ORGANIZATION_ID","ownerId":"OPAQUE_HOST_OWNER_ID","siteIds":["SITE_ID"]}'
|
|
333
|
+
|
|
334
|
+
# Synapse DEV
|
|
335
|
+
synapse convex --dev run rastroFederationAdmin:provisionConnection \
|
|
336
|
+
'{"connectionId":"CONNECTION_ID","organizationId":"ORGANIZATION_ID","ownerId":"OPAQUE_HOST_OWNER_ID","siteIds":["SITE_ID"]}'
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Use the corresponding explicit production target only after rehearsing DEV. The
|
|
340
|
+
command is idempotent for one `connectionId` and replaces its site scope.
|
|
341
|
+
|
|
342
|
+
## 8. Verify in order
|
|
343
|
+
|
|
344
|
+
1. Run `rastroFederation:manifest` without auth and confirm protocol version 1.
|
|
345
|
+
2. Return to **Conexões** in the central dashboard.
|
|
346
|
+
3. Click **Verificar conexão**.
|
|
347
|
+
4. Confirm the connection becomes active and reports the expected site count.
|
|
348
|
+
5. Open Visão geral and confirm the deployment selector names the new host.
|
|
349
|
+
6. Trigger one real browser pageview and confirm Ao vivo updates reactively.
|
|
350
|
+
7. Check Metas/Afiliados with a complete UTC-day range when those features are
|
|
351
|
+
configured.
|
|
352
|
+
|
|
353
|
+
The central verification signs a short-lived RS256 token, calls authenticated
|
|
354
|
+
`connectionStatus` and `listSites`, and refuses incompatible manifests or
|
|
355
|
+
inconsistent site counts.
|
|
356
|
+
|
|
357
|
+
## 9. Revoke safely
|
|
358
|
+
|
|
359
|
+
For immediate denial, revoke the local grant first:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npx convex run --deployment dev rastroFederationAdmin:revokeConnection \
|
|
363
|
+
'{"connectionId":"CONNECTION_ID"}'
|
|
364
|
+
|
|
365
|
+
# Synapse DEV
|
|
366
|
+
synapse convex --dev run rastroFederationAdmin:revokeConnection \
|
|
367
|
+
'{"connectionId":"CONNECTION_ID"}'
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For production, repeat the rehearsed command with the platform's explicit
|
|
371
|
+
production target: `npx convex run --deployment prod ...` or
|
|
372
|
+
`synapse convex --prod run ...`. Never infer the target from the current shell
|
|
373
|
+
state.
|
|
374
|
+
|
|
375
|
+
Then revoke the connection in the central dashboard. Central revocation stops
|
|
376
|
+
new tokens; local revocation rejects already-issued tokens immediately. Without
|
|
377
|
+
the local step, an existing token remains usable until its ten-minute expiry.
|
|
378
|
+
|
|
379
|
+
## Troubleshooting
|
|
380
|
+
|
|
381
|
+
| Symptom or code | Check |
|
|
382
|
+
| ------------------------------ | ----------------------------------------------------------------------------- |
|
|
383
|
+
| Manifest not found | File must be `convex/rastroFederation.ts`; deploy/codegen must have completed |
|
|
384
|
+
| `FEDERATION_UNAUTHENTICATED` | Federation provider missing, token absent, or deployment not redeployed |
|
|
385
|
+
| `FEDERATION_ISSUER_MISMATCH` | `auth.config.ts` domain and helper issuer differ, often by a trailing slash |
|
|
386
|
+
| JWT audience failure | `RASTRO_FEDERATION_AUDIENCE` must equal the functions URL, not the HTTP URL |
|
|
387
|
+
| `FEDERATION_CONNECTION_DENIED` | Grant missing, organization mismatched, empty, malformed, or revoked |
|
|
388
|
+
| `FEDERATION_SITE_DENIED` | Dashboard requested a site outside the local grant |
|
|
389
|
+
| Tracker 404 | Tracker uses the functions URL instead of the HTTP-actions URL |
|
|
390
|
+
| `ORIGIN_NOT_ALLOWED` | Browser host is absent from the site's exact/wildcard domains |
|
|
391
|
+
| `REPORT_INCOMPLETE` | Use complete UTC buckets or reduce a range that exceeds a bounded read budget |
|
|
392
|
+
| `UNTRUSTED_DEPLOYMENT_HOST` | Ask the control-plane operator to allow the exact custom functions origin |
|
|
393
|
+
|
|
394
|
+
Do not fix authorization errors by weakening the resolver, copying site IDs into
|
|
395
|
+
JWT claims, or exposing a permissive report API.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Federated Amage Rastro dashboard
|
|
2
|
+
|
|
3
|
+
This document is the protocol reference. Follow
|
|
4
|
+
[`federation-setup.md`](federation-setup.md) for the executable customer-host
|
|
5
|
+
installation, pairing, verification, and revocation sequence.
|
|
6
|
+
|
|
7
|
+
The federated dashboard model keeps telemetry in the customer's Convex or
|
|
8
|
+
Synapse deployment while a browser loaded from `amagerastro.com` subscribes
|
|
9
|
+
directly to authorized host functions in that deployment.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
tracker -> customer deployment -> Rastro component tables
|
|
13
|
+
|
|
14
|
+
amagerastro.com browser -> customer deployment -> reactive Rastro reports
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The Amage Rastro control plane stores account and connection metadata. It does
|
|
18
|
+
not need to proxy or persist the customer's analytics events.
|
|
19
|
+
|
|
20
|
+
## Connector protocol v1
|
|
21
|
+
|
|
22
|
+
The typed `FEDERATED_ANALYTICS_CONNECTOR_MANIFEST` constant defines the stable
|
|
23
|
+
v1 contract:
|
|
24
|
+
|
|
25
|
+
- protocol: `amage-rastro-analytics`
|
|
26
|
+
- protocol version: `1`
|
|
27
|
+
- canonical host module: `rastroFederation`
|
|
28
|
+
- canonical function names and supported capabilities
|
|
29
|
+
- public request, connection, range, live visitor, and journey limits
|
|
30
|
+
|
|
31
|
+
The host module must be `convex/rastroFederation.ts`, so its public references
|
|
32
|
+
match the function names advertised by the manifest. The generated `manifest`
|
|
33
|
+
query is public because it contains only static protocol metadata. It does not
|
|
34
|
+
resolve a connection or expose customer data.
|
|
35
|
+
|
|
36
|
+
The exact generated surface is:
|
|
37
|
+
|
|
38
|
+
- `manifest`
|
|
39
|
+
- `connectionStatus`
|
|
40
|
+
- `listSites`
|
|
41
|
+
- `overview`
|
|
42
|
+
- `liveVisitors`
|
|
43
|
+
- `listSessions`
|
|
44
|
+
- `sessionJourney`
|
|
45
|
+
- `listConversions`
|
|
46
|
+
- `visitorJourney`
|
|
47
|
+
- `goalsReport`
|
|
48
|
+
- `funnelsReport`
|
|
49
|
+
- `affiliatesReport`
|
|
50
|
+
- `dataCoverage`
|
|
51
|
+
|
|
52
|
+
There are no ingestion, configuration-write, retention-write, raw-site, or site
|
|
53
|
+
mutation functions in this surface. The read-only goal, funnel, and affiliate
|
|
54
|
+
reports include their bounded definitions, and `listConversions` returns only
|
|
55
|
+
the trusted server-side conversion ledger. Browser `conversion` telemetry
|
|
56
|
+
remains untrusted journey data and never enters that ledger. Journey events
|
|
57
|
+
include bounded custom properties. `exposeFederatedAnalyticsApi` accepts only
|
|
58
|
+
`FederatedAnalyticsAuthorizerOptions`; it does not accept or expose an arbitrary
|
|
59
|
+
host `AnalyticsAuthorizer`.
|
|
60
|
+
|
|
61
|
+
`connectionStatus` authenticates and resolves the authoritative connection on
|
|
62
|
+
every call. Its response is deliberately redacted to status, protocol version,
|
|
63
|
+
capabilities, and site count. It does not return connection IDs, organization
|
|
64
|
+
IDs, identity claims, or site IDs.
|
|
65
|
+
|
|
66
|
+
`listSites` takes no arguments. It loads sites only from the `siteIds` in the
|
|
67
|
+
validated local connection and returns `FederatedSiteSummary` values containing
|
|
68
|
+
only `siteId`, `name`, `currency`, `timezone`, and `cookieless`. In particular,
|
|
69
|
+
it never returns `ownerId`, `domains`, or `networkId`.
|
|
70
|
+
|
|
71
|
+
## Authentication contract
|
|
72
|
+
|
|
73
|
+
The customer host must configure Convex authentication to trust the Amage Rastro
|
|
74
|
+
OIDC issuer. Convex validates the JWT signature, issuer, audience, and expiry
|
|
75
|
+
before any host function runs. The helper then requires these custom claims:
|
|
76
|
+
|
|
77
|
+
| Claim | Value |
|
|
78
|
+
| ------------------------ | -------------------------------- |
|
|
79
|
+
| `rastro_connection_id` | The paired deployment connection |
|
|
80
|
+
| `rastro_organization_id` | The Amage Rastro organization |
|
|
81
|
+
| `rastro_permissions` | Must include `analytics:read` |
|
|
82
|
+
|
|
83
|
+
Claim names and the permission constant are exported as
|
|
84
|
+
`FEDERATED_ANALYTICS_CLAIMS` and `FEDERATED_ANALYTICS_READ_PERMISSION`.
|
|
85
|
+
|
|
86
|
+
The audience is the customer's normalized Convex deployment URL, for example
|
|
87
|
+
`https://product-123.convex.cloud`. It is stable across connections to that
|
|
88
|
+
deployment; `rastro_connection_id` identifies the individual grant. A customer
|
|
89
|
+
host configures the provider with the issuer and audience issued during pairing:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// convex/auth.config.ts
|
|
93
|
+
import type { AuthConfig } from "convex/server";
|
|
94
|
+
|
|
95
|
+
export default {
|
|
96
|
+
providers: [
|
|
97
|
+
// Keep the application's existing providers here.
|
|
98
|
+
{
|
|
99
|
+
domain: process.env.RASTRO_FEDERATION_ISSUER!,
|
|
100
|
+
applicationID: process.env.RASTRO_FEDERATION_AUDIENCE!,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
} satisfies AuthConfig;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`RASTRO_FEDERATION_AUDIENCE` must exactly match the deployment URL registered in
|
|
107
|
+
the control plane. Insert the provider object into the host's existing
|
|
108
|
+
`providers` array; the complete insertion fragment is in
|
|
109
|
+
[`federation-setup.md`](federation-setup.md). Never replace existing providers.
|
|
110
|
+
The example control plane publishes OIDC discovery and JWKS endpoints under
|
|
111
|
+
`/federation`, signs RS256 tokens with a ten-minute lifetime, and never sends
|
|
112
|
+
its private key to the browser.
|
|
113
|
+
|
|
114
|
+
## Authoritative local connection
|
|
115
|
+
|
|
116
|
+
JWT claims identify a requested connection but do not define its site access.
|
|
117
|
+
The customer deployment resolves a local connection record on every
|
|
118
|
+
authenticated query. This record is authoritative and makes revocation immediate
|
|
119
|
+
instead of waiting for a token to expire.
|
|
120
|
+
|
|
121
|
+
A host schema can model it as:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
125
|
+
import { v } from "convex/values";
|
|
126
|
+
|
|
127
|
+
export default defineSchema({
|
|
128
|
+
rastroFederationGrants: defineTable({
|
|
129
|
+
connectionId: v.string(),
|
|
130
|
+
organizationId: v.string(),
|
|
131
|
+
siteIds: v.array(v.string()),
|
|
132
|
+
createdAt: v.number(),
|
|
133
|
+
updatedAt: v.number(),
|
|
134
|
+
revokedAt: v.optional(v.number()),
|
|
135
|
+
}).index("by_connectionId", ["connectionId"]),
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Pairing and revocation mutations belong to the host application and must use its
|
|
140
|
+
existing administrator authorization. The Rastro component cannot inspect host
|
|
141
|
+
authentication state.
|
|
142
|
+
|
|
143
|
+
## Host API
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// convex/rastroFederation.ts
|
|
147
|
+
import {
|
|
148
|
+
exposeFederatedAnalyticsApi,
|
|
149
|
+
type FederatedAnalyticsConnection,
|
|
150
|
+
} from "@iann29/rastro";
|
|
151
|
+
import { components } from "./_generated/api";
|
|
152
|
+
import { env, type QueryCtx } from "./_generated/server";
|
|
153
|
+
|
|
154
|
+
const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
|
|
155
|
+
issuer: env.RASTRO_FEDERATION_ISSUER.replace(/\/$/, ""),
|
|
156
|
+
resolveConnection: async (ctx, identity) => {
|
|
157
|
+
const db = ctx.db as unknown as QueryCtx["db"];
|
|
158
|
+
const grant = await db
|
|
159
|
+
.query("rastroFederationGrants")
|
|
160
|
+
.withIndex("by_connectionId", (query) =>
|
|
161
|
+
query.eq("connectionId", identity.connectionId),
|
|
162
|
+
)
|
|
163
|
+
.unique();
|
|
164
|
+
|
|
165
|
+
if (!grant || grant.organizationId !== identity.organizationId) return null;
|
|
166
|
+
return {
|
|
167
|
+
connectionId: grant.connectionId,
|
|
168
|
+
organizationId: grant.organizationId,
|
|
169
|
+
siteIds: grant.siteIds,
|
|
170
|
+
revokedAt: grant.revokedAt,
|
|
171
|
+
} satisfies FederatedAnalyticsConnection;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const {
|
|
176
|
+
manifest,
|
|
177
|
+
connectionStatus,
|
|
178
|
+
listSites,
|
|
179
|
+
overview,
|
|
180
|
+
liveVisitors,
|
|
181
|
+
listSessions,
|
|
182
|
+
sessionJourney,
|
|
183
|
+
listConversions,
|
|
184
|
+
visitorJourney,
|
|
185
|
+
goalsReport,
|
|
186
|
+
funnelsReport,
|
|
187
|
+
affiliatesReport,
|
|
188
|
+
dataCoverage,
|
|
189
|
+
} = federated;
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The resolver receives the authenticated `UserIdentity` as well as the parsed
|
|
193
|
+
connection and organization IDs, so a host may enforce additional local rules.
|
|
194
|
+
It must use an indexed, authoritative host lookup rather than token-provided
|
|
195
|
+
site IDs.
|
|
196
|
+
|
|
197
|
+
## Stable errors
|
|
198
|
+
|
|
199
|
+
`FEDERATED_ANALYTICS_ERROR_CODES` exports all protocol v1 authorization codes:
|
|
200
|
+
|
|
201
|
+
| Code | Meaning |
|
|
202
|
+
| ------------------------------ | -------------------------------------------- |
|
|
203
|
+
| `FEDERATION_UNAUTHENTICATED` | No authenticated identity |
|
|
204
|
+
| `FEDERATION_ISSUER_MISMATCH` | Identity came from another issuer |
|
|
205
|
+
| `FEDERATION_INVALID_CLAIMS` | Required bounded claims or permission absent |
|
|
206
|
+
| `FEDERATION_CONNECTION_DENIED` | Connection missing, revoked, or malformed |
|
|
207
|
+
| `FEDERATION_INVALID_SCOPE` | Requested site list violates public limits |
|
|
208
|
+
| `FEDERATION_SITE_DENIED` | Requested site is outside the connection |
|
|
209
|
+
|
|
210
|
+
Consumers should branch on `ConvexError.data.code`, not human-readable messages.
|
|
211
|
+
|
|
212
|
+
## Enforced invariants
|
|
213
|
+
|
|
214
|
+
- The identity issuer must match exactly.
|
|
215
|
+
- The token must include the read permission.
|
|
216
|
+
- Report queries must request one through ten unique, bounded site IDs.
|
|
217
|
+
- The resolved connection ID and organization must match the token.
|
|
218
|
+
- Revoked, missing, malformed, empty, or duplicate-site connections are denied.
|
|
219
|
+
- The resolved connection may authorize at most 10 unique site IDs.
|
|
220
|
+
- Every report site must exist in the local connection scope.
|
|
221
|
+
- Site discovery trusts only the resolved connection, never caller arguments.
|
|
222
|
+
- Owner-wide enumeration, raw site configuration, and every write are absent.
|
|
223
|
+
- `dataCoverage` is read-only and uses the same connection-scoped site
|
|
224
|
+
authorization.
|
|
225
|
+
|
|
226
|
+
`dataCoverage` accepts one authorized `siteId` and an inclusive integer
|
|
227
|
+
millisecond `from`/`to` range. It returns availability, rollup generation, and
|
|
228
|
+
retention boundaries for each dashboard dataset, but no events, visitor IDs,
|
|
229
|
+
session IDs, or site configuration. Dashboard clients should use it to explain
|
|
230
|
+
`complete`, `partial`, `retained`, and `unavailable` states rather than treating
|
|
231
|
+
an empty report as proof that no telemetry exists.
|
|
232
|
+
|
|
233
|
+
Availability is derived from bounded indexed source reads rather than a shared
|
|
234
|
+
per-ingest watermark. The dataset list distinguishes `overviewHour` from
|
|
235
|
+
`overviewDay`; clients must not apply one granularity's retention boundary to
|
|
236
|
+
the other. Heartbeats advance `sessions` availability only, not `events` or
|
|
237
|
+
either overview dataset.
|
|
238
|
+
|
|
239
|
+
## Control-plane status
|
|
240
|
+
|
|
241
|
+
The production control plane at `https://www.amagerastro.com` and its reference
|
|
242
|
+
implementation under `example/` provide Better Auth sessions,
|
|
243
|
+
organization-scoped connections, OIDC discovery, JWKS, ten-minute RS256 tokens,
|
|
244
|
+
manifest verification, redacted site discovery, audit records, revocation of new
|
|
245
|
+
token issuance, and dynamic browser subscriptions to active deployments.
|
|
246
|
+
|
|
247
|
+
Do not assume that a dist-tag or source checkout contains this protocol. Inspect
|
|
248
|
+
an operator-approved exact npm registry artifact outside the host, without
|
|
249
|
+
lifecycle scripts, and statically verify every runtime and declaration export
|
|
250
|
+
listed by the machine manifest before changing a customer deployment. Automation
|
|
251
|
+
must stop at this gate rather than fetching source or reimplementing the
|
|
252
|
+
connector.
|
|
253
|
+
|
|
254
|
+
Remote local-grant provisioning is intentionally not an unauthenticated control-
|
|
255
|
+
plane write. The customer operator provisions the authoritative grant through an
|
|
256
|
+
internal or host-admin mutation. For immediate revocation, revoke that local
|
|
257
|
+
grant before revoking the central connection; otherwise an already-issued token
|
|
258
|
+
remains valid until its ten-minute expiry.
|