create-ailk 0.1.0 → 0.2.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 (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +12 -5
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +78 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Tests for submitFlowStepAction Server Action (#3957 D8).
3
+ *
4
+ * Acceptance criteria verified here:
5
+ * - submitFlowStepAction(slug, validPayload) calls api-client flows.submitStep
6
+ * and returns { ok: true, response } passthrough.
7
+ * - invalid slug / invalid payload → { ok: false, kind: "invalid_input" }
8
+ * without calling api-client.
9
+ * - api-client 400 → kind "invalid_input"; 409 → kind "step_conflict";
10
+ * 429 → kind "rate_limited" carrying retryAfterSeconds; other statuses /
11
+ * a thrown error → kind "network".
12
+ * - #4730 — the visitor's `x-forwarded-for` (read from THIS action's own
13
+ * incoming request via next/headers) is forwarded on the outbound
14
+ * flows.submitStep call; absent entirely when no header is present.
15
+ * - Parity: submitFlowStepAction is paired with ailkSubmitFlowStep per ADR 0013.
16
+ */
17
+
18
+ import { submitFlowStepAction } from "../flow.actions.js";
19
+
20
+ jest.mock("@working-theory/api-client", () => ({
21
+ flows: {
22
+ submitStep: jest.fn(),
23
+ },
24
+ }));
25
+
26
+ // #4730 — default: no incoming headers to forward, matching every
27
+ // pre-existing test's 2-arg `flows.submitStep(slug, payload)` expectation
28
+ // byte-for-byte. Individual #4730 tests override with a real x-forwarded-for.
29
+ jest.mock("next/headers", () => ({
30
+ __esModule: true,
31
+ headers: jest.fn().mockResolvedValue({ get: () => null }),
32
+ }));
33
+
34
+ import { flows } from "@working-theory/api-client";
35
+ import { headers } from "next/headers";
36
+ const mockSubmitStep = flows.submitStep as jest.Mock;
37
+ const mockHeaders = headers as jest.Mock;
38
+
39
+ afterEach(() => {
40
+ jest.clearAllMocks();
41
+ mockHeaders.mockResolvedValue({ get: () => null });
42
+ });
43
+
44
+ const VALID_PAYLOAD = {
45
+ stepKey: "contact",
46
+ values: { email: "jane@example.com" },
47
+ };
48
+
49
+ describe("submitFlowStepAction — valid input", () => {
50
+ it("calls flows.submitStep and returns the response passthrough on success", async () => {
51
+ mockSubmitStep.mockResolvedValueOnce({
52
+ ok: true,
53
+ body: {
54
+ sessionId: "sess1",
55
+ landed: "contact",
56
+ nextStepKey: "budget",
57
+ completed: false,
58
+ },
59
+ });
60
+
61
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
62
+
63
+ expect(mockSubmitStep).toHaveBeenCalledWith(
64
+ "get-a-quote",
65
+ expect.objectContaining(VALID_PAYLOAD),
66
+ );
67
+ expect(result).toEqual({
68
+ ok: true,
69
+ response: {
70
+ sessionId: "sess1",
71
+ landed: "contact",
72
+ nextStepKey: "budget",
73
+ completed: false,
74
+ },
75
+ });
76
+ });
77
+
78
+ it("threads sessionId and locale through to the api-client call", async () => {
79
+ mockSubmitStep.mockResolvedValueOnce({
80
+ ok: true,
81
+ body: {
82
+ sessionId: "sess1",
83
+ landed: "budget",
84
+ nextStepKey: "thanks",
85
+ completed: false,
86
+ },
87
+ });
88
+
89
+ await submitFlowStepAction("get-a-quote", {
90
+ sessionId: "sess1",
91
+ stepKey: "budget",
92
+ values: { budget: "small" },
93
+ locale: "en",
94
+ });
95
+
96
+ expect(mockSubmitStep).toHaveBeenCalledWith("get-a-quote", {
97
+ sessionId: "sess1",
98
+ stepKey: "budget",
99
+ values: { budget: "small" },
100
+ locale: "en",
101
+ });
102
+ });
103
+ });
104
+
105
+ describe("submitFlowStepAction — invalid input", () => {
106
+ it("returns invalid_input without calling api-client for a malformed slug", async () => {
107
+ const result = await submitFlowStepAction("Not A Slug!", VALID_PAYLOAD);
108
+
109
+ expect(result).toMatchObject({ ok: false, kind: "invalid_input" });
110
+ expect(mockSubmitStep).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it("returns invalid_input without calling api-client for a missing stepKey", async () => {
114
+ const result = await submitFlowStepAction("get-a-quote", {
115
+ stepKey: "",
116
+ values: {},
117
+ });
118
+
119
+ expect(result).toMatchObject({ ok: false, kind: "invalid_input" });
120
+ expect(mockSubmitStep).not.toHaveBeenCalled();
121
+ });
122
+ });
123
+
124
+ describe("submitFlowStepAction — api-client failure mapping", () => {
125
+ it("maps a 400 response to kind invalid_input", async () => {
126
+ mockSubmitStep.mockResolvedValueOnce({ ok: false, status: 400, body: {} });
127
+
128
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
129
+
130
+ expect(result).toEqual({ ok: false, kind: "invalid_input" });
131
+ });
132
+
133
+ it("maps a 409 response to kind step_conflict", async () => {
134
+ mockSubmitStep.mockResolvedValueOnce({ ok: false, status: 409, body: {} });
135
+
136
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
137
+
138
+ expect(result).toEqual({ ok: false, kind: "step_conflict" });
139
+ });
140
+
141
+ it("maps a 429 response to kind rate_limited, carrying retryAfterSeconds (#4730)", async () => {
142
+ mockSubmitStep.mockResolvedValueOnce({
143
+ ok: false,
144
+ status: 429,
145
+ body: { error: "rate_limited", retryAfterSeconds: 37 },
146
+ });
147
+
148
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
149
+
150
+ expect(result).toEqual({
151
+ ok: false,
152
+ kind: "rate_limited",
153
+ retryAfterSeconds: 37,
154
+ });
155
+ });
156
+
157
+ it("maps a 429 response with no retryAfterSeconds to kind rate_limited alone (#4730)", async () => {
158
+ mockSubmitStep.mockResolvedValueOnce({ ok: false, status: 429, body: {} });
159
+
160
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
161
+
162
+ expect(result).toEqual({ ok: false, kind: "rate_limited" });
163
+ });
164
+
165
+ it("maps any other failing status to kind network", async () => {
166
+ mockSubmitStep.mockResolvedValueOnce({ ok: false, status: 500, body: {} });
167
+
168
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
169
+
170
+ expect(result).toEqual({ ok: false, kind: "network" });
171
+ });
172
+
173
+ it("maps a thrown error to kind network", async () => {
174
+ mockSubmitStep.mockRejectedValueOnce(new Error("network down"));
175
+
176
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
177
+
178
+ expect(result).toEqual({ ok: false, kind: "network" });
179
+ });
180
+
181
+ it("logs a thrown error server-side without leaking submitted values", async () => {
182
+ const thrown = new Error("network down");
183
+ mockSubmitStep.mockRejectedValueOnce(thrown);
184
+ const consoleErrorSpy = jest
185
+ .spyOn(console, "error")
186
+ .mockImplementation(() => {});
187
+
188
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
189
+
190
+ expect(result).toEqual({ ok: false, kind: "network" });
191
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
192
+ const [logLine, loggedErr] = consoleErrorSpy.mock.calls[0] as [
193
+ string,
194
+ unknown,
195
+ ];
196
+ expect(logLine).toContain("[submitFlowStepAction]");
197
+ expect(logLine).toContain("get-a-quote");
198
+ expect(logLine).toContain(VALID_PAYLOAD.stepKey);
199
+ expect(logLine).not.toContain("jane@example.com");
200
+ expect(loggedErr).toBe(thrown);
201
+
202
+ consoleErrorSpy.mockRestore();
203
+ });
204
+ });
205
+
206
+ describe("submitFlowStepAction — visitor IP forwarding (#4730)", () => {
207
+ it("forwards x-forwarded-for from the action's own incoming request as init.headers", async () => {
208
+ mockHeaders.mockResolvedValueOnce({
209
+ get: (name: string) =>
210
+ name === "x-forwarded-for" ? "203.0.113.7, 9.9.9.9" : null,
211
+ });
212
+ mockSubmitStep.mockResolvedValueOnce({
213
+ ok: true,
214
+ body: {
215
+ sessionId: "sess1",
216
+ landed: "contact",
217
+ nextStepKey: "budget",
218
+ completed: false,
219
+ },
220
+ });
221
+
222
+ await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
223
+
224
+ expect(mockSubmitStep).toHaveBeenCalledWith(
225
+ "get-a-quote",
226
+ expect.objectContaining(VALID_PAYLOAD),
227
+ { headers: { "x-forwarded-for": "203.0.113.7, 9.9.9.9" } },
228
+ );
229
+ });
230
+
231
+ it("falls back to x-real-ip when x-forwarded-for is absent", async () => {
232
+ mockHeaders.mockResolvedValueOnce({
233
+ get: (name: string) => (name === "x-real-ip" ? "198.51.100.4" : null),
234
+ });
235
+ mockSubmitStep.mockResolvedValueOnce({
236
+ ok: true,
237
+ body: {
238
+ sessionId: "sess1",
239
+ landed: "contact",
240
+ nextStepKey: "budget",
241
+ completed: false,
242
+ },
243
+ });
244
+
245
+ await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
246
+
247
+ expect(mockSubmitStep).toHaveBeenCalledWith(
248
+ "get-a-quote",
249
+ expect.objectContaining(VALID_PAYLOAD),
250
+ { headers: { "x-forwarded-for": "198.51.100.4" } },
251
+ );
252
+ });
253
+
254
+ it("calls flows.submitStep with exactly two arguments when no visitor IP header is present", async () => {
255
+ // Default mock (no x-forwarded-for / x-real-ip) — mirrors every other
256
+ // test in this file; asserted explicitly here as the #4730 regression
257
+ // floor: no header ⇒ no third argument, ever an explicit `undefined`.
258
+ mockSubmitStep.mockResolvedValueOnce({
259
+ ok: true,
260
+ body: {
261
+ sessionId: "sess1",
262
+ landed: "contact",
263
+ nextStepKey: "budget",
264
+ completed: false,
265
+ },
266
+ });
267
+
268
+ await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
269
+
270
+ expect(mockSubmitStep).toHaveBeenCalledWith(
271
+ "get-a-quote",
272
+ expect.objectContaining(VALID_PAYLOAD),
273
+ );
274
+ expect(mockSubmitStep.mock.calls[0]).toHaveLength(2);
275
+ });
276
+
277
+ it("degrades to no forwarded header when headers() itself throws", async () => {
278
+ mockHeaders.mockRejectedValueOnce(new Error("no request scope"));
279
+ mockSubmitStep.mockResolvedValueOnce({
280
+ ok: true,
281
+ body: {
282
+ sessionId: "sess1",
283
+ landed: "contact",
284
+ nextStepKey: "budget",
285
+ completed: false,
286
+ },
287
+ });
288
+
289
+ const result = await submitFlowStepAction("get-a-quote", VALID_PAYLOAD);
290
+
291
+ expect(result).toEqual({
292
+ ok: true,
293
+ response: {
294
+ sessionId: "sess1",
295
+ landed: "contact",
296
+ nextStepKey: "budget",
297
+ completed: false,
298
+ },
299
+ });
300
+ expect(mockSubmitStep.mock.calls[0]).toHaveLength(2);
301
+ });
302
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * #5053 — an interactive tool is two routes.
3
+ *
4
+ * The defect this convention closes is SILENT: a flow mounted at a bare
5
+ * route renders fine, the walk works, and nothing surfaces that the tool is
6
+ * invisible to search and to answer engines. So the assertions below are the
7
+ * only thing standing between the convention and a quiet regression.
8
+ *
9
+ * Three claims are pinned here:
10
+ * 1. The flow route carries `noindex, follow` — and gets it from
11
+ * `flowRouteMetadata`, not from a robots line written into the route
12
+ * (D4: mounted, not remembered). The source-level assertion is
13
+ * deliberate: re-inlining the value would satisfy a value-only test
14
+ * while losing the property the decision is actually about.
15
+ * 2. The landing route sets no `robots` at all, so it indexes.
16
+ * 3. The landing emits the `Assessment` page type's SINGLE-LAYER
17
+ * `@type: "WebApplication"` — no `WebPage` envelope, no `mainEntity`
18
+ * (D2, resting on #5019). The consuming site's two-layer shape is not
19
+ * adopted, and this is what fails if someone adopts it.
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+
24
+ import { render } from "@testing-library/react";
25
+ import type { FlowConfig } from "@working-theory/validation";
26
+
27
+ const mockGet = jest.fn();
28
+ jest.mock("@working-theory/api-client", () => ({
29
+ __esModule: true,
30
+ flows: { get: (...args: unknown[]) => mockGet(...args) },
31
+ }));
32
+
33
+ jest.mock("next/navigation", () => ({
34
+ __esModule: true,
35
+ notFound: () => {
36
+ throw new Error("NEXT_NOT_FOUND");
37
+ },
38
+ }));
39
+
40
+ jest.mock("next-intl/server", () => ({
41
+ __esModule: true,
42
+ getTranslations: async () => (key: string) => key,
43
+ }));
44
+
45
+ jest.mock("@working-theory/ui/sections", () => ({
46
+ __esModule: true,
47
+ SectionRenderer: () => null,
48
+ }));
49
+
50
+ import LandingPage, { generateMetadata as landingMetadata } from "../page";
51
+ import { generateMetadata as flowMetadata } from "../start/page";
52
+
53
+ const SLUG = "readiness-check";
54
+
55
+ function fixtureConfig(): FlowConfig {
56
+ return {
57
+ slug: SLUG,
58
+ defaultLocale: "en",
59
+ steps: [
60
+ {
61
+ key: "team",
62
+ type: "segmentation",
63
+ copy: { title: { en: "How ready is your site?" } },
64
+ questions: [
65
+ {
66
+ name: "team_size",
67
+ options: [
68
+ { value: "solo", label: { en: "Solo" } },
69
+ { value: "agency", label: { en: "Agency" } },
70
+ ],
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ } as FlowConfig;
76
+ }
77
+
78
+ beforeEach(() => {
79
+ mockGet.mockReset();
80
+ mockGet.mockResolvedValue({ ok: true, body: fixtureConfig() });
81
+ });
82
+
83
+ const params = Promise.resolve({ locale: "en", slug: SLUG });
84
+
85
+ describe("the flow route", () => {
86
+ it("carries noindex, follow", async () => {
87
+ const metadata = await flowMetadata({ params });
88
+ expect(metadata.robots).toEqual({ index: false, follow: true });
89
+ });
90
+
91
+ it("still carries noindex, follow when the flow is not configured", async () => {
92
+ mockGet.mockResolvedValue({ ok: false, error: "flow_not_found" });
93
+ const metadata = await flowMetadata({ params });
94
+ expect(metadata.robots).toEqual({ index: false, follow: true });
95
+ });
96
+
97
+ it("gets the value from flowRouteMetadata rather than writing it", () => {
98
+ const code = readFileSync(
99
+ join(__dirname, "..", "start", "page.tsx"),
100
+ "utf8",
101
+ )
102
+ .replace(/\/\*[\s\S]*?\*\//g, "")
103
+ .replace(/\/\/.*$/gm, "");
104
+ expect(code).toContain("flowRouteMetadata");
105
+ expect(code).not.toMatch(/robots/i);
106
+ expect(code).not.toMatch(/noindex/i);
107
+ });
108
+ });
109
+
110
+ describe("the landing route", () => {
111
+ it("sets no robots value, so it indexes", async () => {
112
+ const metadata = await landingMetadata({ params });
113
+ expect(metadata.robots).toBeUndefined();
114
+ expect(metadata.title).toBe("How ready is your site?");
115
+ });
116
+
117
+ it("emits the single-layer WebApplication shape, with no WebPage envelope", async () => {
118
+ const { container } = render(await LandingPage({ params }));
119
+ const script = container.querySelector(
120
+ 'script[type="application/ld+json"]',
121
+ );
122
+ const emitted = JSON.parse(script!.innerHTML) as Record<string, unknown>;
123
+
124
+ expect(emitted["@type"]).toBe("WebApplication");
125
+ expect(emitted).not.toHaveProperty("mainEntity");
126
+ expect(emitted["name"]).toBe("How ready is your site?");
127
+ });
128
+ });
@@ -0,0 +1,254 @@
1
+ "use server";
2
+
3
+ /**
4
+ * checkout.actions.ts — the recommender's two email-moment Server Actions
5
+ * (#4842 D7), siblings of `flow.actions.ts` (ADR 0013 `*.actions.ts`):
6
+ *
7
+ * - `startCheckoutAction` — Pay: validates with the shared
8
+ * `flowCheckoutInputSchema` (#4841 — a body with NO money field; the
9
+ * server recomputes `price()` from the register it served, PRD L104),
10
+ * calls `flows.checkout`, and on `ok` hands the visitor to Stripe's
11
+ * hosted page via `redirect(url)` (D1 — hosted Checkout, Sam 2026-09-05).
12
+ * - `saveRecommendationAction` — "Email me this plan": validates with
13
+ * `flowSaveInputSchema`, calls `flows.save`, returns the lead id.
14
+ *
15
+ * Both mirror `submitFlowStepAction`'s idiom exactly: shared-schema
16
+ * validation, the visitor-IP forwarding helper (duplicated here — the
17
+ * `flow.actions.ts` original is module-private and that file is this unit's
18
+ * regression floor), a status → kind map, and NEVER a throw to the client.
19
+ * Logging is PII-safe: slug + kind only — the email is never logged.
20
+ *
21
+ * `redirect()` throws `NEXT_REDIRECT` by design, so it is called OUTSIDE the
22
+ * try/catch that maps a thrown api-client error to `network` — inside it,
23
+ * the catch would swallow the very navigation the action exists to perform.
24
+ *
25
+ * The Stripe return URLs are BUILT HERE, never accepted from the client
26
+ * (security review, #4875): a direct caller could otherwise mint a session
27
+ * that redirects post-payment to an arbitrary host, since the shared
28
+ * `flowRedirectUrlSchema` pins only `http(s)` + a host. The client hands over
29
+ * the permalink `payload` it already has; the action decodes it, requires it
30
+ * to name THIS flow, and composes `<site origin>/<safe locale>/flows/<slug>/
31
+ * results/<payload>?checkout=success|cancelled` from the app's own configured
32
+ * origin (`resolveBaseUrl()` — the same source `layout.tsx` / `sitemap.xml`
33
+ * use for every absolute URL).
34
+ */
35
+
36
+ import { flows } from "@working-theory/api-client";
37
+ import type { FlowCheckoutInputRaw, FlowSaveInput } from "@working-theory/validation";
38
+ import {
39
+ decodePermalink,
40
+ flowCheckoutInputSchema,
41
+ flowSaveInputSchema,
42
+ flowSlugSchema,
43
+ } from "@working-theory/validation";
44
+ import { headers } from "next/headers";
45
+ import { redirect } from "next/navigation";
46
+
47
+ import { resolveBaseUrl } from "../../../../src/lib/routes";
48
+
49
+ export type EmailMomentFailureKind =
50
+ | "invalid_input"
51
+ | "not_configured"
52
+ | "rate_limited"
53
+ | "unavailable"
54
+ | "network";
55
+
56
+ export interface EmailMomentFailure {
57
+ ok: false;
58
+ kind: EmailMomentFailureKind;
59
+ retryAfterSeconds?: number;
60
+ }
61
+
62
+ /** `startCheckoutAction` never returns on success — it `redirect()`s. */
63
+ export type CheckoutActionResult = EmailMomentFailure;
64
+
65
+ /**
66
+ * The client-supplied checkout input: the wire body MINUS the two return
67
+ * URLs (server-built, above) PLUS the permalink `payload` they are built from.
68
+ */
69
+ export type StartCheckoutInput = Omit<FlowCheckoutInputRaw, "successUrl" | "cancelUrl"> & {
70
+ payload: string;
71
+ };
72
+
73
+ // Mirrors `FlowStepperClient.tsx:80–88` (#4707) — the locale segment of a
74
+ // built URL is validated against the same build-time-baked allowlist, never
75
+ // interpolated raw.
76
+ const DEFAULT_LOCALE = "en";
77
+ const ENABLED_LOCALES = (process.env.NEXT_PUBLIC_ENABLED_LOCALES ?? DEFAULT_LOCALE).split(",");
78
+
79
+ /** Validate a candidate locale against the configured set; unknown → default. */
80
+ function resolveSafeLocale(candidate: string): string {
81
+ return ENABLED_LOCALES.includes(candidate) ? candidate : DEFAULT_LOCALE;
82
+ }
83
+
84
+ /**
85
+ * The permalink's absolute URL on THIS site, or `undefined` when the payload
86
+ * is not a valid permalink of `slug` (never a URL to somewhere else).
87
+ */
88
+ function permalinkUrlFor(slug: string, locale: string, payload: string): string | undefined {
89
+ const decoded = decodePermalink(payload);
90
+ if (!decoded.ok || decoded.value.flowSlug !== slug) return undefined;
91
+ const origin = resolveBaseUrl().replace(/\/+$/, "");
92
+ return `${origin}/${resolveSafeLocale(locale)}/flows/${slug}/results/${payload}`;
93
+ }
94
+
95
+ export type SaveActionResult = { ok: true; leadId: string } | EmailMomentFailure;
96
+
97
+ /**
98
+ * Reads the visitor's real IP from THIS action's own incoming request —
99
+ * never from the browser-supplied payload — preferring `x-forwarded-for`
100
+ * and falling back to `x-real-ip`. Returns `undefined` rather than throwing
101
+ * on any failure (a local copy of `flow.actions.ts`'s helper, #4730).
102
+ */
103
+ async function resolveVisitorIpInit(): Promise<
104
+ { headers: Record<string, string> } | undefined
105
+ > {
106
+ try {
107
+ const incoming = await headers();
108
+ const visitorIp =
109
+ incoming.get("x-forwarded-for") ?? incoming.get("x-real-ip");
110
+ return visitorIp
111
+ ? { headers: { "x-forwarded-for": visitorIp } }
112
+ : undefined;
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ /** PII-safe error identity for logs — the `recommender.ts` idiom (`err.name` only). */
119
+ function errorName(err: unknown): string {
120
+ return err instanceof Error ? err.name : "unknown";
121
+ }
122
+
123
+ /** The shared `status → kind` map (D7). Never throws. */
124
+ function failureFor(res: { status: number; body: unknown }): EmailMomentFailure {
125
+ if (res.status === 400) return { ok: false, kind: "invalid_input" };
126
+ if (res.status === 409) return { ok: false, kind: "not_configured" };
127
+ if (res.status === 429) {
128
+ const body = res.body as { retryAfterSeconds?: number } | null;
129
+ return {
130
+ ok: false,
131
+ kind: "rate_limited",
132
+ ...(typeof body?.retryAfterSeconds === "number"
133
+ ? { retryAfterSeconds: body.retryAfterSeconds }
134
+ : {}),
135
+ };
136
+ }
137
+ return { ok: false, kind: "unavailable" };
138
+ }
139
+
140
+ /**
141
+ * exactOptionalPropertyTypes-safe rebuild of the parsed body: Zod's inferred
142
+ * optional fields type as `T | undefined`, which does not structurally match
143
+ * the api-client's `sessionId?: string` / `locale?: string` — so an absent
144
+ * optional key is OMITTED rather than sent as an explicit `undefined`.
145
+ */
146
+ function emailMomentFields(parsed: FlowSaveInput): FlowSaveInput {
147
+ const { answers, email, locale, sessionId } = parsed;
148
+ return {
149
+ answers,
150
+ email,
151
+ ...(locale !== undefined ? { locale } : {}),
152
+ ...(sessionId !== undefined ? { sessionId } : {}),
153
+ };
154
+ }
155
+
156
+ export async function startCheckoutAction(
157
+ slug: string,
158
+ input: StartCheckoutInput,
159
+ ): Promise<CheckoutActionResult> {
160
+ const parsedSlug = flowSlugSchema.safeParse(slug);
161
+ if (!parsedSlug.success) return { ok: false, kind: "invalid_input" };
162
+
163
+ // Only the named fields cross into the wire body — a caller-supplied
164
+ // `successUrl` / `cancelUrl` (or any other stray key) is dropped here.
165
+ const { answers, email, locale, sessionId, offerKey, seats, period, creditStop, payload } =
166
+ input;
167
+ if (typeof payload !== "string" || payload.length === 0 || payload.length > 4096) {
168
+ return { ok: false, kind: "invalid_input" };
169
+ }
170
+ const permalinkUrl = permalinkUrlFor(parsedSlug.data, locale ?? DEFAULT_LOCALE, payload);
171
+ if (permalinkUrl === undefined) return { ok: false, kind: "invalid_input" };
172
+
173
+ const parsed = flowCheckoutInputSchema.safeParse({
174
+ answers,
175
+ email,
176
+ ...(locale !== undefined ? { locale } : {}),
177
+ ...(sessionId !== undefined ? { sessionId } : {}),
178
+ offerKey,
179
+ seats,
180
+ period,
181
+ ...(creditStop !== undefined ? { creditStop } : {}),
182
+ successUrl: `${permalinkUrl}?checkout=success`,
183
+ cancelUrl: `${permalinkUrl}?checkout=cancelled`,
184
+ });
185
+ if (!parsed.success) return { ok: false, kind: "invalid_input" };
186
+
187
+ const wireBody: FlowCheckoutInputRaw = {
188
+ ...emailMomentFields(parsed.data),
189
+ offerKey: parsed.data.offerKey,
190
+ seats: parsed.data.seats,
191
+ period: parsed.data.period,
192
+ creditStop: parsed.data.creditStop,
193
+ successUrl: parsed.data.successUrl,
194
+ cancelUrl: parsed.data.cancelUrl,
195
+ };
196
+
197
+ let url: string;
198
+ try {
199
+ const visitorIpInit = await resolveVisitorIpInit();
200
+ const res = visitorIpInit
201
+ ? await flows.checkout(parsedSlug.data, wireBody, visitorIpInit)
202
+ : await flows.checkout(parsedSlug.data, wireBody);
203
+ if (!res.ok) {
204
+ const failure = failureFor(res);
205
+ console.error(
206
+ `[startCheckoutAction] slug=${parsedSlug.data} kind=${failure.kind} status=${res.status}`,
207
+ );
208
+ return failure;
209
+ }
210
+ url = res.body.url;
211
+ } catch (err) {
212
+ // Name only — never the error object (it can carry the request payload).
213
+ console.error(
214
+ `[startCheckoutAction] slug=${parsedSlug.data} kind=network errName=${errorName(err)}`,
215
+ );
216
+ return { ok: false, kind: "network" };
217
+ }
218
+
219
+ // Outside the try/catch on purpose — see the file header.
220
+ redirect(url);
221
+ }
222
+
223
+ export async function saveRecommendationAction(
224
+ slug: string,
225
+ body: FlowSaveInput,
226
+ ): Promise<SaveActionResult> {
227
+ const parsedSlug = flowSlugSchema.safeParse(slug);
228
+ if (!parsedSlug.success) return { ok: false, kind: "invalid_input" };
229
+
230
+ const parsed = flowSaveInputSchema.safeParse(body);
231
+ if (!parsed.success) return { ok: false, kind: "invalid_input" };
232
+
233
+ const payload = emailMomentFields(parsed.data);
234
+
235
+ try {
236
+ const visitorIpInit = await resolveVisitorIpInit();
237
+ const res = visitorIpInit
238
+ ? await flows.save(parsedSlug.data, payload, visitorIpInit)
239
+ : await flows.save(parsedSlug.data, payload);
240
+ if (!res.ok) {
241
+ const failure = failureFor(res);
242
+ console.error(
243
+ `[saveRecommendationAction] slug=${parsedSlug.data} kind=${failure.kind} status=${res.status}`,
244
+ );
245
+ return failure;
246
+ }
247
+ return { ok: true, leadId: res.body.leadId };
248
+ } catch (err) {
249
+ console.error(
250
+ `[saveRecommendationAction] slug=${parsedSlug.data} kind=network errName=${errorName(err)}`,
251
+ );
252
+ return { ok: false, kind: "network" };
253
+ }
254
+ }