create-ailk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +12 -5
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +78 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -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,558 @@
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
+ // #5275 — the grant write `start` makes after the row. Append-only; it only
194
+ // needs to resolve here (drafts.test.ts asserts on what it is called with).
195
+ (prisma.consentGrant.createMany as jest.Mock).mockImplementation(
196
+ ({ data }: { data: Row[] }) => Promise.resolve({ count: data.length }),
197
+ );
198
+
199
+ // #5275 — `start` writes the row and its grant in one interactive
200
+ // transaction; the mock hands the same store-backed client through.
201
+ (prisma.$transaction as jest.Mock).mockImplementation(
202
+ async (fn: (tx: typeof prisma) => Promise<unknown>) => fn(prisma),
203
+ );
204
+ (prisma.projectListing.create as jest.Mock).mockImplementation(({ data }: { data: Row }) => {
205
+ const now = new Date();
206
+ const row: Row = {
207
+ id: `prjl_00000000-0000-7000-8000-00000000000${nextId++}`,
208
+ slug: null,
209
+ name: null,
210
+ oneLiner: null,
211
+ problem: null,
212
+ url: null,
213
+ product: null,
214
+ commitment: null,
215
+ teamComposition: null,
216
+ faqs: null,
217
+ anythingElse: null,
218
+ address: null,
219
+ questions: null,
220
+ founderCompany: null,
221
+ leadId: null,
222
+ headline: null,
223
+ logoMarkUrl: null,
224
+ logoWordmarkUrl: null,
225
+ featured: false,
226
+ decidedAt: null,
227
+ decidedBy: null,
228
+ issueUrl: null,
229
+ experimentKey: null,
230
+ windowEndsAt: null,
231
+ createdAt: now,
232
+ updatedAt: now,
233
+ ...data,
234
+ };
235
+ rows.push(row);
236
+ return Promise.resolve(row);
237
+ });
238
+
239
+ (prisma.projectListing.findFirst as jest.Mock).mockImplementation(
240
+ ({ where }: { where: Record<string, unknown> }) =>
241
+ Promise.resolve(rows.find((row) => matches(row, where)) ?? null),
242
+ );
243
+
244
+ (prisma.projectListing.findMany as jest.Mock).mockImplementation(
245
+ ({ where }: { where: Record<string, unknown> }) =>
246
+ Promise.resolve(rows.filter((row) => matches(row, where))),
247
+ );
248
+
249
+ (prisma.projectListing.update as jest.Mock).mockImplementation(
250
+ ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
251
+ const row = rows.find((r) => matches(r, where));
252
+ Object.assign(row!, data, { updatedAt: new Date() });
253
+ return Promise.resolve(row);
254
+ },
255
+ );
256
+
257
+ (prisma.projectListing.updateMany as jest.Mock).mockImplementation(
258
+ ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
259
+ const matched = rows.filter((r) => matches(r, where));
260
+ matched.forEach((r) => Object.assign(r, data, { updatedAt: new Date() }));
261
+ return Promise.resolve({ count: matched.length });
262
+ },
263
+ );
264
+ }
265
+
266
+ // ─── Helpers ────────────────────────────────────────────────────────────────
267
+
268
+ const bearer = (token: string) => ({ authorization: `Bearer ${token}` });
269
+
270
+ async function buildApp(): Promise<FastifyInstance> {
271
+ const app = Fastify();
272
+ await app.register(startProjectListingRoute);
273
+ await app.register(patchProjectListingDraftRoute);
274
+ await app.register(submitProjectListingRoute);
275
+ await app.ready();
276
+ return app;
277
+ }
278
+
279
+ async function startDraft(
280
+ app: FastifyInstance,
281
+ ): Promise<{ id: string; resumeToken: string }> {
282
+ const res = await app.inject({
283
+ method: "POST",
284
+ url: "/v1/project-listings",
285
+ payload: START_PAYLOAD,
286
+ });
287
+ expect(res.statusCode).toBe(201);
288
+ return (res.json() as { data: { id: string; resumeToken: string } }).data;
289
+ }
290
+
291
+ const patch = (
292
+ app: FastifyInstance,
293
+ draft: { id: string; resumeToken: string },
294
+ payload: Record<string, unknown>,
295
+ ) =>
296
+ app.inject({
297
+ method: "PATCH",
298
+ url: `/v1/project-listings/drafts/${draft.id}`,
299
+ headers: bearer(draft.resumeToken),
300
+ payload,
301
+ });
302
+
303
+ const submit = (app: FastifyInstance, draft: { id: string; resumeToken: string }) =>
304
+ app.inject({
305
+ method: "POST",
306
+ url: `/v1/project-listings/drafts/${draft.id}/submit`,
307
+ headers: bearer(draft.resumeToken),
308
+ });
309
+
310
+ const messageOf = (res: { json: () => unknown }) =>
311
+ (res.json() as { error: { message: string } }).error.message;
312
+
313
+ // ─── Tests ──────────────────────────────────────────────────────────────────
314
+
315
+ describe("the site's own application config (#5048 D5/D6)", () => {
316
+ setupLeadFileEnv("project-listing-configured");
317
+ let app: FastifyInstance;
318
+
319
+ beforeEach(async () => {
320
+ jest.clearAllMocks();
321
+ installStore();
322
+ app = await buildApp();
323
+ });
324
+
325
+ afterEach(async () => {
326
+ await app.close();
327
+ });
328
+
329
+ // ── D6 — the word caps are the site's numbers ──
330
+
331
+ describe("configured word caps (D6)", () => {
332
+ /** Three words of headline, five of one-liner — nobody's platform default. */
333
+ const TIGHT_CAPS = { wordCaps: { headline: 3, oneLiner: 5 } };
334
+
335
+ it("patch: refuses a headline over THIS SITE's cap, naming the site's number", async () => {
336
+ configData = TIGHT_CAPS;
337
+ const draft = await startDraft(app);
338
+
339
+ const res = await patch(app, draft, { headline: words(4) });
340
+
341
+ expect(res.statusCode).toBe(400);
342
+ // Pinned to the CONFIGURED number, not to a constant: a message naming
343
+ // ten here would mean the cap came from the package, and the four-word
344
+ // refusal would be a coincidence of the platform default rather than
345
+ // this site's terms being enforced.
346
+ expect(messageOf(res)).toBe("headline must be 3 words or fewer");
347
+ expect(messageOf(res)).not.toContain("10");
348
+ expect(rows[0]!["headline"]).toBeNull();
349
+ });
350
+
351
+ it("patch: ACCEPTS a headline at exactly the site's cap", async () => {
352
+ // The other half of the pair. Without it the test above passes just as
353
+ // well against a surface that refuses every headline it is given.
354
+ configData = TIGHT_CAPS;
355
+ const draft = await startDraft(app);
356
+
357
+ const res = await patch(app, draft, { headline: words(3) });
358
+
359
+ expect(res.statusCode).toBe(200);
360
+ expect(rows[0]!["headline"]).toBe(words(3));
361
+ });
362
+
363
+ it("patch: enforces the site's oneLiner cap too, at its own number", async () => {
364
+ configData = TIGHT_CAPS;
365
+ const draft = await startDraft(app);
366
+
367
+ const over = await patch(app, draft, { oneLiner: words(6) });
368
+ const at = await patch(app, draft, { oneLiner: words(5) });
369
+
370
+ expect(over.statusCode).toBe(400);
371
+ expect(messageOf(over)).toBe("oneLiner must be 5 words or fewer");
372
+ expect(at.statusCode).toBe(200);
373
+ expect(rows[0]!["oneLiner"]).toBe(words(5));
374
+ });
375
+
376
+ it("patch: a site with NO config row keeps the previous ten-word headline behaviour", async () => {
377
+ // The defaults reproduce #5036's constants exactly, so a site that says
378
+ // nothing sees no change — which is the whole claim D6 makes about
379
+ // turning a constant into configuration.
380
+ configData = null;
381
+ const draft = await startDraft(app);
382
+
383
+ const ten = await patch(app, draft, { headline: words(10) });
384
+ const eleven = await patch(app, draft, { headline: words(11) });
385
+
386
+ expect(ten.statusCode).toBe(200);
387
+ expect(eleven.statusCode).toBe(400);
388
+ expect(messageOf(eleven)).toBe("headline must be 10 words or fewer");
389
+ // The eleventh word never landed on the row.
390
+ expect(rows[0]!["headline"]).toBe(words(10));
391
+ });
392
+
393
+ it("patch: a site with NO config row keeps the previous twenty-five-word oneLiner behaviour", async () => {
394
+ configData = null;
395
+ const draft = await startDraft(app);
396
+
397
+ const twentyFive = await patch(app, draft, { oneLiner: words(25) });
398
+ const twentySix = await patch(app, draft, { oneLiner: words(26) });
399
+
400
+ expect(twentyFive.statusCode).toBe(200);
401
+ expect(twentySix.statusCode).toBe(400);
402
+ expect(messageOf(twentySix)).toBe("oneLiner must be 25 words or fewer");
403
+ });
404
+ });
405
+
406
+ // ── D5 — completeness derives from the collected-field set ──
407
+
408
+ describe("derived completeness (D5)", () => {
409
+ it("submit: a site that does not collect `address` accepts a draft that has none", async () => {
410
+ // THE regression guard. #5036's fixed schema required `address` of every
411
+ // site; a site whose step set never asks for one would have its founders
412
+ // refused at submit, naming a question they were never shown.
413
+ configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
414
+ const draft = await startDraft(app);
415
+ await patch(app, draft, APPLICATION_WITHOUT_ADDRESS);
416
+
417
+ const res = await submit(app, draft);
418
+
419
+ expect(res.statusCode).toBe(200);
420
+ expect(res.json()).toEqual({ ok: true, data: { id: draft.id, slug: "my-project" } });
421
+ expect(rows[0]!["status"]).toBe("requested");
422
+ // Never collected, so never stored — not silently defaulted to something.
423
+ expect(rows[0]!["address"]).toBeNull();
424
+ });
425
+
426
+ it("submit: the DEFAULT config still refuses that same draft, naming `address`", async () => {
427
+ // The other half: the field is only optional because THIS SITE said so.
428
+ configData = null;
429
+ const draft = await startDraft(app);
430
+ await patch(app, draft, APPLICATION_WITHOUT_ADDRESS);
431
+
432
+ const res = await submit(app, draft);
433
+
434
+ expect(res.statusCode).toBe(422);
435
+ const body = res.json() as { error: { code: string; missingFields: string[] } };
436
+ expect(body.error.code).toBe("incomplete_application");
437
+ expect(body.error.missingFields).toEqual(["address"]);
438
+ expect(rows[0]!["status"]).toBe("draft");
439
+ });
440
+
441
+ it("patch: REFUSES a write to a field the site does not collect", async () => {
442
+ // The collected set is a WRITE gate, not only a completeness floor.
443
+ // `address` rides the public read by design (#5036 D6), so a site whose
444
+ // step set omits the address step must not have one written into a draft
445
+ // by an anonymous caller who knows the field name — and then published on
446
+ // its listing page. Narrowing `submit` alone left that open, because a
447
+ // value already on the row is simply carried.
448
+ configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
449
+ const draft = await startDraft(app);
450
+
451
+ const res = await patch(app, draft, { address: COMPLETE_APPLICATION.address });
452
+
453
+ expect(res.statusCode).toBe(400);
454
+ expect(rows[0]!["address"]).toBeNull();
455
+ });
456
+
457
+ it("submit: a value ALREADY on the row for an uncollected field does not block submit", async () => {
458
+ // The other direction, and the reason `buildProjectListingSubmitSchema`
459
+ // DROPS an uncollected field rather than making it optional: a site that
460
+ // narrows its step set after a draft was opened must not strand that
461
+ // draft. The write gate above stops NEW writes; this stops an existing
462
+ // value from failing `.strict()`.
463
+ configData = null;
464
+ const draft = await startDraft(app);
465
+ await patch(app, draft, COMPLETE_APPLICATION); // address included
466
+
467
+ configData = { collectedFields: COLLECTED_WITHOUT_ADDRESS };
468
+ const res = await submit(app, draft);
469
+
470
+ expect(res.statusCode).toBe(200);
471
+ expect(rows[0]!["status"]).toBe("requested");
472
+ expect(rows[0]!["address"]).toBe(COMPLETE_APPLICATION.address);
473
+ });
474
+
475
+ it("submit: a collected-but-OPTIONAL field does not block submit when absent", async () => {
476
+ configData = null;
477
+ // These three are collected by default and still optional — the
478
+ // distinction the missing-field list turns on.
479
+ for (const field of ["url", "anythingElse", "founderCompany"]) {
480
+ expect(SITE_LISTING_CONFIG_DEFAULTS.collectedFields).toContain(field);
481
+ }
482
+
483
+ const draft = await startDraft(app);
484
+ await patch(app, draft, COMPLETE_APPLICATION); // carries none of the three
485
+
486
+ const res = await submit(app, draft);
487
+
488
+ expect(res.statusCode).toBe(200);
489
+ expect(rows[0]!["url"]).toBeNull();
490
+ expect(rows[0]!["anythingElse"]).toBeNull();
491
+ expect(rows[0]!["founderCompany"]).toBeNull();
492
+ });
493
+ });
494
+
495
+ // ── D7 — the controls that deliberately do not ship ──
496
+
497
+ describe("no anti-automation control ships (D7)", () => {
498
+ const read = (relative: string) =>
499
+ readFileSync(join(__dirname, "..", "..", "..", relative), "utf8");
500
+
501
+ const SURFACE_FILES = [
502
+ "routes/project-listings/start.ts",
503
+ "routes/project-listings/patch-draft.ts",
504
+ "routes/project-listings/public.ts",
505
+ "routes/project-listings/submit.ts",
506
+ "services/project-listings.ts",
507
+ ];
508
+
509
+ it("no honeypot field exists in the payload the surface accepts", async () => {
510
+ // Asserted on the SCHEMAS, not on the prose: a decoy field would have to
511
+ // be accepted somewhere to be a honeypot at all.
512
+ const fields = [
513
+ ...Object.keys(projectListingStartSchema.shape),
514
+ ...Object.keys(projectListingApplicationFieldsSchema.shape),
515
+ ];
516
+ for (const field of fields) {
517
+ expect(field).not.toMatch(/honey|_?trap|decoy|confirmEmail|website2/i);
518
+ }
519
+ // And nothing on the surface names one.
520
+ for (const file of SURFACE_FILES) {
521
+ expect(read(file)).not.toMatch(/honeypot/i);
522
+ }
523
+ });
524
+
525
+ it("no timing heuristic gates the start-to-submit interval", async () => {
526
+ // #5039 deliberately opens the agent channel a "too fast to be human"
527
+ // check would close, and an agent completes a ten-step form in
528
+ // milliseconds. A named heuristic anywhere on the surface is the
529
+ // regression; so is `submit` reaching for a clock to compare the draft's
530
+ // own `createdAt` against.
531
+ for (const file of SURFACE_FILES) {
532
+ expect(read(file)).not.toMatch(
533
+ /tooFast|too_fast|minElapsed|minSeconds|minDuration|dwellTime|timeOnPage|humanTiming/i,
534
+ );
535
+ }
536
+
537
+ const service = read("services/project-listings.ts");
538
+ const submitBody = service.slice(
539
+ service.indexOf("export async function submitProjectListingDraft"),
540
+ );
541
+ const submitOnly = submitBody.slice(0, submitBody.indexOf("\nfunction isUniqueViolation"));
542
+ expect(submitOnly).not.toContain("createdAt");
543
+ expect(submitOnly).not.toContain("Date.now(");
544
+ });
545
+
546
+ it("an application completed in the same millisecond as its draft still submits", async () => {
547
+ // The behavioural half: nothing about how fast the eleven fields arrived
548
+ // is consulted. This whole test runs inside one tick.
549
+ configData = null;
550
+ const draft = await startDraft(app);
551
+ await patch(app, draft, COMPLETE_APPLICATION);
552
+
553
+ const res = await submit(app, draft);
554
+
555
+ expect(res.statusCode).toBe(200);
556
+ });
557
+ });
558
+ });