@funnelsgrove/cli 0.1.14 → 0.1.18

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 (293) hide show
  1. package/contracts/step-contract-v2.json +733 -0
  2. package/dist/analyticsOutput.d.ts +30 -15
  3. package/dist/analyticsOutput.js +45 -28
  4. package/dist/apiClient.d.ts +85 -0
  5. package/dist/apiClient.js +225 -8
  6. package/dist/cli.d.ts +149 -1
  7. package/dist/cli.js +1232 -71
  8. package/dist/diagnosticOutput.d.ts +42 -0
  9. package/dist/diagnosticOutput.js +83 -0
  10. package/dist/docsOutput.d.ts +19 -0
  11. package/dist/docsOutput.js +73 -0
  12. package/dist/docsTransaction.d.ts +81 -0
  13. package/dist/docsTransaction.js +1755 -0
  14. package/dist/funnelValidation.d.ts +14 -0
  15. package/dist/funnelValidation.js +297 -0
  16. package/dist/localSync.d.ts +79 -0
  17. package/dist/localSync.js +428 -53
  18. package/dist/projectValidator.d.ts +27 -0
  19. package/dist/projectValidator.js +208 -0
  20. package/dist/reskin.js +27 -1
  21. package/dist/stepContractMigration.d.ts +71 -0
  22. package/dist/stepContractMigration.js +1323 -0
  23. package/dist/templateDocs.js +8 -25
  24. package/docs-release-history.json +66 -0
  25. package/legacy-docs-catalog.json +1012 -0
  26. package/package.json +13 -4
  27. package/template_docs/.funnelsgrove-docs.json +144 -0
  28. package/template_docs/AGENTS.md +30 -58
  29. package/template_docs/CLAUDE.md +0 -2
  30. package/template_docs/docs/funnelsgrove/START-HERE.md +73 -0
  31. package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +59 -0
  32. package/template_docs/docs/funnelsgrove/contracts/content-answers.md +44 -0
  33. package/template_docs/docs/funnelsgrove/contracts/flow-routing.md +41 -0
  34. package/template_docs/docs/funnelsgrove/contracts/payments.md +42 -0
  35. package/template_docs/docs/funnelsgrove/contracts/step-metadata.md +57 -0
  36. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
  37. package/template_docs/docs/funnelsgrove/qa/analytics.md +43 -0
  38. package/template_docs/docs/funnelsgrove/qa/local.md +42 -0
  39. package/template_docs/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
  40. package/template_docs/docs/funnelsgrove/qa/publish.md +32 -0
  41. package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +34 -0
  42. package/template_docs/docs/funnelsgrove/recipes/add-step.md +33 -0
  43. package/template_docs/docs/funnelsgrove/recipes/edit-step.md +31 -0
  44. package/template_docs/docs/funnelsgrove/steps/INDEX.md +26 -0
  45. package/template_docs/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
  46. package/template_docs/docs/funnelsgrove/steps/checkout.md +120 -0
  47. package/template_docs/docs/funnelsgrove/steps/form_input.md +115 -0
  48. package/template_docs/docs/funnelsgrove/steps/intro_hero.md +110 -0
  49. package/template_docs/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
  50. package/template_docs/docs/funnelsgrove/steps/paywall_offer.md +115 -0
  51. package/template_docs/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
  52. package/template_docs/docs/funnelsgrove/steps/purchase_completed.md +117 -0
  53. package/template_docs/docs/funnelsgrove/steps/single_step_choice.md +122 -0
  54. package/template_docs/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
  55. package/template_docs/docs/funnelsgrove/steps/social_proof.md +110 -0
  56. package/template_docs/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
  57. package/template_docs/docs/funnelsgrove/steps/subscription_management.md +112 -0
  58. package/template_docs/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
  59. package/template_docs/docs/funnelsgrove/steps/upsell_offer.md +112 -0
  60. package/template_docs/docs/funnelsgrove/steps/value_prop_story.md +110 -0
  61. package/template_docs/funnel-docs.config.json +43 -0
  62. package/template_scaffold/.env.example +41 -0
  63. package/template_scaffold/.funnelsgrove-contract-tools.json +19 -0
  64. package/template_scaffold/.funnelsgrove-docs.json +144 -0
  65. package/template_scaffold/.funnelsgrove-scaffold.json +1078 -0
  66. package/template_scaffold/AGENTS.md +42 -0
  67. package/template_scaffold/CLAUDE.md +1 -0
  68. package/template_scaffold/PLAN.md +81 -0
  69. package/template_scaffold/PRODUCT_SENSE.md +28 -0
  70. package/template_scaffold/context/build-context.md +34 -0
  71. package/template_scaffold/docs/AGENT_FUNNEL_IMPLEMENTATION_SPEC.md +5 -0
  72. package/template_scaffold/docs/ARCHITECTURE.md +5 -0
  73. package/template_scaffold/docs/FLOW_CONFIG_AND_ROUTING.md +5 -0
  74. package/template_scaffold/docs/PAYWALL_AND_PAYMENTS.md +5 -0
  75. package/template_scaffold/docs/README.md +5 -0
  76. package/template_scaffold/docs/funnelsgrove/START-HERE.md +73 -0
  77. package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +59 -0
  78. package/template_scaffold/docs/funnelsgrove/contracts/content-answers.md +44 -0
  79. package/template_scaffold/docs/funnelsgrove/contracts/flow-routing.md +41 -0
  80. package/template_scaffold/docs/funnelsgrove/contracts/payments.md +42 -0
  81. package/template_scaffold/docs/funnelsgrove/contracts/step-metadata.md +57 -0
  82. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
  83. package/template_scaffold/docs/funnelsgrove/qa/analytics.md +43 -0
  84. package/template_scaffold/docs/funnelsgrove/qa/local.md +42 -0
  85. package/template_scaffold/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
  86. package/template_scaffold/docs/funnelsgrove/qa/publish.md +32 -0
  87. package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +34 -0
  88. package/template_scaffold/docs/funnelsgrove/recipes/add-step.md +33 -0
  89. package/template_scaffold/docs/funnelsgrove/recipes/edit-step.md +31 -0
  90. package/template_scaffold/docs/funnelsgrove/steps/INDEX.md +26 -0
  91. package/template_scaffold/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
  92. package/template_scaffold/docs/funnelsgrove/steps/checkout.md +120 -0
  93. package/template_scaffold/docs/funnelsgrove/steps/form_input.md +115 -0
  94. package/template_scaffold/docs/funnelsgrove/steps/intro_hero.md +110 -0
  95. package/template_scaffold/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
  96. package/template_scaffold/docs/funnelsgrove/steps/paywall_offer.md +115 -0
  97. package/template_scaffold/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
  98. package/template_scaffold/docs/funnelsgrove/steps/purchase_completed.md +117 -0
  99. package/template_scaffold/docs/funnelsgrove/steps/single_step_choice.md +122 -0
  100. package/template_scaffold/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
  101. package/template_scaffold/docs/funnelsgrove/steps/social_proof.md +110 -0
  102. package/template_scaffold/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
  103. package/template_scaffold/docs/funnelsgrove/steps/subscription_management.md +112 -0
  104. package/template_scaffold/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
  105. package/template_scaffold/docs/funnelsgrove/steps/upsell_offer.md +112 -0
  106. package/template_scaffold/docs/funnelsgrove/steps/value_prop_story.md +110 -0
  107. package/template_scaffold/docs/references/funnel-sdk-user-answers.md +5 -0
  108. package/template_scaffold/eslint.config.mjs +16 -0
  109. package/template_scaffold/funnel-agent-docs.test.ts +699 -0
  110. package/template_scaffold/funnel-docs.config.json +43 -0
  111. package/template_scaffold/funnel.config.json +5 -0
  112. package/template_scaffold/gitignore.template +5 -0
  113. package/template_scaffold/next-env.d.ts +6 -0
  114. package/template_scaffold/next.config.ts +53 -0
  115. package/template_scaffold/package-lock.json +7737 -0
  116. package/template_scaffold/package.json +37 -0
  117. package/template_scaffold/package.test.ts +23 -0
  118. package/template_scaffold/public/claimbee/bee.png +0 -0
  119. package/template_scaffold/public/claimbee/device-iphone.png +0 -0
  120. package/template_scaffold/public/claimbee/device-macbook.png +0 -0
  121. package/template_scaffold/public/claimbee/device-none.png +0 -0
  122. package/template_scaffold/public/claimbee/device-watch.png +0 -0
  123. package/template_scaffold/public/claimbee/glow.svg +3 -0
  124. package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
  125. package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
  126. package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
  127. package/template_scaffold/public/claimbee/testimonial-star.svg +3 -0
  128. package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
  129. package/template_scaffold/public/paywall/claim-history.png +0 -0
  130. package/template_scaffold/public/paywall/clear-progress.png +0 -0
  131. package/template_scaffold/public/paywall/hero.png +0 -0
  132. package/template_scaffold/public/paywall/money-alerts.png +0 -0
  133. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  134. package/template_scaffold/public/paywall/paywall-image.png +0 -0
  135. package/template_scaffold/public/paywall/r1.png +0 -0
  136. package/template_scaffold/public/paywall/r2.png +0 -0
  137. package/template_scaffold/public/paywall/r3.png +0 -0
  138. package/template_scaffold/public/paywall/r4.png +0 -0
  139. package/template_scaffold/public/paywall/r5.png +0 -0
  140. package/template_scaffold/public/paywall/r6.png +0 -0
  141. package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
  142. package/template_scaffold/public/please/bee.png +0 -0
  143. package/template_scaffold/public/please/device-iphone.png +0 -0
  144. package/template_scaffold/public/please/device-macbook.png +0 -0
  145. package/template_scaffold/public/please/device-none.png +0 -0
  146. package/template_scaffold/public/please/device-watch.png +0 -0
  147. package/template_scaffold/public/please/glow.svg +3 -0
  148. package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
  149. package/template_scaffold/public/please/steps-sprite-default.png +0 -0
  150. package/template_scaffold/public/please/testimonial-avatar.png +0 -0
  151. package/template_scaffold/public/please/testimonial-star.svg +3 -0
  152. package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
  153. package/template_scaffold/src/app/[stepId]/page.test.tsx +59 -0
  154. package/template_scaffold/src/app/[stepId]/page.tsx +62 -0
  155. package/template_scaffold/src/app/globals.css +49 -0
  156. package/template_scaffold/src/app/layout.test.ts +26 -0
  157. package/template_scaffold/src/app/layout.tsx +61 -0
  158. package/template_scaffold/src/app/page.tsx +8 -0
  159. package/template_scaffold/src/app/sdk/page.tsx +673 -0
  160. package/template_scaffold/src/app/subscription-dashboard/page.tsx +274 -0
  161. package/template_scaffold/src/assets/claimbee/bee.png +0 -0
  162. package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
  163. package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
  164. package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
  165. package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
  166. package/template_scaffold/src/assets/claimbee/glow.svg +3 -0
  167. package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
  168. package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
  169. package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
  170. package/template_scaffold/src/assets/claimbee/testimonial-star.svg +3 -0
  171. package/template_scaffold/src/components/FunnelEditorPanel.tsx +194 -0
  172. package/template_scaffold/src/components/FunnelFlow.test.tsx +17 -0
  173. package/template_scaffold/src/components/FunnelFlow.tsx +114 -0
  174. package/template_scaffold/src/components/FunnelStepImage.test.ts +15 -0
  175. package/template_scaffold/src/components/FunnelStepImage.tsx +28 -0
  176. package/template_scaffold/src/components/step/FunnelStepHeader.tsx +35 -0
  177. package/template_scaffold/src/config/billing.plans.test.ts +143 -0
  178. package/template_scaffold/src/config/billing.plans.ts +69 -0
  179. package/template_scaffold/src/config/billing.test.plans.ts +32 -0
  180. package/template_scaffold/src/config/experiments.generated.ts +4 -0
  181. package/template_scaffold/src/config/experiments.test.ts +12 -0
  182. package/template_scaffold/src/config/experiments.ts +9 -0
  183. package/template_scaffold/src/config/funnel.config.ts +12 -0
  184. package/template_scaffold/src/config/funnel.manifest.test.ts +166 -0
  185. package/template_scaffold/src/config/funnel.manifest.ts +139 -0
  186. package/template_scaffold/src/config/offer-sets.generated.ts +4 -0
  187. package/template_scaffold/src/contract/funnel-project.validation.test.ts +80 -0
  188. package/template_scaffold/src/contract/funnel-project.validation.ts +44 -0
  189. package/template_scaffold/src/contract/funnel-validator.vite.config.ts +11 -0
  190. package/template_scaffold/src/contract/golden/cancellation_offer.fixture.ts +29 -0
  191. package/template_scaffold/src/contract/golden/checkout.fixture.ts +37 -0
  192. package/template_scaffold/src/contract/golden/form_input.fixture.ts +28 -0
  193. package/template_scaffold/src/contract/golden/golden-fixtures.test.ts +190 -0
  194. package/template_scaffold/src/contract/golden/intro_hero.fixture.ts +27 -0
  195. package/template_scaffold/src/contract/golden/multi_select_choice.fixture.ts +37 -0
  196. package/template_scaffold/src/contract/golden/paywall_offer.fixture.ts +30 -0
  197. package/template_scaffold/src/contract/golden/progress_interstitial.fixture.ts +28 -0
  198. package/template_scaffold/src/contract/golden/purchase_completed.fixture.ts +33 -0
  199. package/template_scaffold/src/contract/golden/single_step_choice.fixture.ts +36 -0
  200. package/template_scaffold/src/contract/golden/single_step_choice_emoji.fixture.ts +36 -0
  201. package/template_scaffold/src/contract/golden/social_proof.fixture.ts +27 -0
  202. package/template_scaffold/src/contract/golden/subscription_handoff.fixture.ts +30 -0
  203. package/template_scaffold/src/contract/golden/subscription_management.fixture.ts +29 -0
  204. package/template_scaffold/src/contract/golden/summary_confirmation.fixture.ts +27 -0
  205. package/template_scaffold/src/contract/golden/upsell_offer.fixture.ts +29 -0
  206. package/template_scaffold/src/contract/golden/value_prop_story.fixture.ts +27 -0
  207. package/template_scaffold/src/contract/validate-funnel.cli.test.ts +180 -0
  208. package/template_scaffold/src/contract/validate-funnel.cli.ts +199 -0
  209. package/template_scaffold/src/runtime/checkout-runtime-config.test.ts +60 -0
  210. package/template_scaffold/src/runtime/checkout-runtime-config.ts +26 -0
  211. package/template_scaffold/src/runtime/funnel-runtime.test.ts +25 -0
  212. package/template_scaffold/src/runtime/funnel-runtime.ts +118 -0
  213. package/template_scaffold/src/runtime/step-content-context.test.ts +38 -0
  214. package/template_scaffold/src/runtime/step-content-context.ts +65 -0
  215. package/template_scaffold/src/runtime/step-registry.ts +66 -0
  216. package/template_scaffold/src/runtime/step-variable-values.ts +31 -0
  217. package/template_scaffold/src/runtime/use-funnel-flow-controller.integration.test.ts +203 -0
  218. package/template_scaffold/src/runtime/use-funnel-flow-controller.test.ts +76 -0
  219. package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +53 -0
  220. package/template_scaffold/src/steps/content/email-capture.content.ts +29 -0
  221. package/template_scaffold/src/steps/content/manage-subscription.content.ts +62 -0
  222. package/template_scaffold/src/steps/content/paywall.content.ts +191 -0
  223. package/template_scaffold/src/steps/content/step-01.content.ts +23 -0
  224. package/template_scaffold/src/steps/content/step-02.content.ts +77 -0
  225. package/template_scaffold/src/steps/content/step-03.content.ts +40 -0
  226. package/template_scaffold/src/steps/content/step-04.content.ts +49 -0
  227. package/template_scaffold/src/steps/content/step-32-paywall.content.ts +352 -0
  228. package/template_scaffold/src/steps/content/subscription-started.content.ts +68 -0
  229. package/template_scaffold/src/steps/editor/email-capture.editor.ts +58 -0
  230. package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +179 -0
  231. package/template_scaffold/src/steps/editor/paywall.editor.ts +232 -0
  232. package/template_scaffold/src/steps/editor/step-01.editor.ts +22 -0
  233. package/template_scaffold/src/steps/editor/step-02.editor.ts +41 -0
  234. package/template_scaffold/src/steps/editor/step-03.editor.ts +58 -0
  235. package/template_scaffold/src/steps/editor/step-04.editor.ts +98 -0
  236. package/template_scaffold/src/steps/editor/step-32-paywall.editor.ts +478 -0
  237. package/template_scaffold/src/steps/editor/subscription-started.editor.ts +131 -0
  238. package/template_scaffold/src/steps/index.ts +25 -0
  239. package/template_scaffold/src/steps/paywall-checkout-options.test.ts +104 -0
  240. package/template_scaffold/src/steps/paywall-checkout-options.ts +130 -0
  241. package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +12 -0
  242. package/template_scaffold/src/steps/pricing/paywall.pricing.ts +12 -0
  243. package/template_scaffold/src/steps/pricing/shared-plan-order.ts +17 -0
  244. package/template_scaffold/src/steps/pricing/upsell-form.pricing.ts +16 -0
  245. package/template_scaffold/src/steps/shared-header-contract.test.ts +27 -0
  246. package/template_scaffold/src/steps/step-01.test.ts +38 -0
  247. package/template_scaffold/src/steps/step-01.tsx +108 -0
  248. package/template_scaffold/src/steps/step-02.test.ts +43 -0
  249. package/template_scaffold/src/steps/step-02.tsx +140 -0
  250. package/template_scaffold/src/steps/step-03.test.ts +15 -0
  251. package/template_scaffold/src/steps/step-03.tsx +262 -0
  252. package/template_scaffold/src/steps/step-04.test.ts +32 -0
  253. package/template_scaffold/src/steps/step-04.tsx +673 -0
  254. package/template_scaffold/src/steps/step-31-email-capture.test.ts +22 -0
  255. package/template_scaffold/src/steps/step-31-email-capture.tsx +195 -0
  256. package/template_scaffold/src/steps/step-32-paywall.test.ts +770 -0
  257. package/template_scaffold/src/steps/step-32-paywall.tsx +2253 -0
  258. package/template_scaffold/src/steps/step-32b-paywall-b.tsx +35 -0
  259. package/template_scaffold/src/steps/step-33-subscription-started.test.ts +26 -0
  260. package/template_scaffold/src/steps/step-33-subscription-started.tsx +86 -0
  261. package/template_scaffold/src/steps/step-34-upsell-form.test.ts +27 -0
  262. package/template_scaffold/src/steps/step-35-manage-subscription.tsx +46 -0
  263. package/template_scaffold/src/steps/step-content.registry.ts +45 -0
  264. package/template_scaffold/src/steps/step-image-migration.test.ts +20 -0
  265. package/template_scaffold/src/steps/styles/index.css +2 -0
  266. package/template_scaffold/src/steps/styles/shared/final-flow-shared.css +55 -0
  267. package/template_scaffold/src/steps/styles/shared/funnel-base.css +543 -0
  268. package/template_scaffold/src/steps/styles/shared/info-shared.css +139 -0
  269. package/template_scaffold/src/steps/styles/shared/progress-shared.css +117 -0
  270. package/template_scaffold/src/steps/styles/shared/question-shared.css +76 -0
  271. package/template_scaffold/src/steps/styles/shared/responsive-container.css +176 -0
  272. package/template_scaffold/src/steps/types.ts +7 -0
  273. package/template_scaffold/src/steps/use-claimbee-step-content.ts +15 -0
  274. package/template_scaffold/src/theme/theme.ts +81 -0
  275. package/template_scaffold/tsconfig.json +38 -0
  276. package/template_scaffold/vitest.config.ts +32 -0
  277. package/template_validation/.funnelsgrove-contract-tools.json +19 -0
  278. package/template_validation/src/contract/funnel-project.validation.ts +44 -0
  279. package/template_validation/src/contract/funnel-validator.vite.config.ts +11 -0
  280. package/template_validation/src/contract/validate-funnel.cli.ts +199 -0
  281. package/template_docs/docs/ab-experiments.md +0 -55
  282. package/template_docs/docs/analytics.md +0 -65
  283. package/template_docs/docs/editing-flow.md +0 -125
  284. package/template_docs/docs/editing-step.md +0 -125
  285. package/template_docs/docs/editor-and-content.md +0 -58
  286. package/template_docs/docs/funnel-runtime-architecture.md +0 -125
  287. package/template_docs/docs/meta-pixel-conversions-api.md +0 -50
  288. package/template_docs/docs/payment-plans-and-discounts.md +0 -106
  289. package/template_docs/docs/publishing-and-versioning.md +0 -117
  290. package/template_docs/docs/qa-checklist.md +0 -80
  291. package/template_docs/docs/sdk-api-endpoints.md +0 -78
  292. package/template_docs/docs/step-ui-guidelines.md +0 -110
  293. package/template_docs/docs/theme.md +0 -35
