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
@@ -16,42 +16,65 @@ import {
16
16
  routeLead,
17
17
  } from "../../services/lead-routing.js";
18
18
 
19
+ /**
20
+ * Read a named string field from the body, falling back to the same-named
21
+ * field inside `body.payload` when the top-level value is absent — the
22
+ * step-flow envelope (`{ siteId, source, payload: <record> }`) carries its
23
+ * identity fields inside `payload` rather than at the top level. A top-level
24
+ * value always wins.
25
+ */
26
+ function liftString(
27
+ body: Record<string, unknown>,
28
+ payload: Record<string, unknown>,
29
+ key: string,
30
+ ): string {
31
+ const topLevel = body[key];
32
+ if (typeof topLevel === "string") return topLevel;
33
+ const fromPayload = payload[key];
34
+ return (typeof fromPayload === "string" ? fromPayload : topLevel) as string;
35
+ }
36
+
19
37
  /**
20
38
  * Build a typed RouteLeadInput from the raw request body for a given kind.
21
39
  * Explicit destructure per kind prevents unknown fields (e.g. attacker-supplied
22
40
  * `id`) from reaching the service — the service's .strict() Zod schemas provide
23
- * a second layer, but we also keep the handler clean at the boundary.
41
+ * a second layer, but we also keep the handler clean at the boundary. Lifting
42
+ * `email`/`name`/`phone` out of `payload` (see liftString above) reads by
43
+ * explicit field name only — it does not spread `payload`'s other keys, so
44
+ * the same discipline holds. `payload` itself is passed through unchanged.
24
45
  */
