create-ailk 0.1.0 → 0.1.1

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 (605) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +295 -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 +22 -14
  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 +75 -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 +231 -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 +547 -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 +742 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +364 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  118. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  119. package/templates/apps/api/src/routes/project-listings/index.ts +105 -0
  120. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  121. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  122. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  123. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  124. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  125. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  126. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  127. package/templates/apps/api/src/routes/project-listings/start.ts +98 -0
  128. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  129. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  130. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  131. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  132. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  133. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  134. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  135. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  136. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  137. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  138. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  139. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  140. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  141. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  143. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  144. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  145. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  146. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  147. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  148. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  149. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  150. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  151. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  152. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  153. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  154. package/templates/apps/api/src/server.ts +293 -7
  155. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  156. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  157. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  158. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  159. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  160. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  161. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  162. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  163. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  164. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  165. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  166. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  167. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  168. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  169. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  170. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  171. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  172. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  173. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  174. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  175. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  176. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  177. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  178. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  179. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  180. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  181. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  182. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  183. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  184. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  185. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  186. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  187. package/templates/apps/api/src/services/entitlements.ts +102 -0
  188. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  189. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  190. package/templates/apps/api/src/services/flow-engine.ts +1278 -0
  191. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  192. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  193. package/templates/apps/api/src/services/listing-config.ts +102 -0
  194. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  195. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  196. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  197. package/templates/apps/api/src/services/project-listing-decision.ts +490 -0
  198. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  199. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  200. package/templates/apps/api/src/services/project-listings.ts +1124 -0
  201. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  202. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  203. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  204. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  205. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  206. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  207. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  208. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  209. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  210. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  211. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  212. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  213. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  214. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  215. package/templates/apps/api/src/vercel-handler.ts +165 -0
  216. package/templates/apps/api/tsconfig.json +0 -12
  217. package/templates/apps/mcp/CLAUDE.md +19 -9
  218. package/templates/apps/mcp/LICENSE +201 -0
  219. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  220. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  221. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  222. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  223. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  224. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  225. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  226. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  227. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  228. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  229. package/templates/apps/mcp/package.json +6 -4
  230. package/templates/apps/mcp/src/config/modules.ts +37 -0
  231. package/templates/apps/mcp/src/server.ts +109 -18
  232. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  233. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  234. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  235. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  236. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  237. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  238. package/templates/apps/mcp/src/tools/index.ts +25 -0
  239. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  240. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  241. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  242. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  243. package/templates/apps/mcp/tsconfig.json +1 -9
  244. package/templates/apps/web/.env.example +44 -0
  245. package/templates/apps/web/CLAUDE.md +28 -0
  246. package/templates/apps/web/LICENSE +201 -0
  247. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  248. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  249. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  250. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  251. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  252. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  253. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  254. package/templates/apps/web/__tests__/setup.ts +22 -32
  255. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  256. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  257. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  258. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  259. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  260. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  261. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  265. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  266. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  267. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  268. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  269. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  270. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  271. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  279. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  280. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  281. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  282. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  283. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  284. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  285. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  286. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  287. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  288. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  289. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  290. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  291. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  292. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  293. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  294. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  295. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  296. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  297. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  298. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  299. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  300. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  301. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  302. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  303. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  304. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  305. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  306. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  307. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  308. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  309. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  310. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  311. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  312. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  313. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  314. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  315. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  316. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  317. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  318. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  323. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  324. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  325. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  326. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  327. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  328. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  329. package/templates/apps/web/app/[locale]/layout.tsx +115 -16
  330. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  331. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  332. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  333. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  334. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  335. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  336. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  337. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  339. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  340. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  341. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  342. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  343. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  344. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  345. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  346. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  347. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  348. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  349. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  350. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  351. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  352. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  353. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  354. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  355. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  356. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  357. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  358. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  359. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  360. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  361. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  362. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  363. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  364. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  365. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  366. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  367. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/globals.css +35 -0
  370. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  371. package/templates/apps/web/app/og/route.tsx +10 -2
  372. package/templates/apps/web/auth.ts +8 -0
  373. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  374. package/templates/apps/web/content/en/blog/README.txt +25 -0
  375. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  376. package/templates/apps/web/jest.config.cjs +84 -27
  377. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  378. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  379. package/templates/apps/web/lib/account-config.ts +13 -0
  380. package/templates/apps/web/lib/server-api.ts +85 -0
  381. package/templates/apps/web/lib/site-brand.tsx +118 -0
  382. package/templates/apps/web/messages/en.json +275 -7
  383. package/templates/apps/web/middleware.ts +11 -3
  384. package/templates/apps/web/next.config.mjs +8 -2
  385. package/templates/apps/web/package.json +16 -7
  386. package/templates/apps/web/postcss.config.mjs +23 -0
  387. package/templates/apps/web/public/.well-known/security.txt +3 -3
  388. package/templates/apps/web/src/lib/routes.ts +15 -2
  389. package/templates/apps/web/tsconfig.json +1 -16
  390. package/templates/content/_site.mdx +24 -25
  391. package/templates/content/en/AboutPage/about.mdx +24 -14
  392. package/templates/content/en/ContactPage/contact.mdx +23 -14
  393. package/templates/content/en/FAQPage/faq.mdx +10 -392
  394. package/templates/content/en/HomePage/home.mdx +14 -285
  395. package/templates/database/CHANGELOG.md +382 -0
  396. package/templates/database/LICENSE +201 -0
  397. package/templates/database/README.md +3 -1
  398. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  399. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  400. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  401. package/templates/database/__tests__/seed.test.ts +88 -0
  402. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  403. package/templates/database/auth/schema.prisma +521 -0
  404. package/templates/database/config/schema.prisma +234 -0
  405. package/templates/database/content/schema.prisma +263 -0
  406. package/templates/database/flows/listing-diagnostic.ts +182 -0
  407. package/templates/database/flows/presets.ts +315 -0
  408. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  409. package/templates/database/inbox/schema.prisma +476 -3
  410. package/templates/database/jest.config.cjs +8 -0
  411. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  412. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  413. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  414. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  415. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  416. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  417. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  418. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  419. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  420. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  421. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  422. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  423. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  424. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  425. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  426. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  427. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  428. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  429. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  430. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  431. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  432. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  433. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  434. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  435. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  436. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  437. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  438. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  439. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  440. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  441. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  442. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  443. package/templates/database/ops/schema.prisma +27 -0
  444. package/templates/database/package.json +42 -4
  445. package/templates/database/prisma.config.ts +1 -1
  446. package/templates/database/schema.prisma +19 -435
  447. package/templates/database/scripts/db-generate-locked.sh +0 -0
  448. package/templates/database/seed-test.ts +14 -4
  449. package/templates/database/seed.ts +148 -0
  450. package/templates/database/tenancy.ts +45 -0
  451. package/templates/database/tsconfig.flows.json +15 -0
  452. package/templates/database/tsconfig.json +7 -1
  453. package/templates/database/tsconfig.tenancy.json +14 -0
  454. package/templates/package.json +30 -55
  455. package/templates/pnpm-workspace.yaml +0 -3
  456. package/templates/project.yaml +1 -1
  457. package/templates/tsconfig.json +1 -37
  458. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  459. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  460. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  461. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  462. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  463. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  464. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  465. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  466. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  467. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  468. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  469. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  470. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  471. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  472. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  473. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  474. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  475. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  476. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  477. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  478. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  479. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  480. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  481. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  482. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  483. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  484. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  485. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  486. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  487. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  488. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  489. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  490. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  491. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  492. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  493. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  494. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  495. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  496. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  497. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  498. package/templates/content/.gitkeep +0 -0
  499. package/templates/content/CLAUDE.md +0 -85
  500. package/templates/content/ar/HomePage/home.mdx +0 -123
  501. package/templates/content/ar/_site.mdx +0 -35
  502. package/templates/content/de/_site.mdx +0 -29
  503. package/templates/content/en/.gitkeep +0 -0
  504. package/templates/content/en/AboutPage/about.schema.json +0 -82
  505. package/templates/content/en/BookCall/book-call.mdx +0 -19
  506. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  507. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  508. package/templates/content/en/CareersList/careers.mdx +0 -24
  509. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  510. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  511. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  512. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  513. package/templates/content/en/ClientsList/clients.mdx +0 -38
  514. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  515. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  516. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  517. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  518. package/templates/content/en/CoursesList/courses.mdx +0 -22
  519. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  520. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  521. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  522. package/templates/content/en/EventsList/events.mdx +0 -26
  523. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  524. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  525. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  526. package/templates/content/en/HomePage/home.schema.json +0 -88
  527. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  528. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  529. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  530. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  531. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  532. package/templates/content/en/Integration/github-actions.mdx +0 -31
  533. package/templates/content/en/Integration/openai-api.mdx +0 -26
  534. package/templates/content/en/Integration/vercel.mdx +0 -32
  535. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  536. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  537. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  538. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  539. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  540. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  541. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  542. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  543. package/templates/content/en/LandingPage/developers.mdx +0 -19
  544. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  545. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  546. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  547. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  548. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  549. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  550. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  551. package/templates/content/en/LandingPage/platform.mdx +0 -369
  552. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  553. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  554. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  555. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  556. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  557. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  558. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  559. package/templates/content/en/Organization/site-owner.mdx +0 -16
  560. package/templates/content/en/Partner/prisma.mdx +0 -31
  561. package/templates/content/en/Partner/supabase.mdx +0 -30
  562. package/templates/content/en/Partner/vercel.mdx +0 -28
  563. package/templates/content/en/PartnersList/partners.mdx +0 -22
  564. package/templates/content/en/Person/founder.mdx +0 -18
  565. package/templates/content/en/Pricing/pricing.mdx +0 -538
  566. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  567. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  568. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  569. package/templates/content/en/Product/ailk.mdx +0 -19
  570. package/templates/content/en/Product/ailk.schema.json +0 -94
  571. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  572. package/templates/content/en/Product/launch-kit.mdx +0 -28
  573. package/templates/content/en/ProductsList/products.mdx +0 -26
  574. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  575. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  576. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  577. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  578. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  579. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  580. package/templates/content/en/Service/consulting.mdx +0 -22
  581. package/templates/content/en/Service/implementation.mdx +0 -30
  582. package/templates/content/en/Service/launch-service.mdx +0 -232
  583. package/templates/content/en/Service/launch.mdx +0 -228
  584. package/templates/content/en/ServicesList/services.mdx +0 -84
  585. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  586. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  587. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  588. package/templates/content/en/TeamList/team.mdx +0 -63
  589. package/templates/content/en/TeamMember/founder.mdx +0 -72
  590. package/templates/content/en/TermsPage/terms.mdx +0 -19
  591. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  592. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  593. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  594. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  595. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  596. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  597. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  598. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  599. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  600. package/templates/content/es/_site.mdx +0 -30
  601. package/templates/content/fr/_site.mdx +0 -30
  602. package/templates/content/ja/_site.mdx +0 -30
  603. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  604. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  605. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -52,6 +52,19 @@ import {
52
52
  } from "../index";