@@ -23,6 +23,26 @@ export type AnalyticsOverviewResponse = {
23
23
  chargedRevenueMinor: number;
24
24
  }>;
25
25
  };
26
+ export type AnalyticsFunnelFlow = {
27
+ steps: Array<{
28
+ stepId: string;
29
+ stepName: string | null;
30
+ stepPath: string | null;
31
+ stepTitle: string | null;
32
+ stepType: string | null;
33
+ stepKind: string | null;
34
+ stepOrder: number;
35
+ }>;
36
+ experiments: unknown[];
37
+ branches: unknown[];
38
+ };
39
+ export type PublishedFunnelFlowResponse = {
40
+ funnelId: string;
41
+ publishedVersionId: string | null;
42
+ status: 'ready' | 'syncing' | 'unpublished';
43
+ flow: AnalyticsFunnelFlow | null;
44
+ updatedAt: string | null;
45
+ };
26
46
  export type AnalyticsStepDropoffResponse = {
27
47
  generatedAt: string;
28
48
  from: string | null;
@@ -61,19 +81,7 @@ export type AnalyticsStepDropoffResponse = {
61
81
  chartPoints: unknown[];
62
82
  }>;
63
83
  steps: AnalyticsStepTransition[];
64
- flow: {
65
- steps: Array<{
66
- stepId: string;
67
- stepName: string | null;
68
- stepPath: string | null;
69
- stepTitle: string | null;
70
- stepType: string | null;
71
- stepKind: string | null;
72
- stepOrder: number;
73
- }>;
74
- experiments: unknown[];
75
- branches: unknown[];
76
- } | null;
84
+ flow: AnalyticsFunnelFlow | null;
77
85
  };
