@iann29/rastro 0.1.0-alpha.9 → 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 +246 -57
- 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 +441 -98
- 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 +50 -13
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +3 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +6 -0
- 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/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 +48 -27
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +223 -304
- 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 +44 -13
- 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 +345 -11
- 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 +34 -13
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +23 -2
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +3 -0
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +3 -0
- package/dist/tracker/generated.js.map +1 -1
- package/docs/federation-setup.md +104 -27
- package/docs/federation.md +168 -31
- package/docs/upgrading.md +158 -21
- package/llms.txt +8 -6
- package/package.json +2 -1
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +47 -12
- package/src/component/constants.ts +6 -0
- package/src/component/coverage.ts +46 -6
- package/src/component/geo.ts +82 -28
- package/src/component/http.ts +61 -26
- package/src/component/ingest.ts +327 -439
- 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 +27 -1
- package/src/tracker/generated.ts +12 -0
package/agent/integration.md
CHANGED
|
@@ -17,11 +17,15 @@ not which mutable tag currently contains them.
|
|
|
17
17
|
Inspect the tag without changing the host:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm view @iann29/rastro
|
|
20
|
+
npm view @iann29/rastro version
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
After the operator approves that exact version, inspect it outside the host
|
|
24
|
-
without executing package code or lifecycle scripts
|
|
24
|
+
without executing package code or lifecycle scripts. Run the block from a
|
|
25
|
+
directory whose `node_modules` resolves `typescript` — the host checkout after
|
|
26
|
+
`npm install` is the natural place. From anywhere else the inspector exits with
|
|
27
|
+
`ERR_MODULE_NOT_FOUND` for `typescript` and `npx --no-install tsc` finds no
|
|
28
|
+
compiler; neither outcome is a verdict about the package.
|
|
25
29
|
|
|
26
30
|
```bash
|
|
27
31
|
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
|
@@ -244,7 +248,7 @@ repository commit.
|
|
|
244
248
|
```text
|
|
245
249
|
product browser -> customer HTTP-actions URL -> customer Rastro tables
|
|
246
250
|
|
|
247
|
-
amagerastro.com browser -> customer functions URL ->
|
|
251
|
+
amagerastro.com browser -> customer functions URL -> reports (+ opt-in configure)
|
|
248
252
|
^
|
|
249
253
|
|
|
|
250
254
|
short-lived control-plane JWT
|
|
@@ -316,7 +320,11 @@ Confirm local analytics first:
|
|
|
316
320
|
2. The site exists and its domains include the exact product hostname.
|
|
317
321
|
3. The tracker loads from `<HTTP_ACTIONS_URL>/rastro/tracker.js`.
|
|
318
322
|
4. A real `POST /rastro/events` returns HTTP 200 with accepted counters.
|
|
319
|
-
5. An authenticated host query can read a report for that exact site.
|
|
323
|
+
5. An authenticated host query can read a report for that exact site. Check with
|
|
324
|
+
`liveVisitors` or `sessionJourney`, which reflect an accepted request
|
|
325
|
+
immediately; `listSessions` and the bucketed `overview` can trail it by
|
|
326
|
+
seconds, so an empty result from those two right after the request is not a
|
|
327
|
+
failed ingestion.
|
|
320
328
|
6. Retained routes, IDs, campaigns, affiliates, location fields, conversion
|
|
321
329
|
records, and custom properties contain no unexpected personal data, access
|
|
322
330
|
tokens, secrets, or form values. Confirm that every intended organization
|
|
@@ -347,6 +355,7 @@ rastroFederationGrants: defineTable({
|
|
|
347
355
|
connectionId: v.string(),
|
|
348
356
|
organizationId: v.string(),
|
|
349
357
|
siteIds: v.array(v.string()),
|
|
358
|
+
permissions: v.optional(v.array(v.string())),
|
|
350
359
|
revokedAt: v.optional(v.number()),
|
|
351
360
|
createdAt: v.number(),
|
|
352
361
|
updatedAt: v.number(),
|
|
@@ -356,7 +365,12 @@ rastroFederationGrants: defineTable({
|
|
|
356
365
|
Create `convex/rastroFederationAdmin.ts` with internal or equivalently protected
|
|
357
366
|
operator functions that:
|
|
358
367
|
|
|
359
|
-
- accept a connection ID, organization ID, opaque host owner ID,
|
|
368
|
+
- accept a connection ID, organization ID, opaque host owner ID, site IDs, and
|
|
369
|
+
an optional `permissions` list validated with
|
|
370
|
+
`federatedAnalyticsPermissionValidator` (absent means read only;
|
|
371
|
+
`"analytics:configure"` opts the connection into the configure scope, and
|
|
372
|
+
`"analytics:public"` lets the dashboard publish read-only public links of the
|
|
373
|
+
granted sites);
|
|
360
374
|
- reject empty/duplicate IDs and more than ten unique sites;
|
|
361
375
|
- load every site from Rastro and verify it belongs to that owner;
|
|
362
376
|
- upsert by the `by_connectionId` index and clear `revokedAt` when
|
|
@@ -376,10 +390,16 @@ object. If the app has no `env` object, add one without changing any other
|
|
|
376
390
|
`defineApp` option. Do not add another Rastro mount:
|
|
377
391
|
|
|
378
392
|
```ts
|
|
379
|
-
RASTRO_FEDERATION_ISSUER: v.string(),
|
|
380
|
-
RASTRO_FEDERATION_AUDIENCE: v.string(),
|
|
393
|
+
RASTRO_FEDERATION_ISSUER: v.optional(v.string()),
|
|
394
|
+
RASTRO_FEDERATION_AUDIENCE: v.optional(v.string()),
|
|
381
395
|
```
|
|
382
396
|
|
|
397
|
+
Declare them optional. A required `v.string()` makes the push fail on every
|
|
398
|
+
deployment where the values are not set yet, which is every deployment that
|
|
399
|
+
pushes code before pairing. Set both values before pushing: `auth.config.ts`
|
|
400
|
+
reads them at deploy time, so values set after a push take effect only on the
|
|
401
|
+
next push.
|
|
402
|
+
|
|
383
403
|
Insert this conditional spread into the existing `providers` array; never
|
|
384
404
|
replace the array or its current entries:
|
|
385
405
|
|
|
@@ -432,12 +452,21 @@ name `rastroFederation`.
|
|
|
432
452
|
import {
|
|
433
453
|
exposeFederatedAnalyticsApi,
|
|
434
454
|
type FederatedAnalyticsConnection,
|
|
455
|
+
type FederatedAnalyticsPermission,
|
|
435
456
|
} from "@iann29/rastro";
|
|
436
457
|
import { components } from "./_generated/api";
|
|
437
458
|
import { env, type QueryCtx } from "./_generated/server";
|
|
438
459
|
|
|
460
|
+
/** The production Amage Rastro OIDC issuer; override with RASTRO_FEDERATION_ISSUER. */
|
|
461
|
+
export const DEFAULT_FEDERATION_ISSUER =
|
|
462
|
+
"https://site.api.amagerastro.com/federation";
|
|
463
|
+
|
|
464
|
+
export const federationIssuer = (
|
|
465
|
+
env.RASTRO_FEDERATION_ISSUER ?? DEFAULT_FEDERATION_ISSUER
|
|
466
|
+
).replace(/\/$/, "");
|
|
467
|
+
|
|
439
468
|
const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
|
|
440
|
-
issuer:
|
|
469
|
+
issuer: federationIssuer,
|
|
441
470
|
resolveConnection: async (ctx, identity) => {
|
|
442
471
|
const db = ctx.db as unknown as QueryCtx["db"];
|
|
443
472
|
const grant = await db
|
|
@@ -452,6 +481,7 @@ const federated = exposeFederatedAnalyticsApi(components.rastroAnalytics, {
|
|
|
452
481
|
connectionId: grant.connectionId,
|
|
453
482
|
organizationId: grant.organizationId,
|
|
454
483
|
siteIds: grant.siteIds,
|
|
484
|
+
permissions: grant.permissions as FederatedAnalyticsPermission[],
|
|
455
485
|
revokedAt: grant.revokedAt,
|
|
456
486
|
} satisfies FederatedAnalyticsConnection;
|
|
457
487
|
},
|
|
@@ -473,39 +503,94 @@ export const {
|
|
|
473
503
|
vitalsReport,
|
|
474
504
|
siteMap,
|
|
475
505
|
dataCoverage,
|
|
506
|
+
siteSettings,
|
|
507
|
+
updateSite,
|
|
508
|
+
listGoals,
|
|
509
|
+
upsertGoal,
|
|
510
|
+
removeGoal,
|
|
511
|
+
listFunnels,
|
|
512
|
+
upsertFunnel,
|
|
513
|
+
removeFunnel,
|
|
514
|
+
listAffiliates,
|
|
515
|
+
upsertAffiliate,
|
|
516
|
+
removeAffiliate,
|
|
517
|
+
retentionStatus,
|
|
518
|
+
setRetentionPolicy,
|
|
519
|
+
disableRetentionPolicy,
|
|
476
520
|
} = federated;
|
|
477
521
|
```
|
|
478
522
|
|
|
479
|
-
`manifest` is public static metadata. Every
|
|
480
|
-
requires both a valid JWT and a matching, non-revoked local grant.
|
|
523
|
+
`manifest` is public static metadata. Every report function is read-only and
|
|
524
|
+
requires both a valid JWT and a matching, non-revoked local grant. The functions
|
|
525
|
+
from `siteSettings` down form the configure scope (optional capability
|
|
526
|
+
`configure`, hosts from `alpha.11`): they additionally require
|
|
527
|
+
`analytics:configure` on the token — the control plane claims it only for
|
|
528
|
+
organization owners and admins — and in the grant's `permissions`, and fail with
|
|
529
|
+
`FEDERATION_CONFIGURE_FORBIDDEN` otherwise. Export them even when every grant
|
|
530
|
+
stays read only, so a later opt-in needs no host deploy; the dashboard never
|
|
531
|
+
calls them for a read-only connection. The issuer falls back to the production
|
|
532
|
+
control plane so the module compiles and pushes before the environment variable
|
|
533
|
+
exists; tokens are still rejected until `auth.config.ts` trusts the same issuer,
|
|
534
|
+
which needs both variables set and a push afterwards.
|
|
535
|
+
|
|
536
|
+
A public link's token carries `analytics:public` instead of `analytics:read` and
|
|
537
|
+
`rastro_site_ids` naming the link's sites. It runs only `manifest`,
|
|
538
|
+
`connectionStatus`, `listSites`, `overview` (with zero revenue and conversions
|
|
539
|
+
and empty campaigns, mediums, events, and goals), `liveVisitors` (with
|
|
540
|
+
`visitorId` replaced by the session id and without the entry path, return,
|
|
541
|
+
campaign, affiliate, conversion, and funnel fields), and `dataCoverage`;
|
|
542
|
+
everything else fails with `FEDERATION_PUBLIC_FORBIDDEN`, and so does every call
|
|
543
|
+
while the grant does not list `analytics:public`. A host from before the
|
|
544
|
+
permission rejects the token with `FEDERATION_INVALID_CLAIMS`.
|
|
481
545
|
|
|
482
546
|
## Function contract
|
|
483
547
|
|
|
484
|
-
| Function | Input summary | Important output/behavior
|
|
485
|
-
| ------------------ | ------------------------------------------------------- |
|
|
486
|
-
| `manifest` | `{}` | Static protocol, capabilities, names, and limits; no auth
|
|
487
|
-
| `connectionStatus` | `{}` | Redacted status, protocol version, capabilities, site count
|
|
488
|
-
| `listSites` | `{}` | Only `siteId`, `name`, `currency`, `timezone`, `cookieless`
|
|
489
|
-
| `overview` | `siteIds`, `from`, `to`, optional hour/day interval | Aggregate metrics and time series
|
|
490
|
-
| `liveVisitors` | `siteIds`, `now`, optional limit | At most 500 visitors seen in the last five minutes
|
|
491
|
-
| `listSessions` | one `siteId`, range, `{numItems,cursor}` keyset options | Paginated sessions
|
|
492
|
-
| `sessionJourney` | one site/session, range, `{numItems,cursor}` options | Paginated bounded events
|
|
493
|
-
| `listConversions` | one site, range, `{numItems,cursor}` options | Paginated trusted server-side conversion ledger
|
|
494
|
-
| `visitorJourney` | site IDs, visitor ID, range, optional limit | Cross-site journey inside the grant only
|
|
495
|
-
| `goalsReport` | one site and range | Read-only goal definitions plus aggregates
|
|
496
|
-
| `funnelsReport` | one site and complete UTC-day range | Read-only definitions/steps plus aggregates; max 90 days
|
|
497
|
-
| `affiliatesReport` | one site and complete UTC-day range | Read-only affiliate definitions plus aggregates
|
|
498
|
-
| `vitalsReport` | one site and complete UTC-day range | Web Vitals p75/ratings per metric, page, and device; max 90 days
|
|
499
|
-
| `siteMap` | site IDs and complete UTC-day range | Routes with derived exits/bounces plus transitions; max 90 days
|
|
500
|
-
| `dataCoverage` | one site and inclusive integer range | Coverage metadata; echoes site ID, no event/visitor/session IDs
|
|
501
|
-
|
|
502
|
-
The
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
548
|
+
| Function | Input summary | Important output/behavior |
|
|
549
|
+
| ------------------ | ------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
550
|
+
| `manifest` | `{}` | Static protocol, capabilities, names, and limits; no auth |
|
|
551
|
+
| `connectionStatus` | `{}` | Redacted status, protocol version, capabilities, site count |
|
|
552
|
+
| `listSites` | `{}` | Only `siteId`, `name`, `currency`, `timezone`, `cookieless` |
|
|
553
|
+
| `overview` | `siteIds`, `from`, `to`, optional hour/day interval | Aggregate metrics and time series |
|
|
554
|
+
| `liveVisitors` | `siteIds`, `now`, optional limit | At most 500 visitors seen in the last five minutes, with intent facts |
|
|
555
|
+
| `listSessions` | one `siteId`, range, `{numItems,cursor}` keyset options | Paginated sessions |
|
|
556
|
+
| `sessionJourney` | one site/session, range, `{numItems,cursor}` options | Paginated bounded events |
|
|
557
|
+
| `listConversions` | one site, range, `{numItems,cursor}` options | Paginated trusted server-side conversion ledger |
|
|
558
|
+
| `visitorJourney` | site IDs, visitor ID, range, optional limit | Cross-site journey inside the grant only |
|
|
559
|
+
| `goalsReport` | one site and range | Read-only goal definitions plus aggregates |
|
|
560
|
+
| `funnelsReport` | one site and complete UTC-day range | Read-only definitions/steps plus aggregates; max 90 days |
|
|
561
|
+
| `affiliatesReport` | one site and complete UTC-day range | Read-only affiliate definitions plus aggregates |
|
|
562
|
+
| `vitalsReport` | one site and complete UTC-day range | Web Vitals p75/ratings per metric, page, and device; max 90 days |
|
|
563
|
+
| `siteMap` | site IDs and complete UTC-day range | Routes with derived exits/bounces plus transitions; max 90 days |
|
|
564
|
+
| `dataCoverage` | one site and inclusive integer range | Coverage metadata; echoes site ID, no event/visitor/session IDs |
|
|
565
|
+
|
|
566
|
+
The configure scope, every function of which requires `analytics:configure` on
|
|
567
|
+
the token and in the grant:
|
|
568
|
+
|
|
569
|
+
| Function | Input summary | Important output/behavior |
|
|
570
|
+
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------- |
|
|
571
|
+
| `siteSettings` | one `siteId` | `name`, `domains`, `timezone`, `currency`, `cookieless`; no owner |
|
|
572
|
+
| `updateSite` | one `siteId`, optional `name`, `domains`, `timezone` | Changes only those three; currency and `cookieless` stay outside |
|
|
573
|
+
| `listGoals` | one `siteId` | Goal definitions, at most 50 |
|
|
574
|
+
| `upsertGoal` | `siteId`, optional `goalId`, key, name, matcher, value, flag | Returns the goal ID; keys and active matchers are unique per site |
|
|
575
|
+
| `removeGoal` | `siteId`, `goalId` | Deletes the definition; rollups already written remain |
|
|
576
|
+
| `listFunnels` | one `siteId` | Funnel definitions, at most 20 |
|
|
577
|
+
| `upsertFunnel` | `siteId`, optional `funnelId`, key, name, 2–10 steps, window | Returns the funnel ID; window between one minute and 90 days |
|
|
578
|
+
| `removeFunnel` | `siteId`, `funnelId` | Deletes the definition |
|
|
579
|
+
| `listAffiliates` | one `siteId` | Affiliate definitions, at most 100 |
|
|
580
|
+
| `upsertAffiliate` | `siteId`, optional `affiliateId`, slug, name, bps, window | Returns the affiliate ID; commission 0–10000 bps, window 1–365 days |
|
|
581
|
+
| `removeAffiliate` | `siteId`, `affiliateId` | Deletes the definition; accounted commissions remain |
|
|
582
|
+
| `retentionStatus` | one `siteId` | The policy, its cleanup jobs, and the backlog |
|
|
583
|
+
| `setRetentionPolicy` | `siteId` and five day counts between 1 and 3650 | Starts the recurring cleanup chains |
|
|
584
|
+
| `disableRetentionPolicy` | one `siteId` | Stops automatic cleanup; nothing is restored |
|
|
585
|
+
|
|
586
|
+
The federated surface deliberately excludes ingestion, owner IDs, network IDs,
|
|
587
|
+
and arbitrary host functions, and shows a site's domains only through
|
|
588
|
+
`siteSettings` to a connection allowed to change them. Goal, funnel, and
|
|
589
|
+
affiliate reports include their read-only definitions. Journey events include
|
|
590
|
+
bounded custom `properties`; hosts must follow the tracker contract and never
|
|
591
|
+
place personal data or secrets in them. A session belongs to one site; only
|
|
592
|
+
`visitorJourney` can join the same pseudonymous visitor across explicitly
|
|
593
|
+
granted sites.
|
|
509
594
|
|
|
510
595
|
All times are Unix epoch milliseconds. The global report ceiling is 366 days,
|
|
511
596
|
but hourly overview is limited to 24 hours and funnel reports to 90 complete UTC
|
|
@@ -552,14 +637,21 @@ synapse convex --dev run rastroFederationAdmin:provisionConnection \
|
|
|
552
637
|
```
|
|
553
638
|
|
|
554
639
|
Never copy site IDs from JWT claims or grant every site belonging to an owner.
|
|
555
|
-
The operator must name one through ten exact site IDs.
|
|
640
|
+
The operator must name one through ten exact site IDs. Add
|
|
641
|
+
`"permissions":["analytics:configure"]` to the same JSON only when the operator
|
|
642
|
+
wants the dashboard's owners and admins to configure those sites; omit it for a
|
|
643
|
+
read-only connection, and rerun without it to revoke the scope.
|
|
556
644
|
|
|
557
645
|
## Phase 6: verify in order
|
|
558
646
|
|
|
559
647
|
Do not mark integration complete until all checks pass:
|
|
560
648
|
|
|
561
649
|
1. Call `rastroFederation:manifest` without auth and deep-compare its connector
|
|
562
|
-
fields with
|
|
650
|
+
fields with the `connector` object of
|
|
651
|
+
<https://www.amagerastro.com/agent/manifest.json>. Compare structurally
|
|
652
|
+
(`assert.deepStrictEqual`, `toEqual`, or an equivalent order-insensitive
|
|
653
|
+
check), never as serialized text: the served object and the JSON file list
|
|
654
|
+
the same keys in a different order.
|
|
563
655
|
2. In **Conexões**, click **Verificar conexão** and confirm active status and
|
|
564
656
|
the exact site count.
|
|
565
657
|
3. Confirm `listSites` omits domains, owner IDs, network IDs, connection IDs,
|
|
@@ -583,41 +675,106 @@ Do not mark integration complete until all checks pass:
|
|
|
583
675
|
report, and require `FEDERATION_CONNECTION_DENIED` before central
|
|
584
676
|
revocation.
|
|
585
677
|
11. Reprovision only if the rehearsal requires restoring the development grant.
|
|
678
|
+
12. In the same suite, call `upsertGoal` with a read-only identity and require
|
|
679
|
+
`FEDERATION_CONFIGURE_FORBIDDEN`; when the grant lists
|
|
680
|
+
`analytics:configure`, call it with an identity whose `rastro_permissions`
|
|
681
|
+
includes `analytics:configure` and require the goal in `listGoals`, then
|
|
682
|
+
require `FEDERATION_SITE_DENIED` for a site outside the grant.
|
|
683
|
+
13. In the same suite, call `listSessions` with an identity whose
|
|
684
|
+
`rastro_permissions` is `["analytics:public"]` and require
|
|
685
|
+
`FEDERATION_PUBLIC_FORBIDDEN`; when the grant lists `analytics:public`,
|
|
686
|
+
require `overview` to answer that identity with zero `revenueCents` and an
|
|
687
|
+
empty `topGoals`.
|
|
688
|
+
|
|
689
|
+
Use this harness when the host has no `convex-test` suite yet. Convex functions
|
|
690
|
+
run under `edge-runtime` in tests, which is what a deployed function gets, so
|
|
691
|
+
install `vitest`, `convex-test`, and `@edge-runtime/vm` as dev dependencies and
|
|
692
|
+
configure the environment and the issuer the identity will claim:
|
|
586
693
|
|
|
587
|
-
|
|
588
|
-
|
|
694
|
+
```ts
|
|
695
|
+
// vitest.config.ts
|
|
696
|
+
import { defineConfig } from "vitest/config";
|
|
697
|
+
|
|
698
|
+
export default defineConfig({
|
|
699
|
+
test: {
|
|
700
|
+
environment: "edge-runtime",
|
|
701
|
+
env: {
|
|
702
|
+
RASTRO_FEDERATION_ISSUER: "https://site.api.amagerastro.com/federation",
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Register the host schema and modules together with the Rastro component; the
|
|
709
|
+
package's `test` entry registers it under the mounted name `rastroAnalytics`:
|
|
589
710
|
|
|
590
711
|
```ts
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
t.withIdentity(identity).query(api.rastroFederation.overview, {
|
|
605
|
-
...allowedArgs,
|
|
606
|
-
siteIds: [KNOWN_OUTSIDE_GRANT_SITE_ID],
|
|
607
|
-
}),
|
|
608
|
-
).rejects.toMatchObject({ data: { code: "FEDERATION_SITE_DENIED" } });
|
|
712
|
+
// convex/setup.test.ts
|
|
713
|
+
/// <reference types="vite/client" />
|
|
714
|
+
import { convexTest } from "convex-test";
|
|
715
|
+
import component from "@iann29/rastro/test";
|
|
716
|
+
import schema from "./schema";
|
|
717
|
+
|
|
718
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
719
|
+
|
|
720
|
+
export function initConvexTest() {
|
|
721
|
+
const t = convexTest(schema, modules);
|
|
722
|
+
component.register(t);
|
|
723
|
+
return t;
|
|
724
|
+
}
|
|
609
725
|
```
|
|
610
726
|
|
|
727
|
+
Then use this assertion shape, with `federationIssuer` imported from
|
|
728
|
+
`convex/rastroFederation.ts` so the identity claims the issuer the module
|
|
729
|
+
verifies:
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
// convex/rastroFederation.test.ts
|
|
733
|
+
import { expect, test } from "vitest";
|
|
734
|
+
import { api } from "./_generated/api";
|
|
735
|
+
import { federationIssuer } from "./rastroFederation";
|
|
736
|
+
import { initConvexTest } from "./setup.test";
|
|
737
|
+
|
|
738
|
+
test("denies a site outside the grant", async () => {
|
|
739
|
+
const t = initConvexTest();
|
|
740
|
+
const identity = {
|
|
741
|
+
issuer: federationIssuer,
|
|
742
|
+
subject: "rastro-negative-test",
|
|
743
|
+
tokenIdentifier: `${federationIssuer}|rastro-negative-test`,
|
|
744
|
+
rastro_connection_id: CONNECTION_ID,
|
|
745
|
+
rastro_organization_id: ORGANIZATION_ID,
|
|
746
|
+
rastro_permissions: ["analytics:read"],
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
await expect(
|
|
750
|
+
t.query(api.rastroFederation.overview, allowedArgs),
|
|
751
|
+
).rejects.toMatchObject({ data: { code: "FEDERATION_UNAUTHENTICATED" } });
|
|
752
|
+
await expect(
|
|
753
|
+
t.withIdentity(identity).query(api.rastroFederation.overview, {
|
|
754
|
+
...allowedArgs,
|
|
755
|
+
siteIds: [KNOWN_OUTSIDE_GRANT_SITE_ID],
|
|
756
|
+
}),
|
|
757
|
+
).rejects.toMatchObject({ data: { code: "FEDERATION_SITE_DENIED" } });
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Provision the grant for `CONNECTION_ID` inside the test with
|
|
762
|
+
`t.mutation(internal.rastroFederationAdmin.provisionConnection, ...)` after
|
|
763
|
+
creating a site through the host's own admin function; `allowedArgs` names that
|
|
764
|
+
site and a complete UTC-day range.
|
|
765
|
+
|
|
611
766
|
Consumers must branch on `ConvexError.data.code`, not human-readable text:
|
|
612
767
|
|
|
613
|
-
| Code
|
|
614
|
-
|
|
|
615
|
-
| `FEDERATION_UNAUTHENTICATED`
|
|
616
|
-
| `FEDERATION_ISSUER_MISMATCH`
|
|
617
|
-
| `FEDERATION_INVALID_CLAIMS`
|
|
618
|
-
| `FEDERATION_CONNECTION_DENIED`
|
|
619
|
-
| `FEDERATION_INVALID_SCOPE`
|
|
620
|
-
| `FEDERATION_SITE_DENIED`
|
|
768
|
+
| Code | Meaning |
|
|
769
|
+
| -------------------------------- | ------------------------------------------------------------------------------ |
|
|
770
|
+
| `FEDERATION_UNAUTHENTICATED` | No authenticated Rastro identity |
|
|
771
|
+
| `FEDERATION_ISSUER_MISMATCH` | Identity came from another issuer |
|
|
772
|
+
| `FEDERATION_INVALID_CLAIMS` | Required bounded claims or permission are absent |
|
|
773
|
+
| `FEDERATION_CONNECTION_DENIED` | Local grant is missing, malformed, mismatched, or revoked |
|
|
774
|
+
| `FEDERATION_INVALID_SCOPE` | Requested site list is empty, duplicate, malformed, or over limit |
|
|
775
|
+
| `FEDERATION_SITE_DENIED` | A requested site is outside the local grant |
|
|
776
|
+
| `FEDERATION_CONFIGURE_FORBIDDEN` | The token or the grant lacks `analytics:configure` |
|
|
777
|
+
| `FEDERATION_PUBLIC_FORBIDDEN` | A public token asked beyond its surface, or the grant lacks `analytics:public` |
|
|
621
778
|
|
|
622
779
|
## Revocation
|
|
623
780
|
|
|
@@ -649,6 +806,7 @@ Package/version: <exact eligible registry version>
|
|
|
649
806
|
Existing auth providers preserved: yes/no
|
|
650
807
|
Manifest protocol: <value>
|
|
651
808
|
Granted site count: <1..10>
|
|
809
|
+
Configure scope granted: yes/no
|
|
652
810
|
Ingestion verified: yes/no
|
|
653
811
|
Authorized reports verified: yes/no
|
|
654
812
|
Wrong-site denial verified: yes/no
|
package/agent/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://www.amagerastro.com/agent/manifest.schema.json",
|
|
3
3
|
"name": "amage-rastro",
|
|
4
4
|
"title": "Amage Rastro",
|
|
5
|
-
"description": "Privacy-first, real-time web analytics for Convex with a
|
|
5
|
+
"description": "Privacy-first, real-time web analytics for Convex with a federated dashboard that reads every granted site and configures only those whose host opts in.",
|
|
6
6
|
"manifestVersion": "1.0.0",
|
|
7
7
|
"language": "en",
|
|
8
8
|
"canonicalBaseUrl": "https://www.amagerastro.com",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"upgrading": "https://www.amagerastro.com/docs/reference/upgrading.md",
|
|
16
16
|
"integrity": {
|
|
17
17
|
"algorithm": "sha256",
|
|
18
|
-
"agent": "
|
|
19
|
-
"llms": "
|
|
20
|
-
"protocol": "
|
|
21
|
-
"setup": "
|
|
22
|
-
"upgrading": "
|
|
18
|
+
"agent": "36afaee7c37f8ccf8b17e2b8c9fcb35de137745f59b9e387b1e7d89d6c70a524",
|
|
19
|
+
"llms": "08e9581c9ee626642753db971e9c4da22d131ddb62870cca402d2e93ce4f7567",
|
|
20
|
+
"protocol": "a297bf4dc610b3f0658fa131a2dc9a61b04a3bffe49320e27a58f0abe1ae716e",
|
|
21
|
+
"setup": "f0777e3ce5785ab1d6c9e13dae0ef3c3ca59f1c1d84bbeb7e61f8b909f236d16",
|
|
22
|
+
"upgrading": "8f37826ce035431103ffe3fe47f7f302b134886be5f4c4252a60e536d6f6cce7"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"controlPlane": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"package": {
|
|
36
36
|
"name": "@iann29/rastro",
|
|
37
|
-
"distTag": "
|
|
37
|
+
"distTag": "latest",
|
|
38
38
|
"availability": "verify-exact-registry-artifact",
|
|
39
39
|
"requiredRuntimeExports": [
|
|
40
40
|
"exposeFederatedAnalyticsApi",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"FederatedAnalyticsAuthorizerOptions",
|
|
47
47
|
"FederatedSiteSummary"
|
|
48
48
|
],
|
|
49
|
-
"versionDiscoveryCommand": "npm view @iann29/rastro
|
|
49
|
+
"versionDiscoveryCommand": "npm view @iann29/rastro version",
|
|
50
50
|
"inspectionPolicy": "static-tarball-inspection-no-lifecycle-scripts",
|
|
51
51
|
"agentActionWhenUnavailable": "stop-and-request-eligible-release"
|
|
52
52
|
},
|
|
@@ -69,7 +69,21 @@
|
|
|
69
69
|
"affiliatesReport": "affiliatesReport",
|
|
70
70
|
"dataCoverage": "dataCoverage",
|
|
71
71
|
"vitalsReport": "vitalsReport",
|
|
72
|
-
"siteMap": "siteMap"
|
|
72
|
+
"siteMap": "siteMap",
|
|
73
|
+
"siteSettings": "siteSettings",
|
|
74
|
+
"updateSite": "updateSite",
|
|
75
|
+
"listGoals": "listGoals",
|
|
76
|
+
"upsertGoal": "upsertGoal",
|
|
77
|
+
"removeGoal": "removeGoal",
|
|
78
|
+
"listFunnels": "listFunnels",
|
|
79
|
+
"upsertFunnel": "upsertFunnel",
|
|
80
|
+
"removeFunnel": "removeFunnel",
|
|
81
|
+
"listAffiliates": "listAffiliates",
|
|
82
|
+
"upsertAffiliate": "upsertAffiliate",
|
|
83
|
+
"removeAffiliate": "removeAffiliate",
|
|
84
|
+
"retentionStatus": "retentionStatus",
|
|
85
|
+
"setRetentionPolicy": "setRetentionPolicy",
|
|
86
|
+
"disableRetentionPolicy": "disableRetentionPolicy"
|
|
73
87
|
},
|
|
74
88
|
"capabilities": [
|
|
75
89
|
"siteSummaries",
|
|
@@ -84,7 +98,9 @@
|
|
|
84
98
|
"affiliates",
|
|
85
99
|
"dataCoverage",
|
|
86
100
|
"vitals",
|
|
87
|
-
"siteMap"
|
|
101
|
+
"siteMap",
|
|
102
|
+
"localDays",
|
|
103
|
+
"configure"
|
|
88
104
|
],
|
|
89
105
|
"limits": {
|
|
90
106
|
"maxSitesPerRequest": 10,
|
|
@@ -98,10 +114,13 @@
|
|
|
98
114
|
"providerType": "customJwt",
|
|
99
115
|
"audience": "exact-normalized-customer-functions-url",
|
|
100
116
|
"permission": "analytics:read",
|
|
117
|
+
"configurePermission": "analytics:configure",
|
|
118
|
+
"publicPermission": "analytics:public",
|
|
101
119
|
"claims": {
|
|
102
120
|
"connectionId": "rastro_connection_id",
|
|
103
121
|
"organizationId": "rastro_organization_id",
|
|
104
|
-
"permissions": "rastro_permissions"
|
|
122
|
+
"permissions": "rastro_permissions",
|
|
123
|
+
"siteIds": "rastro_site_ids"
|
|
105
124
|
},
|
|
106
125
|
"authoritativeScope": "customer-deployment-local-grant"
|
|
107
126
|
},
|
|
@@ -160,7 +179,7 @@
|
|
|
160
179
|
"Never accept ownerId, userId, organizationId, or site scope from an untrusted client as authorization.",
|
|
161
180
|
"Never replace existing authentication providers; append the Rastro provider.",
|
|
162
181
|
"Never use JWT site IDs as authoritative scope; resolve the indexed local grant on every query.",
|
|
163
|
-
"Never expose
|
|
182
|
+
"Never expose ingestion, owner-wide enumeration, or arbitrary host functions through the federated module; configuration writes run only when both the token and the local grant carry analytics:configure.",
|
|
164
183
|
"Never run a deployment-affecting command without an explicit development or production target.",
|
|
165
184
|
"Revoke the local grant before central revocation when immediate denial is required.",
|
|
166
185
|
"Keep telemetry in the customer deployment."
|
|
@@ -182,6 +201,8 @@
|
|
|
182
201
|
"FEDERATION_INVALID_CLAIMS",
|
|
183
202
|
"FEDERATION_CONNECTION_DENIED",
|
|
184
203
|
"FEDERATION_INVALID_SCOPE",
|
|
185
|
-
"FEDERATION_SITE_DENIED"
|
|
204
|
+
"FEDERATION_SITE_DENIED",
|
|
205
|
+
"FEDERATION_CONFIGURE_FORBIDDEN",
|
|
206
|
+
"FEDERATION_PUBLIC_FORBIDDEN"
|
|
186
207
|
]
|
|
187
208
|
}
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
],
|
|
115
115
|
"properties": {
|
|
116
116
|
"name": { "const": "@iann29/rastro" },
|
|
117
|
-
"distTag": { "const": "
|
|
117
|
+
"distTag": { "const": "latest" },
|
|
118
118
|
"availability": { "const": "verify-exact-registry-artifact" },
|
|
119
119
|
"requiredRuntimeExports": {
|
|
120
120
|
"type": "array",
|
|
@@ -169,7 +169,21 @@
|
|
|
169
169
|
"affiliatesReport",
|
|
170
170
|
"dataCoverage",
|
|
171
171
|
"vitalsReport",
|
|
172
|
-
"siteMap"
|
|
172
|
+
"siteMap",
|
|
173
|
+
"siteSettings",
|
|
174
|
+
"updateSite",
|
|
175
|
+
"listGoals",
|
|
176
|
+
"upsertGoal",
|
|
177
|
+
"removeGoal",
|
|
178
|
+
"listFunnels",
|
|
179
|
+
"upsertFunnel",
|
|
180
|
+
"removeFunnel",
|
|
181
|
+
"listAffiliates",
|
|
182
|
+
"upsertAffiliate",
|
|
183
|
+
"removeAffiliate",
|
|
184
|
+
"retentionStatus",
|
|
185
|
+
"setRetentionPolicy",
|
|
186
|
+
"disableRetentionPolicy"
|
|
173
187
|
],
|
|
174
188
|
"properties": {
|
|
175
189
|
"manifest": { "const": "manifest" },
|
|
@@ -186,14 +200,28 @@
|
|
|
186
200
|
"affiliatesReport": { "const": "affiliatesReport" },
|
|
187
201
|
"dataCoverage": { "const": "dataCoverage" },
|
|
188
202
|
"vitalsReport": { "const": "vitalsReport" },
|
|
189
|
-
"siteMap": { "const": "siteMap" }
|
|
203
|
+
"siteMap": { "const": "siteMap" },
|
|
204
|
+
"siteSettings": { "const": "siteSettings" },
|
|
205
|
+
"updateSite": { "const": "updateSite" },
|
|
206
|
+
"listGoals": { "const": "listGoals" },
|
|
207
|
+
"upsertGoal": { "const": "upsertGoal" },
|
|
208
|
+
"removeGoal": { "const": "removeGoal" },
|
|
209
|
+
"listFunnels": { "const": "listFunnels" },
|
|
210
|
+
"upsertFunnel": { "const": "upsertFunnel" },
|
|
211
|
+
"removeFunnel": { "const": "removeFunnel" },
|
|
212
|
+
"listAffiliates": { "const": "listAffiliates" },
|
|
213
|
+
"upsertAffiliate": { "const": "upsertAffiliate" },
|
|
214
|
+
"removeAffiliate": { "const": "removeAffiliate" },
|
|
215
|
+
"retentionStatus": { "const": "retentionStatus" },
|
|
216
|
+
"setRetentionPolicy": { "const": "setRetentionPolicy" },
|
|
217
|
+
"disableRetentionPolicy": { "const": "disableRetentionPolicy" }
|
|
190
218
|
},
|
|
191
219
|
"additionalProperties": false
|
|
192
220
|
},
|
|
193
221
|
"capabilities": {
|
|
194
222
|
"type": "array",
|
|
195
|
-
"minItems":
|
|
196
|
-
"maxItems":
|
|
223
|
+
"minItems": 15,
|
|
224
|
+
"maxItems": 15,
|
|
197
225
|
"uniqueItems": true,
|
|
198
226
|
"items": {
|
|
199
227
|
"enum": [
|
|
@@ -209,7 +237,9 @@
|
|
|
209
237
|
"affiliates",
|
|
210
238
|
"dataCoverage",
|
|
211
239
|
"vitals",
|
|
212
|
-
"siteMap"
|
|
240
|
+
"siteMap",
|
|
241
|
+
"localDays",
|
|
242
|
+
"configure"
|
|
213
243
|
]
|
|
214
244
|
}
|
|
215
245
|
},
|
|
@@ -240,6 +270,8 @@
|
|
|
240
270
|
"providerType",
|
|
241
271
|
"audience",
|
|
242
272
|
"permission",
|
|
273
|
+
"configurePermission",
|
|
274
|
+
"publicPermission",
|
|
243
275
|
"claims",
|
|
244
276
|
"authoritativeScope"
|
|
245
277
|
],
|
|
@@ -247,13 +279,21 @@
|
|
|
247
279
|
"providerType": { "const": "customJwt" },
|
|
248
280
|
"audience": { "const": "exact-normalized-customer-functions-url" },
|
|
249
281
|
"permission": { "const": "analytics:read" },
|
|
282
|
+
"configurePermission": { "const": "analytics:configure" },
|
|
283
|
+
"publicPermission": { "const": "analytics:public" },
|
|
250
284
|
"claims": {
|
|
251
285
|
"type": "object",
|
|
252
|
-
"required": [
|
|
286
|
+
"required": [
|
|
287
|
+
"connectionId",
|
|
288
|
+
"organizationId",
|
|
289
|
+
"permissions",
|
|
290
|
+
"siteIds"
|
|
291
|
+
],
|
|
253
292
|
"properties": {
|
|
254
293
|
"connectionId": { "const": "rastro_connection_id" },
|
|
255
294
|
"organizationId": { "const": "rastro_organization_id" },
|
|
256
|
-
"permissions": { "const": "rastro_permissions" }
|
|
295
|
+
"permissions": { "const": "rastro_permissions" },
|
|
296
|
+
"siteIds": { "const": "rastro_site_ids" }
|
|
257
297
|
},
|
|
258
298
|
"additionalProperties": false
|
|
259
299
|
},
|
|
@@ -379,8 +419,8 @@
|
|
|
379
419
|
},
|
|
380
420
|
"stableErrors": {
|
|
381
421
|
"type": "array",
|
|
382
|
-
"minItems":
|
|
383
|
-
"maxItems":
|
|
422
|
+
"minItems": 8,
|
|
423
|
+
"maxItems": 8,
|
|
384
424
|
"uniqueItems": true,
|
|
385
425
|
"items": {
|
|
386
426
|
"enum": [
|
|
@@ -389,7 +429,9 @@
|
|
|
389
429
|
"FEDERATION_INVALID_CLAIMS",
|
|
390
430
|
"FEDERATION_CONNECTION_DENIED",
|
|
391
431
|
"FEDERATION_INVALID_SCOPE",
|
|
392
|
-
"FEDERATION_SITE_DENIED"
|
|
432
|
+
"FEDERATION_SITE_DENIED",
|
|
433
|
+
"FEDERATION_CONFIGURE_FORBIDDEN",
|
|
434
|
+
"FEDERATION_PUBLIC_FORBIDDEN"
|
|
393
435
|
]
|
|
394
436
|
}
|
|
395
437
|
}
|