25
46
  function buildInput(
26
47
  kind: LeadKind,
27
48
  body: Record<string, unknown>,
28
49
  ): RouteLeadInput {
29
- if (kind === "lead") {
50
+ const payload = (body["payload"] ?? {}) as Record<string, unknown>;
51
+
52
+ if (kind === "submit") {
30
53
  return {
31
54
  kind,
32
55
  siteId: body["siteId"] as string,
33
56
  source: body["source"] as string,
34
- email: body["email"] as string,
35
- payload: (body["payload"] ?? {}) as Record<string, unknown>,
57
+ email: liftString(body, payload, "email"),
58
+ payload,
36
59
  };
37
60
  }
38
61
  if (kind === "subscribe") {
39
62
  return {
40
63
  kind,
41
64
  siteId: body["siteId"] as string,
42
- email: body["email"] as string,
65
+ email: liftString(body, payload, "email"),
43
66
  source: body["source"] as string,
44
67
  };
45
68
  }
46
69
  return {
47
70
  kind,
48
71
  siteId: body["siteId"] as string,
49
- name: body["name"] as string,
50
- phone: body["phone"] as string,
72
+ name: liftString(body, payload, "name"),
73
+ phone: liftString(body, payload, "phone"),
51
74
  ...(body["preferredTime"] !== undefined && {
52
75
  preferredTime: body["preferredTime"] as string,
53
76
  }),
54
- payload: (body["payload"] ?? {}) as Record<string, unknown>,
77
+ payload,
55
78
  };
56
79
  }
57
80
 
@@ -59,6 +82,17 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
59
82
  return async (fastify) => {
60
83
  fastify.post(`/v1/leads/${kind}`, async (request, reply) => {
61
84
  const body = request.body as Record<string, unknown>;
85
+ // request.ip became attacker-influenceable when trustProxy: true went
86
+ // instance-wide (server.ts, #4730/#4778) — leads/index.ts was never
87
+ // itself touched by that PR. Traced every consumer of this value
88
+ // (#5112, DEF-004): lead-routing.ts's structured log field and its
89
+ // opt-in (LEAD_CAPTURE_INCLUDE_IP) persisted metadata.ip both RECORD
90
+ // the value; neither branches routing, persistence, or dedup on it.
91
+ // The best-effort/record-only posture is judged sufficient — the same
92
+ // trade-off server.ts's own trustProxy comment already accepts. The
93
+ // private @working-theory/lead-routing package (not present in this
94
+ // repo, not inspectable) is the one consumer this trace could not
95
+ // complete; see docs/specs/5112-leads-ip-trace-spec.md §8.
62
96
  const sourceMeta = {
63
97
  ip: request.ip,
64
98
  userAgent: request.headers["user-agent"],
@@ -125,6 +159,6 @@ function makeLeadsRoute(kind: LeadKind): FastifyPluginAsync {
125
159
  };
126
160
  }
127
161
 
128
- export const leadSubmitRoute = makeLeadsRoute("lead");
162
+ export const leadSubmitRoute = makeLeadsRoute("submit");
129
163
  export const leadSubscribeRoute = makeLeadsRoute("subscribe");
130
164
  export const leadCallbackRoute = makeLeadsRoute("callback");
@@ -0,0 +1,547 @@
1
+ /**
2
+ * @file configured-application.test.ts
3
+ *
4
+ * THE ACCEPTANCE GATE for the two halves of the application the SITE now
5
+ * decides (#5048 D5/D6), plus the control that deliberately does not ship
6
+ * (D7).
7
+ *
8
+ * #5036 validated a fixed payload against a fixed schema: ten words of
9
+ * headline, twenty-five of one-liner, and every one of eleven fields
10
+ * required. Both were one consuming site's editorial decisions compiled into
11
+ * the published package, and the second was worse than a preference — steps
12
+ * are individually optional, so a field whose step a site never renders
13
+ * cannot be supplied, and requiring it refuses a valid application with a
14
+ * message naming a question the founder was never shown.
15
+ *
16
+ * Both now derive from the site's SERVER-side `SiteListingConfig`
17
+ * (`wordCaps`, `collectedFields`), and the schemas are compiled per request
18
+ * by `buildProjectListingDraftPatchSchema` / `buildProjectListingSubmitSchema`.
19
+ *
20
+ * The tests that would fail QUIETLY, and are the reason this file exists:
21
+ * - a configured cap that is not actually consulted looks identical to one
22
+ * that is, until a site sets three and the tenth word still passes — so
23
+ * each cap assertion pins the SITE's number and shows the value AT the
24
+ * cap is accepted, and a default-config pair proves the previous
25
+ * behaviour is unchanged for a site that says nothing;
26
+ * - a site that does not collect `address` submitting successfully is the
27
+ * core regression guard for D5: the fixed schema #5036 shipped would
28
+ * refuse it, with `address` named as missing;
29
+ * - no honeypot and no start-to-submit timing check exists anywhere on this
30
+ * surface — #316 D6b removed both and #5039 deliberately keeps that
31
+ * channel open, so their ABSENCE is worth pinning before someone
32
+ * helpfully re-adds one.
33
+ *
34
+ * Same harness as `drafts.test.ts`: real routes, real services, mocked
35
+ * `lib/prisma.js` over a small in-memory row store, `routeLead` running for
36
+ * real against a per-test temp path.
37
+ */
38
+ import { readFileSync } from "node:fs";
39
+ import { join } from "node:path";
40
+
41
+ // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock
42
+ jest.mock("../../../lib/prisma.js");
43
+
44
+ jest.mock("@working-theory/email", () => ({
45
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
46
+ }));
47
+
48
+ jest.mock("@working-theory/license", () => ({
49
+ checkLicense: jest.fn().mockResolvedValue({
50
+ tier: "free",
51
+ plan: null,
52
+ features: [],
53
+ expiresAt: null,
54
+ revoked: false,
55
+ }),
56
+ }));
57
+
58
+ import Fastify from "fastify";
59
+ import type { FastifyInstance } from "fastify";
60
+ import {
61
+ projectListingApplicationFieldsSchema,
62
+ projectListingStartSchema,
63
+ SITE_LISTING_CONFIG_DEFAULTS,
64
+ } from "@working-theory/validation";
65
+
66
+ import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
67
+ import { prisma } from "../../../lib/prisma.js";
68
+ import { patchProjectListingDraftRoute } from "../patch-draft.js";
69
+ import { startProjectListingRoute } from "../start.js";
70
+ import { submitProjectListingRoute } from "../submit.js";
71
+
72
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
73
+
74
+ const SITE_ID = "site-1";
75
+ const SITE_KEY = "pk-site-1";
76
+
77
+ const opt = (value: string, label: string) => ({ value, label });
78
+
79
+ const VALID_QUESTIONS = {
80
+ q2: {
81
+ prompt: "Why are you here?",
82
+ options: [opt("scale", "Scaling my team"), opt("just-curious", "Just curious")],
83
+ },
84
+ q3: { prompt: "Stage?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
85
+ q4: { prompt: "Team size?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
86
+ q5: {
87
+ prompt: "Urgency?",
88
+ options: [opt("urgent", "Now"), opt("planning", "Soon"), opt("exploring", "Someday")],
89
+ },
90
+ q6: {
91
+ prompt: "Pain?",
92
+ options: [
93
+ opt("low", "Low"),
94
+ opt("medium", "Medium"),
95
+ opt("high", "High"),
96
+ opt("critical", "Critical"),
97
+ ],
98
+ },
99
+ q7: { prompt: "Anything else?" },
100
+ extras: [],
101
+ };
102
+
103
+ const START_PAYLOAD = {
104
+ siteKey: SITE_KEY,
105
+ founderName: "Ada Founder",
106
+ founderEmail: "ada@example.com",
107
+ consent: true,
108
+ };
109
+
110
+ /** Everything the ten steps after step one collect. */
111
+ const COMPLETE_APPLICATION = {
112
+ name: "My Project",
113
+ headline: "Ten words or fewer for the tile title here",
114
+ oneLiner: "A short sentence describing what the project does for its people.",
115
+ problem: "The problem is real and this describes it.",
116
+ product: "wireframes",
117
+ commitment: "part-time",
118
+ teamComposition: "co-founders",
119
+ faqs: [
120
+ { question: "Q one?", answer: "A one." },
121
+ { question: "Q two?", answer: "A two." },
122
+ { question: "Q three?", answer: "A three." },
123
+ ],
124
+ address: "1 Founder Way, Springfield",
125
+ questions: VALID_QUESTIONS,
126
+ };
127
+
128
+ /** The same application with no `address` — the D5 case. */
129
+ const { address: _address, ...APPLICATION_WITHOUT_ADDRESS } = COMPLETE_APPLICATION;
130
+
131
+ /** Every collected field except `address`, in the shape an operator writes. */
132
+ const COLLECTED_WITHOUT_ADDRESS = SITE_LISTING_CONFIG_DEFAULTS.collectedFields.filter(
133
+ (field) => field !== "address",
134
+ );
135
+
136
+ /** `n` whitespace-separated words. */
137
+ const words = (n: number) => Array.from({ length: n }, (_, i) => `word${i + 1}`).join(" ");
138
+
139
+ // ─── The in-memory row store the Prisma mock is backed by ───────────────────
140
+
141
+ type Row = Record<string, unknown>;
142
+
143
+ let rows: Row[];
144
+ let nextId: number;
145
+ /**
146
+ * The `SiteListingConfig.data` blob this site is running on, per test. `null`
147
+ * means NO ROW — the platform defaults, which is the "site that says nothing"
148
+ * half of every pair below.
149
+ */
150
+ let configData: Record<string, unknown> | null;
151
+
152
+ function matches(row: Row, where: Record<string, unknown>): boolean {
153
+ return Object.entries(where).every(([key, value]) => {
154
+ if (key === "OR") {
155
+ return (value as Record<string, unknown>[]).some((clause) => matches(row, clause));
156
+ }
157
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
158
+ const clause = value as Record<string, unknown>;
159
+ if ("in" in clause) return (clause["in"] as unknown[]).includes(row[key]);
160
+ if ("not" in clause) return row[key] !== clause["not"];
161
+ if ("gt" in clause) return (row[key] as Date) > (clause["gt"] as Date);
162
+ if ("startsWith" in clause) {
163
+ return (
164
+ typeof row[key] === "string" &&
165
+ (row[key] as string).startsWith(clause["startsWith"] as string)
166
+ );
167
+ }
168
+ }
169
+ return row[key] === value;
170
+ });
171
+ }
172
+
173
+ function installStore(): void {
174
+ rows = [];
175
+ nextId = 1;
176
+ configData = null;
177
+
178
+ (prisma.lead.create as jest.Mock).mockImplementation(() =>
179
+ Promise.resolve({ id: `lead_00000000-0000-7000-8000-00000000000${nextId}` }),
180
+ );
181
+
182
+ // One delegate, two lookups (#5048 D2/D6): `where.publicKey` resolves the
183
+ // site key, `where.siteId` reads the config the schemas are compiled from.
184
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockImplementation(
185
+ ({ where }: { where: { siteId?: string; publicKey?: string } }) => {
186
+ if (where.publicKey !== undefined) {
187
+ return Promise.resolve(where.publicKey === SITE_KEY ? { siteId: SITE_ID } : null);
188
+ }
189
+ return Promise.resolve(configData === null ? null : { data: configData });
190
+ },
191
+ );
192
+
193
+ (prisma.projectListing.create as jest.Mock).mockImplementation(({ data }: { data: Row }) => {
194
+ const now = new Date();
195
+ const row: Row = {
196
+ id: `prjl_00000000-0000-7000-8000-00000000000${nextId++}`,
197
+ slug: null,
198
+ name: null,
199
+ oneLiner: null,
200
+ problem: null,
201
+ url: null,
202
+ product: null,
203
+ commitment: null,
204
+ teamComposition: null,
205
+ faqs: null,
206
+ anythingElse: null,
207
+ address: null,
208
+ questions: null,
209
+ founderCompany: null,
210
+ leadId: null,
211
+ headline: null,
212
+ logoMarkUrl: null,
213
+ logoWordmarkUrl: null,
214
+ featured: false,
215
+ decidedAt: null,
216
+ decidedBy: null,
217
+ issueUrl: null,
218
+ experimentKey: null,
219
+ windowEndsAt: null,
220
+ createdAt: now,
221
+ updatedAt: now,
222
+ ...data,
223
+ };
224
+ rows.push(row);
225
+ return Promise.resolve(row);
226
+ });
227
+
228
+ (prisma.projectListing.findFirst as jest.Mock).mockImplementation(
229
+ ({ where }: { where: Record<string, unknown> }) =>
230
+ Promise.resolve(rows.find((row) => matches(row, where)) ?? null),
231
+ );
232
+
233
+ (prisma.projectListing.findMany as jest.Mock).mockImplementation(
234
+ ({ where }: { where: Record<string, unknown> }) =>
235
+ Promise.resolve(rows.filter((row) => matches(row, where))),
236
+ );
237
+
238
+ (prisma.projectListing.update as jest.Mock).mockImplementation(
239
+ ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
240
+ const row = rows.find((r) => matches(r, where));
241
+ Object.assign(row!, data, { updatedAt: new Date() });
242
+ return Promise.resolve(row);
243
+ },
244
+ );
245
+
246
+ (prisma.projectListing.updateMany as jest.Mock).mockImplementation(
247
+ ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
248
+ const matched = rows.filter((r) => matches(r, where));
249
+ matched.forEach((r) => Object.assign(r, data, { updatedAt: new Date() }));
250
+ return Promise.resolve({ count: matched.length });
251
+ },
252
+ );
253
+ }
254
+
255
+ // ─── Helpers ────────────────────────────────────────────────────────────────
256
+
257
+ const bearer = (token: string) => ({ authorization: `Bearer ${token}` });
258
+
259
+ async function buildApp(): Promise<FastifyInstance> {
260
+ const app = Fastify();
261
+ await app.register(startProjectListingRoute);
262
+ await app.register(patchProjectListingDraftRoute);
263
+ await app.register(submitProjectListingRoute);
264
+ await app.ready();
265
+ return app;
266
+ }
267
+
268
+ async function startDraft(
269
+ app: FastifyInstance,
270
+ ): Promise<{ id: string; resumeToken: string }> {
271
+ const res = await app.inject({
272
+ method: "POST",
273
+ url: "/v1/project-listings",
274
+ payload: START_PAYLOAD,
275
+ });
276
+ expect(res.statusCode).toBe(201);
277
+ return (res.json() as { data: { id: string; resumeToken: string } }).data;
278
+ }
279
+
280
+ const patch = (
281
+ app: FastifyInstance,
282
+ draft: { id: string; resumeToken: string },
283
+ payload: Record<string, unknown>,
284
+ ) =>
285
+ app.inject({
286
+ method: "PATCH",
287
+ url: `/v1/project-listings/drafts/${draft.id}`,
288
+ headers: bearer(draft.resumeToken),
289
+ payload,
290
+ });
291
+
292
+ const submit = (app: FastifyInstance, draft: { id: string; resumeToken: string }) =>
293
+ app.inject({
294
+ method: "POST",
295
+ url: `/v1/project-listings/drafts/${draft.id}/submit`,
296
+ headers: bearer(draft.resumeToken),
297
+ });
298
+
299
+ const messageOf = (res: { json: () => unknown }) =>
300
+ (res.json() as { error: { message: string } }).error.message;
301
+
302
+ // ─── Tests ──────────────────────────────────────────────────────────────────
303
+
304
+ describe("the site's own application config (#5048 D5/D6)", () => {
305
+ setupLeadFileEnv("project-listing-configured");
306
+ let app: FastifyInstance;
307
+
308
+ beforeEach(async () => {
309
+ jest.clearAllMocks();
310
+ installStore();
311
+ app = await buildApp();
312
+ });
313
+
314
+ afterEach(async () => {
315
+ await app.close();
316
+ });
317
+
318
+ // ── D6 — the word caps are the site's numbers ──
319
+
320
+ describe("configured word caps (D6)", () => {
321
+ /** Three words of headline, five of one-liner — nobody's platform default. */
322
+ const TIGHT_CAPS = { wordCaps: { headline: 3, oneLiner: 5 } };
323
+
324
+ it("patch: refuses a headline over THIS SITE's cap, naming the site's number", async () => {
325
+ configData = TIGHT_CAPS;
326
+ const draft = await startDraft(app);
327
+
328
+ const res = await patch(app, draft, { headline: words(4) });
329
+
330
+ expect(res.statusCode).toBe(400);
331
+ // Pinned to the CONFIGURED number, not to a constant: a message naming
332
+ // ten here would mean the cap came from the package, and the four-word
333
+ // refusal would be a coincidence of the platform default rather than
334
+ // this site's terms being enforced.
335
+ expect(messageOf(res)).toBe("headline must be 3 words or fewer");
336
+ expect(messageOf(res)).not.toContain("10");
337
+ expect(rows[0]!["headline"]).toBeNull();
338
+ });
339
+
340
+ it("patch: ACCEPTS a headline at exactly the site's cap", async () => {
341
+ // The other half of the pair. Without it the test above passes just as
342
+ // well against a surface that refuses every headline it is given.
343
+ configData = TIGHT_CAPS;
344
+ const draft = await startDraft(app);
345
+
346
+ const res = await patch(app, draft, { headline: words(3) });
347
+
348
+ expect(res.statusCode).toBe(200);
349
+ expect(rows[0]!["headline"]).toBe(words(3));
350
+ });
351
+
352
+ it("patch: enforces the site's oneLiner cap too, at its own number", async () => {
353
+ configData = TIGHT_CAPS;
354
+ const draft = await startDraft(app);
355
+
356
+ const over = await patch(app, draft, { oneLiner: words(6) });
357
+ const at = await patch(app, draft, { oneLiner: words(5) });
358
+
359
+ expect(over.statusCode).toBe(400);
360
+ expect(messageOf(over)).toBe("oneLiner must be 5 words or fewer");
361
+ expect(at.statusCode).toBe(200);
362
+ expect(rows[0]!["oneLiner"]).toBe(words(5));
363
+ });
364
+
365
+ it("patch: a site with NO config row keeps the previous ten-word headline behaviour", async () => {
366
+ // The defaults reproduce #5036's constants exactly, so a site that says
367
+ // nothing sees no change — which is the whole claim D6 makes about
368
+ // turning a constant into configuration.
369
+ configData = null;
370
+ const draft = await startDraft(app);
371
+
372
+ const ten = await patch(app, draft, { headline: words(10) });
373
+ const eleven = await patch(app, draft, { headline: words(11) });
374
+
375
+ expect(ten.statusCode).toBe(200);
376
+ expect(eleven.statusCode).toBe(400);
377
+ expect(messageOf(eleven)).toBe("headline must be 10 words or fewer");
378
+ // The eleventh word never landed on the row.
379
+ expect(rows[0]!["headline"]).toBe(words(10));
380
+ });
381
+
382
+ it("patch: a site with NO config row keeps the previous twenty-five-word oneLiner behaviour", async () => {
383
+ configData = null;
384
+ const draft = await startDraft(app);
385
+
386
+ const twentyFive = await patch(app, draft, { oneLiner: words(25) });
387
+ const twentySix = await patch(app, draft, { oneLiner: words(26) });
388
+
389
+ expect(twentyFive.statusCode).toBe(200);
390
+ expect(twentySix.statusCode).toBe(400);
391
+ expect(messageOf(twentySix)).toBe("oneLiner must be 25 words or fewer");
392
+ });
393
+ });
394
+
395
+ // ── D5 — completeness derives from the collected-field set ──
396
+
397
+ describe("derived completeness (D5)", () => {
398
+ it("submit: a site that does not collect `address` accepts a draft that has none", async () => {
399
+ // THE regression guard. #5036's fixed schema required `address` of every
400
+ // site; a site whose step set never asks for one would have its founders
401
+ // refused at submit, naming a question they were never shown.
402
+ configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
403
+ const draft = await startDraft(app);
404
+ await patch(app, draft, APPLICATION_WITHOUT_ADDRESS);
405
+
406
+ const res = await submit(app, draft);
407
+
408
+ expect(res.statusCode).toBe(200);
409
+ expect(res.json()).toEqual({ ok: true, data: { id: draft.id, slug: "my-project" } });
410
+ expect(rows[0]!["status"]).toBe("requested");
411
+ // Never collected, so never stored — not silently defaulted to something.
412
+ expect(rows[0]!["address"]).toBeNull();
413
+ });
414
+
415
+ it("submit: the DEFAULT config still refuses that same draft, naming `address`", async () => {
416
+ // The other half: the field is only optional because THIS SITE said so.
417
+ configData = null;
418
+ const draft = await startDraft(app);
419
+ await patch(app, draft, APPLICATION_WITHOUT_ADDRESS);
420
+
421
+ const res = await submit(app, draft);
422
+
423
+ expect(res.statusCode).toBe(422);
424
+ const body = res.json() as { error: { code: string; missingFields: string[] } };
425
+ expect(body.error.code).toBe("incomplete_application");
426
+ expect(body.error.missingFields).toEqual(["address"]);
427
+ expect(rows[0]!["status"]).toBe("draft");
428
+ });
429
+
430
+ it("patch: REFUSES a write to a field the site does not collect", async () => {
431
+ // The collected set is a WRITE gate, not only a completeness floor.
432
+ // `address` rides the public read by design (#5036 D6), so a site whose
433
+ // step set omits the address step must not have one written into a draft
434
+ // by an anonymous caller who knows the field name — and then published on
435
+ // its listing page. Narrowing `submit` alone left that open, because a
436
+ // value already on the row is simply carried.
437
+ configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
438
+ const draft = await startDraft(app);
439
+
440
+ const res = await patch(app, draft, { address: COMPLETE_APPLICATION.address });
441
+
442
+ expect(res.statusCode).toBe(400);
443
+ expect(rows[0]!["address"]).toBeNull();
444
+ });
445
+
446
+ it("submit: a value ALREADY on the row for an uncollected field does not block submit", async () => {
447
+ // The other direction, and the reason `buildProjectListingSubmitSchema`
448
+ // DROPS an uncollected field rather than making it optional: a site that
449
+ // narrows its step set after a draft was opened must not strand that
450
+ // draft. The write gate above stops NEW writes; this stops an existing
451
+ // value from failing `.strict()`.
452
+ configData = null;
453
+ const draft = await startDraft(app);
454
+ await patch(app, draft, COMPLETE_APPLICATION); // address included
455
+
456
+ configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
457
+ const res = await submit(app, draft);
458
+
459
+ expect(res.statusCode).toBe(200);
460
+ expect(rows[0]!["status"]).toBe("requested");
461
+ expect(rows[0]!["address"]).toBe(COMPLETE_APPLICATION.address);
462
+ });
463
+
464
+ it("submit: a collected-but-OPTIONAL field does not block submit when absent", async () => {
465
+ configData = null;
466
+ // These three are collected by default and still optional — the
467
+ // distinction the missing-field list turns on.
468
+ for (const field of ["url", "anythingElse", "founderCompany"]) {
469
+ expect(SITE_LISTING_CONFIG_DEFAULTS.collectedFields).toContain(field);
470
+ }
471
+
472
+ const draft = await startDraft(app);
473
+ await patch(app, draft, COMPLETE_APPLICATION); // carries none of the three
474
+
475
+ const res = await submit(app, draft);
476
+
477
+ expect(res.statusCode).toBe(200);
478
+ expect(rows[0]!["url"]).toBeNull();
479
+ expect(rows[0]!["anythingElse"]).toBeNull();
480
+ expect(rows[0]!["founderCompany"]).toBeNull();
481
+ });
482
+ });
483
+
484
+ // ── D7 — the controls that deliberately do not ship ──
485
+
486
+ describe("no anti-automation control ships (D7)", () => {
487
+ const read = (relative: string) =>
488
+ readFileSync(join(__dirname, "..", "..", "..", relative), "utf8");
489
+
490
+ const SURFACE_FILES = [
491
+ "routes/project-listings/start.ts",
492
+ "routes/project-listings/patch-draft.ts",
493
+ "routes/project-listings/public.ts",
494
+ "routes/project-listings/submit.ts",
495
+ "services/project-listings.ts",
496
+ ];
497
+
498
+ it("no honeypot field exists in the payload the surface accepts", async () => {
499
+ // Asserted on the SCHEMAS, not on the prose: a decoy field would have to
500
+ // be accepted somewhere to be a honeypot at all.
501
+ const fields = [
502
+ ...Object.keys(projectListingStartSchema.shape),
503
+ ...Object.keys(projectListingApplicationFieldsSchema.shape),
504
+ ];
505
+ for (const field of fields) {
506
+ expect(field).not.toMatch(/honey|_?trap|decoy|confirmEmail|website2/i);
507
+ }
508
+ // And nothing on the surface names one.
509
+ for (const file of SURFACE_FILES) {
510
+ expect(read(file)).not.toMatch(/honeypot/i);
511
+ }
512
+ });
513
+
514
+ it("no timing heuristic gates the start-to-submit interval", async () => {
515
+ // #5039 deliberately opens the agent channel a "too fast to be human"
516
+ // check would close, and an agent completes a ten-step form in
517
+ // milliseconds. A named heuristic anywhere on the surface is the
518
+ // regression; so is `submit` reaching for a clock to compare the draft's
519
+ // own `createdAt` against.
520
+ for (const file of SURFACE_FILES) {
521
+ expect(read(file)).not.toMatch(
522
+ /tooFast|too_fast|minElapsed|minSeconds|minDuration|dwellTime|timeOnPage|humanTiming/i,
523
+ );
524
+ }
525
+
526
+ const service = read("services/project-listings.ts");
527
+ const submitBody = service.slice(
528
+ service.indexOf("export async function submitProjectListingDraft"),
529
+ );
530
+ const submitOnly = submitBody.slice(0, submitBody.indexOf("\nfunction isUniqueViolation"));
531
+ expect(submitOnly).not.toContain("createdAt");
532
+ expect(submitOnly).not.toContain("Date.now(");
533
+ });
534
+
535
+ it("an application completed in the same millisecond as its draft still submits", async () => {
536
+ // The behavioural half: nothing about how fast the eleven fields arrived
537
+ // is consulted. This whole test runs inside one tick.
538
+ configData = null;
539
+ const draft = await startDraft(app);
540
+ await patch(app, draft, COMPLETE_APPLICATION);
541
+
542
+ const res = await submit(app, draft);
543
+
544
+ expect(res.statusCode).toBe(200);
545
+ });
546
+ });
547
+ });