@iann29/rastro 0.10.9 → 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.
Files changed (178) hide show
  1. package/README.md +8 -0
  2. package/agent/integration.md +45 -0
  3. package/agent/manifest.json +4 -4
  4. package/dist/client/federation.d.ts +82 -9
  5. package/dist/client/federation.d.ts.map +1 -1
  6. package/dist/client/federation.js +40 -2
  7. package/dist/client/federation.js.map +1 -1
  8. package/dist/client/index.d.ts +464 -8
  9. package/dist/client/index.d.ts.map +1 -1
  10. package/dist/client/index.js +259 -7
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/component/_generated/api.d.ts +10 -0
  13. package/dist/component/_generated/api.d.ts.map +1 -1
  14. package/dist/component/_generated/api.js.map +1 -1
  15. package/dist/component/_generated/component.d.ts +273 -21
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/_generated/server.d.ts +2 -0
  18. package/dist/component/_generated/server.d.ts.map +1 -1
  19. package/dist/component/_generated/server.js.map +1 -1
  20. package/dist/component/constants.d.ts +31 -0
  21. package/dist/component/constants.d.ts.map +1 -1
  22. package/dist/component/constants.js +31 -0
  23. package/dist/component/constants.js.map +1 -1
  24. package/dist/component/convex.config.d.ts +2 -0
  25. package/dist/component/convex.config.js +7 -0
  26. package/dist/component/convex.config.js.map +1 -1
  27. package/dist/component/errors.d.ts +1 -1
  28. package/dist/component/errors.d.ts.map +1 -1
  29. package/dist/component/errors.js.map +1 -1
  30. package/dist/component/http.d.ts.map +1 -1
  31. package/dist/component/http.js +64 -0
  32. package/dist/component/http.js.map +1 -1
  33. package/dist/component/leadAdsValidators.d.ts +1092 -0
  34. package/dist/component/leadAdsValidators.d.ts.map +1 -0
  35. package/dist/component/leadAdsValidators.js +304 -0
  36. package/dist/component/leadAdsValidators.js.map +1 -0
  37. package/dist/component/leadFormRules.d.ts +60 -0
  38. package/dist/component/leadFormRules.d.ts.map +1 -0
  39. package/dist/component/leadFormRules.js +140 -0
  40. package/dist/component/leadFormRules.js.map +1 -0
  41. package/dist/component/leadFormStore.d.ts +16 -0
  42. package/dist/component/leadFormStore.d.ts.map +1 -0
  43. package/dist/component/leadFormStore.js +92 -0
  44. package/dist/component/leadFormStore.js.map +1 -0
  45. package/dist/component/leadgenToken.d.ts +56 -0
  46. package/dist/component/leadgenToken.d.ts.map +1 -0
  47. package/dist/component/leadgenToken.js +262 -0
  48. package/dist/component/leadgenToken.js.map +1 -0
  49. package/dist/component/leads.d.ts +76 -7
  50. package/dist/component/leads.d.ts.map +1 -1
  51. package/dist/component/leads.js +112 -78
  52. package/dist/component/leads.js.map +1 -1
  53. package/dist/component/meta.d.ts.map +1 -1
  54. package/dist/component/meta.js +21 -1
  55. package/dist/component/meta.js.map +1 -1
  56. package/dist/component/metaFake.d.ts +44 -0
  57. package/dist/component/metaFake.d.ts.map +1 -1
  58. package/dist/component/metaFake.js +400 -1
  59. package/dist/component/metaFake.js.map +1 -1
  60. package/dist/component/metaGraph.d.ts +91 -0
  61. package/dist/component/metaGraph.d.ts.map +1 -1
  62. package/dist/component/metaGraph.js +217 -0
  63. package/dist/component/metaGraph.js.map +1 -1
  64. package/dist/component/metaLeads.d.ts +681 -0
  65. package/dist/component/metaLeads.d.ts.map +1 -0
  66. package/dist/component/metaLeads.js +2634 -0
  67. package/dist/component/metaLeads.js.map +1 -0
  68. package/dist/component/schema.d.ts +414 -5
  69. package/dist/component/schema.d.ts.map +1 -1
  70. package/dist/component/schema.js +169 -1
  71. package/dist/component/schema.js.map +1 -1
  72. package/dist/component/validators.d.ts +235 -11
  73. package/dist/component/validators.d.ts.map +1 -1
  74. package/dist/component/validators.js +14 -1
  75. package/dist/component/validators.js.map +1 -1
  76. package/dist/tracker/generated.d.ts +1 -1
  77. package/dist/tracker/generated.js +1 -1
  78. package/dist/ui/campaigns/CampaignsView.d.ts +5 -1
  79. package/dist/ui/campaigns/CampaignsView.d.ts.map +1 -1
  80. package/dist/ui/campaigns/CampaignsView.js.map +1 -1
  81. package/dist/ui/campaigns/Drawer.d.ts.map +1 -1
  82. package/dist/ui/campaigns/Drawer.js +8 -26
  83. package/dist/ui/campaigns/Drawer.js.map +1 -1
  84. package/dist/ui/campaigns/labels.d.ts +29 -1
  85. package/dist/ui/campaigns/labels.d.ts.map +1 -1
  86. package/dist/ui/campaigns/labels.js +77 -2
  87. package/dist/ui/campaigns/labels.js.map +1 -1
  88. package/dist/ui/campaigns/model.js +2 -2
  89. package/dist/ui/campaigns/model.js.map +1 -1
  90. package/dist/ui/forms/FirstContact.d.ts +19 -0
  91. package/dist/ui/forms/FirstContact.d.ts.map +1 -0
  92. package/dist/ui/forms/FirstContact.js +81 -0
  93. package/dist/ui/forms/FirstContact.js.map +1 -0
  94. package/dist/ui/forms/FormLead.d.ts +28 -0
  95. package/dist/ui/forms/FormLead.d.ts.map +1 -0
  96. package/dist/ui/forms/FormLead.js +199 -0
  97. package/dist/ui/forms/FormLead.js.map +1 -0
  98. package/dist/ui/forms/FormRulesDrawer.d.ts +49 -0
  99. package/dist/ui/forms/FormRulesDrawer.d.ts.map +1 -0
  100. package/dist/ui/forms/FormRulesDrawer.js +149 -0
  101. package/dist/ui/forms/FormRulesDrawer.js.map +1 -0
  102. package/dist/ui/forms/FormsView.d.ts +10 -0
  103. package/dist/ui/forms/FormsView.d.ts.map +1 -0
  104. package/dist/ui/forms/FormsView.js +215 -0
  105. package/dist/ui/forms/FormsView.js.map +1 -0
  106. package/dist/ui/forms/Icons.d.ts +46 -0
  107. package/dist/ui/forms/Icons.d.ts.map +1 -0
  108. package/dist/ui/forms/Icons.js +45 -0
  109. package/dist/ui/forms/Icons.js.map +1 -0
  110. package/dist/ui/forms/RuleRow.d.ts +37 -0
  111. package/dist/ui/forms/RuleRow.d.ts.map +1 -0
  112. package/dist/ui/forms/RuleRow.js +80 -0
  113. package/dist/ui/forms/RuleRow.js.map +1 -0
  114. package/dist/ui/forms/WhatsAppButtonGuide.d.ts +33 -0
  115. package/dist/ui/forms/WhatsAppButtonGuide.d.ts.map +1 -0
  116. package/dist/ui/forms/WhatsAppButtonGuide.js +90 -0
  117. package/dist/ui/forms/WhatsAppButtonGuide.js.map +1 -0
  118. package/dist/ui/forms/index.d.ts +11 -0
  119. package/dist/ui/forms/index.d.ts.map +1 -0
  120. package/dist/ui/forms/index.js +7 -0
  121. package/dist/ui/forms/index.js.map +1 -0
  122. package/dist/ui/forms/journey.d.ts +19 -0
  123. package/dist/ui/forms/journey.d.ts.map +1 -0
  124. package/dist/ui/forms/journey.js +54 -0
  125. package/dist/ui/forms/journey.js.map +1 -0
  126. package/dist/ui/forms/model.d.ts +239 -0
  127. package/dist/ui/forms/model.d.ts.map +1 -0
  128. package/dist/ui/forms/model.js +556 -0
  129. package/dist/ui/forms/model.js.map +1 -0
  130. package/dist/ui/forms/sample.d.ts +36 -0
  131. package/dist/ui/forms/sample.d.ts.map +1 -0
  132. package/dist/ui/forms/sample.js +444 -0
  133. package/dist/ui/forms/sample.js.map +1 -0
  134. package/dist/ui/forms/types.d.ts +454 -0
  135. package/dist/ui/forms/types.d.ts.map +1 -0
  136. package/dist/ui/forms/types.js +2 -0
  137. package/dist/ui/forms/types.js.map +1 -0
  138. package/dist/ui/index.d.ts +2 -0
  139. package/dist/ui/index.d.ts.map +1 -1
  140. package/dist/ui/index.js +4 -0
  141. package/dist/ui/index.js.map +1 -1
  142. package/dist/ui/integrations/Overlay.d.ts +3 -1
  143. package/dist/ui/integrations/Overlay.d.ts.map +1 -1
  144. package/dist/ui/integrations/Overlay.js +2 -2
  145. package/dist/ui/integrations/Overlay.js.map +1 -1
  146. package/dist/ui/portfolio/PortfolioView.d.ts +4 -1
  147. package/dist/ui/portfolio/PortfolioView.d.ts.map +1 -1
  148. package/dist/ui/portfolio/PortfolioView.js.map +1 -1
  149. package/dist/ui/sample.d.ts +3 -1
  150. package/dist/ui/sample.d.ts.map +1 -1
  151. package/dist/ui/sample.js +2 -0
  152. package/dist/ui/sample.js.map +1 -1
  153. package/dist/ui/seed.d.ts +1 -1
  154. package/dist/ui/ui.css +2301 -0
  155. package/docs/federation.md +19 -0
  156. package/docs/meta-ads.md +713 -7
  157. package/docs/upgrading.md +70 -0
  158. package/llms.txt +1 -0
  159. package/package.json +1 -1
  160. package/src/component/_generated/api.ts +10 -0
  161. package/src/component/_generated/component.ts +289 -20
  162. package/src/component/_generated/server.ts +2 -0
  163. package/src/component/constants.ts +33 -0
  164. package/src/component/convex.config.ts +7 -0
  165. package/src/component/errors.ts +3 -1
  166. package/src/component/http.ts +75 -0
  167. package/src/component/leadAdsValidators.ts +365 -0
  168. package/src/component/leadFormRules.ts +202 -0
  169. package/src/component/leadFormStore.ts +129 -0
  170. package/src/component/leadgenToken.ts +348 -0
  171. package/src/component/leads.ts +136 -84
  172. package/src/component/meta.ts +21 -1
  173. package/src/component/metaFake.ts +485 -1
  174. package/src/component/metaGraph.ts +327 -0
  175. package/src/component/metaLeads.ts +3101 -0
  176. package/src/component/schema.ts +187 -1
  177. package/src/component/validators.ts +17 -1
  178. package/src/tracker/generated.ts +1 -1
package/docs/upgrading.md CHANGED
@@ -1,5 +1,75 @@
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
+
3
73
  ## Sent means Meta received it (0.10.9)
4
74
 
5
75
  Install `@iann29/rastro@0.10.9` 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
@@ -6,7 +6,7 @@
6
6
  "bugs": {
7
7
  "url": "https://github.com/amageweb/amage-rastro/issues"
8
8
  },
9
- "version": "0.10.9",
9
+ "version": "0.11.0",
10
10
  "license": "Apache-2.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
@@ -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
  });
@@ -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,
@@ -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",