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,202 @@
1
+ /**
2
+ * Flows route group (#3956 D4).
3
+ *
4
+ * Routes:
5
+ * GET /v1/flows/:slug — serve a validated flow config
6
+ * POST /v1/flows/:slug/steps — submit one step's answers; persists
7
+ * server-side on landing
8
+ * GET /v1/flows/:slug/aggregate — anonymous, PII-free per-question
9
+ * response counts across all completed
10
+ * respondents (#4771), for the results
11
+ * reveal
12
+ *
13
+ * `siteId` is not carried in the URL or the request body — flows follow the
14
+ * same anonymous single-site default `apps/mcp/src/tools/capture_lead.ts`
15
+ * already established for agent-facing anonymous surfaces
16
+ * (`process.env.AILK_SITE_ID ?? "default"`), rather than the leads routes'
17
+ * explicit client-supplied `siteId` (spec D4 documents no siteId field for
18
+ * either flows route — this is the design decision that fills that gap).
19
+ *
20
+ * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
21
+ * `{ ok: false, error: { code, message } }`, never 200-with-ok:false.
22
+ */
23
+ import {
24
+ flowSlugSchema,
25
+ submitFlowStepInputSchema,
26
+ } from "@working-theory/validation";
27
+ import type { FastifyPluginAsync, FastifyReply } from "fastify";
28
+
29
+ import { currentSiteId } from "../../lib/site.js";
30
+ import { getFlowAggregate } from "../../services/flow-aggregate.js";
31
+ import {
32
+ serveFlow,
33
+ submitStep,
34
+ type FlowEngineError,
35
+ type FlowEngineErrorKind,
36
+ } from "../../services/flow-engine.js";
37
+
38
+ const FLOW_ERROR_STATUS: Record<FlowEngineErrorKind, number> = {
39
+ invalid_input: 400,
40
+ flow_not_found: 404,
41
+ step_conflict: 409,
42
+ flows_not_configured: 503,
43
+ };
44
+
45
+ function sendFlowError(reply: FastifyReply, error: FlowEngineError) {
46
+ return reply
47
+ .status(FLOW_ERROR_STATUS[error.kind])
48
+ .send({ ok: false, error: { code: error.kind, message: error.message } });
49
+ }
50
+
51
+ export const getFlowRoute: FastifyPluginAsync = async (fastify) => {
52
+ fastify.get<{ Params: { slug: string } }>(
53
+ "/v1/flows/:slug",
54
+ async (request, reply) => {
55
+ const parsedSlug = flowSlugSchema.safeParse(request.params.slug);
56
+ if (!parsedSlug.success) {
57
+ return reply.status(400).send({
58
+ ok: false,
59
+ error: { code: "invalid_input", message: "Invalid flow slug." },
60
+ });
61
+ }
62
+
63
+ const result = await serveFlow(currentSiteId(), parsedSlug.data);
64
+ if (!result.ok) return sendFlowError(reply, result.error);
65
+
66
+ return reply.send({ ok: true, data: result.config });
67
+ },
68
+ );
69
+ };
70
+
71
+ export const getFlowAggregateRoute: FastifyPluginAsync = async (fastify) => {
72
+ fastify.get<{ Params: { slug: string } }>(
73
+ "/v1/flows/:slug/aggregate",
74
+ async (request, reply) => {
75
+ const parsedSlug = flowSlugSchema.safeParse(request.params.slug);
76
+ if (!parsedSlug.success) {
77
+ return reply.status(400).send({
78
+ ok: false,
79
+ error: { code: "invalid_input", message: "Invalid flow slug." },
80
+ });
81
+ }
82
+
83
+ const result = await getFlowAggregate(currentSiteId(), parsedSlug.data);
84
+ if (!result.ok) return sendFlowError(reply, result.error);
85
+
86
+ return reply.send({ ok: true, data: result.data });
87
+ },
88
+ );
89
+ };
90
+
91
+ export const submitFlowStepRoute: FastifyPluginAsync = async (fastify) => {
92
+ fastify.post<{ Params: { slug: string } }>(
93
+ "/v1/flows/:slug/steps",
94
+ async (request, reply) => {
95
+ const parsedSlug = flowSlugSchema.safeParse(request.params.slug);
96
+ if (!parsedSlug.success) {
97
+ return reply.status(400).send({
98
+ ok: false,
99
+ error: { code: "invalid_input", message: "Invalid flow slug." },
100
+ });
101
+ }
102
+
103
+ // Validate the RAW body directly against the .strict() schema — an
104
+ // unexpected top-level key (e.g. attacker-supplied `siteId`) is
105
+ // rejected here rather than silently dropped by a pre-filter, so the
106
+ // 400 invalid_input D4 documents for "schema validation" actually
107
+ // fires on it.
108
+ const body = request.body as Record<string, unknown>;
109
+ const parsedBody = submitFlowStepInputSchema.safeParse(body);
110
+ if (!parsedBody.success) {
111
+ return reply.status(400).send({
112
+ ok: false,
113
+ error: {
114
+ code: "invalid_input",
115
+ message:
116
+ parsedBody.error.issues[0]?.message ?? "Invalid step submission.",
117
+ },
118
+ });
119
+ }
120
+
121
+ const sourceMeta = {
122
+ ip: request.ip,
123
+ userAgent: request.headers["user-agent"],
124
+ receivedAt: new Date().toISOString(),
125
+ };
126
+
127
+ const result = await submitStep(
128
+ {
129
+ siteId: currentSiteId(),
130
+ slug: parsedSlug.data,
131
+ ...(parsedBody.data.sessionId !== undefined && {
132
+ sessionId: parsedBody.data.sessionId,
133
+ }),
134
+ stepKey: parsedBody.data.stepKey,
135
+ values: parsedBody.data.values,
136
+ ...(parsedBody.data.locale !== undefined && {
137
+ locale: parsedBody.data.locale,
138
+ }),
139
+ },
140
+ sourceMeta,
141
+ );
142
+
143
+ if (!result.ok) {
144
+ if (result.error.kind === "invalid_input") {
145
+ // Client-fixable — no server-side log needed.
146
+ } else if (result.error.kind !== "flows_not_configured") {
147
+ // step_conflict / flow_not_found — PII-safe operational visibility.
148
+ request.log.warn(
149
+ {
150
+ slug: parsedSlug.data,
151
+ stepKey: body["stepKey"],
152
+ kind: result.error.kind,
153
+ },
154
+ "flow step submission rejected",
155
+ );
156
+ }
157
+ return sendFlowError(reply, result.error);
158
+ }
159
+
160
+ // PII-safe structured log — mirrors leads route: no email/values/payload.
161
+ request.log.info(
162
+ {
163
+ slug: parsedSlug.data,
164
+ sessionId: result.sessionId,
165
+ stepKey: result.landed,
166
+ completed: result.completed,
167
+ },
168
+ "flow step landed",
169
+ );
170
+
171
+ if (result.leadRoutingFailed) {
172
+ // Completion ran but routeLead's own db+file fallback both failed —
173
+ // the session (not a Lead row) is the durable record for this
174
+ // completion. PII-safe: no email/payload, matches the shape above.
175
+ request.log.warn(
176
+ {
177
+ slug: parsedSlug.data,
178
+ sessionId: result.sessionId,
179
+ },
180
+ "flow completed but lead routing failed — session is the record",
181
+ );
182
+ }
183
+
184
+ const { sessionId, landed, nextStepKey, completed, leadId, segment, aeoScore } =
185
+ result;
186
+ return reply.send({
187
+ ok: true,
188
+ data: {
189
+ sessionId,
190
+ landed,
191
+ nextStepKey,
192
+ completed,
193
+ ...(leadId !== undefined && { leadId }),
194
+ ...(segment !== undefined && { segment }),
195
+ // D9 (#4663) — forward the aeo_score step's own result, or the
196
+ // persisted result on the completion response.
197
+ ...(aeoScore !== undefined && { aeoScore }),
198
+ },
199
+ });
200
+ },
201
+ );
202
+ };
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Recommender email-moment routes — #4841 (Epic #4760 C2).
3
+ *
4
+ * Routes (both anonymous, registered inside server.ts's `flowsScope` beside
5
+ * the three existing flow routes, so they inherit the per-IP rate limiter):
6
+ * POST /v1/flows/:slug/save — "email me this plan": FlowSession + Lead
7
+ * (+ the T+0 grant) for the permalink's
8
+ * answers. No Stripe call.
9
+ * POST /v1/flows/:slug/checkout — the purchase moment: the email moment
10
+ * above, then the buyer row at a
11
+ * SERVER-recomputed price, then a Stripe
12
+ * TEST-mode Checkout Session whose metadata
13
+ * carries the permalink + offer key.
14
+ *
15
+ * The body carries NO amount of any name (`flowCheckoutInputSchema` is
16
+ * `.strict()`): the server recomputes `price()` from the register it served
17
+ * (PRD L104) and `#4837`'s `recordCheckout` is the authority — it resolves
18
+ * the register from the DB itself and verifies the session + lead ids are
19
+ * this site's before writing (the PR #4851 security-review conditions).
20
+ *
21
+ * Gate order on checkout (spec FR5, + #4912 FR1): sandbox key → body →
22
+ * redirect origin (400) → register (404) → `stripeConfigRef` +
23
+ * `rateTable.currency` (409) → `offerKey` (400) → the capture → the row →
24
+ * Stripe. A Stripe failure lands AFTER the session, lead and row exist, so the
25
+ * buyer record survives a Stripe outage (FR6); a foreign redirect origin lands
26
+ * BEFORE all of them, so a refused redirect writes nothing at all.
27
+ *
28
+ * `siteId` is `currentSiteId()` — the anonymous single-site default every
29
+ * flows route takes (`index.ts`); `sourceMeta` is the same triple
30
+ * `submitFlowStepRoute` hands `routeLead`.
31
+ *
32
+ * Security invariants (the `checkout/sessions.ts` posture, inherited):
33
+ * - Test mode only: a key outside the `sk_test_` allow-list is refused at
34
+ * request time, before any DB or Stripe call.
35
+ * - No PII in logs: only `slug`, 8-char id prefixes, and an outcome.
36
+ * - No user input echoed in any error body.
37
+ * - The two redirect URLs must be on the site's own origin (#4912 D1) — the
38
+ * shared `flowRedirectUrlSchema` pins only `http(s)` + a host, so without
39
+ * this gate a direct API caller could aim a real Checkout Session's
40
+ * post-payment redirect at a host they control.
41
+ * - The `X-Internal-Auth` gate is deliberately NOT applied — these are the
42
+ * visitor's own anonymous moment (spec Clarifications).
43
+ *
44
+ * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
45
+ * `{ ok: false, error: { code, message } }`, never 200-with-ok:false.
46
+ *
47
+ * API↔MCP parity: no MCP twin — recorded exception (apps/api/CLAUDE.md): an
48
+ * agent walking a flow has no email to give and no card to pay with.
49
+ */
50
+ import {
51
+ encodePermalink,
52
+ flowCheckoutInputSchema,
53
+ flowSaveInputSchema,
54
+ flowSlugSchema,
55
+ PERMALINK_VERSION,
56
+ resolveLocalizedString,
57
+ } from "@working-theory/validation";
58
+ import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
59
+
60
+ import { currentSiteId } from "../../lib/site.js";
61
+ import {
62
+ assertSandboxKey,
63
+ createQuotedCheckoutSession,
64
+ expireCheckoutSession,
65
+ StripeConfigError,
66
+ } from "../../lib/stripe.js";
67
+ import {
68
+ attachStripeSession,
69
+ captureRecommenderChoice,
70
+ expireFlowCheckout,
71
+ findRegisterForSlug,
72
+ recordCheckout,
73
+ type RecommenderCaptureError,
74
+ type RecordCheckoutErrorKind,
75
+ } from "../../services/recommender-capture.js";
76
+
77
+ // ─── Error mapping ───────────────────────────────────────────────────────────
78
+
79
+ const CAPTURE_ERROR_STATUS: Record<RecommenderCaptureError["kind"], number> = {
80
+ invalid_input: 400,
81
+ flow_not_found: 404,
82
+ not_recommending: 409,
83
+ lead_failed: 502,
84
+ flows_not_configured: 503,
85
+ };
86
+
87
+ /** #4837 §6's `RecordCheckoutResult.kind` vocabulary → this unit's 400/409 split (spec Clarifications — provenance). */
88
+ const RECORD_CHECKOUT_STATUS: Record<RecordCheckoutErrorKind, { status: number; code: string }> = {
89
+ no_register: { status: 404, code: "flow_not_found" },
90
+ session_mismatch: { status: 400, code: "invalid_input" },
91
+ unknown_offer: { status: 400, code: "invalid_input" },
92
+ no_seat_rate: { status: 400, code: "invalid_input" },
93
+ invalid_seats: { status: 400, code: "invalid_input" },
94
+ unknown_credit_stop: { status: 400, code: "invalid_input" },
95
+ custom_stop: { status: 400, code: "invalid_input" },
96
+ no_rate_table: { status: 409, code: "checkout_not_configured" },
97
+ no_currency: { status: 409, code: "checkout_not_configured" },
98
+ };
99
+
100
+ function sendError(reply: FastifyReply, status: number, code: string, message: string) {
101
+ return reply.status(status).send({ ok: false, error: { code, message } });
102
+ }
103
+
104
+ function sendCaptureError(reply: FastifyReply, error: RecommenderCaptureError) {
105
+ const code = error.kind === "not_recommending" ? "checkout_not_configured" : error.kind;
106
+ // Never echo user input — the service's messages are fixed strings.
107
+ return sendError(reply, CAPTURE_ERROR_STATUS[error.kind], code, error.message);
108
+ }
109
+
110
+ /** Stripe caps a metadata value at 500 characters; a long permalink is chunked, never truncated (spec D5). */
111
+ export const METADATA_VALUE_MAX = 500;
112
+
113
+ export function permalinkMetadata(permalink: string): Record<string, string> {
114
+ if (permalink.length <= METADATA_VALUE_MAX) return { permalink };
115
+ const out: Record<string, string> = {};
116
+ for (let i = 0, n = 1; i < permalink.length; i += METADATA_VALUE_MAX, n++) {
117
+ out[n === 1 ? "permalink" : `permalink_${n}`] = permalink.slice(i, i + METADATA_VALUE_MAX);
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /**
123
+ * The site's own origin — the ONLY origin a checkout redirect may resolve to
124
+ * (#4912 FR1/D1).
125
+ *
126
+ * `FRONTEND_URL` is the single site-origin contract this API already carries:
127
+ * the CORS origin (`server.ts`), the origin check in `middleware/auth.ts`, and
128
+ * the billing-portal return URL (`routes/billing/portal.ts`) all read it. The
129
+ * fallback is byte-identical to the one `config/index.ts` applies, and it is
130
+ * read per request for the same reason `STRIPE_SECRET_KEY` is: this route's
131
+ * environment-sourced security inputs are resolved at request time, not frozen
132
+ * at import.
133
+ *
134
+ * Returns `null` when the value is set but unparseable — a misconfiguration
135
+ * the route reports as a config error rather than silently widening.
136
+ */
137
+ function siteRedirectOrigin(): string | null {
138
+ const raw = process.env["FRONTEND_URL"] || "http://localhost:3000";
139
+ let parsed: URL;
140
+ try {
141
+ parsed = new URL(raw);
142
+ } catch {
143
+ return null;
144
+ }
145
+ // A non-http(s) value parses without throwing and yields the literal origin
146
+ // `"null"` (`mailto:…`, `javascript:…`, a bare `host:port`). That would deny
147
+ // every redirect — fail-closed, but undiagnosable — so it is reported as the
148
+ // misconfiguration it is rather than as a 400 the operator cannot explain.
149
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
150
+ return parsed.origin;
151
+ }
152
+
153
+ /**
154
+ * Exact origin equality — scheme + host + port. A prefix or suffix comparison
155
+ * would accept `https://<site-origin>.evil.example`; a scheme check keeps
156
+ * `javascript:` and `data:` out even though the shared schema already refuses
157
+ * them.
158
+ */
159
+ function isSameOriginRedirect(raw: string, siteOrigin: string): boolean {
160
+ let resolved: URL;
161
+ try {
162
+ resolved = new URL(raw);
163
+ } catch {
164
+ return false;
165
+ }
166
+ if (resolved.protocol !== "https:" && resolved.protocol !== "http:") return false;
167
+ return resolved.origin === siteOrigin;
168
+ }
169
+
170
+ function sourceMetaOf(request: FastifyRequest) {
171
+ return {
172
+ ip: request.ip,
173
+ userAgent: request.headers["user-agent"],
174
+ receivedAt: new Date().toISOString(),
175
+ };
176
+ }
177
+
178
+ function prefix(id: string): string {
179
+ return id.substring(0, 8);
180
+ }
181
+
182
+ // ─── POST /v1/flows/:slug/save ───────────────────────────────────────────────
183
+
184
+ export const saveRecommendationRoute: FastifyPluginAsync = async (fastify) => {
185
+ fastify.post<{ Params: { slug: string } }>("/v1/flows/:slug/save", async (request, reply) => {
186
+ const parsedSlug = flowSlugSchema.safeParse(request.params.slug);
187
+ if (!parsedSlug.success) return sendError(reply, 400, "invalid_input", "Invalid flow slug.");
188
+
189
+ const parsedBody = flowSaveInputSchema.safeParse(request.body);
190
+ if (!parsedBody.success) {
191
+ return sendError(
192
+ reply,
193
+ 400,
194
+ "invalid_input",
195
+ parsedBody.error.issues[0]?.message ?? "Invalid save request.",
196
+ );
197
+ }
198
+ const body = parsedBody.data;
199
+
200
+ const result = await captureRecommenderChoice({
201
+ siteId: currentSiteId(),
202
+ flowSlug: parsedSlug.data,
203
+ answers: body.answers,
204
+ email: body.email,
205
+ ...(body.locale !== undefined && { locale: body.locale }),
206
+ ...(body.sessionId !== undefined && { sessionId: body.sessionId }),
207
+ choice: "save",
208
+ sourceMeta: sourceMetaOf(request),
209
+ });
210
+ if (!result.ok) return sendCaptureError(reply, result.error);
211
+
212
+ request.log.info(
213
+ { slug: parsedSlug.data, sessionIdPrefix: prefix(result.sessionId), outcome: "saved" },
214
+ "[recommender] plan saved",
215
+ );
216
+ return reply.send({ ok: true, data: { leadId: result.leadId, sessionId: result.sessionId } });
217
+ });
218
+ };
219
+
220
+ // ─── POST /v1/flows/:slug/checkout ───────────────────────────────────────────
221
+
222
+ export const checkoutRecommendationRoute: FastifyPluginAsync = async (fastify) => {
223
+ fastify.post<{ Params: { slug: string } }>("/v1/flows/:slug/checkout", async (request, reply) => {
224
+ // 1. Test mode only — before any DB or Stripe call (the sessions.ts posture).
225
+ const secretKey = process.env["STRIPE_SECRET_KEY"];
226
+ if (!secretKey) return sendError(reply, 500, "config-error", "Stripe is not configured.");
227
+ try {
228
+ assertSandboxKey(secretKey);
229
+ } catch {
230
+ return sendError(reply, 500, "live-mode-forbidden", "Stripe live mode is not permitted.");
231
+ }
232
+
233
+ // 2. Body — strict; an amount of any name is a 400 here, never read.
234
+ const parsedSlug = flowSlugSchema.safeParse(request.params.slug);
235
+ if (!parsedSlug.success) return sendError(reply, 400, "invalid_input", "Invalid flow slug.");
236
+ const slug = parsedSlug.data;
237
+ const parsedBody = flowCheckoutInputSchema.safeParse(request.body);
238
+ if (!parsedBody.success) {
239
+ return sendError(
240
+ reply,
241
+ 400,
242
+ "invalid_input",
243
+ parsedBody.error.issues[0]?.message ?? "Invalid checkout request.",
244
+ );
245
+ }
246
+ const body = parsedBody.data;
247
+ const siteId = currentSiteId();
248
+
249
+ // 2b. #4912 FR1 — both redirect URLs must land on this site's own origin.
250
+ // Before every gate below, so a refused redirect creates no session,
251
+ // no lead and no row, and never reaches Stripe.
252
+ const siteOrigin = siteRedirectOrigin();
253
+ if (siteOrigin === null) {
254
+ request.log.error({ slug }, "[recommender] FRONTEND_URL is not a valid absolute URL");
255
+ return sendError(reply, 500, "config-error", "The site origin is not configured.");
256
+ }
257
+ if (
258
+ !isSameOriginRedirect(body.successUrl, siteOrigin) ||
259
+ !isSameOriginRedirect(body.cancelUrl, siteOrigin)
260
+ ) {
261
+ // Never echo the offending URL back.
262
+ request.log.warn({ slug, outcome: "redirect_origin_refused" }, "[recommender] redirect origin refused");
263
+ return sendError(reply, 400, "invalid_input", "Redirect URLs must be on this site's origin.");
264
+ }
265
+
266
+ // 3. Register gates — 404 unknown, 409 not set up to sell, 400 unknown offer.
267
+ const register = await findRegisterForSlug(siteId, slug);
268
+ if (!register) return sendError(reply, 404, "flow_not_found", "Flow not found.");
269
+ if (register.stripeConfigRef === null || !register.rateTable?.currency) {
270
+ return sendError(
271
+ reply,
272
+ 409,
273
+ "checkout_not_configured",
274
+ "This flow is not configured for checkout.",
275
+ );
276
+ }
277
+ const offer = register.offers?.find((o) => o.key === body.offerKey);
278
+ if (!offer) return sendError(reply, 400, "invalid_input", "Unknown offer.");
279
+
280
+ // 4. The email moment — session + lead (+ grant).
281
+ const capture = await captureRecommenderChoice({
282
+ siteId,
283
+ flowSlug: slug,
284
+ answers: body.answers,
285
+ email: body.email,
286
+ ...(body.locale !== undefined && { locale: body.locale }),
287
+ ...(body.sessionId !== undefined && { sessionId: body.sessionId }),
288
+ choice: "checkout",
289
+ sourceMeta: sourceMetaOf(request),
290
+ });
291
+ if (!capture.ok) return sendCaptureError(reply, capture.error);
292
+
293
+ // 5. The buyer row at the SERVER's price — the request carried no amount.
294
+ const permalink = encodePermalink({ version: PERMALINK_VERSION, flowSlug: slug, answers: body.answers });
295
+ const recorded = await recordCheckout({
296
+ siteId,
297
+ flowSlug: slug,
298
+ experimentKey: register.key,
299
+ sessionId: capture.sessionId,
300
+ leadId: capture.leadId,
301
+ email: body.email,
302
+ offerKey: body.offerKey,
303
+ seats: body.seats,
304
+ period: body.period,
305
+ creditStop: body.creditStop,
306
+ permalink,
307
+ });
308
+ if (!recorded.ok) {
309
+ const mapped = RECORD_CHECKOUT_STATUS[recorded.error.kind];
310
+ return sendError(reply, mapped.status, mapped.code, recorded.error.message);
311
+ }
312
+
313
+ // 5b. #4912 FR5 — an identical resubmission inside the idempotency window
314
+ // replays the first attempt's session instead of opening a second one.
315
+ // `replayed` is the union's discriminant, so the two Stripe fields are
316
+ // non-null by construction here — there is no fall-through that could
317
+ // open a second session for a request the service already replayed.
318
+ if (recorded.replayed) {
319
+ request.log.info(
320
+ {
321
+ slug,
322
+ flowCheckoutIdPrefix: prefix(recorded.flowCheckoutId),
323
+ stripeSessionIdPrefix: prefix(recorded.stripeSessionId),
324
+ outcome: "checkout_session_replayed",
325
+ },
326
+ "[recommender] checkout session replayed",
327
+ );
328
+ return reply
329
+ .status(201)
330
+ .send({ ok: true, data: { url: recorded.checkoutUrl, sessionId: recorded.stripeSessionId } });
331
+ }
332
+
333
+ // 6. Stripe — after the record exists, so an outage never loses it (FR6).
334
+ const locale = body.locale ?? capture.config.defaultLocale;
335
+ const productName = resolveLocalizedString(offer.copy.name, locale, capture.config.defaultLocale);
336
+ let session: { sessionId: string; url: string };
337
+ try {
338
+ session = await createQuotedCheckoutSession({
339
+ currency: recorded.currency,
340
+ unitAmount: recorded.amountMinor,
341
+ productName,
342
+ period: body.period,
343
+ customerEmail: body.email,
344
+ successUrl: body.successUrl,
345
+ cancelUrl: body.cancelUrl,
346
+ metadata: {
347
+ flowCheckoutId: recorded.flowCheckoutId,
348
+ flowSlug: slug,
349
+ offerKey: body.offerKey,
350
+ sessionId: capture.sessionId,
351
+ leadId: capture.leadId,
352
+ ...permalinkMetadata(permalink),
353
+ },
354
+ });
355
+ } catch (err) {
356
+ const logCtx = {
357
+ slug,
358
+ flowCheckoutIdPrefix: prefix(recorded.flowCheckoutId),
359
+ errName: err instanceof Error ? err.name : "unknown",
360
+ };
361
+ if (err instanceof StripeConfigError) {
362
+ request.log.error(logCtx, "[recommender] stripe config error");
363
+ return sendError(reply, 500, "config-error", "Stripe is not configured.");
364
+ }
365
+ request.log.error(logCtx, "[recommender] stripe error; checkout-unavailable");
366
+ return sendError(reply, 502, "checkout-unavailable", "Checkout is temporarily unavailable.");
367
+ }
368
+
369
+ const attached = await attachStripeSession(
370
+ siteId,
371
+ recorded.flowCheckoutId,
372
+ session.sessionId,
373
+ session.url,
374
+ );
375
+ if (attached.count !== 1) {
376
+ // The Stripe session exists but the row it must join on did not take
377
+ // the stamp — handing the visitor that URL would produce a payment the
378
+ // #4838 webhook cannot attribute. Refuse rather than 201.
379
+ //
380
+ // #4912 FR7 — and the session is live and payable until somebody closes
381
+ // it, so it is expired and the row marked dead BEFORE the 502 returns
382
+ // (#4863). Both compensations are best-effort: a failure is logged and
383
+ // the 502 still returns, because the visitor's answer does not change.
384
+ let sessionCompensation = "expired";
385
+ try {
386
+ await expireCheckoutSession(session.sessionId);
387
+ } catch (err) {
388
+ sessionCompensation = "expire_failed";
389
+ request.log.error(
390
+ {
391
+ slug,
392
+ flowCheckoutIdPrefix: prefix(recorded.flowCheckoutId),
393
+ errName: err instanceof Error ? err.name : "unknown",
394
+ outcome: "attach_miss_expire_failed",
395
+ },
396
+ "[recommender] could not expire the orphaned stripe session",
397
+ );
398
+ }
399
+ // The row mark uses the same `{ id, siteId }` match that just failed to
400
+ // attach, so it can legitimately match nothing — a zero count is not a
401
+ // throw, and reporting it as "expired" would make the audit trail lie.
402
+ let rowCompensation = "marked_expired";
403
+ try {
404
+ const marked = await expireFlowCheckout(siteId, recorded.flowCheckoutId);
405
+ if (marked.count !== 1) rowCompensation = "row_mark_missed";
406
+ } catch (err) {
407
+ rowCompensation = "row_mark_failed";
408
+ request.log.error(
409
+ {
410
+ slug,
411
+ flowCheckoutIdPrefix: prefix(recorded.flowCheckoutId),
412
+ errName: err instanceof Error ? err.name : "unknown",
413
+ outcome: "attach_miss_row_mark_failed",
414
+ },
415
+ "[recommender] could not mark the orphaned checkout row expired",
416
+ );
417
+ }
418
+ request.log.warn(
419
+ {
420
+ slug,
421
+ flowCheckoutIdPrefix: prefix(recorded.flowCheckoutId),
422
+ outcome: "attach_miss",
423
+ sessionCompensation,
424
+ rowCompensation,
425
+ },
426
+ "[recommender] stripe session id did not attach to the checkout row",
427
+ );
428
+ return sendError(reply, 502, "checkout-unavailable", "Checkout is temporarily unavailable.");
429
+ }
430
+
431
+ request.log.info(
432
+ {
433
+ slug,
434
+ sessionIdPrefix: prefix(capture.sessionId),
435
+ flowCheckoutIdPrefix: prefix(recorded.flowCheckoutId),
436
+ stripeSessionIdPrefix: prefix(session.sessionId),
437
+ outcome: "checkout_session_created",
438
+ },
439
+ "[recommender] checkout session created",
440
+ );
441
+ return reply.status(201).send({ ok: true, data: { url: session.url, sessionId: session.sessionId } });
442
+ });
443
+ };