78
86
  export type AnalyticsStepTransition = {
79
87
  stepId: string;
@@ -97,6 +105,9 @@ export type MarketingCohortPerformanceRow = {
97
105
  subscribersAlive: number;
98
106
  subscribersAlivePercent: number | null;
99
107
  revenue: number;
108
+ revenueDay3: number;
109
+ revenueDay90: number;
110
+ revenueDay180: number;
100
111
  pLtv: number | null;
101
112
  pLtvPredictedNetRevenueDay365Minor: number;
102
113
  pLtvPredictedCustomerCount: number;
@@ -130,7 +141,7 @@ export declare const parseAnalyticsDate: (value: string) => ParsedAnalyticsDate;
130
141
  export declare const normalizeAnalyticsOutputFormat: (value: string | undefined) => AnalyticsOutputFormat;
131
142
  export declare const assertHasConversionData: (date: string, overview: AnalyticsOverviewResponse, stepDropoff: AnalyticsStepDropoffResponse) => void;
132
143
  export declare const assertHasTransitionData: (date: string, stepDropoff: AnalyticsStepDropoffResponse) => void;
133
- export declare const assertHasFunnelPathData: (date: string, stepDropoff: AnalyticsStepDropoffResponse) => void;
144
+ export declare const assertHasFunnelPathData: (date: string, stepDropoff: AnalyticsStepDropoffResponse, publishedFunnelFlow: PublishedFunnelFlowResponse) => void;
134
145
  export declare const assertHasCohortData: (date: string, report: MarketingCohortPerformanceReport) => void;
135
146
  export declare const buildTransitionRows: (stepDropoff: AnalyticsStepDropoffResponse) => {
136
147
  stepId: string;
@@ -141,7 +152,7 @@ export declare const buildTransitionRows: (stepDropoff: AnalyticsStepDropoffResp
141
152
  dropoffs: number;
142
153
  dropoffRate: number;
143
154
  }[];
144
- export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResponse) => {
155
+ export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResponse, publishedFunnelFlow: PublishedFunnelFlowResponse) => {
145
156
  stepId: string;
146
157
  path: string;
147
158
  label: string;
@@ -154,6 +165,7 @@ export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResp
154
165
  export declare const buildConversionReportPayload: (input: ReportContext & {
155
166
  overview: AnalyticsOverviewResponse;
156
167
  stepDropoff: AnalyticsStepDropoffResponse;
168
+ publishedFunnelFlow: PublishedFunnelFlowResponse;
157
169
  }) => {
158
170
  generatedAt: string;
159
171
  date: string;
@@ -199,6 +211,7 @@ export declare const buildConversionReportPayload: (input: ReportContext & {
199
211
  };
200
212
  export declare const buildFunnelPathReportPayload: (input: ReportContext & {
201
213
  stepDropoff: AnalyticsStepDropoffResponse;
214
+ publishedFunnelFlow: PublishedFunnelFlowResponse;
202
215
  }) => {
203
216
  generatedAt: string;
204
217
  date: string;
@@ -263,11 +276,13 @@ export declare const formatTransitionsTable: (input: {
263
276
  export declare const formatFunnelPathTable: (input: {
264
277
  date: string;
265
278
  stepDropoff: AnalyticsStepDropoffResponse;
279
+ publishedFunnelFlow: PublishedFunnelFlowResponse;
266
280
  }) => string;
267
281
  export declare const formatConversionsTable: (input: {
268
282
  date: string;
269
283
  overview: AnalyticsOverviewResponse;
270
284
  stepDropoff: AnalyticsStepDropoffResponse;
285
+ publishedFunnelFlow: PublishedFunnelFlowResponse;
271
286
  }) => string;
272
287
  export declare const formatCohortTable: (input: {
273
288
  date: string;
@@ -55,8 +55,8 @@ export const assertHasTransitionData = (date, stepDropoff) => {
55
55
  }
56
56
  throw new Error(`No synced step transition data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked step traffic that day.`);
57
57
  };
58
- export const assertHasFunnelPathData = (date, stepDropoff) => {
59
- if (buildFunnelPathRows(stepDropoff).length > 0 && hasPositiveNumber(stepDropoff.steps)) {
58
+ export const assertHasFunnelPathData = (date, stepDropoff, publishedFunnelFlow) => {
59
+ if (buildFunnelPathRows(stepDropoff, publishedFunnelFlow).length > 0) {
60
60
  return;
61
61
  }
62
62
  throw new Error(`No synced full funnel path data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked path traffic that day.`);
@@ -85,13 +85,6 @@ const formatCurrency = (value) => new Intl.NumberFormat('en-US', {
85
85
  }).format(value);
86
86
  const formatMinorCurrency = (value) => formatCurrency((Number(value) || 0) / 100);
87
87
  const stepDisplayName = (step) => step.stepName || step.stepId;
88
- const flowStepDisplay = (stepDropoff, step) => {
89
- const flowStep = stepDropoff.flow?.steps.find((candidate) => candidate.stepId === step.stepId);
90
- return {
91
- label: flowStep?.stepTitle || step.stepName || flowStep?.stepName || step.stepId,
92
- path: flowStep?.stepPath || step.stepId,
93
- };
94
- };
95
88
  export const buildTransitionRows = (stepDropoff) => [...stepDropoff.steps]
96
89
  .sort((left, right) => left.stepOrder - right.stepOrder || left.stepId.localeCompare(right.stepId))
97
90
  .map((step) => ({
@@ -103,20 +96,28 @@ export const buildTransitionRows = (stepDropoff) => [...stepDropoff.steps]
103
96
  dropoffs: step.dropoffs,
104
97
  dropoffRate: step.dropoffRate,
105
98
  }));
106
- export const buildFunnelPathRows = (stepDropoff) => buildTransitionRows(stepDropoff).map((row) => {
107
- const step = stepDropoff.steps.find((candidate) => candidate.stepId === row.stepId);
108
- const display = flowStepDisplay(stepDropoff, step);
109
- return {
110
- stepId: row.stepId,
111
- path: display.path,
112
- label: display.label,
113
- users: row.entrants,
114
- advancedToNextStep: row.advancedToNextStep,
115
- conversionToNextStep: row.entrants > 0 ? row.advancedToNextStep / row.entrants : 0,
116
- dropoffs: row.dropoffs,
117
- dropoffRate: row.dropoffRate,
118
- };
119
- });
99
+ export const buildFunnelPathRows = (stepDropoff, publishedFunnelFlow) => {
100
+ if (publishedFunnelFlow.status !== 'ready' || !publishedFunnelFlow.flow) {
101
+ return [];
102
+ }
103
+ const transitionsByStepId = new Map(stepDropoff.steps.map((step) => [step.stepId, step]));
104
+ return [...publishedFunnelFlow.flow.steps]
105
+ .sort((left, right) => left.stepOrder - right.stepOrder || left.stepId.localeCompare(right.stepId))
106
+ .map((flowStep) => {
107
+ const step = transitionsByStepId.get(flowStep.stepId);
108
+ const entrants = step?.entrants ?? 0;
109
+ return {
110
+ stepId: flowStep.stepId,
111
+ path: flowStep.stepPath || flowStep.stepId,
112
+ label: flowStep.stepTitle || flowStep.stepName || flowStep.stepId,
113
+ users: entrants,
114
+ advancedToNextStep: step?.advancedToNextStep ?? 0,
115
+ conversionToNextStep: entrants > 0 ? (step?.advancedToNextStep ?? 0) / entrants : 0,
116
+ dropoffs: step?.dropoffs ?? 0,
117
+ dropoffRate: step?.dropoffRate ?? 0,
118
+ };
119
+ });
120
+ };
120
121
  export const buildConversionReportPayload = (input) => ({
121
122
  generatedAt: new Date().toISOString(),
122
123
  date: input.date,
@@ -126,7 +127,7 @@ export const buildConversionReportPayload = (input) => ({
126
127
  overview: input.overview,
127
128
  totals: input.stepDropoff.totals,
128
129
  metrics: input.stepDropoff.metrics,
129
- funnelPath: buildFunnelPathRows(input.stepDropoff),
130
+ funnelPath: buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow),
130
131
  transitions: buildTransitionRows(input.stepDropoff),
131
132
  });
132
133
  export const buildFunnelPathReportPayload = (input) => ({
@@ -135,7 +136,7 @@ export const buildFunnelPathReportPayload = (input) => ({
135
136
  workspaceId: input.workspaceId,
136
137
  project: input.project,
137
138
  funnel: input.stepDropoff.funnel,
138
- funnelPath: buildFunnelPathRows(input.stepDropoff),
139
+ funnelPath: buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow),
139
140
  });
140
141
  export const buildTransitionReportPayload = (input) => ({
141
142
  generatedAt: new Date().toISOString(),
@@ -176,7 +177,7 @@ export const formatFunnelPathTable = (input) => {
176
177
  `Full funnel path\t${input.date}\t${input.stepDropoff.funnel.name}`,
177
178
  'step\tpath\tusers\tadvanced\tconversionToNext\tdropoffs\tdropoffRate',
178
179
  ];
179
- for (const row of buildFunnelPathRows(input.stepDropoff)) {
180
+ for (const row of buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow)) {
180
181
  lines.push([
181
182
  row.label,
182
183
  row.path,
@@ -219,6 +220,7 @@ export const formatConversionsTable = (input) => {
219
220
  lines.push('', formatFunnelPathTable({
220
221
  date: input.date,
221
222
  stepDropoff: input.stepDropoff,
223
+ publishedFunnelFlow: input.publishedFunnelFlow,
222
224
  }).trimEnd());
223
225
  return `${lines.join('\n')}\n`;
224
226
  };
@@ -229,14 +231,24 @@ const predictedAverageLtv = (row) => {
229
231
  }
230
232
  return Number(row.pLtv) || 0;
231
233
  };
234
+ const ratioToSpend = (value, spend) => (Number.isFinite(value) && Number.isFinite(spend) && spend !== 0 ? value / spend : null);
235
+ const formatRoi = (profit, spend) => {
236
+ const ratio = ratioToSpend(profit, spend);
237
+ return ratio === null ? '' : `${(ratio * 100).toFixed(2)}%`;
238
+ };
239
+ const formatRoas = (revenue, spend) => {
240
+ const ratio = ratioToSpend(revenue, spend);
241
+ return ratio === null ? '' : `${ratio.toFixed(2)}x`;
242
+ };
232
243
  export const formatCohortTable = (input) => {
233
244
  const lines = [
234
245
  `Cohort\t${input.date}`,
235
- 'cohort\tmediaSource\tfunnelUrl\tspend\tsubscribers\taliveRate\trevenue\tpLtv\tcpa\tpredictedProfit',
246
+ 'cohort\tmediaSource\tfunnelUrl\tspend\tsubscribers\taliveRate\trevenue\tpLtv\tcpa\tpredictedProfit\tpROI\tROAS\tROAS d3\tROAS d90\tROAS d180',
236
247
  ];
237
248
  for (const row of flattenCohortRows(input.cohort.rows)) {
238
249
  const cpa = row.subscribers > 0 ? row.spend / row.subscribers : 0;
239
250
  const predictedRevenue = predictedAverageLtv(row) * row.subscribers;
251
+ const predictedProfit = predictedRevenue - row.spend;
240
252
  lines.push([
241
253
  row.cohortDate,
242
254
  row.mediaSource,
@@ -247,7 +259,12 @@ export const formatCohortTable = (input) => {
247
259
  formatCurrency(row.revenue),
248
260
  row.pLtv === null ? '' : formatCurrency(row.pLtv),
249
261
  row.subscribers > 0 ? formatCurrency(cpa) : '',
250
- formatCurrency(predictedRevenue - row.spend),
262
+ formatCurrency(predictedProfit),
263
+ formatRoi(predictedProfit, row.spend),
264
+ formatRoas(row.revenue, row.spend),
265
+ formatRoas(row.revenueDay3, row.spend),
266
+ formatRoas(row.revenueDay90, row.spend),
267
+ formatRoas(row.revenueDay180, row.spend),
251
268
  ].join('\t'));
252
269
  }
253
270
  return `${lines.join('\n')}\n`;
@@ -1,3 +1,5 @@
1
+ import { type FunnelContractDiagnosticCode } from '@funnelsgrove/runtime';
2
+ import type { SourceCandidateOperation } from './localSync.js';
1
3
  type FetchFn = typeof fetch;
2
4
  type TrpcProcedureType = 'query' | 'mutation';
3
5
  export type TrpcCallInput = {
@@ -7,7 +9,90 @@ export type TrpcCallInput = {
7
9
  input?: unknown;
8
10
  token?: string | null;
9
11
  fetchFn?: FetchFn;
12
+ signal?: AbortSignal;
10
13
  };
14
+ export type FunnelSourceCandidateApiContext = {
15
+ apiUrl: string;
16
+ token?: string | null;
17
+ fetchFn?: FetchFn;
18
+ signal?: AbortSignal;
19
+ };
20
+ export type FunnelSourceCandidateStatus = 'staging' | 'validating' | 'committed' | 'aborted' | 'expired' | 'failed';
21
+ export type BeginFunnelSourceCandidateInput = {
22
+ workspaceId: string;
23
+ funnelId: string;
24
+ baseDraftVersionId: string | null;
25
+ idempotencyKey: string;
26
+ };
27
+ export type StageFunnelSourceCandidateInput = {
28
+ workspaceId: string;
29
+ candidateId: string;
30
+ idempotencyKey: string;
31
+ operations: SourceCandidateOperation[];
32
+ };
33
+ export type FinishFunnelSourceCandidateInput = {
34
+ workspaceId: string;
35
+ candidateId: string;
36
+ };
37
+ export type FinalizeFunnelSourceCandidateInput = FinishFunnelSourceCandidateInput & {
38
+ message?: string;
39
+ };
40
+ export type FunnelSourceCandidateSummary = {
41
+ candidateId: string;
42
+ status: FunnelSourceCandidateStatus;
43
+ baseDraftVersionId: string | null;
44
+ fileCount?: number;
45
+ byteCount?: number;
46
+ batchCount?: number;
47
+ };
48
+ export type StageFunnelSourceCandidateResult = {
49
+ candidateId: string;
50
+ stageId: string;
51
+ batchIndex: number;
52
+ operationCount: number;
53
+ fileCount: number;
54
+ byteCount: number;
55
+ totalOperationCount: number;
56
+ idempotent: boolean;
57
+ };
58
+ export type FinalizeFunnelSourceCandidateResult = {
59
+ candidateId: string;
60
+ projectId: string;
61
+ funnelId: string;
62
+ versionId: string;
63
+ status: 'committed';
64
+ idempotent: boolean;
65
+ diagnostics: readonly FunnelContractApiDiagnostic[];
66
+ };
67
+ export declare const FUNNEL_CONTRACT_API_INGRESSES: readonly ["agent-transaction", "cli-sync", "github-sync", "builder-authoring", "preview-publish", "production-publish", "published-artifact-serving"];
68
+ export type FunnelContractApiIngress = (typeof FUNNEL_CONTRACT_API_INGRESSES)[number];
69
+ export type FunnelContractApiDiagnostic = {
70
+ code: FunnelContractDiagnosticCode;
71
+ severity: 'error';
72
+ ingress: FunnelContractApiIngress;
73
+ file: string | null;
74
+ stepId: string | null;
75
+ expected: unknown;
76
+ received: unknown;
77
+ reason: string;
78
+ guide: string;
79
+ repair: string;
80
+ };
81
+ export type FunnelContractApiErrorData = {
82
+ schemaVersion: 1;
83
+ contractVersion: number;
84
+ ingress: FunnelContractApiIngress;
85
+ diagnostics: readonly FunnelContractApiDiagnostic[];
86
+ };
87
+ export declare class FunnelContractApiError extends Error {
88
+ readonly funnelContract: FunnelContractApiErrorData;
89
+ constructor(message: string, funnelContract: FunnelContractApiErrorData);
90
+ }
91
+ export declare const parseFunnelContractApiErrorData: (value: unknown) => FunnelContractApiErrorData | null;
11
92
  export declare function parseTrpcJsonResponse<T = unknown>(response: unknown): T;
12
93
  export declare function callTrpcProcedure<T = unknown>(input: TrpcCallInput): Promise<T>;
94
+ export declare const beginFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: BeginFunnelSourceCandidateInput) => Promise<FunnelSourceCandidateSummary>;
95
+ export declare const stageFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: StageFunnelSourceCandidateInput) => Promise<StageFunnelSourceCandidateResult>;
96
+ export declare const finalizeFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: FinalizeFunnelSourceCandidateInput) => Promise<FinalizeFunnelSourceCandidateResult>;
97
+ export declare const abortFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: FinishFunnelSourceCandidateInput) => Promise<FunnelSourceCandidateSummary>;
13
98
  export {};
package/dist/apiClient.js CHANGED
@@ -1,22 +1,227 @@
1
+ import { CURRENT_STEP_CONTRACT_VERSION, FUNNEL_CONTRACT_DIAGNOSTIC_CODES, } from '@funnelsgrove/runtime';
2
+ export const FUNNEL_CONTRACT_API_INGRESSES = Object.freeze([
3
+ 'agent-transaction',
4
+ 'cli-sync',
5
+ 'github-sync',
6
+ 'builder-authoring',
7
+ 'preview-publish',
8
+ 'production-publish',
9
+ 'published-artifact-serving',
10
+ ]);
11
+ export class FunnelContractApiError extends Error {
12
+ funnelContract;
13
+ constructor(message, funnelContract) {
14
+ super(`tRPC request failed: ${message}`);
15
+ this.name = 'FunnelContractApiError';
16
+ this.funnelContract = funnelContract;
17
+ }
18
+ }
19
+ const ENVELOPE_KEYS = Object.freeze([
20
+ 'schemaVersion',
21
+ 'contractVersion',
22
+ 'ingress',
23
+ 'diagnostics',
24
+ ]);
25
+ const DIAGNOSTIC_KEYS = Object.freeze([
26
+ 'code',
27
+ 'severity',
28
+ 'ingress',
29
+ 'file',
30
+ 'stepId',
31
+ 'expected',
32
+ 'received',
33
+ 'reason',
34
+ 'guide',
35
+ 'repair',
36
+ ]);
37
+ const DIAGNOSTIC_CODE_SET = new Set(FUNNEL_CONTRACT_DIAGNOSTIC_CODES);
38
+ const INGRESS_SET = new Set(FUNNEL_CONTRACT_API_INGRESSES);
39
+ const MAX_DIAGNOSTICS = 256;
40
+ const MAX_JSON_DEPTH = 8;
41
+ const MAX_JSON_ITEMS = 256;
42
+ const MAX_TRANSPORT_STRING_LENGTH = 4_096;
43
+ const UNSAFE_JSON_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
1
44
  function isObject(value) {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value);
45
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
46
+ return false;
47
+ try {
48
+ const prototype = Object.getPrototypeOf(value);
49
+ return prototype === Object.prototype || prototype === null;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
3
54
  }
55
+ const ownDataValue = (value, key) => {
56
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
57
+ return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
58
+ };
59
+ const hasExactDataProperties = (value, keys) => {
60
+ if (!isObject(value)) {
61
+ return false;
62
+ }
63
+ let ownKeys;
64
+ try {
65
+ ownKeys = Reflect.ownKeys(value);
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ if (ownKeys.length !== keys.length || ownKeys.some((key) => typeof key !== 'string')) {
71
+ return false;
72
+ }
73
+ const expectedKeys = new Set(keys);
74
+ return ownKeys.every((key) => {
75
+ if (typeof key !== 'string' || !expectedKeys.has(key)) {
76
+ return false;
77
+ }
78
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
79
+ return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
80
+ });
81
+ };
82
+ const readDenseDataArray = (value, maximum) => {
83
+ if (!Array.isArray(value))
84
+ return null;
85
+ const descriptors = (() => {
86
+ try {
87
+ return Object.getOwnPropertyDescriptors(value);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ })();
93
+ if (descriptors === null)
94
+ return null;
95
+ const length = descriptors.length?.value;
96
+ if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0 || length > maximum) {
97
+ return null;
98
+ }
99
+ const keys = Reflect.ownKeys(descriptors);
100
+ if (keys.length !== length + 1
101
+ || keys.some((key) => (typeof key !== 'string'
102
+ || (key !== 'length' && !/^(?:0|[1-9]\d*)$/.test(key)))))
103
+ return null;
104
+ const result = [];
105
+ for (let index = 0; index < length; index += 1) {
106
+ const descriptor = descriptors[String(index)];
107
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value'))
108
+ return null;
109
+ result.push(descriptor.value);
110
+ }
111
+ return result;
112
+ };
113
+ const isJsonValue = (value, seen = new Set(), depth = 0) => {
114
+ if (value === null
115
+ || typeof value === 'boolean') {
116
+ return true;
117
+ }
118
+ if (typeof value === 'string')
119
+ return value.length <= MAX_TRANSPORT_STRING_LENGTH;
120
+ if (typeof value === 'number') {
121
+ return Number.isFinite(value);
122
+ }
123
+ if (typeof value !== 'object' || seen.has(value) || depth >= MAX_JSON_DEPTH) {
124
+ return false;
125
+ }
126
+ seen.add(value);
127
+ try {
128
+ if (Array.isArray(value)) {
129
+ const items = readDenseDataArray(value, MAX_JSON_ITEMS);
130
+ return items !== null && items.every((item) => isJsonValue(item, seen, depth + 1));
131
+ }
132
+ const prototype = Object.getPrototypeOf(value);
133
+ if (prototype !== Object.prototype && prototype !== null) {
134
+ return false;
135
+ }
136
+ const keys = Reflect.ownKeys(value);
137
+ return keys.length <= MAX_JSON_ITEMS && keys.every((key) => {
138
+ if (typeof key !== 'string') {
139
+ return false;
140
+ }
141
+ if (UNSAFE_JSON_KEYS.has(key))
142
+ return false;
143
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
144
+ return descriptor?.enumerable === true
145
+ && Object.hasOwn(descriptor, 'value')
146
+ && isJsonValue(descriptor.value, seen, depth + 1);
147
+ });
148
+ }
149
+ finally {
150
+ seen.delete(value);
151
+ }
152
+ };
153
+ const isNullableString = (value) => (value === null
154
+ || (typeof value === 'string' && value.length <= MAX_TRANSPORT_STRING_LENGTH));
155
+ const isBoundedString = (value) => (typeof value === 'string' && value.length <= MAX_TRANSPORT_STRING_LENGTH);
156
+ const isFunnelContractApiDiagnostic = (value, ingress) => {
157
+ if (!hasExactDataProperties(value, DIAGNOSTIC_KEYS))
158
+ return false;
159
+ const code = ownDataValue(value, 'code');
160
+ return typeof code === 'string'
161
+ && DIAGNOSTIC_CODE_SET.has(code)
162
+ && ownDataValue(value, 'severity') === 'error'
163
+ && ownDataValue(value, 'ingress') === ingress
164
+ && isNullableString(ownDataValue(value, 'file'))
165
+ && isNullableString(ownDataValue(value, 'stepId'))
166
+ && isJsonValue(ownDataValue(value, 'expected'))
167
+ && isJsonValue(ownDataValue(value, 'received'))
168
+ && isBoundedString(ownDataValue(value, 'reason'))
169
+ && isBoundedString(ownDataValue(value, 'guide'))
170
+ && isBoundedString(ownDataValue(value, 'repair'));
171
+ };
172
+ export const parseFunnelContractApiErrorData = (value) => {
173
+ if (!hasExactDataProperties(value, ENVELOPE_KEYS)
174
+ || ownDataValue(value, 'schemaVersion') !== 1
175
+ || ownDataValue(value, 'contractVersion') !== CURRENT_STEP_CONTRACT_VERSION) {
176
+ return null;
177
+ }
178
+ const ingressValue = ownDataValue(value, 'ingress');
179
+ if (typeof ingressValue !== 'string' || !INGRESS_SET.has(ingressValue))
180
+ return null;
181
+ const diagnostics = readDenseDataArray(ownDataValue(value, 'diagnostics'), MAX_DIAGNOSTICS);
182
+ if (diagnostics === null)
183
+ return null;
184
+ const ingress = ingressValue;
185
+ return diagnostics.every((diagnostic) => (isFunnelContractApiDiagnostic(diagnostic, ingress))) ? value : null;
186
+ };
187
+ const readFunnelContractApiErrorData = (error) => {
188
+ const data = ownDataValue(error, 'data');
189
+ if (!isObject(data)) {
190
+ return null;
191
+ }
192
+ const descriptor = Object.getOwnPropertyDescriptor(data, 'funnelContract');
193
+ return descriptor && Object.hasOwn(descriptor, 'value')
194
+ ? parseFunnelContractApiErrorData(descriptor.value)
195
+ : null;
196
+ };
4
197
  export function parseTrpcJsonResponse(response) {
5
198
  if (!isObject(response)) {
6
199
  throw new Error('Invalid tRPC response');
7
200
  }
8
- if (isObject(response.error)) {
9
- const message = typeof response.error.message === 'string' ? response.error.message : 'Unknown error';
201
+ const errorValue = ownDataValue(response, 'error');
202
+ if (isObject(errorValue)) {
203
+ const messageValue = ownDataValue(errorValue, 'message');
204
+ const message = typeof messageValue === 'string' ? messageValue : 'Unknown error';
205
+ const funnelContract = readFunnelContractApiErrorData(errorValue);
206
+ if (funnelContract) {
207
+ throw new FunnelContractApiError(message, funnelContract);
208
+ }
10
209
  throw new Error(`tRPC request failed: ${message}`);
11
210
  }
12
- const result = response.result;
13
- if (!isObject(result) || !('data' in result)) {
211
+ const result = ownDataValue(response, 'result');
212
+ if (!isObject(result)) {
14
213
  throw new Error('Invalid tRPC response');
15
214
  }
16
- if (isObject(result.data) && 'json' in result.data) {
17
- return result.data.json;
215
+ const dataDescriptor = Object.getOwnPropertyDescriptor(result, 'data');
216
+ if (!dataDescriptor || !Object.hasOwn(dataDescriptor, 'value'))
217
+ throw new Error('Invalid tRPC response');
218
+ const data = dataDescriptor.value;
219
+ if (isObject(data)) {
220
+ const jsonDescriptor = Object.getOwnPropertyDescriptor(data, 'json');
221
+ if (jsonDescriptor && Object.hasOwn(jsonDescriptor, 'value'))
222
+ return jsonDescriptor.value;
18
223
  }
19
- return result.data;
224
+ return data;
20
225
  }
21
226
  const trimTrailingSlash = (value) => value.trim().replace(/\/+$/, '');
22
227
  const buildHeaders = (input) => {
@@ -36,11 +241,13 @@ export async function callTrpcProcedure(input) {
36
241
  ? {
37
242
  method: 'GET',
38
243
  headers: buildHeaders({ token: input.token }),
244
+ signal: input.signal,
39
245
  }
40
246
  : {
41
247
  method: 'POST',
42
248
  headers: buildHeaders({ token: input.token, contentType: 'application/json' }),
43
249
  body: JSON.stringify(input.input ?? {}),
250
+ signal: input.signal,
44
251
  };
45
252
  const url = input.type === 'query' && input.input !== undefined
46
253
  ? `${baseUrl}?input=${encodeURIComponent(JSON.stringify(input.input))}`
@@ -59,3 +266,13 @@ export async function callTrpcProcedure(input) {
59
266
  }
60
267
  return parseTrpcJsonResponse(json);
61
268
  }
269
+ const callFunnelSourceCandidateMutation = (context, path, input) => callTrpcProcedure({
270
+ ...context,
271
+ path,
272
+ type: 'mutation',
273
+ input,
274
+ });
275
+ export const beginFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.beginSourceCandidate', input);
276
+ export const stageFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.stageSourceCandidate', input);
277
+ export const finalizeFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.finalizeSourceCandidate', input);
278
+ export const abortFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.abortSourceCandidate', input);