53
53
  import { prisma } from "../../../lib/prisma.js";
54
54
  import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
55
+ // #3979 AC1 — the single shared fixture (packages/validation, which apps/api
56
+ // may_import_from per architecture.yaml) for the NewsletterFormInline
57
+ // emitted-body shape, also consumed by the layer-a block test
58
+ // (packages/ui/src/blocks/__tests__/NewsletterFormInline.test.tsx), so a
59
+ // change to the block's emitted keys can't silently leave this layer
60
+ // asserting a stale contract. #4017 D4 extends the same fixture module with
61
+ // the ContactForm `submit`-kind body, consumed by the layer-a section test
62
+ // (packages/ui/src/sections/__tests__/ContactForm.test.tsx).
63
+ import {
64
+ AGENCY_WAITLIST_LEAD_BODY,
65
+ CONTACT_FORM_LEAD_BODY,
66
+ NEWSLETTER_DEFAULT_LEAD_BODY,
67
+ } from "../../../../../../packages/validation/src/__tests__/fixtures/newsletter-lead-body.fixture.js";
55
68
 
56
69
  const fileEnv = setupLeadFileEnv("index");
57
70
 
@@ -82,16 +95,16 @@ describe("POST /api/leads — id isolation", () => {
82
95
  delete process.env.LEAD_CAPTURE_INCLUDE_IP;
83
96
  });
