@iann29/rastro 0.10.8 → 0.11.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 +8 -0
- package/agent/integration.md +45 -0
- package/agent/manifest.json +4 -4
- package/dist/client/federation.d.ts +82 -9
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +40 -2
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +464 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +259 -7
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +10 -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 +273 -21
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +2 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/constants.d.ts +31 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +31 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +2 -0
- package/dist/component/convex.config.js +7 -0
- package/dist/component/convex.config.js.map +1 -1
- 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/http.d.ts.map +1 -1
- package/dist/component/http.js +64 -0
- package/dist/component/http.js.map +1 -1
- package/dist/component/leadAdsValidators.d.ts +1092 -0
- package/dist/component/leadAdsValidators.d.ts.map +1 -0
- package/dist/component/leadAdsValidators.js +304 -0
- package/dist/component/leadAdsValidators.js.map +1 -0
- package/dist/component/leadFormRules.d.ts +60 -0
- package/dist/component/leadFormRules.d.ts.map +1 -0
- package/dist/component/leadFormRules.js +140 -0
- package/dist/component/leadFormRules.js.map +1 -0
- package/dist/component/leadFormStore.d.ts +16 -0
- package/dist/component/leadFormStore.d.ts.map +1 -0
- package/dist/component/leadFormStore.js +92 -0
- package/dist/component/leadFormStore.js.map +1 -0
- package/dist/component/leadgenToken.d.ts +56 -0
- package/dist/component/leadgenToken.d.ts.map +1 -0
- package/dist/component/leadgenToken.js +262 -0
- package/dist/component/leadgenToken.js.map +1 -0
- package/dist/component/leads.d.ts +76 -7
- package/dist/component/leads.d.ts.map +1 -1
- package/dist/component/leads.js +112 -78
- package/dist/component/leads.js.map +1 -1
- package/dist/component/meta.d.ts.map +1 -1
- package/dist/component/meta.js +21 -1
- package/dist/component/meta.js.map +1 -1
- package/dist/component/metaCapi.d.ts.map +1 -1
- package/dist/component/metaCapi.js +10 -0
- package/dist/component/metaCapi.js.map +1 -1
- package/dist/component/metaFake.d.ts +44 -0
- package/dist/component/metaFake.d.ts.map +1 -1
- package/dist/component/metaFake.js +400 -1
- package/dist/component/metaFake.js.map +1 -1
- package/dist/component/metaGraph.d.ts +91 -0
- package/dist/component/metaGraph.d.ts.map +1 -1
- package/dist/component/metaGraph.js +217 -0
- package/dist/component/metaGraph.js.map +1 -1
- package/dist/component/metaLeads.d.ts +681 -0
- package/dist/component/metaLeads.d.ts.map +1 -0
- package/dist/component/metaLeads.js +2634 -0
- package/dist/component/metaLeads.js.map +1 -0
- package/dist/component/schema.d.ts +414 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +169 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +235 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +14 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +1 -1
- package/dist/tracker/generated.js +1 -1
- package/dist/ui/campaigns/CampaignsView.d.ts +5 -1
- package/dist/ui/campaigns/CampaignsView.d.ts.map +1 -1
- package/dist/ui/campaigns/CampaignsView.js.map +1 -1
- package/dist/ui/campaigns/Drawer.d.ts.map +1 -1
- package/dist/ui/campaigns/Drawer.js +8 -26
- package/dist/ui/campaigns/Drawer.js.map +1 -1
- package/dist/ui/campaigns/labels.d.ts +29 -1
- package/dist/ui/campaigns/labels.d.ts.map +1 -1
- package/dist/ui/campaigns/labels.js +77 -2
- package/dist/ui/campaigns/labels.js.map +1 -1
- package/dist/ui/campaigns/model.js +2 -2
- package/dist/ui/campaigns/model.js.map +1 -1
- package/dist/ui/forms/FirstContact.d.ts +19 -0
- package/dist/ui/forms/FirstContact.d.ts.map +1 -0
- package/dist/ui/forms/FirstContact.js +81 -0
- package/dist/ui/forms/FirstContact.js.map +1 -0
- package/dist/ui/forms/FormLead.d.ts +28 -0
- package/dist/ui/forms/FormLead.d.ts.map +1 -0
- package/dist/ui/forms/FormLead.js +199 -0
- package/dist/ui/forms/FormLead.js.map +1 -0
- package/dist/ui/forms/FormRulesDrawer.d.ts +49 -0
- package/dist/ui/forms/FormRulesDrawer.d.ts.map +1 -0
- package/dist/ui/forms/FormRulesDrawer.js +149 -0
- package/dist/ui/forms/FormRulesDrawer.js.map +1 -0
- package/dist/ui/forms/FormsView.d.ts +10 -0
- package/dist/ui/forms/FormsView.d.ts.map +1 -0
- package/dist/ui/forms/FormsView.js +215 -0
- package/dist/ui/forms/FormsView.js.map +1 -0
- package/dist/ui/forms/Icons.d.ts +46 -0
- package/dist/ui/forms/Icons.d.ts.map +1 -0
- package/dist/ui/forms/Icons.js +45 -0
- package/dist/ui/forms/Icons.js.map +1 -0
- package/dist/ui/forms/RuleRow.d.ts +37 -0
- package/dist/ui/forms/RuleRow.d.ts.map +1 -0
- package/dist/ui/forms/RuleRow.js +80 -0
- package/dist/ui/forms/RuleRow.js.map +1 -0
- package/dist/ui/forms/WhatsAppButtonGuide.d.ts +33 -0
- package/dist/ui/forms/WhatsAppButtonGuide.d.ts.map +1 -0
- package/dist/ui/forms/WhatsAppButtonGuide.js +90 -0
- package/dist/ui/forms/WhatsAppButtonGuide.js.map +1 -0
- package/dist/ui/forms/index.d.ts +11 -0
- package/dist/ui/forms/index.d.ts.map +1 -0
- package/dist/ui/forms/index.js +7 -0
- package/dist/ui/forms/index.js.map +1 -0
- package/dist/ui/forms/journey.d.ts +19 -0
- package/dist/ui/forms/journey.d.ts.map +1 -0
- package/dist/ui/forms/journey.js +54 -0
- package/dist/ui/forms/journey.js.map +1 -0
- package/dist/ui/forms/model.d.ts +239 -0
- package/dist/ui/forms/model.d.ts.map +1 -0
- package/dist/ui/forms/model.js +556 -0
- package/dist/ui/forms/model.js.map +1 -0
- package/dist/ui/forms/sample.d.ts +36 -0
- package/dist/ui/forms/sample.d.ts.map +1 -0
- package/dist/ui/forms/sample.js +444 -0
- package/dist/ui/forms/sample.js.map +1 -0
- package/dist/ui/forms/types.d.ts +454 -0
- package/dist/ui/forms/types.d.ts.map +1 -0
- package/dist/ui/forms/types.js +2 -0
- package/dist/ui/forms/types.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +4 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/integrations/Overlay.d.ts +3 -1
- package/dist/ui/integrations/Overlay.d.ts.map +1 -1
- package/dist/ui/integrations/Overlay.js +2 -2
- package/dist/ui/integrations/Overlay.js.map +1 -1
- package/dist/ui/portfolio/PortfolioView.d.ts +4 -1
- package/dist/ui/portfolio/PortfolioView.d.ts.map +1 -1
- package/dist/ui/portfolio/PortfolioView.js.map +1 -1
- package/dist/ui/sample.d.ts +3 -1
- package/dist/ui/sample.d.ts.map +1 -1
- package/dist/ui/sample.js +2 -0
- package/dist/ui/sample.js.map +1 -1
- package/dist/ui/seed.d.ts +1 -1
- package/dist/ui/ui.css +2301 -0
- package/docs/federation.md +19 -0
- package/docs/meta-ads.md +713 -7
- package/docs/upgrading.md +75 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/component/_generated/api.ts +10 -0
- package/src/component/_generated/component.ts +289 -20
- package/src/component/_generated/server.ts +2 -0
- package/src/component/constants.ts +33 -0
- package/src/component/convex.config.ts +7 -0
- package/src/component/errors.ts +3 -1
- package/src/component/http.ts +75 -0
- package/src/component/leadAdsValidators.ts +365 -0
- package/src/component/leadFormRules.ts +202 -0
- package/src/component/leadFormStore.ts +129 -0
- package/src/component/leadgenToken.ts +348 -0
- package/src/component/leads.ts +136 -84
- package/src/component/meta.ts +21 -1
- package/src/component/metaCapi.ts +13 -1
- package/src/component/metaFake.ts +485 -1
- package/src/component/metaGraph.ts +327 -0
- package/src/component/metaLeads.ts +3101 -0
- package/src/component/schema.ts +187 -1
- package/src/component/validators.ts +17 -1
- package/src/tracker/generated.ts +1 -1
package/docs/upgrading.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# Upgrading Amage Rastro
|
|
2
2
|
|
|
3
|
+
## Meta Lead Ads (0.11.0)
|
|
4
|
+
|
|
5
|
+
Additive: new tables only, and a host that does not opt in changes nothing (no
|
|
6
|
+
env var, re-export or handler, and its manifest advertises nothing new). Lead
|
|
7
|
+
reads widen `channel` from `"whatsapp"` to `"whatsapp" | "form"` and may carry
|
|
8
|
+
the origins `lead_form` and `lead_form_organic`; a host that switches on
|
|
9
|
+
`channel` or on origins handles the new values. A host that brings Meta instant
|
|
10
|
+
forms into its CRM, in one deploy:
|
|
11
|
+
|
|
12
|
+
1. Declares two more optional env bindings and passes them to the component:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const app = defineApp({
|
|
16
|
+
env: {
|
|
17
|
+
// ...the Meta bindings of 0.10.0
|
|
18
|
+
RASTRO_FEDERATION_ISSUER: v.optional(v.string()),
|
|
19
|
+
RASTRO_FEDERATION_AUDIENCE: v.optional(v.string()),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
app.use(rastro, {
|
|
23
|
+
env: {
|
|
24
|
+
// ...
|
|
25
|
+
RASTRO_FEDERATION_ISSUER: app.env.RASTRO_FEDERATION_ISSUER,
|
|
26
|
+
RASTRO_FEDERATION_AUDIENCE: app.env.RASTRO_FEDERATION_AUDIENCE,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Set `RASTRO_FEDERATION_AUDIENCE` on the deployment to the host's functions
|
|
32
|
+
URL (the `applicationID` its `auth.config.ts` names). The issuer defaults to
|
|
33
|
+
the production dashboard; a host paired with another dashboard (a DEV control
|
|
34
|
+
plane) sets `RASTRO_FEDERATION_ISSUER` to that dashboard's issuer, the one
|
|
35
|
+
its `auth.config.ts` trusts. Without the audience the leadgen route answers
|
|
36
|
+
503 and `linkPage` refuses with `FEDERATION_NOT_CONFIGURED`; with an issuer
|
|
37
|
+
other than the dashboard's, the dashboard routes no lead to the host.
|
|
38
|
+
|
|
39
|
+
2. Registers its form lead handler, an internal mutation validated by
|
|
40
|
+
`formLeadPayloadValidator`, once:
|
|
41
|
+
`rastro.setFormLeadHandler(ctx, { handle: await createFunctionHandle(internal.crm.formLeadArrived) })`.
|
|
42
|
+
This is the opt-in: from here on each Meta connect and `listMetaAdAccounts`
|
|
43
|
+
also reads the owner's Facebook Pages and keeps each Page's token sealed, and
|
|
44
|
+
the owners already connected have theirs read right away. A host that never
|
|
45
|
+
registers it makes no Pages read and keeps no Page token: registering the
|
|
46
|
+
handler is what makes Pages appear in Formulários, and until then the screen
|
|
47
|
+
says the host is not ready (`metaLinkedPages` answers `handlerReady: false`).
|
|
48
|
+
3. Passes `metaLeads: true` to `exposeFederatedAnalyticsApi` (with
|
|
49
|
+
`metaAds: true`: `metaLeads` alone throws at setup) and re-exports
|
|
50
|
+
`metaPages`, `metaForms`, `metaFormRules`, `simulateMetaFormRules`,
|
|
51
|
+
`metaFormsReport`, `metaLinkedPages`, `linkMetaPage`, `unlinkMetaPage`,
|
|
52
|
+
`refreshMetaForms`, `setMetaFormRules` and `returnMetaFormLead`.
|
|
53
|
+
4. Asks the dashboard operator to add the connection's deployment origin
|
|
54
|
+
(`https://<name>.convex.cloud`, the functions URL) to the control plane's
|
|
55
|
+
`RASTRO_META_LEADS_DEPLOYMENT_ORIGINS`, and, when the host's site URL is a
|
|
56
|
+
custom domain, that origin to `RASTRO_FEDERATION_ALLOWED_DEPLOYMENT_ORIGINS`.
|
|
57
|
+
Until then the dashboard forwards no webhook to the host, and leads arrive
|
|
58
|
+
only through the 15-minute reconciliation.
|
|
59
|
+
5. Deploys and uses **Verificar novamente** in the dashboard's Conexões.
|
|
60
|
+
|
|
61
|
+
`metaLinkedPages` answers
|
|
62
|
+
`{ pages, leadgenReady, issuer, audience, handlerReady }`; the dashboard routes
|
|
63
|
+
a Page to the host only when `leadgenReady` is true, `issuer` is its own and
|
|
64
|
+
`audience` is the host's deployment URL. Forwarding needs the component mounted
|
|
65
|
+
with `httpPrefix: "/rastro/"`, the path the dashboard posts to. `metaPages` and
|
|
66
|
+
`metaForms` give a Page's `lastError` as a code (`META_PAGE_ERROR_CODES`), and
|
|
67
|
+
`metaPages` adds `handlerReady`. `leadsDaily` takes `origin: "paid"` for every
|
|
68
|
+
lead an ad bought (`ctwa_ad` and `lead_form`) with their spend, and every host
|
|
69
|
+
serving `leads` advertises `leadsPaid` to say so.
|
|
70
|
+
|
|
71
|
+
See [Meta ads, Lead Ads](meta-ads.md#lead-ads-instant-forms).
|
|
72
|
+
|
|
73
|
+
## Sent means Meta received it (0.10.9)
|
|
74
|
+
|
|
75
|
+
Install `@iann29/rastro@0.10.9` and deploy the host. No schema, env or export
|
|
76
|
+
change.
|
|
77
|
+
|
|
3
78
|
## The spend sync finishes its ranges (0.10.8)
|
|
4
79
|
|
|
5
80
|
Install `@iann29/rastro@0.10.8` and deploy the host. No schema, env or export
|
package/llms.txt
CHANGED
|
@@ -37,6 +37,7 @@ Do not infer federation availability from this website, a dist-tag, or a source
|
|
|
37
37
|
- A host on 0.5.0 re-exports `listTrackedLinks`, `createTrackedLink`, `updateTrackedLink` and `deleteTrackedLink` from that module and reverifies, which advertises the optional `trackedLinks` capability; listing needs a reader, the other three need `analytics:configure`. A host that skips it keeps working without the capability.
|
|
38
38
|
- A host on 0.8.0 that records leads from its server re-exports `leadsReport`, `listLeads` and `leadJourney` and passes `leads: true` to `exposeFederatedAnalyticsApi`, which advertises the opt-in `leads` capability for any reader; without the option the three refuse and nothing is advertised.
|
|
39
39
|
- A host on 0.10.0 also re-exports `leadsDaily` under `leads`. One that links Meta ad accounts or a pixel passes `metaAds: true` and re-exports `metaStatus` and `metaConversions`, which advertises the opt-in `metaAds` capability for any reader; without the option those two refuse with `FEDERATION_INVALID_SCOPE`. With `metaAds: true` the host also exports the dashboard's own Meta setup (`metaConnection`, `metaEventMap`, `metaUnlinkPreview`, and the `analytics:configure` writes `connectMeta`, `refreshMetaAccounts`, `setMetaPixel`, `disconnectMeta`, `linkMetaAdAccount`, `unlinkMetaAdAccount`, `removeMetaPixel`, `setMetaEventMap`, `syncMetaNow`) plus the internal `authorizeMetaAction`; see docs/upgrading.md for the 0.10.0 steps.
|
|
40
|
+
- A host whose CRM takes Meta instant-form leads (Lead Ads, unreleased after 0.10.9) passes `metaLeads: true` beside `metaAds: true` (alone it throws at setup), passes `RASTRO_FEDERATION_ISSUER` and `RASTRO_FEDERATION_AUDIENCE` to the component in `app.use`, registers a form lead handler with `setFormLeadHandler`, re-exports `metaPages`, `metaForms`, `metaFormRules`, `simulateMetaFormRules`, `metaFormsReport`, `metaLinkedPages`, `linkMetaPage`, `unlinkMetaPage`, `refreshMetaForms`, `setMetaFormRules` and `returnMetaFormLead`, and asks the dashboard operator to list its deployment origin in `RASTRO_META_LEADS_DEPLOYMENT_ORIGINS`; a form lead's personal answers are never stored or logged by the component. See docs/upgrading.md.
|
|
40
41
|
- Expose only the manifest, connection status, site summaries, reports, journeys, conversions, affiliates, funnels, goals, web vitals, coverage, tracked links, and the configure functions listed in the manifest; the configure functions run only when the token and the local grant both carry `analytics:configure`. A public link's token carries `analytics:public` alone and reaches only the manifest, connection status, site summaries, a redacted overview, redacted live visitors, and coverage, and only when the local grant lists `analytics:public`.
|
|
41
42
|
- Use the functions URL as JWT audience and the HTTP-actions URL for tracker ingestion.
|
|
42
43
|
- Rehearse on an explicit development target. Production changes require fresh operator consent.
|
package/package.json
CHANGED
|
@@ -22,6 +22,10 @@ import type * as guards from "../guards.js";
|
|
|
22
22
|
import type * as http from "../http.js";
|
|
23
23
|
import type * as identity from "../identity.js";
|
|
24
24
|
import type * as ingest from "../ingest.js";
|
|
25
|
+
import type * as leadAdsValidators from "../leadAdsValidators.js";
|
|
26
|
+
import type * as leadFormRules from "../leadFormRules.js";
|
|
27
|
+
import type * as leadFormStore from "../leadFormStore.js";
|
|
28
|
+
import type * as leadgenToken from "../leadgenToken.js";
|
|
25
29
|
import type * as leads from "../leads.js";
|
|
26
30
|
import type * as live from "../live.js";
|
|
27
31
|
import type * as localTime from "../localTime.js";
|
|
@@ -29,6 +33,7 @@ import type * as meta from "../meta.js";
|
|
|
29
33
|
import type * as metaCapi from "../metaCapi.js";
|
|
30
34
|
import type * as metaFake from "../metaFake.js";
|
|
31
35
|
import type * as metaGraph from "../metaGraph.js";
|
|
36
|
+
import type * as metaLeads from "../metaLeads.js";
|
|
32
37
|
import type * as metaSync from "../metaSync.js";
|
|
33
38
|
import type * as origin from "../origin.js";
|
|
34
39
|
import type * as people from "../people.js";
|
|
@@ -69,6 +74,10 @@ const fullApi: ApiFromModules<{
|
|
|
69
74
|
http: typeof http;
|
|
70
75
|
identity: typeof identity;
|
|
71
76
|
ingest: typeof ingest;
|
|
77
|
+
leadAdsValidators: typeof leadAdsValidators;
|
|
78
|
+
leadFormRules: typeof leadFormRules;
|
|
79
|
+
leadFormStore: typeof leadFormStore;
|
|
80
|
+
leadgenToken: typeof leadgenToken;
|
|
72
81
|
leads: typeof leads;
|
|
73
82
|
live: typeof live;
|
|
74
83
|
localTime: typeof localTime;
|
|
@@ -76,6 +85,7 @@ const fullApi: ApiFromModules<{
|
|
|
76
85
|
metaCapi: typeof metaCapi;
|
|
77
86
|
metaFake: typeof metaFake;
|
|
78
87
|
metaGraph: typeof metaGraph;
|
|
88
|
+
metaLeads: typeof metaLeads;
|
|
79
89
|
metaSync: typeof metaSync;
|
|
80
90
|
origin: typeof origin;
|
|
81
91
|
people: typeof people;
|
|
@@ -281,9 +281,36 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
281
281
|
revenueCents: number | null;
|
|
282
282
|
type: "created" | "qualified" | "won" | "lost" | "open";
|
|
283
283
|
}>;
|
|
284
|
+
form?: {
|
|
285
|
+
answers: Array<{
|
|
286
|
+
label: string;
|
|
287
|
+
questionKey: string;
|
|
288
|
+
values: Array<string>;
|
|
289
|
+
}>;
|
|
290
|
+
decision: { column: string | null } | { discard: string };
|
|
291
|
+
leadgenId: string;
|
|
292
|
+
origin: {
|
|
293
|
+
adId: string | null;
|
|
294
|
+
adName: string | null;
|
|
295
|
+
adsetId: string | null;
|
|
296
|
+
adsetName: string | null;
|
|
297
|
+
campaignId: string | null;
|
|
298
|
+
campaignName: string | null;
|
|
299
|
+
formId: string;
|
|
300
|
+
formName: string | null;
|
|
301
|
+
isOrganic: boolean;
|
|
302
|
+
pageId: string;
|
|
303
|
+
pageName: string | null;
|
|
304
|
+
platform: string | null;
|
|
305
|
+
};
|
|
306
|
+
receivedAt: number;
|
|
307
|
+
returned: { at: number; reason: string } | null;
|
|
308
|
+
ruleIndex: number | null;
|
|
309
|
+
rulePath: Array<number>;
|
|
310
|
+
} | null;
|
|
284
311
|
lead: {
|
|
285
312
|
adId: string | null;
|
|
286
|
-
channel: "whatsapp";
|
|
313
|
+
channel: "whatsapp" | "form";
|
|
287
314
|
entryPoint: string | null;
|
|
288
315
|
firstAt: number;
|
|
289
316
|
hasVisitor: boolean;
|
|
@@ -313,7 +340,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
313
340
|
},
|
|
314
341
|
Array<{
|
|
315
342
|
adId: string | null;
|
|
316
|
-
channel: "whatsapp";
|
|
343
|
+
channel: "whatsapp" | "form";
|
|
317
344
|
entryPoint: string | null;
|
|
318
345
|
firstAt: number;
|
|
319
346
|
hasVisitor: boolean;
|
|
@@ -339,7 +366,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
339
366
|
sourceUrl?: string;
|
|
340
367
|
title?: string;
|
|
341
368
|
};
|
|
342
|
-
channel: "whatsapp";
|
|
369
|
+
channel: "whatsapp" | "form";
|
|
343
370
|
column?: string;
|
|
344
371
|
entryPoint?: string;
|
|
345
372
|
label?: string;
|
|
@@ -501,23 +528,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
501
528
|
},
|
|
502
529
|
Name
|
|
503
530
|
>;
|
|
504
|
-
connectWithCode: FunctionReference<
|
|
505
|
-
"action",
|
|
506
|
-
"internal",
|
|
507
|
-
{ code: string; ownerKey: string; redirectUri: string },
|
|
508
|
-
{
|
|
509
|
-
adAccounts: Array<{
|
|
510
|
-
adAccountId: string;
|
|
511
|
-
currency: string;
|
|
512
|
-
name: string;
|
|
513
|
-
timezone: string;
|
|
514
|
-
}>;
|
|
515
|
-
businessName: string | null;
|
|
516
|
-
connectionId: string;
|
|
517
|
-
pixels: Array<{ name: string; pixelId: string }>;
|
|
518
|
-
},
|
|
519
|
-
Name
|
|
520
|
-
>;
|
|
521
531
|
connection: FunctionReference<
|
|
522
532
|
"query",
|
|
523
533
|
"internal",
|
|
@@ -550,6 +560,23 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
550
560
|
Array<string>,
|
|
551
561
|
Name
|
|
552
562
|
>;
|
|
563
|
+
connectWithCode: FunctionReference<
|
|
564
|
+
"action",
|
|
565
|
+
"internal",
|
|
566
|
+
{ code: string; ownerKey: string; redirectUri: string },
|
|
567
|
+
{
|
|
568
|
+
adAccounts: Array<{
|
|
569
|
+
adAccountId: string;
|
|
570
|
+
currency: string;
|
|
571
|
+
name: string;
|
|
572
|
+
timezone: string;
|
|
573
|
+
}>;
|
|
574
|
+
businessName: string | null;
|
|
575
|
+
connectionId: string;
|
|
576
|
+
pixels: Array<{ name: string; pixelId: string }>;
|
|
577
|
+
},
|
|
578
|
+
Name
|
|
579
|
+
>;
|
|
553
580
|
conversions: FunctionReference<
|
|
554
581
|
"query",
|
|
555
582
|
"internal",
|
|
@@ -698,6 +725,248 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
698
725
|
Name
|
|
699
726
|
>;
|
|
700
727
|
};
|
|
728
|
+
metaLeads: {
|
|
729
|
+
formRules: FunctionReference<
|
|
730
|
+
"query",
|
|
731
|
+
"internal",
|
|
732
|
+
{ formId: string; now?: number; ownerKey?: string; siteId: string },
|
|
733
|
+
{
|
|
734
|
+
configured: boolean;
|
|
735
|
+
entryColumn: string | null;
|
|
736
|
+
firstContact: {
|
|
737
|
+
brakes?: {
|
|
738
|
+
businessHoursOnly: boolean;
|
|
739
|
+
maxPerHour: number;
|
|
740
|
+
waitMinutes: number;
|
|
741
|
+
};
|
|
742
|
+
mode: "lead_starts" | "one_click" | "automatic";
|
|
743
|
+
text?: string;
|
|
744
|
+
};
|
|
745
|
+
formId: string;
|
|
746
|
+
own?: boolean;
|
|
747
|
+
rules: Array<{
|
|
748
|
+
answers: Array<string>;
|
|
749
|
+
monthCount: number | null;
|
|
750
|
+
questionKey: string;
|
|
751
|
+
to: { column: string } | { discard: string };
|
|
752
|
+
}>;
|
|
753
|
+
siteId: string;
|
|
754
|
+
updatedAt: number | null;
|
|
755
|
+
whatsappButtonConfirmed: boolean;
|
|
756
|
+
},
|
|
757
|
+
Name
|
|
758
|
+
>;
|
|
759
|
+
forms: FunctionReference<
|
|
760
|
+
"query",
|
|
761
|
+
"internal",
|
|
762
|
+
{ now?: number; ownerKey?: string; siteId: string },
|
|
763
|
+
Array<{
|
|
764
|
+
forms: Array<{
|
|
765
|
+
createdTime: number | null;
|
|
766
|
+
daily: Array<number> | null;
|
|
767
|
+
formId: string;
|
|
768
|
+
lastLeadAt: number | null;
|
|
769
|
+
leads30d: number | null;
|
|
770
|
+
name: string;
|
|
771
|
+
questions: Array<{
|
|
772
|
+
key: string;
|
|
773
|
+
label: string;
|
|
774
|
+
options?: Array<{ key: string; value: string }>;
|
|
775
|
+
type: string;
|
|
776
|
+
}>;
|
|
777
|
+
rules: {
|
|
778
|
+
configured: boolean;
|
|
779
|
+
count: number;
|
|
780
|
+
entryColumn: string | null;
|
|
781
|
+
own?: boolean;
|
|
782
|
+
};
|
|
783
|
+
status: string;
|
|
784
|
+
syncedAt: number;
|
|
785
|
+
whatsappButton: boolean | null;
|
|
786
|
+
whatsappButtonConfirmed: boolean;
|
|
787
|
+
}>;
|
|
788
|
+
handlerReady: boolean;
|
|
789
|
+
lastError:
|
|
790
|
+
| "META_TOKEN_INVALID"
|
|
791
|
+
| "META_PERMISSION_MISSING"
|
|
792
|
+
| "HANDLER_MISSING"
|
|
793
|
+
| "HANDLER_FAILED"
|
|
794
|
+
| "HANDLER_GAVE_UP"
|
|
795
|
+
| "SECRETS_KEY_MISSING"
|
|
796
|
+
| "META_TRUNCATED"
|
|
797
|
+
| "META_RATE_LIMITED"
|
|
798
|
+
| "META_UNAVAILABLE"
|
|
799
|
+
| "RUN_TIMEOUT"
|
|
800
|
+
| null;
|
|
801
|
+
lastLeadAt: number | null;
|
|
802
|
+
lastReconcileAt: number | null;
|
|
803
|
+
leadsAllowed: boolean;
|
|
804
|
+
own?: boolean;
|
|
805
|
+
pageId: string;
|
|
806
|
+
pageName: string;
|
|
807
|
+
subscribedAt: number | null;
|
|
808
|
+
}>,
|
|
809
|
+
Name
|
|
810
|
+
>;
|
|
811
|
+
formsReport: FunctionReference<
|
|
812
|
+
"query",
|
|
813
|
+
"internal",
|
|
814
|
+
{ from: number; siteIds: Array<string>; to: number },
|
|
815
|
+
{
|
|
816
|
+
forms: Array<{
|
|
817
|
+
columns: Array<{ column: string; leads: number }>;
|
|
818
|
+
discarded: Array<{ leads: number; reason: string }>;
|
|
819
|
+
entry: number;
|
|
820
|
+
formId: string;
|
|
821
|
+
leads: number;
|
|
822
|
+
name: string | null;
|
|
823
|
+
pageId: string;
|
|
824
|
+
qualified: number;
|
|
825
|
+
returned: number;
|
|
826
|
+
revenueCents: number;
|
|
827
|
+
siteId: string;
|
|
828
|
+
won: number;
|
|
829
|
+
}>;
|
|
830
|
+
partial: boolean;
|
|
831
|
+
},
|
|
832
|
+
Name
|
|
833
|
+
>;
|
|
834
|
+
linkedPages: FunctionReference<
|
|
835
|
+
"query",
|
|
836
|
+
"internal",
|
|
837
|
+
{ siteIds: Array<string> },
|
|
838
|
+
{
|
|
839
|
+
audience: string | null;
|
|
840
|
+
handlerReady: boolean;
|
|
841
|
+
issuer: string;
|
|
842
|
+
leadgenReady: boolean;
|
|
843
|
+
pages: Array<{ pageId: string; siteId: string }>;
|
|
844
|
+
},
|
|
845
|
+
Name
|
|
846
|
+
>;
|
|
847
|
+
linkPage: FunctionReference<
|
|
848
|
+
"action",
|
|
849
|
+
"internal",
|
|
850
|
+
{
|
|
851
|
+
backfillDays?: number;
|
|
852
|
+
onlyOwn?: boolean;
|
|
853
|
+
ownerKey: string;
|
|
854
|
+
pageId: string;
|
|
855
|
+
siteId: string;
|
|
856
|
+
},
|
|
857
|
+
{ forms: number; linked: boolean },
|
|
858
|
+
Name
|
|
859
|
+
>;
|
|
860
|
+
pages: FunctionReference<
|
|
861
|
+
"query",
|
|
862
|
+
"internal",
|
|
863
|
+
{ ownerKey: string },
|
|
864
|
+
Array<{
|
|
865
|
+
handlerReady: boolean;
|
|
866
|
+
lastError:
|
|
867
|
+
| "META_TOKEN_INVALID"
|
|
868
|
+
| "META_PERMISSION_MISSING"
|
|
869
|
+
| "HANDLER_MISSING"
|
|
870
|
+
| "HANDLER_FAILED"
|
|
871
|
+
| "HANDLER_GAVE_UP"
|
|
872
|
+
| "SECRETS_KEY_MISSING"
|
|
873
|
+
| "META_TRUNCATED"
|
|
874
|
+
| "META_RATE_LIMITED"
|
|
875
|
+
| "META_UNAVAILABLE"
|
|
876
|
+
| "RUN_TIMEOUT"
|
|
877
|
+
| null;
|
|
878
|
+
leadsAllowed: boolean;
|
|
879
|
+
linkedOutsideScope?: boolean;
|
|
880
|
+
linkedSiteId: string | null;
|
|
881
|
+
name: string;
|
|
882
|
+
own: boolean | null;
|
|
883
|
+
pageId: string;
|
|
884
|
+
subscribedAt: number | null;
|
|
885
|
+
tasks: Array<string>;
|
|
886
|
+
}>,
|
|
887
|
+
Name
|
|
888
|
+
>;
|
|
889
|
+
refreshForms: FunctionReference<
|
|
890
|
+
"action",
|
|
891
|
+
"internal",
|
|
892
|
+
{ pageId?: string; siteId: string },
|
|
893
|
+
{ forms: number; pages: number },
|
|
894
|
+
Name
|
|
895
|
+
>;
|
|
896
|
+
returnFormLead: FunctionReference<
|
|
897
|
+
"mutation",
|
|
898
|
+
"internal",
|
|
899
|
+
{ leadId: string; ownerKey?: string; siteId: string },
|
|
900
|
+
{ returned: boolean },
|
|
901
|
+
Name
|
|
902
|
+
>;
|
|
903
|
+
setFormLeadHandler: FunctionReference<
|
|
904
|
+
"mutation",
|
|
905
|
+
"internal",
|
|
906
|
+
{ handle: string | null },
|
|
907
|
+
null,
|
|
908
|
+
Name
|
|
909
|
+
>;
|
|
910
|
+
setFormRules: FunctionReference<
|
|
911
|
+
"mutation",
|
|
912
|
+
"internal",
|
|
913
|
+
{
|
|
914
|
+
entryColumn: string | null;
|
|
915
|
+
firstContact: {
|
|
916
|
+
brakes?: {
|
|
917
|
+
businessHoursOnly: boolean;
|
|
918
|
+
maxPerHour: number;
|
|
919
|
+
waitMinutes: number;
|
|
920
|
+
};
|
|
921
|
+
mode: "lead_starts" | "one_click" | "automatic";
|
|
922
|
+
text?: string;
|
|
923
|
+
};
|
|
924
|
+
formId: string;
|
|
925
|
+
onlyOwn?: boolean;
|
|
926
|
+
ownerKey?: string;
|
|
927
|
+
rules: Array<{
|
|
928
|
+
answers: Array<string>;
|
|
929
|
+
questionKey: string;
|
|
930
|
+
to: { column: string } | { discard: string };
|
|
931
|
+
}>;
|
|
932
|
+
siteId: string;
|
|
933
|
+
whatsappButtonConfirmed?: boolean;
|
|
934
|
+
},
|
|
935
|
+
null,
|
|
936
|
+
Name
|
|
937
|
+
>;
|
|
938
|
+
simulateFormRules: FunctionReference<
|
|
939
|
+
"query",
|
|
940
|
+
"internal",
|
|
941
|
+
{
|
|
942
|
+
entryColumn: string | null;
|
|
943
|
+
formId: string;
|
|
944
|
+
now: number;
|
|
945
|
+
rules: Array<{
|
|
946
|
+
answers: Array<string>;
|
|
947
|
+
questionKey: string;
|
|
948
|
+
to: { column: string } | { discard: string };
|
|
949
|
+
}>;
|
|
950
|
+
siteId: string;
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
columns: Array<{ column: string; leads: number }>;
|
|
954
|
+
discarded: Array<{ leads: number; reason: string }>;
|
|
955
|
+
entry: number;
|
|
956
|
+
partial: boolean;
|
|
957
|
+
rules: Array<number>;
|
|
958
|
+
total: number;
|
|
959
|
+
},
|
|
960
|
+
Name
|
|
961
|
+
>;
|
|
962
|
+
unlinkPage: FunctionReference<
|
|
963
|
+
"action",
|
|
964
|
+
"internal",
|
|
965
|
+
{ ownerKey?: string; pageId: string; siteId: string },
|
|
966
|
+
{ unlinked: boolean },
|
|
967
|
+
Name
|
|
968
|
+
>;
|
|
969
|
+
};
|
|
701
970
|
people: {
|
|
702
971
|
backfillSearch: FunctionReference<
|
|
703
972
|
"mutation",
|
|
@@ -40,6 +40,8 @@ type Env = {
|
|
|
40
40
|
readonly CONVEX_CLOUD_URL: string;
|
|
41
41
|
readonly CONVEX_SITE_URL: string;
|
|
42
42
|
readonly RASTRO_CLIENT_RATE_LIMIT: "off" | undefined;
|
|
43
|
+
readonly RASTRO_FEDERATION_AUDIENCE: string | undefined;
|
|
44
|
+
readonly RASTRO_FEDERATION_ISSUER: string | undefined;
|
|
43
45
|
readonly RASTRO_GEOIP_DAILY_LIMIT: string | undefined;
|
|
44
46
|
readonly RASTRO_GEOIP_PROVIDER: "ipinfo" | "ipwhois" | undefined;
|
|
45
47
|
readonly RASTRO_GEOIP_TOKEN: string | undefined;
|
|
@@ -118,3 +118,36 @@ export const MAX_SITE_MAP_ROUTES_PER_DAY = 200;
|
|
|
118
118
|
export const ROUTE_TRANSITION_SLOTS = 24;
|
|
119
119
|
export const MAX_SITE_MAP_ROWS = 8_000;
|
|
120
120
|
export const SITE_MAP_OTHER_ROUTE = "(other)";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `leads.daily`'s `origin` for every paid origin together (`ctwa_ad` and
|
|
124
|
+
* `lead_form`), with the spend that bought them (decision D4). A host on a
|
|
125
|
+
* release before it counts nothing under this key, so a dashboard asks for it
|
|
126
|
+
* only where the host serves Lead Ads, the only hosts with `lead_form` leads.
|
|
127
|
+
*/
|
|
128
|
+
export const PAID_LEADS_ORIGIN = "paid";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The token a control plane signs to hand one Meta lead notification to a
|
|
132
|
+
* host (`POST <host site>/rastro/meta/leadgen`, contract §12.3/§12.4). The
|
|
133
|
+
* control plane signs it and the host's leadgen route verifies it, so both
|
|
134
|
+
* read these names from here. Same issuer, keys and audience as a federation
|
|
135
|
+
* connection token, but no `rastro_permissions`: it can never be replayed as a
|
|
136
|
+
* read token, and the route requires the purpose claim, which no connection
|
|
137
|
+
* token carries. The Page and lead claims must equal the request body's.
|
|
138
|
+
*/
|
|
139
|
+
export const META_LEADGEN_TOKEN = Object.freeze({
|
|
140
|
+
purpose: "meta_leadgen",
|
|
141
|
+
subject: "rastro:meta-webhook",
|
|
142
|
+
/** What the control plane signs; the host refuses a longer-lived token. */
|
|
143
|
+
lifetimeSeconds: 120,
|
|
144
|
+
maxLifetimeSeconds: 600,
|
|
145
|
+
claims: Object.freeze({
|
|
146
|
+
purpose: "rastro_purpose",
|
|
147
|
+
connectionId: "rastro_connection_id",
|
|
148
|
+
organizationId: "rastro_organization_id",
|
|
149
|
+
version: "rastro_connection_version",
|
|
150
|
+
pageId: "rastro_page_id",
|
|
151
|
+
leadgenId: "rastro_leadgen_id",
|
|
152
|
+
} as const),
|
|
153
|
+
} as const);
|
|
@@ -21,5 +21,12 @@ export default defineComponent("rastroAnalytics", {
|
|
|
21
21
|
// both, only a pasted token connects.
|
|
22
22
|
RASTRO_META_APP_ID: v.optional(v.string()),
|
|
23
23
|
RASTRO_META_APP_SECRET: v.optional(v.string()),
|
|
24
|
+
// The Rastro dashboard's federation issuer and this host's audience, the
|
|
25
|
+
// same values the host trusts in its auth.config.ts. The Lead Ads route
|
|
26
|
+
// (`/meta/leadgen`, http.ts) checks the control plane's forwarding token
|
|
27
|
+
// against them. The issuer defaults to the production dashboard; without
|
|
28
|
+
// the audience (the host's functions URL) it refuses every forward.
|
|
29
|
+
RASTRO_FEDERATION_ISSUER: v.optional(v.string()),
|
|
30
|
+
RASTRO_FEDERATION_AUDIENCE: v.optional(v.string()),
|
|
24
31
|
},
|
|
25
32
|
});
|
package/src/component/errors.ts
CHANGED
|
@@ -14,7 +14,9 @@ export type RastroErrorCode =
|
|
|
14
14
|
| "SECRETS_KEY_MISSING"
|
|
15
15
|
| "META_TOKEN_INVALID"
|
|
16
16
|
| "META_LOGIN_UNAVAILABLE"
|
|
17
|
-
| "META_UNAVAILABLE"
|
|
17
|
+
| "META_UNAVAILABLE"
|
|
18
|
+
| "META_PERMISSION_MISSING"
|
|
19
|
+
| "FEDERATION_NOT_CONFIGURED";
|
|
18
20
|
|
|
19
21
|
export function fail(
|
|
20
22
|
code: RastroErrorCode,
|
package/src/component/http.ts
CHANGED
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
resolveGeoIp,
|
|
20
20
|
} from "./geo.js";
|
|
21
21
|
import { isPlainRecord } from "./guards.js";
|
|
22
|
+
import {
|
|
23
|
+
LeadgenTokenError,
|
|
24
|
+
leadgenTrust,
|
|
25
|
+
verifyLeadgenToken,
|
|
26
|
+
} from "./leadgenToken.js";
|
|
22
27
|
import { alignClock, sanitizeEvent } from "./sanitize.js";
|
|
23
28
|
import { deriveClientKey, deriveVisitorKey, randomSecret } from "./identity.js";
|
|
24
29
|
import { isTrackedLinkSlug, trackedLinkLocation } from "./trackedLinks.js";
|
|
@@ -348,6 +353,76 @@ http.route({
|
|
|
348
353
|
}),
|
|
349
354
|
});
|
|
350
355
|
|
|
356
|
+
// Lead Ads: the control plane forwards Meta's `leadgen` webhook here, one
|
|
357
|
+
// lead at a time, with a short-lived token from the federation issuer bound
|
|
358
|
+
// to the Page and the lead (leadgenToken.ts). Only ids travel: the lead is
|
|
359
|
+
// read from Meta with the Page's own token, then handed to the host.
|
|
360
|
+
const MAX_LEADGEN_BODY = 4_096;
|
|
361
|
+
http.route({
|
|
362
|
+
path: "/meta/leadgen",
|
|
363
|
+
method: "POST",
|
|
364
|
+
handler: httpAction(async (ctx, request) => {
|
|
365
|
+
// The same trust `metaLinkedPages` reports to the control plane
|
|
366
|
+
// (`leadgenReady`, `issuer`), so it never routes a Page here that this
|
|
367
|
+
// route would refuse.
|
|
368
|
+
const { issuer, audience } = leadgenTrust(env);
|
|
369
|
+
if (!audience) {
|
|
370
|
+
return json({ error: { code: "FEDERATION_NOT_CONFIGURED" } }, 503);
|
|
371
|
+
}
|
|
372
|
+
let pageId: string;
|
|
373
|
+
let leadgenId: string;
|
|
374
|
+
try {
|
|
375
|
+
const bytes = await readLimitedBody(request, MAX_LEADGEN_BODY);
|
|
376
|
+
const body: unknown = JSON.parse(new TextDecoder().decode(bytes));
|
|
377
|
+
if (!isPlainRecord(body)) throw new Error("body");
|
|
378
|
+
if (
|
|
379
|
+
typeof body.pageId !== "string" ||
|
|
380
|
+
typeof body.leadgenId !== "string" ||
|
|
381
|
+
!/^\d{1,32}$/u.test(body.pageId) ||
|
|
382
|
+
!/^\d{1,32}$/u.test(body.leadgenId)
|
|
383
|
+
) {
|
|
384
|
+
throw new Error("ids");
|
|
385
|
+
}
|
|
386
|
+
pageId = body.pageId;
|
|
387
|
+
leadgenId = body.leadgenId;
|
|
388
|
+
} catch {
|
|
389
|
+
return json({ error: { code: "INVALID_BODY" } }, 400);
|
|
390
|
+
}
|
|
391
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
392
|
+
const token = /^Bearer\s+(\S+)$/iu.exec(authorization)?.[1];
|
|
393
|
+
if (!token) return json({ error: { code: "UNAUTHENTICATED" } }, 401);
|
|
394
|
+
try {
|
|
395
|
+
await verifyLeadgenToken(token, { issuer, audience, pageId, leadgenId });
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof LeadgenTokenError) {
|
|
398
|
+
console.warn(
|
|
399
|
+
`[rastro] refused a leadgen forward for Page ${pageId}: ${error.reason}`,
|
|
400
|
+
);
|
|
401
|
+
// The issuer's keys could not be read: not the token's fault, and the
|
|
402
|
+
// control plane retries a 503 (a 401 is final for the forward).
|
|
403
|
+
if (error.transient) {
|
|
404
|
+
return json({ error: { code: "ISSUER_UNREACHABLE" } }, 503);
|
|
405
|
+
}
|
|
406
|
+
return json({ error: { code: "UNAUTHENTICATED" } }, 401);
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
const { scheduled } = await ctx.runMutation(
|
|
411
|
+
internal.metaLeads.scheduleLeadgen,
|
|
412
|
+
{ pageId, leadgenId },
|
|
413
|
+
);
|
|
414
|
+
// A Page this host does not link is not an error for the forwarder:
|
|
415
|
+
// retrying would change nothing.
|
|
416
|
+
return json(
|
|
417
|
+
{
|
|
418
|
+
accepted: scheduled,
|
|
419
|
+
...(scheduled ? {} : { reason: "PAGE_NOT_LINKED" }),
|
|
420
|
+
},
|
|
421
|
+
scheduled ? 202 : 200,
|
|
422
|
+
);
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
425
|
+
|
|
351
426
|
http.route({
|
|
352
427
|
path: "/health",
|
|
353
428
|
method: "GET",
|