create-ailk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +12 -5
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +78 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -0,0 +1,707 @@
1
+ /**
2
+ * @file site-answers.test.ts
3
+ *
4
+ * THE ACCEPTANCE GATE for the anonymous-write bounds on the site-answers
5
+ * store (#5285), and for the three contract fields that became writable
6
+ * beside it.
7
+ *
8
+ * The store is what lets a site ask a question the platform does not read —
9
+ * no column, no migration, no release. The write path is ANONYMOUS: a resume
10
+ * token and nothing else. So every bound on it is read from the site's
11
+ * SERVER-side `SiteListingConfig`, exactly as `wordCaps` and `collectedFields`
12
+ * already are, and the tests below pin each bound to the SITE's number so a
13
+ * cap that came from the package could not pass by coincidence.
14
+ *
15
+ * The tests that would fail QUIETLY, and are the reason this file exists:
16
+ *
17
+ * - an answer for an undeclared id must 400 with NOTHING written. Dropping
18
+ * the unknown key instead is indistinguishable, from the client's side,
19
+ * from a successful save — and hands the size of the store to whoever is
20
+ * posting into it. Asserted on the ROW, not only on the status code.
21
+ * - an `operator-only` answer must be absent from the PUBLIC read. A
22
+ * visibility flag that is declared and never consulted looks identical to
23
+ * one that is, right up until a founder's private answer is on a page.
24
+ * - `requestedWindowDays` must be CLAMPED and not stored as posted. The
25
+ * site's maximum is its commercial terms; a founder who could post their
26
+ * own bound could post an unbounded one.
27
+ * - adding three optional fields widens the DEFAULT `collectedFields`
28
+ * (it derives from the application schema's own shape), so an existing
29
+ * site's submit floor is pinned as unchanged.
30
+ *
31
+ * Same harness as `configured-application.test.ts`: real routes, real
32
+ * services, mocked `lib/prisma.js` over a small in-memory row store.
33
+ */
34
+
35
+ // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock
36
+ jest.mock("../../../lib/prisma.js");
37
+
38
+ jest.mock("@working-theory/email", () => ({
39
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
40
+ }));
41
+
42
+ jest.mock("@working-theory/license", () => ({
43
+ checkLicense: jest.fn().mockResolvedValue({
44
+ tier: "free",
45
+ plan: null,
46
+ features: [],
47
+ expiresAt: null,
48
+ revoked: false,
49
+ }),
50
+ }));
51
+
52
+ import Fastify from "fastify";
53
+ import type { FastifyInstance } from "fastify";
54
+ import { SITE_LISTING_CONFIG_DEFAULTS } from "@working-theory/validation";
55
+
56
+ import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
57
+ import { prisma } from "../../../lib/prisma.js";
58
+ import { listProjectListings } from "../../../services/project-listings.js";
59
+ import { patchProjectListingDraftRoute } from "../patch-draft.js";
60
+ import { publicProjectListingsRoute } from "../public.js";
61
+ import { startProjectListingRoute } from "../start.js";
62
+ import { submitProjectListingRoute } from "../submit.js";
63
+
64
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
65
+
66
+ const SITE_ID = "site-1";
67
+ const SITE_KEY = "pk-site-1";
68
+
69
+ const opt = (value: string, label: string) => ({ value, label });
70
+
71
+ const VALID_QUESTIONS = {
72
+ q2: {
73
+ prompt: "Why are you here?",
74
+ options: [opt("scale", "Scaling my team"), opt("just-curious", "Just curious")],
75
+ },
76
+ q3: { prompt: "Stage?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
77
+ q4: { prompt: "Team size?", options: [opt("a", "A"), opt("b", "B"), opt("c", "C")] },
78
+ q5: {
79
+ prompt: "Urgency?",
80
+ options: [opt("urgent", "Now"), opt("planning", "Soon"), opt("exploring", "Someday")],
81
+ },
82
+ q6: {
83
+ prompt: "Pain?",
84
+ options: [
85
+ opt("low", "Low"),
86
+ opt("medium", "Medium"),
87
+ opt("high", "High"),
88
+ opt("critical", "Critical"),
89
+ ],
90
+ },
91
+ q7: { prompt: "Anything else?" },
92
+ extras: [],
93
+ };
94
+
95
+ const START_PAYLOAD = {
96
+ siteKey: SITE_KEY,
97
+ founderName: "Ada Founder",
98
+ founderEmail: "ada@example.com",
99
+ consent: true,
100
+ };
101
+
102
+ /** Everything the screens after step one collect, under the default config. */
103
+ const COMPLETE_APPLICATION = {
104
+ name: "My Project",
105
+ headline: "Ten words or fewer for the tile title here",
106
+ oneLiner: "A short sentence describing what the project does for its people.",
107
+ problem: "The problem is real and this describes it.",
108
+ product: "wireframes",
109
+ commitment: "part-time",
110
+ teamComposition: "co-founders",
111
+ faqs: [
112
+ { question: "Q one?", answer: "A one." },
113
+ { question: "Q two?", answer: "A two." },
114
+ { question: "Q three?", answer: "A three." },
115
+ ],
116
+ address: "1 Founder Way, Springfield",
117
+ questions: VALID_QUESTIONS,
118
+ };
119
+
120
+ /**
121
+ * The `collectedFields` an operator wrote BEFORE #5285 — the roster as it
122
+ * stood, with none of the three new fields in it. A stored list like this is
123
+ * what every configured site is actually running on.
124
+ */
125
+ /**
126
+ * A site that DELIBERATELY asks for the three #5285 fields. They are opt-in:
127
+ * excluded from the platform's default mask so that adding them to the payload
128
+ * could not silently widen what an anonymous caller may write on a site that
129
+ * never asked. Every round-trip test below therefore configures this rather
130
+ * than leaning on the default.
131
+ */
132
+ const COLLECTED_WITH_OPT_IN = [
133
+ ...SITE_LISTING_CONFIG_DEFAULTS.collectedFields,
134
+ "logoMarkUrl",
135
+ "logoWordmarkUrl",
136
+ "requestedWindowDays",
137
+ ];
138
+
139
+ const COLLECTED_BEFORE_5285 = [
140
+ "name",
141
+ "headline",
142
+ "oneLiner",
143
+ "problem",
144
+ "url",
145
+ "product",
146
+ "commitment",
147
+ "teamComposition",
148
+ "faqs",
149
+ "anythingElse",
150
+ "address",
151
+ "founderCompany",
152
+ "questions",
153
+ ];
154
+
155
+ /** A site that asks four questions of its own, one of them for itself only. */
156
+ const SITE_ANSWERS_CONFIG = {
157
+ maxQuestions: 3,
158
+ maxAnswerLength: 40,
159
+ questions: [
160
+ { id: "why_now", control: "textarea", maxLength: 10 },
161
+ { id: "referrer", control: "select", options: ["twitter", "friend"] },
162
+ { id: "demo_url", control: "url" },
163
+ { id: "billing_email", control: "email", visibility: "operator-only" },
164
+ ],
165
+ };
166
+
167
+ // ─── The in-memory row store the Prisma mock is backed by ───────────────────
168
+
169
+ type Row = Record<string, unknown>;
170
+
171
+ let rows: Row[];
172
+ let nextId: number;
173
+ /** The `SiteListingConfig.data` blob this site runs on; `null` means no row. */
174
+ let configData: Record<string, unknown> | null;
175
+
176
+ function matches(row: Row, where: Record<string, unknown>): boolean {
177
+ return Object.entries(where).every(([key, value]) => {
178
+ if (key === "OR") {
179
+ return (value as Record<string, unknown>[]).some((clause) => matches(row, clause));
180
+ }
181
+ if (value !== null && typeof value === "object" && !(value instanceof Date)) {
182
+ const clause = value as Record<string, unknown>;
183
+ if ("in" in clause) return (clause["in"] as unknown[]).includes(row[key]);
184
+ if ("not" in clause) return row[key] !== clause["not"];
185
+ if ("gt" in clause) return (row[key] as Date) > (clause["gt"] as Date);
186
+ if ("startsWith" in clause) {
187
+ return (
188
+ typeof row[key] === "string" &&
189
+ (row[key] as string).startsWith(clause["startsWith"] as string)
190
+ );
191
+ }
192
+ }
193
+ return row[key] === value;
194
+ });
195
+ }
196
+
197
+ function installStore(): void {
198
+ rows = [];
199
+ nextId = 1;
200
+ configData = null;
201
+
202
+ (prisma.lead.create as jest.Mock).mockImplementation(() =>
203
+ Promise.resolve({ id: `lead_00000000-0000-7000-8000-00000000000${nextId}` }),
204
+ );
205
+
206
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockImplementation(
207
+ ({ where }: { where: { siteId?: string; publicKey?: string } }) => {
208
+ if (where.publicKey !== undefined) {
209
+ return Promise.resolve(where.publicKey === SITE_KEY ? { siteId: SITE_ID } : null);
210
+ }
211
+ return Promise.resolve(configData === null ? null : { data: configData });
212
+ },
213
+ );
214
+
215
+ (prisma.consentGrant.createMany as jest.Mock).mockImplementation(
216
+ ({ data }: { data: Row[] }) => Promise.resolve({ count: data.length }),
217
+ );
218
+
219
+ (prisma.$transaction as jest.Mock).mockImplementation(
220
+ async (fn: (tx: typeof prisma) => Promise<unknown>) => fn(prisma),
221
+ );
222
+
223
+ (prisma.projectListing.create as jest.Mock).mockImplementation(({ data }: { data: Row }) => {
224
+ const now = new Date();
225
+ const row: Row = {
226
+ id: `prjl_00000000-0000-7000-8000-00000000000${nextId++}`,
227
+ slug: null,
228
+ name: null,
229
+ oneLiner: null,
230
+ problem: null,
231
+ url: null,
232
+ product: null,
233
+ commitment: null,
234
+ teamComposition: null,
235
+ faqs: null,
236
+ anythingElse: null,
237
+ address: null,
238
+ addressLine1: null,
239
+ addressLine2: null,
240
+ addressCity: null,
241
+ addressRegion: null,
242
+ addressPostalCode: null,
243
+ addressCountry: null,
244
+ questions: null,
245
+ siteAnswers: null,
246
+ founderCompany: null,
247
+ leadId: null,
248
+ headline: null,
249
+ logoMarkUrl: null,
250
+ logoWordmarkUrl: null,
251
+ featured: false,
252
+ decidedAt: null,
253
+ decidedBy: null,
254
+ issueUrl: null,
255
+ experimentKey: null,
256
+ windowDays: null,
257
+ windowEndsAt: null,
258
+ createdAt: now,
259
+ updatedAt: now,
260
+ ...data,
261
+ };
262
+ rows.push(row);
263
+ return Promise.resolve(row);
264
+ });
265
+
266
+ (prisma.projectListing.findFirst as jest.Mock).mockImplementation(
267
+ ({ where }: { where: Record<string, unknown> }) =>
268
+ Promise.resolve(rows.find((row) => matches(row, where)) ?? null),
269
+ );
270
+
271
+ (prisma.projectListing.findMany as jest.Mock).mockImplementation(
272
+ ({ where }: { where: Record<string, unknown> }) =>
273
+ Promise.resolve(rows.filter((row) => matches(row, where))),
274
+ );
275
+
276
+ (prisma.projectListing.update as jest.Mock).mockImplementation(
277
+ ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
278
+ const row = rows.find((r) => matches(r, where));
279
+ Object.assign(row!, data, { updatedAt: new Date() });
280
+ return Promise.resolve(row);
281
+ },
282
+ );
283
+
284
+ (prisma.projectListing.updateMany as jest.Mock).mockImplementation(
285
+ ({ where, data }: { where: Record<string, unknown>; data: Row }) => {
286
+ const matched = rows.filter((r) => matches(r, where));
287
+ matched.forEach((r) => Object.assign(r, data, { updatedAt: new Date() }));
288
+ return Promise.resolve({ count: matched.length });
289
+ },
290
+ );
291
+ }
292
+
293
+ // ─── Helpers ────────────────────────────────────────────────────────────────
294
+
295
+ const bearer = (token: string) => ({ authorization: `Bearer ${token}` });
296
+
297
+ async function buildApp(): Promise<FastifyInstance> {
298
+ const app = Fastify();
299
+ await app.register(startProjectListingRoute);
300
+ await app.register(patchProjectListingDraftRoute);
301
+ await app.register(submitProjectListingRoute);
302
+ await app.register(publicProjectListingsRoute);
303
+ await app.ready();
304
+ return app;
305
+ }
306
+
307
+ async function startDraft(
308
+ app: FastifyInstance,
309
+ ): Promise<{ id: string; resumeToken: string }> {
310
+ const res = await app.inject({
311
+ method: "POST",
312
+ url: "/v1/project-listings",
313
+ payload: START_PAYLOAD,
314
+ });
315
+ expect(res.statusCode).toBe(201);
316
+ return (res.json() as { data: { id: string; resumeToken: string } }).data;
317
+ }
318
+
319
+ const patch = (
320
+ app: FastifyInstance,
321
+ draft: { id: string; resumeToken: string },
322
+ payload: Record<string, unknown>,
323
+ ) =>
324
+ app.inject({
325
+ method: "PATCH",
326
+ url: `/v1/project-listings/drafts/${draft.id}`,
327
+ headers: bearer(draft.resumeToken),
328
+ payload,
329
+ });
330
+
331
+ const submit = (app: FastifyInstance, draft: { id: string; resumeToken: string }) =>
332
+ app.inject({
333
+ method: "POST",
334
+ url: `/v1/project-listings/drafts/${draft.id}/submit`,
335
+ headers: bearer(draft.resumeToken),
336
+ });
337
+
338
+ const publicRead = (app: FastifyInstance) =>
339
+ app.inject({ method: "GET", url: `/v1/project-listings/public?siteKey=${SITE_KEY}` });
340
+
341
+ const messageOf = (res: { json: () => unknown }) =>
342
+ (res.json() as { error: { message: string } }).error.message;
343
+
344
+ /** Approve the one row in the store, so it reaches the public read. */
345
+ function publish(): void {
346
+ Object.assign(rows[0]!, {
347
+ status: "active",
348
+ decidedAt: new Date(),
349
+ windowEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
350
+ });
351
+ }
352
+
353
+ // ─── Tests ──────────────────────────────────────────────────────────────────
354
+
355
+ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
356
+ setupLeadFileEnv("project-listing-site-answers");
357
+ let app: FastifyInstance;
358
+
359
+ beforeEach(async () => {
360
+ jest.clearAllMocks();
361
+ installStore();
362
+ app = await buildApp();
363
+ });
364
+
365
+ afterEach(async () => {
366
+ await app.close();
367
+ });
368
+
369
+ // ── The declaration is the gate ──
370
+
371
+ describe("the declaration list is the gate", () => {
372
+ it("patch: refuses an answer for an id the site did NOT declare, and writes NOTHING", async () => {
373
+ // THE property of this change. A dropped key looks like a saved key to
374
+ // the client that sent it, and would leave the size of this store in the
375
+ // hands of whoever is posting into it.
376
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
377
+ const draft = await startDraft(app);
378
+
379
+ const res = await patch(app, draft, {
380
+ siteAnswers: { why_now: "June", what_colour: "blue" },
381
+ });
382
+
383
+ expect(res.statusCode).toBe(400);
384
+ expect(messageOf(res)).toContain("what_colour");
385
+ // The declared answer beside it did not land either — the body is
386
+ // refused whole, not per key.
387
+ expect(rows[0]!["siteAnswers"]).toBeNull();
388
+ });
389
+
390
+ it("patch: a site that declares NOTHING accepts no site answer at all", async () => {
391
+ // The default, and what every unconfigured site runs on: the store
392
+ // cannot be written by a caller who merely knows the field name.
393
+ configData = null;
394
+ const draft = await startDraft(app);
395
+
396
+ const res = await patch(app, draft, { siteAnswers: { why_now: "June" } });
397
+
398
+ expect(res.statusCode).toBe(400);
399
+ expect(rows[0]!["siteAnswers"]).toBeNull();
400
+ });
401
+
402
+ it("patch: stores a declared answer", async () => {
403
+ // The other half of every pair above — without it, a surface that
404
+ // refused everything would pass them all.
405
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
406
+ const draft = await startDraft(app);
407
+
408
+ const res = await patch(app, draft, { siteAnswers: { why_now: "June" } });
409
+
410
+ expect(res.statusCode).toBe(200);
411
+ expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "June" });
412
+ });
413
+
414
+ it("patch: MERGES answers rather than replacing them — a founder saves one screen at a time", async () => {
415
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
416
+ const draft = await startDraft(app);
417
+
418
+ await patch(app, draft, { siteAnswers: { why_now: "June" } });
419
+ const res = await patch(app, draft, { siteAnswers: { referrer: "friend" } });
420
+
421
+ expect(res.statusCode).toBe(200);
422
+ expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "June", referrer: "friend" });
423
+ });
424
+ });
425
+
426
+ // ── The caps are the site's own numbers ──
427
+
428
+ describe("the caps come from the site's config", () => {
429
+ it("patch: refuses more answers than the site's maxQuestions, naming the site's number", async () => {
430
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
431
+ const draft = await startDraft(app);
432
+
433
+ const res = await patch(app, draft, {
434
+ siteAnswers: {
435
+ why_now: "June",
436
+ referrer: "friend",
437
+ demo_url: "https://example.com",
438
+ billing_email: "ada@example.com",
439
+ },
440
+ });
441
+
442
+ expect(res.statusCode).toBe(400);
443
+ // Pinned to the SITE's three. A message naming the platform's twenty
444
+ // would mean the cap never came from this site's row.
445
+ expect(messageOf(res)).toContain("at most 3");
446
+ expect(rows[0]!["siteAnswers"]).toBeNull();
447
+ });
448
+
449
+ it("patch: refuses an answer longer than the site's per-question cap", async () => {
450
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
451
+ const draft = await startDraft(app);
452
+
453
+ const over = await patch(app, draft, { siteAnswers: { why_now: "x".repeat(11) } });
454
+ const at = await patch(app, draft, { siteAnswers: { why_now: "x".repeat(10) } });
455
+
456
+ expect(over.statusCode).toBe(400);
457
+ expect(messageOf(over)).toContain("10 characters");
458
+ expect(at.statusCode).toBe(200);
459
+ expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "x".repeat(10) });
460
+ });
461
+
462
+ it("patch: refuses a select answer outside the options the site declared", async () => {
463
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
464
+ const draft = await startDraft(app);
465
+
466
+ const outside = await patch(app, draft, { siteAnswers: { referrer: "billboard" } });
467
+ const inside = await patch(app, draft, { siteAnswers: { referrer: "twitter" } });
468
+
469
+ expect(outside.statusCode).toBe(400);
470
+ expect(messageOf(outside)).toContain("billboard");
471
+ expect(inside.statusCode).toBe(200);
472
+ expect(rows[0]!["siteAnswers"]).toEqual({ referrer: "twitter" });
473
+ });
474
+
475
+ it("patch: enforces the declared control — a url answer must be http(s)", async () => {
476
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
477
+ const draft = await startDraft(app);
478
+
479
+ const bad = await patch(app, draft, { siteAnswers: { demo_url: "javascript:alert(1)" } });
480
+
481
+ expect(bad.statusCode).toBe(400);
482
+ expect(rows[0]!["siteAnswers"]).toBeNull();
483
+ });
484
+ });
485
+
486
+ // ── Visibility ──
487
+
488
+ describe("operator-only answers never reach the public read", () => {
489
+ it("the public read carries the public answer and NOT the operator-only one", async () => {
490
+ // A visibility flag that is declared and never consulted looks exactly
491
+ // like one that is — until a founder's private answer is on a page.
492
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
493
+ const draft = await startDraft(app);
494
+ await patch(app, draft, { siteAnswers: { why_now: "June" } });
495
+ await patch(app, draft, { siteAnswers: { billing_email: "ada@example.com" } });
496
+ await patch(app, draft, COMPLETE_APPLICATION);
497
+ await submit(app, draft);
498
+ publish();
499
+
500
+ const res = await publicRead(app);
501
+
502
+ expect(res.statusCode).toBe(200);
503
+ const [listing] = (res.json() as { data: { siteAnswers: Record<string, string> }[] }).data;
504
+ expect(listing!.siteAnswers).toEqual({ why_now: "June" });
505
+ expect(listing!.siteAnswers).not.toHaveProperty("billing_email");
506
+ // And the value itself is nowhere in the serialized response at all.
507
+ expect(res.payload).not.toContain("ada@example.com");
508
+ });
509
+
510
+ it("the tenant dashboard read carries BOTH — it is the audience operator-only exists for", async () => {
511
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
512
+ const draft = await startDraft(app);
513
+ await patch(app, draft, { siteAnswers: { why_now: "June" } });
514
+ await patch(app, draft, { siteAnswers: { billing_email: "ada@example.com" } });
515
+
516
+ const [listing] = await listProjectListings(SITE_ID);
517
+
518
+ expect(listing!.siteAnswers).toEqual({
519
+ why_now: "June",
520
+ billing_email: "ada@example.com",
521
+ });
522
+ });
523
+
524
+ it("an answer whose id the site no longer declares is dropped from the public read", async () => {
525
+ // "No declaration" carries no visibility, and no visibility cannot be
526
+ // read as public on the read that publishes.
527
+ configData = { siteAnswers: SITE_ANSWERS_CONFIG };
528
+ const draft = await startDraft(app);
529
+ await patch(app, draft, { siteAnswers: { why_now: "June" } });
530
+ await patch(app, draft, COMPLETE_APPLICATION);
531
+ await submit(app, draft);
532
+ publish();
533
+
534
+ // The operator retires the question after the answer was written.
535
+ configData = { siteAnswers: { ...SITE_ANSWERS_CONFIG, questions: [] } };
536
+ const res = await publicRead(app);
537
+
538
+ const [listing] = (res.json() as { data: { siteAnswers: Record<string, string> }[] }).data;
539
+ expect(listing!.siteAnswers).toEqual({});
540
+ // Still on the row — the founder's answer is not destroyed, only unpublished.
541
+ expect(rows[0]!["siteAnswers"]).toEqual({ why_now: "June" });
542
+ });
543
+ });
544
+
545
+ // ── The three contract fields ──
546
+
547
+ describe("the three contract fields that became writable", () => {
548
+ it("patch: the two logo urls round-trip onto the row, on a site that asked", async () => {
549
+ configData = { collectedFields: COLLECTED_WITH_OPT_IN };
550
+ const draft = await startDraft(app);
551
+
552
+ const res = await patch(app, draft, {
553
+ logoMarkUrl: "https://cdn.example.com/mark.svg",
554
+ logoWordmarkUrl: "https://cdn.example.com/wordmark.svg",
555
+ });
556
+
557
+ expect(res.statusCode).toBe(200);
558
+ expect(rows[0]!["logoMarkUrl"]).toBe("https://cdn.example.com/mark.svg");
559
+ expect(rows[0]!["logoWordmarkUrl"]).toBe("https://cdn.example.com/wordmark.svg");
560
+ });
561
+
562
+ it("patch: REFUSES all three on a site that never opted in, writing nothing", async () => {
563
+ // The defect the back-compat review found, at the route. `collectedFields`
564
+ // defaults to every field the payload defines, so adding these three
565
+ // silently made them writable on every site that had configured nothing —
566
+ // and the logo fields ride the PUBLIC read as image sources. An anonymous
567
+ // caller holding a resume token could have attached a third-party image
568
+ // to the public listing card of a site that never asked for a logo.
569
+ configData = null;
570
+ const draft = await startDraft(app);
571
+
572
+ for (const body of [
573
+ { logoMarkUrl: "https://evil.example/a.png" },
574
+ { logoWordmarkUrl: "https://evil.example/b.svg" },
575
+ { requestedWindowDays: 9000 },
576
+ ]) {
577
+ expect((await patch(app, draft, body)).statusCode).toBe(400);
578
+ }
579
+
580
+ expect(rows[0]!["logoMarkUrl"]).toBeNull();
581
+ expect(rows[0]!["logoWordmarkUrl"]).toBeNull();
582
+ });
583
+
584
+ it("patch: refuses a logo url that is not http(s) — they render as image sources", async () => {
585
+ configData = { collectedFields: COLLECTED_WITH_OPT_IN };
586
+ const draft = await startDraft(app);
587
+
588
+ const res = await patch(app, draft, { logoMarkUrl: "javascript:alert(1)" });
589
+
590
+ expect(res.statusCode).toBe(400);
591
+ expect(rows[0]!["logoMarkUrl"]).toBeNull();
592
+ });
593
+
594
+ it("patch: CLAMPS requestedWindowDays to the site's maximum rather than storing what was posted", async () => {
595
+ // The one that would fail silently and expensively: an unclamped value
596
+ // is a founder setting their own commercial terms. The three numbers are
597
+ // deliberately distinct — 14 (the site's default, written at start), 90
598
+ // (what was asked for) and 45 (the site's maximum, what must be stored).
599
+ configData = {
600
+ maxWindowDays: 45,
601
+ defaultWindowDays: 14,
602
+ collectedFields: COLLECTED_WITH_OPT_IN,
603
+ };
604
+ const draft = await startDraft(app);
605
+ expect(rows[0]!["windowDays"]).toBe(14);
606
+
607
+ const res = await patch(app, draft, { requestedWindowDays: "90" });
608
+
609
+ expect(res.statusCode).toBe(200);
610
+ expect(rows[0]!["windowDays"]).toBe(45);
611
+ });
612
+
613
+ it("patch: stores a requested window BELOW the site's maximum as asked", async () => {
614
+ configData = {
615
+ maxWindowDays: 45,
616
+ defaultWindowDays: 14,
617
+ collectedFields: COLLECTED_WITH_OPT_IN,
618
+ };
619
+ const draft = await startDraft(app);
620
+
621
+ await patch(app, draft, { requestedWindowDays: 21 });
622
+
623
+ expect(rows[0]!["windowDays"]).toBe(21);
624
+ });
625
+
626
+ it("patch: refuses a window that is not a positive whole number of days", async () => {
627
+ configData = {
628
+ maxWindowDays: 45,
629
+ defaultWindowDays: 14,
630
+ collectedFields: COLLECTED_WITH_OPT_IN,
631
+ };
632
+ const draft = await startDraft(app);
633
+
634
+ expect((await patch(app, draft, { requestedWindowDays: 0 })).statusCode).toBe(400);
635
+ expect((await patch(app, draft, { requestedWindowDays: -7 })).statusCode).toBe(400);
636
+ expect((await patch(app, draft, { requestedWindowDays: "soon" })).statusCode).toBe(400);
637
+ expect(rows[0]!["windowDays"]).toBe(14);
638
+ });
639
+ });
640
+
641
+ // ── The floor the three new fields must not move ──
642
+
643
+ describe("submit requirements are unchanged by the three added fields", () => {
644
+ it("the three are OPT-IN, absent from the default collected set, and never required at submit", async () => {
645
+ // This assertion was inverted by the back-compat review. The default
646
+ // `collectedFields` derives from the application schema's own shape, so
647
+ // adding a field WIDENED what an anonymous caller could write on every
648
+ // site that never wrote an explicit mask — and the two logo fields ride
649
+ // the public read and render as image sources. They are excluded from
650
+ // the default now; a site that wants them names them.
651
+ for (const field of ["logoMarkUrl", "logoWordmarkUrl", "requestedWindowDays"]) {
652
+ expect(SITE_LISTING_CONFIG_DEFAULTS.collectedFields).not.toContain(field);
653
+ }
654
+
655
+ configData = null;
656
+ const draft = await startDraft(app);
657
+ await patch(app, draft, COMPLETE_APPLICATION); // carries none of the three
658
+
659
+ const res = await submit(app, draft);
660
+
661
+ expect(res.statusCode).toBe(200);
662
+ expect(rows[0]!["status"]).toBe("requested");
663
+ expect(rows[0]!["logoMarkUrl"]).toBeNull();
664
+ expect(rows[0]!["logoWordmarkUrl"]).toBeNull();
665
+ });
666
+
667
+ it("a site whose stored collectedFields predates #5285 submits exactly as it did", async () => {
668
+ // What every configured site is actually running on: a list written
669
+ // before these three existed. It must keep working untouched.
670
+ configData = { collectedFields: COLLECTED_BEFORE_5285 };
671
+ const draft = await startDraft(app);
672
+ await patch(app, draft, COMPLETE_APPLICATION);
673
+
674
+ const res = await submit(app, draft);
675
+
676
+ expect(res.statusCode).toBe(200);
677
+ expect(rows[0]!["status"]).toBe("requested");
678
+ });
679
+
680
+ it("a site whose stored collectedFields predates #5285 cannot have a logo written into it", async () => {
681
+ // The collected set is a WRITE gate as well as a completeness floor
682
+ // (#5048 D5): a site that never asks for a logo must not have one
683
+ // written by an anonymous caller who knows the field name.
684
+ configData = { collectedFields: COLLECTED_BEFORE_5285 };
685
+ const draft = await startDraft(app);
686
+
687
+ const res = await patch(app, draft, { logoMarkUrl: "https://cdn.example.com/m.svg" });
688
+
689
+ expect(res.statusCode).toBe(400);
690
+ expect(rows[0]!["logoMarkUrl"]).toBeNull();
691
+ });
692
+
693
+ it("the missing-field list an empty draft is refused with names none of the three", async () => {
694
+ configData = null;
695
+ const draft = await startDraft(app);
696
+ await patch(app, draft, { name: "My Project" });
697
+
698
+ const res = await submit(app, draft);
699
+
700
+ expect(res.statusCode).toBe(422);
701
+ const body = res.json() as { error: { missingFields: string[] } };
702
+ expect(body.error.missingFields).not.toContain("logoMarkUrl");
703
+ expect(body.error.missingFields).not.toContain("logoWordmarkUrl");
704
+ expect(body.error.missingFields).not.toContain("requestedWindowDays");
705
+ });
706
+ });
707
+ });