84
97
 
85
- it("POST /api/leads/lead — attacker-supplied id is not returned (server uuid used)", async () => {
86
- const serverGeneratedId = "cuid-lead-server-1";
98
+ it("POST /api/leads/submit — attacker-supplied id is not returned (server uuid used)", async () => {
99
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a1";
87
100
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
88
101
 
89
102
  const res = await app.inject({
90
103
  method: "POST",
91
- url: "/v1/leads/lead",
104
+ url: "/v1/leads/submit",
92
105
  payload: {
93
106
  id: "attacker",
94
- kind: "lead",
107
+ kind: "submit",
95
108
  siteId: "site-abc",
96
109
  source: "contact",
97
110
  email: "test@example.com",
@@ -110,7 +123,7 @@ describe("POST /api/leads — id isolation", () => {
110
123
  });
111
124
 
112
125
  it("POST /api/leads/subscribe — attacker-supplied id is not returned (server uuid used)", async () => {
113
- const serverGeneratedId = "cuid-sub-server-1";
126
+ const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000a2";
114
127
  (prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
115
128
 
116
129
  const res = await app.inject({
@@ -133,8 +146,114 @@ describe("POST /api/leads — id isolation", () => {
133
146
  }
134
147
  });
135
148
 
149
+ // #3965 D4 layer (b) — the block's exact emitted shape (layer (a) pins it
150
+ // as { siteId, source, email } via application/json) persists through the
151
+ // REAL registered /v1/leads/subscribe plugin. #3979 AC1 retargets the body
152
+ // literal at the shared fixture (packages/validation) — it had already
153
+ // drifted from layer-a's own hand-copied literal (different `email`
154
+ // values), with nothing catching it.
155
+ it("POST /v1/leads/subscribe — the block's emitted JSON shape ({ siteId, source, email }) is accepted and persisted", async () => {
156
+ const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000b1";
157
+ (prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
158
+
159
+ // The exact body NewsletterFormInline emits at its defaults (#3965 D1) —
160
+ // no `kind` key: buildInput() injects `kind` server-side from the route,
161
+ // never from the body.
162
+ const res = await app.inject({
163
+ method: "POST",
164
+ url: "/v1/leads/subscribe",
165
+ headers: { "content-type": "application/json" },
166
+ payload: JSON.stringify(NEWSLETTER_DEFAULT_LEAD_BODY),
167
+ });
168
+
169
+ expect(res.statusCode).toBe(201);
170
+ const body = res.json();
171
+ expect(body.id).toBe(serverGeneratedId);
172
+ expect(body.persisted).toBe("db");
173
+ expect(prisma.subscriber.create).toHaveBeenCalledTimes(1);
174
+ const record = await fileEnv.readLastCaptureRecord();
175
+ expect(record.id).toBe(serverGeneratedId);
176
+ expect(record.source).toBe(NEWSLETTER_DEFAULT_LEAD_BODY.source);
177
+ });
178
+
179
+ // #3979 AC2 — the agency-waitlist instance (content/en/LandingPage/
180
+ // agency-waitlist.mdx: action "/v1/leads/submit", source "agency-waitlist",
181
+ // siteId unset → "default") had NO layer-b case at all before this issue —
182
+ // only /v1/leads/subscribe was covered. Unconditional 201 (not the
183
+ // permissive [201, 400] idiom used by the id-isolation tests above), since
184
+ // this asserts the block's real emitted shape is accepted, not merely that
185
+ // the id-isolation walls hold regardless of validation outcome.
186
+ it("POST /v1/leads/submit — the agency-waitlist instance's emitted JSON shape ({ siteId, source, email }) is accepted and persisted", async () => {
187
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b2";
188
+ (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
189
+
190
+ // The exact body NewsletterFormInline emits for the agency-waitlist
191
+ // instance — no `kind`, no `payload` key: buildInput() injects `kind`
192
+ // server-side from the route and defaults `payload` to {} when absent.
193
+ const res = await app.inject({
194
+ method: "POST",
195
+ url: "/v1/leads/submit",
196
+ headers: { "content-type": "application/json" },
197
+ payload: JSON.stringify(AGENCY_WAITLIST_LEAD_BODY),
198
+ });
199
+
200
+ expect(res.statusCode).toBe(201);
201
+ const body = res.json();
202
+ expect(body.id).toBe(serverGeneratedId);
203
+ expect(body.persisted).toBe("db");
204
+ expect(prisma.lead.create).toHaveBeenCalledTimes(1);
205
+ const record = await fileEnv.readLastCaptureRecord();
206
+ expect(record.id).toBe(serverGeneratedId);
207
+ expect(record.source).toBe(AGENCY_WAITLIST_LEAD_BODY.source);
208
+ });
209
+
210
+ // #4017 D4 layer (b) — the ContactForm section's exact emitted shape
211
+ // (layer (a) pins it as { siteId, source, email, payload } via
212
+ // application/json) persists through the REAL registered
213
+ // /v1/leads/submit plugin. `email` is lifted by field name; every other
214
+ // declared field lands in `payload` — the shared fixture (packages/validation)
215
+ // is the single definition both layers assert against (#3979 AC1's
216
+ // pattern), so a change to the section's emitted keys can't silently leave
217
+ // this layer asserting a stale contract.
218
+ it("POST /v1/leads/submit — the ContactForm section's emitted JSON shape ({ siteId, source, email, payload }) is accepted and persisted", async () => {
219
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b3";
220
+ (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
221
+
222
+ // The exact body ContactForm emits at its component defaults (#4017 D1) —
223
+ // no `kind` key: buildInput() injects `kind` server-side from the route,
224
+ // never from the body.
225
+ const res = await app.inject({
226
+ method: "POST",
227
+ url: "/v1/leads/submit",
228
+ headers: { "content-type": "application/json" },
229
+ payload: JSON.stringify(CONTACT_FORM_LEAD_BODY),
230
+ });
231
+
232
+ expect(res.statusCode).toBe(201);
233
+ const body = res.json();
234
+ expect(body.id).toBe(serverGeneratedId);
235
+ expect(body.persisted).toBe("db");
236
+ expect(prisma.lead.create).toHaveBeenCalledTimes(1);
237
+ expect(prisma.lead.create).toHaveBeenCalledWith({
238
+ data: {
239
+ siteId: CONTACT_FORM_LEAD_BODY.siteId,
240
+ source: CONTACT_FORM_LEAD_BODY.source,
241
+ email: CONTACT_FORM_LEAD_BODY.email,
242
+ payload: CONTACT_FORM_LEAD_BODY.payload,
243
+ },
244
+ select: { id: true },
245
+ });
246
+ const record = await fileEnv.readLastCaptureRecord();
247
+ expect(record.id).toBe(serverGeneratedId);
248
+ expect(record.source).toBe(CONTACT_FORM_LEAD_BODY.source);
249
+ expect(record.data).toEqual({
250
+ email: CONTACT_FORM_LEAD_BODY.email,
251
+ ...CONTACT_FORM_LEAD_BODY.payload,
252
+ });
253
+ });
254
+
136
255
  it("POST /api/leads/callback — attacker-supplied id is not returned (server uuid used)", async () => {
137
- const serverGeneratedId = "cuid-cb-server-1";
256
+ const serverGeneratedId = "callbk_00000000-0000-7000-8000-0000000000a3";
138
257
  (prisma.callback.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
139
258
 
140
259
  const res = await app.inject({
@@ -161,14 +280,14 @@ describe("POST /api/leads — id isolation", () => {
161
280
  // ── Lead capture store integration cases ──────────────────────────────────
162
281
 
163
282
  it("file-on-success — appendLeadRecord writes to LEAD_CAPTURE_FILE on successful DB persist", async () => {
164
- const serverGeneratedId = "cuid-file-success-1";
283
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a4";
165
284
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
166
285
 
167
286
  const res = await app.inject({
168
287
  method: "POST",
169
- url: "/v1/leads/lead",
288
+ url: "/v1/leads/submit",
170
289
  payload: {
171
- kind: "lead",
290
+ kind: "submit",
172
291
  siteId: "site-abc",
173
292
  source: "contact",
174
293
  email: "test@example.com",
@@ -186,14 +305,14 @@ describe("POST /api/leads — id isolation", () => {
186
305
  });
187
306
 
188
307
  it("dual-write — real lead-capture-store AND DB are both invoked on success", async () => {
189
- const serverGeneratedId = "cuid-dual-write-1";
308
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a5";
190
309
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
191
310
 
192
311
  const res = await app.inject({
193
312
  method: "POST",
194
- url: "/v1/leads/lead",
313
+ url: "/v1/leads/submit",
195
314
  payload: {
196
- kind: "lead",
315
+ kind: "submit",
197
316
  siteId: "site-abc",
198
317
  source: "contact",
199
318
  email: "dual@example.com",
@@ -211,14 +330,14 @@ describe("POST /api/leads — id isolation", () => {
211
330
  it("disabled — appendLeadRecord is skipped when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
212
331
  process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
213
332
 
214
- const serverGeneratedId = "cuid-disabled-1";
333
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a6";
215
334
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
216
335
 
217
336
  const res = await app.inject({
218
337
  method: "POST",
219
- url: "/v1/leads/lead",
338
+ url: "/v1/leads/submit",
220
339
  payload: {
221
- kind: "lead",
340
+ kind: "submit",
222
341
  siteId: "site-abc",
223
342
  source: "contact",
224
343
  email: "disabled@example.com",
@@ -242,9 +361,9 @@ describe("POST /api/leads — id isolation", () => {
242
361
 
243
362
  const res = await app.inject({
244
363
  method: "POST",
245
- url: "/v1/leads/lead",
364
+ url: "/v1/leads/submit",
246
365
  payload: {
247
- kind: "lead",
366
+ kind: "submit",
248
367
  siteId: "site-abc",
249
368
  source: "contact",
250
369
  email: "nocapture@example.com",
@@ -261,15 +380,15 @@ describe("POST /api/leads — id isolation", () => {
261
380
 
262
381
  it("IP-include — metadata.ip present in capture record when LEAD_CAPTURE_INCLUDE_IP=true", async () => {
263
382
  process.env.LEAD_CAPTURE_INCLUDE_IP = "true";
264
- const serverGeneratedId = "cuid-ip-include-1";
383
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a7";
265
384
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
266
385
 
267
386
  const res = await app.inject({
268
387
  method: "POST",
269
- url: "/v1/leads/lead",
388
+ url: "/v1/leads/submit",
270
389
  headers: { "x-forwarded-for": "1.2.3.4" },
271
390
  payload: {
272
- kind: "lead",
391
+ kind: "submit",
273
392
  siteId: "site-abc",
274
393
  source: "contact",
275
394
  email: "ip@example.com",
@@ -286,11 +405,11 @@ describe("POST /api/leads — id isolation", () => {
286
405
  // ── buildInput branch coverage ─────────────────────────────────────────────
287
406
 
288
407
  it("lead — payload absent from body defaults to empty object via ?? (payload ?? {} branch)", async () => {
289
- const serverGeneratedId = "cuid-nopayload-1";
408
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000a8";
290
409
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
291
410
 
292
411
  const bodyWithoutPayload: Record<string, unknown> = {
293
- kind: "lead",
412
+ kind: "submit",
294
413
  siteId: "site-abc",
295
414
  source: "contact",
296
415
  email: "nopayload@example.com",
@@ -299,7 +418,7 @@ describe("POST /api/leads — id isolation", () => {
299
418
 
300
419
  const res = await app.inject({
301
420
  method: "POST",
302
- url: "/v1/leads/lead",
421
+ url: "/v1/leads/submit",
303
422
  payload: bodyWithoutPayload,
304
423
  });
305
424
 
@@ -310,7 +429,7 @@ describe("POST /api/leads — id isolation", () => {
310
429
  });
311
430
 
312
431
  it("callback — preferredTime included when provided (preferredTime spread branch)", async () => {
313
- const serverGeneratedId = "cuid-callback-ptime-1";
432
+ const serverGeneratedId = "callbk_00000000-0000-7000-8000-0000000000a9";
314
433
  (prisma.callback.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
315
434
 
316
435
  const res = await app.inject({
@@ -333,7 +452,7 @@ describe("POST /api/leads — id isolation", () => {
333
452
  });
334
453
 
335
454
  it("callback — payload absent from body defaults to empty object via ?? (callback payload ?? {} branch)", async () => {
336
- const serverGeneratedId = "cuid-callback-nopayload-1";
455
+ const serverGeneratedId = "callbk_00000000-0000-7000-8000-0000000000aa";
337
456
  (prisma.callback.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
338
457
 
339
458
  const bodyWithoutPayload: Record<string, unknown> = {
@@ -361,14 +480,14 @@ describe("POST /api/leads — id isolation", () => {
361
480
  it("email skipped — emailKind=skipped logged when env vars are unset", async () => {
362
481
  delete process.env.AILK_LEAD_NOTIFICATION_TO;
363
482
  delete process.env.AILK_LEAD_NOTIFICATION_FROM;
364
- const serverGeneratedId = "cuid-email-skipped-1";
483
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000ab";
365
484
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
366
485
 
367
486
  const res = await app.inject({
368
487
  method: "POST",
369
- url: "/v1/leads/lead",
488
+ url: "/v1/leads/submit",
370
489
  payload: {
371
- kind: "lead",
490
+ kind: "submit",
372
491
  siteId: "site-abc",
373
492
  source: "contact",
374
493
  email: "skipped@example.com",
@@ -384,20 +503,178 @@ describe("POST /api/leads — id isolation", () => {
384
503
  }
385
504
  });
386
505
 
506
+ // ── #4985 — step-flow envelope ({ siteId, source, payload: <record> }) ────
507
+ // StepFlow (packages/ui/src/sections/StepFlow.tsx L548) posts email INSIDE
508
+ // `payload`, not top-level. buildInput() now lifts `email` from `payload`
509
+ // when the top-level field is absent.
510
+
511
+ it("envelope submit — { siteId, source, payload: { email, ... } } returns 201, writes one Lead with that email and the full payload, and routes the notification email", async () => {
512
+ const { sendLeadNotification } = jest.requireMock("@working-theory/email");
513
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000e1";
514
+ (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
515
+
516
+ const envelopePayload = {
517
+ email: "envelope@example.com",
518
+ name: "Envelope Visitor",
519
+ message: "hello from the step-flow",
520
+ };
521
+
522
+ const res = await app.inject({
523
+ method: "POST",
524
+ url: "/v1/leads/submit",
525
+ payload: {
526
+ siteId: "site-abc",
527
+ source: "step-flow",
528
+ payload: envelopePayload,
529
+ },
530
+ });
531
+
532
+ expect(res.statusCode).toBe(201);
533
+ const body = res.json();
534
+ expect(body.id).toBe(serverGeneratedId);
535
+ expect(prisma.lead.create).toHaveBeenCalledTimes(1);
536
+ expect(prisma.lead.create).toHaveBeenCalledWith({
537
+ data: {
538
+ siteId: "site-abc",
539
+ source: "step-flow",
540
+ email: "envelope@example.com",
541
+ payload: envelopePayload,
542
+ },
543
+ select: { id: true },
544
+ });
545
+ expect(sendLeadNotification).toHaveBeenCalledTimes(1);
546
+ expect(body.email).toHaveProperty("ok", true);
547
+ });
548
+
549
+ it("envelope subscribe — { siteId, source, payload: { email } } returns 201 and writes one Subscriber", async () => {
550
+ const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000e2";
551
+ (prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
552
+
553
+ const res = await app.inject({
554
+ method: "POST",
555
+ url: "/v1/leads/subscribe",
556
+ payload: {
557
+ siteId: "site-abc",
558
+ source: "step-flow",
559
+ payload: { email: "sub-envelope@example.com" },
560
+ },
561
+ });
562
+
563
+ expect(res.statusCode).toBe(201);
564
+ const body = res.json();
565
+ expect(body.id).toBe(serverGeneratedId);
566
+ expect(prisma.subscriber.create).toHaveBeenCalledTimes(1);
567
+ expect(prisma.subscriber.create).toHaveBeenCalledWith({
568
+ data: {
569
+ siteId: "site-abc",
570
+ email: "sub-envelope@example.com",
571
+ source: "step-flow",
572
+ },
573
+ select: { id: true },
574
+ });
575
+ });
576
+
577
+ it("envelope submit — email absent in both the top level and payload → the existing 400 invalid_input", async () => {
578
+ const res = await app.inject({
579
+ method: "POST",
580
+ url: "/v1/leads/submit",
581
+ payload: {
582
+ siteId: "site-abc",
583
+ source: "step-flow",
584
+ payload: { message: "no email anywhere" },
585
+ },
586
+ });
587
+
588
+ expect(res.statusCode).toBe(400);
589
+ const body = res.json();
590
+ expect(body.error).toBe("invalid_input");
591
+ expect(prisma.lead.create).not.toHaveBeenCalled();
592
+ });
593
+
594
+ it("envelope subscribe — email absent in both the top level and payload → the existing 400 invalid_input", async () => {
595
+ const res = await app.inject({
596
+ method: "POST",
597
+ url: "/v1/leads/subscribe",
598
+ payload: {
599
+ siteId: "site-abc",
600
+ source: "step-flow",
601
+ payload: {},
602
+ },
603
+ });
604
+
605
+ expect(res.statusCode).toBe(400);
606
+ const body = res.json();
607
+ expect(body.error).toBe("invalid_input");
608
+ expect(prisma.subscriber.create).not.toHaveBeenCalled();
609
+ });
610
+
611
+ it("submit — a top-level email wins over a payload email", async () => {
612
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000e3";
613
+ (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
614
+
615
+ const res = await app.inject({
616
+ method: "POST",
617
+ url: "/v1/leads/submit",
618
+ payload: {
619
+ siteId: "site-abc",
620
+ source: "step-flow",
621
+ email: "top-level@example.com",
622
+ payload: { email: "shadowed-payload@example.com", message: "hi" },
623
+ },
624
+ });
625
+
626
+ expect(res.statusCode).toBe(201);
627
+ expect(prisma.lead.create).toHaveBeenCalledWith({
628
+ data: {
629
+ siteId: "site-abc",
630
+ source: "step-flow",
631
+ email: "top-level@example.com",
632
+ payload: { email: "shadowed-payload@example.com", message: "hi" },
633
+ },
634
+ select: { id: true },
635
+ });
636
+ });
637
+
638
+ it("subscribe — a top-level email wins over a payload email", async () => {
639
+ const serverGeneratedId = "subscr_00000000-0000-7000-8000-0000000000e4";
640
+ (prisma.subscriber.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
641
+
642
+ const res = await app.inject({
643
+ method: "POST",
644
+ url: "/v1/leads/subscribe",
645
+ payload: {
646
+ siteId: "site-abc",
647
+ source: "step-flow",
648
+ email: "top-level-sub@example.com",
649
+ payload: { email: "shadowed-sub@example.com" },
650
+ },
651
+ });
652
+
653
+ expect(res.statusCode).toBe(201);
654
+ expect(prisma.subscriber.create).toHaveBeenCalledWith({
655
+ data: {
656
+ siteId: "site-abc",
657
+ email: "top-level-sub@example.com",
658
+ source: "step-flow",
659
+ },
660
+ select: { id: true },
661
+ });
662
+ });
663
+
387
664
  it("email error — emailKind=error logged when sendLeadNotification returns ok:false", async () => {
388
665
  const { sendLeadNotification } = jest.requireMock("@working-theory/email");
389
666
  (sendLeadNotification as jest.Mock).mockResolvedValueOnce({
390
667
  ok: false,
391
668
  error: { kind: "send_error", message: "SMTP timeout" },
392
669
  });
393
- const serverGeneratedId = "cuid-email-error-1";
670
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000ac";
394
671
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
395
672
 
396
673
  const res = await app.inject({
397
674
  method: "POST",
398
- url: "/v1/leads/lead",
675
+ url: "/v1/leads/submit",
399
676
  payload: {
400
- kind: "lead",
677
+ kind: "submit",
401
678
  siteId: "site-abc",
402
679
  source: "contact",
403
680
  email: "erroremail@example.com",
@@ -413,3 +690,133 @@ describe("POST /api/leads — id isolation", () => {
413
690
  }
414
691
  });
415
692
  });
693
+
694
+ // ── #4985 — PII-safe logging still holds when email arrives via `payload` ──
695
+
696
+ describe("POST /v1/leads/submit — structured log stays PII-safe for envelope input", () => {
697
+ afterEach(async () => {
698
+ delete process.env.AILK_LEAD_NOTIFICATION_TO;
699
+ delete process.env.AILK_LEAD_NOTIFICATION_FROM;
700
+ delete process.env.DATABASE_URL;
701
+ });
702
+
703
+ it("the 'lead routed' log line carries no email or payload value when email is lifted from payload", async () => {
704
+ jest.clearAllMocks();
705
+ process.env.AILK_LEAD_NOTIFICATION_TO = "owner@example.com";
706
+ process.env.AILK_LEAD_NOTIFICATION_FROM = "noreply@example.com";
707
+ process.env.DATABASE_URL = "postgres://stub";
708
+
709
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000e5";
710
+ (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
711
+
712
+ const logLines: string[] = [];
713
+ const logCapture = require("fastify")({
714
+ logger: {
715
+ level: "info",
716
+ stream: { write: (msg: string) => logLines.push(msg) },
717
+ },
718
+ }) as FastifyInstance;
719
+ await logCapture.register(leadSubmitRoute);
720
+ await logCapture.ready();
721
+
722
+ try {
723
+ const res = await logCapture.inject({
724
+ method: "POST",
725
+ url: "/v1/leads/submit",
726
+ payload: {
727
+ siteId: "site-abc",
728
+ source: "step-flow",
729
+ payload: {
730
+ email: "secret-envelope@example.com",
731
+ message: "this message body must never be logged",
732
+ },
733
+ },
734
+ });
735
+
736
+ expect(res.statusCode).toBe(201);
737
+
738
+ const routedLine = logLines.find((line) => line.includes("lead routed"));
739
+ expect(routedLine).toBeDefined();
740
+ expect(routedLine).not.toMatch(/secret-envelope@example\.com/);
741
+ expect(routedLine).not.toMatch(/this message body must never be logged/);
742
+
743
+ const logged = JSON.parse(routedLine!) as Record<string, unknown>;
744
+ expect(logged).not.toHaveProperty("email");
745
+ expect(logged).not.toHaveProperty("payload");
746
+ expect(logged).toMatchObject({
747
+ siteId: "site-abc",
748
+ kind: "submit",
749
+ persisted: "db",
750
+ });
751
+ } finally {
752
+ await logCapture.close();
753
+ }
754
+ });
755
+ });
756
+
757
+ // ── D5 — registration binds the client's literal path (issue #3964) ───────────
758
+ //
759
+ // The drift class this issue fixed: every test above hand-supplied the same
760
+ // (wrong) URL the route was actually registered at, so the mismatch between
761
+ // the registered path and the path every consumer calls shipped silently.
762
+ // This suite registers the real exported plugin and asserts against the
763
+ // EXACT literal packages/api-client/src/leads.ts uses (cross-referenced
764
+ // below), so a future re-drift fails here instead of shipping.
765
+
766
+ describe("POST /v1/leads/submit — registration binds the client's literal path", () => {
767
+ let app: FastifyInstance;
768
+
769
+ beforeEach(async () => {
770
+ jest.clearAllMocks();
771
+ process.env.AILK_LEAD_NOTIFICATION_TO = "owner@example.com";
772
+ process.env.AILK_LEAD_NOTIFICATION_FROM = "noreply@example.com";
773
+ process.env.DATABASE_URL = "postgres://stub";
774
+
775
+ const fastify = require("fastify")();
776
+ await fastify.register(leadSubmitRoute);
777
+ app = fastify;
778
+ });
779
+
780
+ afterEach(async () => {
781
+ await app.close();
782
+ delete process.env.AILK_LEAD_NOTIFICATION_TO;
783
+ delete process.env.AILK_LEAD_NOTIFICATION_FROM;
784
+ delete process.env.DATABASE_URL;
785
+ });
786
+
787
+ it("resolves POST /v1/leads/submit — the exact literal packages/api-client/src/leads.ts's leads.submit() posts to", async () => {
788
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000d1";
789
+ (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
790
+
791
+ // Cross-reference: packages/api-client/src/leads.ts —
792
+ // submit: (payload) => post<LeadWriteResponse>("/v1/leads/submit", payload)
793
+ const res = await app.inject({
794
+ method: "POST",
795
+ url: "/v1/leads/submit",
796
+ payload: {
797
+ siteId: "site-abc",
798
+ source: "contact",
799
+ email: "d5@example.com",
800
+ payload: { message: "hello" },
801
+ },
802
+ });
803
+
804
+ expect(res.statusCode).toBe(201);
805
+ expect(res.json().id).toBe(serverGeneratedId);
806
+ });
807
+
808
+ it("POST /v1/leads/lead — no longer registered (404); the pre-fix path", async () => {
809
+ const res = await app.inject({
810
+ method: "POST",
811
+ url: "/v1/leads/lead",
812
+ payload: {
813
+ siteId: "site-abc",
814
+ source: "contact",
815
+ email: "old-path@example.com",
816
+ payload: { message: "hello" },
817
+ },
818
+ });
819
+
820
+ expect(res.statusCode).toBe(404);
821
+ });
822
+ });
@@ -74,14 +74,14 @@ describe("POST /api/leads — track() telemetry", () => {
74
74
  });
75
75
 
76
76
  it("calls track('lead-captured') with source and locale on successful submission", async () => {
77
- const serverGeneratedId = "cuid-track-success-1";
77
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b1";
78
78
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
79
79
 
80
80
  const res = await app.inject({
81
81
  method: "POST",
82
- url: "/v1/leads/lead",
82
+ url: "/v1/leads/submit",
83
83
  payload: {
84
- kind: "lead",
84
+ kind: "submit",
85
85
  siteId: "site-abc",
86
86
  source: "newsletter",
87
87
  email: "test@example.com",
@@ -108,9 +108,9 @@ describe("POST /api/leads — track() telemetry", () => {
108
108
 
109
109
  const res = await app.inject({
110
110
  method: "POST",
111
- url: "/v1/leads/lead",
111
+ url: "/v1/leads/submit",
112
112
  payload: {
113
- kind: "lead",
113
+ kind: "submit",
114
114
  siteId: "site-abc",
115
115
  source: "contact",
116
116
  email: "fail@example.com",
@@ -125,10 +125,10 @@ describe("POST /api/leads — track() telemetry", () => {
125
125
  it("does NOT call track() when validation fails (missing required field → 400 or 503)", async () => {
126
126
  const res = await app.inject({
127
127
  method: "POST",
128
- url: "/v1/leads/lead",
128
+ url: "/v1/leads/submit",
129
129
  payload: {
130
130
  // missing email — required field for lead kind
131
- kind: "lead",
131
+ kind: "submit",
132
132
  siteId: "site-abc",
133
133
  source: "contact",
134
134
  payload: {},
@@ -140,14 +140,14 @@ describe("POST /api/leads — track() telemetry", () => {
140
140
  });
141
141
 
142
142
  it("track() props contain only source and locale — no PII fields", async () => {
143
- const serverGeneratedId = "cuid-pii-check-1";
143
+ const serverGeneratedId = "lead_00000000-0000-7000-8000-0000000000b2";
144
144
  (prisma.lead.create as jest.Mock).mockResolvedValue({ id: serverGeneratedId });
145
145
 
146
146
  const res = await app.inject({
147
147
  method: "POST",
148
- url: "/v1/leads/lead",
148
+ url: "/v1/leads/submit",
149
149
  payload: {
150
- kind: "lead",
150
+ kind: "submit",
151
151
  siteId: "site-abc",
152
152
  source: "landing-page",
153
153
  email: "pii@example.com",