create-ailk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (605) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +295 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +22 -14
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +75 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +231 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +547 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +742 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +364 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  118. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  119. package/templates/apps/api/src/routes/project-listings/index.ts +105 -0
  120. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  121. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  122. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  123. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  124. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  125. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  126. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  127. package/templates/apps/api/src/routes/project-listings/start.ts +98 -0
  128. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  129. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  130. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  131. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  132. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  133. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  134. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  135. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  136. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  137. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  138. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  139. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  140. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  141. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  143. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  144. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  145. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  146. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  147. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  148. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  149. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  150. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  151. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  152. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  153. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  154. package/templates/apps/api/src/server.ts +293 -7
  155. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  156. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  157. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  158. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  159. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  160. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  161. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  162. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  163. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  164. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  165. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  166. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  167. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  168. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  169. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  170. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  171. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  172. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  173. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  174. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  175. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  176. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  177. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  178. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  179. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  180. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  181. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  182. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  183. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  184. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  185. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  186. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  187. package/templates/apps/api/src/services/entitlements.ts +102 -0
  188. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  189. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  190. package/templates/apps/api/src/services/flow-engine.ts +1278 -0
  191. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  192. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  193. package/templates/apps/api/src/services/listing-config.ts +102 -0
  194. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  195. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  196. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  197. package/templates/apps/api/src/services/project-listing-decision.ts +490 -0
  198. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  199. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  200. package/templates/apps/api/src/services/project-listings.ts +1124 -0
  201. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  202. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  203. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  204. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  205. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  206. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  207. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  208. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  209. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  210. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  211. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  212. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  213. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  214. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  215. package/templates/apps/api/src/vercel-handler.ts +165 -0
  216. package/templates/apps/api/tsconfig.json +0 -12
  217. package/templates/apps/mcp/CLAUDE.md +19 -9
  218. package/templates/apps/mcp/LICENSE +201 -0
  219. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  220. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  221. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  222. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  223. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  224. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  225. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  226. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  227. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  228. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  229. package/templates/apps/mcp/package.json +6 -4
  230. package/templates/apps/mcp/src/config/modules.ts +37 -0
  231. package/templates/apps/mcp/src/server.ts +109 -18
  232. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  233. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  234. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  235. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  236. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  237. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  238. package/templates/apps/mcp/src/tools/index.ts +25 -0
  239. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  240. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  241. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  242. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  243. package/templates/apps/mcp/tsconfig.json +1 -9
  244. package/templates/apps/web/.env.example +44 -0
  245. package/templates/apps/web/CLAUDE.md +28 -0
  246. package/templates/apps/web/LICENSE +201 -0
  247. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  248. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  249. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  250. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  251. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  252. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  253. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  254. package/templates/apps/web/__tests__/setup.ts +22 -32
  255. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  256. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  257. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  258. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  259. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  260. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  261. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  265. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  266. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  267. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  268. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  269. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  270. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  271. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  279. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  280. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  281. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  282. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  283. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  284. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  285. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  286. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  287. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  288. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  289. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  290. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  291. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  292. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  293. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  294. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  295. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  296. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  297. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  298. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  299. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  300. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  301. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  302. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  303. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  304. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  305. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  306. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  307. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  308. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  309. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  310. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  311. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  312. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  313. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  314. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  315. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  316. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  317. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  318. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  323. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  324. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  325. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  326. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  327. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  328. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  329. package/templates/apps/web/app/[locale]/layout.tsx +115 -16
  330. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  331. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  332. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  333. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  334. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  335. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  336. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  337. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  339. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  340. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  341. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  342. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  343. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  344. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  345. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  346. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  347. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  348. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  349. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  350. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  351. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  352. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  353. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  354. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  355. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  356. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  357. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  358. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  359. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  360. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  361. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  362. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  363. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  364. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  365. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  366. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  367. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/globals.css +35 -0
  370. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  371. package/templates/apps/web/app/og/route.tsx +10 -2
  372. package/templates/apps/web/auth.ts +8 -0
  373. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  374. package/templates/apps/web/content/en/blog/README.txt +25 -0
  375. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  376. package/templates/apps/web/jest.config.cjs +84 -27
  377. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  378. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  379. package/templates/apps/web/lib/account-config.ts +13 -0
  380. package/templates/apps/web/lib/server-api.ts +85 -0
  381. package/templates/apps/web/lib/site-brand.tsx +118 -0
  382. package/templates/apps/web/messages/en.json +275 -7
  383. package/templates/apps/web/middleware.ts +11 -3
  384. package/templates/apps/web/next.config.mjs +8 -2
  385. package/templates/apps/web/package.json +16 -7
  386. package/templates/apps/web/postcss.config.mjs +23 -0
  387. package/templates/apps/web/public/.well-known/security.txt +3 -3
  388. package/templates/apps/web/src/lib/routes.ts +15 -2
  389. package/templates/apps/web/tsconfig.json +1 -16
  390. package/templates/content/_site.mdx +24 -25
  391. package/templates/content/en/AboutPage/about.mdx +24 -14
  392. package/templates/content/en/ContactPage/contact.mdx +23 -14
  393. package/templates/content/en/FAQPage/faq.mdx +10 -392
  394. package/templates/content/en/HomePage/home.mdx +14 -285
  395. package/templates/database/CHANGELOG.md +382 -0
  396. package/templates/database/LICENSE +201 -0
  397. package/templates/database/README.md +3 -1
  398. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  399. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  400. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  401. package/templates/database/__tests__/seed.test.ts +88 -0
  402. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  403. package/templates/database/auth/schema.prisma +521 -0
  404. package/templates/database/config/schema.prisma +234 -0
  405. package/templates/database/content/schema.prisma +263 -0
  406. package/templates/database/flows/listing-diagnostic.ts +182 -0
  407. package/templates/database/flows/presets.ts +315 -0
  408. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  409. package/templates/database/inbox/schema.prisma +476 -3
  410. package/templates/database/jest.config.cjs +8 -0
  411. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  412. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  413. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  414. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  415. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  416. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  417. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  418. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  419. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  420. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  421. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  422. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  423. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  424. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  425. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  426. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  427. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  428. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  429. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  430. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  431. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  432. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  433. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  434. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  435. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  436. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  437. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  438. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  439. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  440. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  441. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  442. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  443. package/templates/database/ops/schema.prisma +27 -0
  444. package/templates/database/package.json +42 -4
  445. package/templates/database/prisma.config.ts +1 -1
  446. package/templates/database/schema.prisma +19 -435
  447. package/templates/database/scripts/db-generate-locked.sh +0 -0
  448. package/templates/database/seed-test.ts +14 -4
  449. package/templates/database/seed.ts +148 -0
  450. package/templates/database/tenancy.ts +45 -0
  451. package/templates/database/tsconfig.flows.json +15 -0
  452. package/templates/database/tsconfig.json +7 -1
  453. package/templates/database/tsconfig.tenancy.json +14 -0
  454. package/templates/package.json +30 -55
  455. package/templates/pnpm-workspace.yaml +0 -3
  456. package/templates/project.yaml +1 -1
  457. package/templates/tsconfig.json +1 -37
  458. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  459. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  460. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  461. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  462. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  463. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  464. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  465. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  466. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  467. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  468. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  469. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  470. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  471. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  472. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  473. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  474. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  475. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  476. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  477. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  478. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  479. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  480. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  481. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  482. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  483. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  484. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  485. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  486. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  487. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  488. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  489. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  490. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  491. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  492. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  493. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  494. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  495. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  496. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  497. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  498. package/templates/content/.gitkeep +0 -0
  499. package/templates/content/CLAUDE.md +0 -85
  500. package/templates/content/ar/HomePage/home.mdx +0 -123
  501. package/templates/content/ar/_site.mdx +0 -35
  502. package/templates/content/de/_site.mdx +0 -29
  503. package/templates/content/en/.gitkeep +0 -0
  504. package/templates/content/en/AboutPage/about.schema.json +0 -82
  505. package/templates/content/en/BookCall/book-call.mdx +0 -19
  506. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  507. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  508. package/templates/content/en/CareersList/careers.mdx +0 -24
  509. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  510. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  511. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  512. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  513. package/templates/content/en/ClientsList/clients.mdx +0 -38
  514. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  515. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  516. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  517. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  518. package/templates/content/en/CoursesList/courses.mdx +0 -22
  519. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  520. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  521. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  522. package/templates/content/en/EventsList/events.mdx +0 -26
  523. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  524. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  525. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  526. package/templates/content/en/HomePage/home.schema.json +0 -88
  527. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  528. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  529. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  530. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  531. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  532. package/templates/content/en/Integration/github-actions.mdx +0 -31
  533. package/templates/content/en/Integration/openai-api.mdx +0 -26
  534. package/templates/content/en/Integration/vercel.mdx +0 -32
  535. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  536. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  537. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  538. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  539. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  540. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  541. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  542. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  543. package/templates/content/en/LandingPage/developers.mdx +0 -19
  544. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  545. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  546. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  547. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  548. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  549. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  550. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  551. package/templates/content/en/LandingPage/platform.mdx +0 -369
  552. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  553. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  554. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  555. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  556. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  557. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  558. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  559. package/templates/content/en/Organization/site-owner.mdx +0 -16
  560. package/templates/content/en/Partner/prisma.mdx +0 -31
  561. package/templates/content/en/Partner/supabase.mdx +0 -30
  562. package/templates/content/en/Partner/vercel.mdx +0 -28
  563. package/templates/content/en/PartnersList/partners.mdx +0 -22
  564. package/templates/content/en/Person/founder.mdx +0 -18
  565. package/templates/content/en/Pricing/pricing.mdx +0 -538
  566. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  567. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  568. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  569. package/templates/content/en/Product/ailk.mdx +0 -19
  570. package/templates/content/en/Product/ailk.schema.json +0 -94
  571. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  572. package/templates/content/en/Product/launch-kit.mdx +0 -28
  573. package/templates/content/en/ProductsList/products.mdx +0 -26
  574. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  575. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  576. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  577. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  578. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  579. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  580. package/templates/content/en/Service/consulting.mdx +0 -22
  581. package/templates/content/en/Service/implementation.mdx +0 -30
  582. package/templates/content/en/Service/launch-service.mdx +0 -232
  583. package/templates/content/en/Service/launch.mdx +0 -228
  584. package/templates/content/en/ServicesList/services.mdx +0 -84
  585. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  586. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  587. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  588. package/templates/content/en/TeamList/team.mdx +0 -63
  589. package/templates/content/en/TeamMember/founder.mdx +0 -72
  590. package/templates/content/en/TermsPage/terms.mdx +0 -19
  591. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  592. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  593. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  594. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  595. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  596. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  597. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  598. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  599. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  600. package/templates/content/es/_site.mdx +0 -30
  601. package/templates/content/fr/_site.mdx +0 -30
  602. package/templates/content/ja/_site.mdx +0 -30
  603. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  604. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  605. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Project-listings route group (#4998 — the founder's application row;
3
+ * reshaped by #5036 — the application became a resumable draft).
4
+ *
5
+ * Routes:
6
+ * POST /v1/project-listings — anonymous start (leadsScope)
7
+ * PATCH /v1/project-listings/drafts/:id — anonymous save-as-you-go, token-gated
8
+ * POST /v1/project-listings/drafts/:id/submit — anonymous submit, token-gated
9
+ * GET /v1/project-listings/public — anonymous page-render read (leadsScope)
10
+ * GET /v1/project-listings — tenant-scoped dashboard list
11
+ * GET /v1/project-listings/:id — tenant-scoped single read
12
+ * PATCH /v1/project-listings/:id — tenant-scoped decision + featured flag
13
+ * PATCH /v1/project-listings/:id/copy — tenant-scoped operator copy edit
14
+ *
15
+ * The anonymous set is registered in `server.ts`'s rate-limited `leadsScope`
16
+ * alongside `/v1/leads/*`; the tenant-scoped set is registered inside
17
+ * `tenantScope`, behind `tenantPreHandlerPlugin`, the same as
18
+ * `waitlistExperimentRoutes`. The `/drafts/` path segment is what keeps the
19
+ * anonymous token-gated PATCH and the operator's tenant-guarded PATCH apart
20
+ * at the router — they differ in segment count, so neither shadows the other.
21
+ *
22
+ * API<->MCP parity: no MCP twin. The anonymous set mirrors the leads routes'
23
+ * own identity-blind posture (an applying founder has no account, and the
24
+ * resume token is a per-draft capability rather than an identity); the
25
+ * tenant-scoped set is an identity-bound dashboard surface, the same #3591
26
+ * exception the sibling `/v1/waitlist-experiments/*` routes record.
27
+ */
28
+ import type { FastifyPluginAsync } from "fastify";
29
+
30
+ import { patchProjectListingDraftRoute } from "./patch-draft.js";
31
+ import { publicProjectListingsRoute } from "./public.js";
32
+ import { startProjectListingRoute } from "./start.js";
33
+ import { submitProjectListingRoute } from "./submit.js";
34
+
35
+ export const projectListingStartRoute = startProjectListingRoute;
36
+ export const projectListingDraftPatchRoute = patchProjectListingDraftRoute;
37
+ export const projectListingSubmitRoute = submitProjectListingRoute;
38
+ export const projectListingPublicRoute = publicProjectListingsRoute;
39
+
40
+ /**
41
+ * Tenant-scoped handlers, as { specifier, exportName }, reached at REGISTRATION
42
+ * time rather than named as static imports (#5182).
43
+ *
44
+ * `get.ts`, `list.ts`, `patch.ts` and `patch-copy.ts` are multi-tenant SLICE
45
+ * files the OSS strip removes; the other four route modules in this directory
46
+ * are the free anonymous surface and stay static. This directory is mixed on
47
+ * purpose, which is why the boundary is drawn here at the registration
48
+ * call-site rather than by splitting the directory.
49
+ *
50
+ * The specifier is read from this table rather than written inline because
51
+ * `tsc` resolves a string-literal `import("...")` EXPRESSION whether or not the
52
+ * branch runs, and would report TS2307 on the stripped tree. The plugin body
53
+ * only executes when `server.ts` registers it, which it does inside
54
+ * `isModuleEnabled('multi-tenant')` — so nothing here runs on an OSS build.
55
+ *
56
+ * The export name is unchanged so `server.ts` needs no edit: it still reaches
57
+ * `projectListingTenantRoutes` from this module.
58
+ */
59
+ const TENANT_HANDLERS: ReadonlyArray<{
60
+ method: "get" | "patch";
61
+ url: string;
62
+ specifier: string;
63
+ exportName: string;
64
+ }> = [
65
+ {
66
+ method: "get",
67
+ url: "/v1/project-listings",
68
+ specifier: "./list.js",
69
+ exportName: "listProjectListingsHandler",
70
+ },
71
+ {
72
+ method: "get",
73
+ url: "/v1/project-listings/:id",
74
+ specifier: "./get.js",
75
+ exportName: "getProjectListingHandler",
76
+ },
77
+ {
78
+ method: "patch",
79
+ url: "/v1/project-listings/:id",
80
+ specifier: "./patch.js",
81
+ exportName: "patchProjectListingHandler",
82
+ },
83
+ {
84
+ method: "patch",
85
+ url: "/v1/project-listings/:id/copy",
86
+ specifier: "./patch-copy.js",
87
+ exportName: "patchProjectListingCopyHandler",
88
+ },
89
+ ];
90
+
91
+ export const projectListingTenantRoutes: FastifyPluginAsync = async (fastify) => {
92
+ for (const { method, url, specifier, exportName } of TENANT_HANDLERS) {
93
+ const mod = (await import(specifier)) as Record<
94
+ string,
95
+ Parameters<typeof fastify.get>[1] | undefined
96
+ >;
97
+ const handler = mod[exportName];
98
+ if (!handler) {
99
+ throw new Error(
100
+ `multi-tenant slice drift: '${specifier}' does not export '${exportName}'`,
101
+ );
102
+ }
103
+ fastify[method](url, handler);
104
+ }
105
+ };
@@ -0,0 +1,47 @@
1
+ // oss: false
2
+ /**
3
+ * GET /v1/project-listings
4
+ *
5
+ * Tenant-scoped dashboard list (#4998 AC2), the `waitlist-experiments` list
6
+ * route's own pattern: scope derived server-side from `request.tenant`
7
+ * (never a client-supplied site id), role floor `owner|admin` checked before
8
+ * any read, response re-parsed against its own contract schema before send.
9
+ */
10
+ import { projectListingListResponseSchema } from "@working-theory/validation";
11
+ import type { FastifyReply, FastifyRequest } from "fastify";
12
+
13
+ import { listProjectListings, PROJECT_LISTING_ROLES } from "../../services/project-listings.js";
14
+
15
+ import { sendValidated } from "./respond.js";
16
+
17
+ function unauthorized(reply: FastifyReply) {
18
+ return reply.status(401).send({
19
+ ok: false,
20
+ error: { code: "unauthorized", message: "Authentication required" },
21
+ });
22
+ }
23
+
24
+ function forbidden(reply: FastifyReply) {
25
+ return reply.status(403).send({
26
+ ok: false,
27
+ error: { code: "forbidden", message: "Forbidden" },
28
+ });
29
+ }
30
+
31
+ export async function listProjectListingsHandler(
32
+ request: FastifyRequest,
33
+ reply: FastifyReply,
34
+ ): Promise<void> {
35
+ const tenant = request.tenant;
36
+ if (!tenant) return unauthorized(reply);
37
+ if (!PROJECT_LISTING_ROLES.has(tenant.role)) {
38
+ request.log.warn(
39
+ { method: request.method, path: request.url, status: 403 },
40
+ "[project-listings] list denied; insufficient role",
41
+ );
42
+ return forbidden(reply);
43
+ }
44
+
45
+ const data = await listProjectListings(tenant.workspaceId);
46
+ return sendValidated(reply, projectListingListResponseSchema, data, "list");
47
+ }
@@ -0,0 +1,222 @@
1
+ // oss: false
2
+ /**
3
+ * PATCH /v1/project-listings/:id/copy
4
+ *
5
+ * The operator's copy edit — the THIRD review outcome, beside approve and
6
+ * reject.
7
+ *
8
+ * Before this route a submitted listing's copy was frozen for everyone: the
9
+ * legal transitions out of `requested` are `active` and `rejected` only, there
10
+ * is no route back to `draft`, and `PATCH /v1/project-listings/drafts/:id` is
11
+ * scoped to `status: "draft"` plus the applicant's own resume-token hash. A
12
+ * listing whose headline needed a trim could therefore be published exactly as
13
+ * written or rejected outright, and the founder could not revise it either.
14
+ *
15
+ * Deliberately NOT folded into `PATCH /v1/project-listings/:id`. That body is
16
+ * `.strict()`, carries at most `status` and `featured`, and exists to express
17
+ * the DECISION transition; entangling "edit the copy" with "decide the
18
+ * listing" would muddy a contract that is currently clean. The lifecycle is
19
+ * untouched by this route — it adds no status, no transition and no decision
20
+ * target.
21
+ *
22
+ * Auth, tenant scoping and the error shapes are `patch.ts`'s: the same
23
+ * owner-or-admin floor, the same `siteId` scoping from `request.tenant`, and
24
+ * the same 401 / 403 / 400 / 404 bodies. A foreign or missing id is `404`, so a
25
+ * cross-tenant probe cannot tell "exists but not yours" from "does not exist".
26
+ *
27
+ * `questions` is replaced wholesale, and its `extras` array carries a
28
+ * `.default([])` in the package schema, so a payload omitting that key would
29
+ * parse to an empty array and silently delete the founder's extra questions.
30
+ * A `questions` body without an explicit `extras` is therefore a `400`, which
31
+ * makes clearing them something the caller has to say rather than something it
32
+ * can do by forgetting.
33
+ *
34
+ * The status floor (`requested` or `active` only) lives in the service's WHERE
35
+ * clause rather than in a read-then-write check, so a concurrent decision
36
+ * cannot slip the row out of the editable set between the check and the write.
37
+ * A `draft`, `rejected` or `closed` row is `404` and unchanged — `closed`
38
+ * included, even though nothing stores it: the WHERE transcribes
39
+ * `computeListingStatus`'s accepting branches rather than its stored string,
40
+ * so an elapsed window refuses the edit exactly as it refuses on a read.
41
+ */
42
+ import { zProjectListingId } from "@working-theory/ids";
43
+ import {
44
+ buildProjectListingDraftPatchSchema,
45
+ projectListingSchema,
46
+ } from "@working-theory/validation";
47
+ import type { FastifyReply, FastifyRequest } from "fastify";
48
+ import { z } from "zod";
49
+
50
+ import { listingConfigFor } from "../../services/listing-config.js";
51
+ import { patchListingCopy, PROJECT_LISTING_ROLES } from "../../services/project-listings.js";
52
+
53
+ import { sendValidated } from "./respond.js";
54
+
55
+ const paramsSchema = z.object({ id: zProjectListingId });
56
+
57
+ /**
58
+ * The five PUBLISHED copy fields this route may rewrite.
59
+ *
60
+ * `questions` is in because its prompts and option labels render on the public
61
+ * page, which is exactly where abusive language would land. `name` is out
62
+ * because the slug is derived from it at submit and stored, so editing it would
63
+ * desync the public URL from the name. `url`, `product`, `commitment`,
64
+ * `teamComposition`, `address` and `founderCompany` are out because they are
65
+ * facts rather than copy — a wrong fact is a question to the founder or a
66
+ * reject, not an edit. `anythingElse` is out because it is a note to the
67
+ * reviewer and never renders.
68
+ */
69
+ const COPY_FIELDS = ["headline", "oneLiner", "problem", "faqs", "questions"] as const;
70
+
71
+ /**
72
+ * The copy-edit body, DERIVED from the application's own schema at THIS SITE's
73
+ * configured bounds rather than re-declared.
74
+ *
75
+ * Deriving is what carries the word caps, the exactly-three-FAQ rule and every
76
+ * question refinement into this route for free, and what keeps them from
77
+ * drifting: a bound the application gains is a bound the copy edit gains in the
78
+ * same commit.
79
+ *
80
+ * Built from `buildProjectListingDraftPatchSchema` — the same builder the
81
+ * applicant's own write path uses — rather than from the fixed
82
+ * `projectListingApplicationFieldsSchema`, because since #5048 D5/D6 the word
83
+ * caps and the collected field set are the SITE's configuration, not platform
84
+ * constants. Against the fixed schema an operator could not re-save copy the
85
+ * applicant was legitimately allowed to submit: a site configured to fifteen
86
+ * words would accept a fourteen-word headline at submit and refuse the same
87
+ * text here. Passing the intersection of the site's `collectedFields` with
88
+ * `COPY_FIELDS` as the collected set is what narrows the builder's full roster
89
+ * to this route's five, so a field the site does not collect cannot be authored
90
+ * onto a founder's public page by the operator either — the same security
91
+ * property the builder's own docblock records.
92
+ *
93
+ * Unlike `patch-draft.ts`, which must resolve the draft before it knows the
94
+ * site and therefore validates inside the service, this route reads the site
95
+ * from the caller's own session, so the schema is built here and the editable
96
+ * set stays visible in this file.
97
+ *
98
+ * NOT lifted into `@working-theory/validation` as a sibling of
99
+ * `projectListingPatchSchema`, tempting as the symmetry is. This file is
100
+ * vendored byte-for-byte into a consuming repo that installs that package from
101
+ * a PUBLISHED version, so a new export would leave the synced copy referencing
102
+ * a symbol its installed version does not have — broken until a publish and a
103
+ * version bump land. `buildProjectListingDraftPatchSchema` is already public in
104
+ * the version that repo has (`services/project-listings.ts` imports it), so
105
+ * this local derivation syncs cleanly.
106
+ *
107
+ * The builder supplies the at-least-one-key refine, so an empty body is a 400
108
+ * rather than a silent no-op the caller would read as a successful save.
109
+ */
110
+ function copyPatchSchemaFor(
111
+ wordCaps: Parameters<typeof buildProjectListingDraftPatchSchema>[0],
112
+ collectedFields: readonly string[],
113
+ ) {
114
+ return buildProjectListingDraftPatchSchema(
115
+ wordCaps,
116
+ COPY_FIELDS.filter((field) => collectedFields.includes(field)),
117
+ );
118
+ }
119
+
120
+ function unauthorized(reply: FastifyReply) {
121
+ return reply.status(401).send({
122
+ ok: false,
123
+ error: { code: "unauthorized", message: "Authentication required" },
124
+ });
125
+ }
126
+
127
+ function forbidden(reply: FastifyReply) {
128
+ return reply.status(403).send({
129
+ ok: false,
130
+ error: { code: "forbidden", message: "Forbidden" },
131
+ });
132
+ }
133
+
134
+ function invalidParams(reply: FastifyReply) {
135
+ return reply.status(400).send({
136
+ ok: false,
137
+ error: { code: "validation_failed", message: "Invalid project listing id" },
138
+ });
139
+ }
140
+
141
+ function invalidBody(reply: FastifyReply, message: string) {
142
+ return reply.status(400).send({
143
+ ok: false,
144
+ error: { code: "invalid_input", message },
145
+ });
146
+ }
147
+
148
+ function notFound(reply: FastifyReply) {
149
+ return reply.status(404).send({
150
+ ok: false,
151
+ error: { code: "not_found", message: "Project listing not found" },
152
+ });
153
+ }
154
+
155
+ export async function patchProjectListingCopyHandler(
156
+ request: FastifyRequest,
157
+ reply: FastifyReply,
158
+ ): Promise<void> {
159
+ const tenant = request.tenant;
160
+ if (!tenant) return unauthorized(reply);
161
+ if (!PROJECT_LISTING_ROLES.has(tenant.role)) {
162
+ request.log.warn(
163
+ { method: request.method, path: request.url, status: 403 },
164
+ "[project-listings] copy patch denied; insufficient role",
165
+ );
166
+ return forbidden(reply);
167
+ }
168
+
169
+ const parsedParams = paramsSchema.safeParse(request.params);
170
+ if (!parsedParams.success) return invalidParams(reply);
171
+
172
+ const listingConfig = await listingConfigFor(tenant.workspaceId);
173
+ const editableFields = COPY_FIELDS.filter((field) =>
174
+ listingConfig.collectedFields.includes(field),
175
+ );
176
+ if (editableFields.length === 0) {
177
+ // A site whose application collects none of the five copy fields has no
178
+ // copy to edit. Refused here rather than handed to the builder, whose
179
+ // `pickCollected` throws on an empty mask — that throw guards the
180
+ // applicant's surface and would be a 500 on this one.
181
+ return invalidBody(reply, "this site collects none of the editable copy fields");
182
+ }
183
+
184
+ const parsedBody = copyPatchSchemaFor(
185
+ listingConfig.wordCaps,
186
+ listingConfig.collectedFields,
187
+ ).safeParse(request.body);
188
+ if (!parsedBody.success) {
189
+ return invalidBody(reply, parsedBody.error.issues[0]?.message ?? "invalid input");
190
+ }
191
+
192
+ // `questions.extras` carries a `.default([])` in the package schema, so a
193
+ // `questions` payload that OMITS the key parses to `extras: []` —
194
+ // indistinguishable from a deliberate clear. `questions` is replaced
195
+ // wholesale, so accepting the ambiguous body would silently delete the
196
+ // founder's up-to-three extra questions with no error. Refuse it instead and
197
+ // make the caller say which it meant. Checked against the RAW body because
198
+ // the parsed value always carries the key once zod has applied the default.
199
+ if (parsedBody.data.questions !== undefined) {
200
+ const rawQuestions = (request.body as { questions?: unknown }).questions;
201
+ if (
202
+ typeof rawQuestions !== "object" ||
203
+ rawQuestions === null ||
204
+ !("extras" in rawQuestions)
205
+ ) {
206
+ return invalidBody(
207
+ reply,
208
+ "questions must carry an explicit 'extras' array; send [] to clear it",
209
+ );
210
+ }
211
+ }
212
+
213
+ const result = await patchListingCopy(
214
+ tenant.workspaceId,
215
+ parsedParams.data.id,
216
+ parsedBody.data,
217
+ tenant.userId,
218
+ );
219
+ if (!result.ok) return notFound(reply);
220
+
221
+ return sendValidated(reply, projectListingSchema, result.value, "patch-copy");
222
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * PATCH /v1/project-listings/drafts/:id
3
+ *
4
+ * `patch` — the save-as-you-go write (#5036 D1). Anonymous, token-gated,
5
+ * registered in the rate-limited `leadsScope`.
6
+ *
7
+ * It merges a partial application into a draft and ENFORCES NO COMPLETENESS.
8
+ * A present field is still checked against its own bounds (length, enum
9
+ * membership, and the SITE's own word caps — #5048 D6, which is why the body
10
+ * is validated inside the service, where the draft's site is known) because
11
+ * this is an anonymous write surface; what
12
+ * it deliberately does not assert is that the application is finished. That is
13
+ * `submit`'s job alone, and the split is the whole point of the lifecycle.
14
+ *
15
+ * The credential is the `Authorization: Bearer` resume token minted at
16
+ * `start`, and it is bound to ONE listing: a valid token presented against a
17
+ * different id resolves to nothing. A wrong token, a spent token, an unknown
18
+ * id and an already-submitted row are all the same `404` — the anonymous
19
+ * surface must not distinguish "exists but not yours" from "does not exist".
20
+ *
21
+ * The response is `projectListingDraftSchema`, NOT the tenant dashboard's
22
+ * row shape: an anonymous caller addresses its site by an opaque public key
23
+ * and must not be handed the real `siteId` back on every save (security
24
+ * review on this change, finding 3).
25
+ *
26
+ * Distinct from the tenant-scoped `PATCH /v1/project-listings/:id`, which
27
+ * carries the operator's decision and lives behind the tenant guard. The
28
+ * `/drafts/` segment keeps the two apart at the router.
29
+ */
30
+ import { zProjectListingId } from "@working-theory/ids";
31
+ import { projectListingDraftSchema } from "@working-theory/validation";
32
+ import type { FastifyPluginAsync } from "fastify";
33
+ import { z } from "zod";
34
+
35
+ import { patchProjectListingDraft } from "../../services/project-listings.js";
36
+
37
+ import { sendValidated } from "./respond.js";
38
+ import { resumeTokenFrom } from "./resume-token.js";
39
+
40
+ const paramsSchema = z.object({ id: zProjectListingId });
41
+
42
+ export const patchProjectListingDraftRoute: FastifyPluginAsync = async (fastify) => {
43
+ fastify.patch("/v1/project-listings/drafts/:id", async (request, reply) => {
44
+ const parsedParams = paramsSchema.safeParse(request.params);
45
+ if (!parsedParams.success) {
46
+ return reply.status(400).send({
47
+ ok: false,
48
+ error: { code: "validation_failed", message: "Invalid project listing id" },
49
+ });
50
+ }
51
+
52
+ const resumeToken = resumeTokenFrom(request);
53
+ if (resumeToken === null) {
54
+ return reply.status(401).send({
55
+ ok: false,
56
+ error: { code: "unauthorized", message: "A resume token is required" },
57
+ });
58
+ }
59
+
60
+ // The body is validated INSIDE the service (#5048 D6), not here: the word
61
+ // caps are the site's own configuration, and which site this draft belongs
62
+ // to is not knowable until the draft is resolved. A caller holding no valid
63
+ // token still never reaches body validation — the service answers
64
+ // `not_found` first.
65
+ const result = await patchProjectListingDraft(
66
+ parsedParams.data.id,
67
+ resumeToken,
68
+ request.body,
69
+ );
70
+ if (!result.ok) {
71
+ if (result.error.kind === "invalid_input") {
72
+ return reply.status(400).send({
73
+ ok: false,
74
+ error: { code: "invalid_input", message: result.error.message },
75
+ });
76
+ }
77
+ return reply.status(404).send({
78
+ ok: false,
79
+ error: { code: "not_found", message: "Draft not found" },
80
+ });
81
+ }
82
+
83
+ // The omission is explicit at the send site rather than hidden in a
84
+ // schema strip: `projectListingDraftSchema` is `.strict()`, so a key that
85
+ // stops being dropped here becomes a 500 instead of quietly shipping.
86
+ const {
87
+ siteId: _siteId,
88
+ leadId: _leadId,
89
+ decidedBy: _decidedBy,
90
+ issueUrl: _issueUrl,
91
+ experimentKey: _experimentKey,
92
+ ...draft
93
+ } = result.value;
94
+
95
+ return sendValidated(reply, projectListingDraftSchema, draft, "patch-draft");
96
+ });
97
+ };
@@ -0,0 +1,136 @@
1
+ // oss: false
2
+ /**
3
+ * PATCH /v1/project-listings/:id
4
+ *
5
+ * Tenant-scoped listing mutation (#4998 AC3, reshaped by #5033). The body is
6
+ * `.strict()` and carries at most two optional fields, at least one:
7
+ *
8
+ * - `status` — `active` | `rejected`, the only transition there is. The
9
+ * lifecycle collapsed to five states with `closed` COMPUTED (#5033 D3),
10
+ * so `approved -> live` and `live -> closed` no longer exist and `closed`
11
+ * is not a value this route (or any route) can write. Routed through
12
+ * `decideListing` — the SAME function `POST /v1/slack/actions` calls — so
13
+ * the stamp, the window end and the "already decided" idempotency live in
14
+ * one place. On a changed decision the founder is emailed once via
15
+ * `sendListingDecisionEmail`.
16
+ * - `featured` — the operator's shelf flag (#5033 D6), through
17
+ * `setListingFeatured`. Whether it reaches the public read at all is the
18
+ * site's `featuredEnabled` config, read server-side; this route sets the
19
+ * row's own flag and never the config.
20
+ *
21
+ * Sent together, the featured write lands first and the decision's response
22
+ * is what the caller gets back, so one call cannot return a row that
23
+ * contradicts either write.
24
+ *
25
+ * `:id` is validated at the boundary the same way `get.ts` does; a foreign or
26
+ * missing id is `404`. An operator re-PATCHing an already-decided listing
27
+ * gets `200` with the row unchanged (`decideListing`'s idempotent "second
28
+ * click" branch — #4999 AC).
29
+ */
30
+ import { zProjectListingId } from "@working-theory/ids";
31
+ import { projectListingPatchSchema, projectListingSchema } from "@working-theory/validation";
32
+ import type { FastifyReply, FastifyRequest } from "fastify";
33
+ import { z } from "zod";
34
+
35
+ import {
36
+ decideListing,
37
+ sendListingDecisionEmail,
38
+ } from "../../services/project-listing-decision.js";
39
+ import { setListingFeatured, PROJECT_LISTING_ROLES } from "../../services/project-listings.js";
40
+
41
+ import { sendValidated } from "./respond.js";
42
+
43
+ const paramsSchema = z.object({ id: zProjectListingId });
44
+
45
+ function unauthorized(reply: FastifyReply) {
46
+ return reply.status(401).send({
47
+ ok: false,
48
+ error: { code: "unauthorized", message: "Authentication required" },
49
+ });
50
+ }
51
+
52
+ function forbidden(reply: FastifyReply) {
53
+ return reply.status(403).send({
54
+ ok: false,
55
+ error: { code: "forbidden", message: "Forbidden" },
56
+ });
57
+ }
58
+
59
+ function invalidParams(reply: FastifyReply) {
60
+ return reply.status(400).send({
61
+ ok: false,
62
+ error: { code: "validation_failed", message: "Invalid project listing id" },
63
+ });
64
+ }
65
+
66
+ function invalidBody(reply: FastifyReply, message: string) {
67
+ return reply.status(400).send({
68
+ ok: false,
69
+ error: { code: "invalid_input", message },
70
+ });
71
+ }
72
+
73
+ function notFound(reply: FastifyReply) {
74
+ return reply.status(404).send({
75
+ ok: false,
76
+ error: { code: "not_found", message: "Project listing not found" },
77
+ });
78
+ }
79
+
80
+ export async function patchProjectListingHandler(
81
+ request: FastifyRequest,
82
+ reply: FastifyReply,
83
+ ): Promise<void> {
84
+ const tenant = request.tenant;
85
+ if (!tenant) return unauthorized(reply);
86
+ if (!PROJECT_LISTING_ROLES.has(tenant.role)) {
87
+ request.log.warn(
88
+ { method: request.method, path: request.url, status: 403 },
89
+ "[project-listings] patch denied; insufficient role",
90
+ );
91
+ return forbidden(reply);
92
+ }
93
+
94
+ const parsedParams = paramsSchema.safeParse(request.params);
95
+ if (!parsedParams.success) return invalidParams(reply);
96
+
97
+ const parsedBody = projectListingPatchSchema.safeParse(request.body);
98
+ if (!parsedBody.success) {
99
+ return invalidBody(reply, parsedBody.error.issues[0]?.message ?? "invalid input");
100
+ }
101
+
102
+ const { status, featured } = parsedBody.data;
103
+
104
+ if (featured !== undefined) {
105
+ const featuredResult = await setListingFeatured(
106
+ tenant.workspaceId,
107
+ parsedParams.data.id,
108
+ featured,
109
+ );
110
+ if (!featuredResult.ok) return notFound(reply);
111
+ if (status === undefined) {
112
+ return sendValidated(reply, projectListingSchema, featuredResult.value, "patch");
113
+ }
114
+ }
115
+
116
+ if (status === undefined) {
117
+ // Unreachable — the body schema refuses an empty object — but the
118
+ // narrowing has to be discharged before `decideListing`.
119
+ return invalidBody(reply, "patch body must carry at least one of 'status' or 'featured'");
120
+ }
121
+
122
+ const decisionResult = await decideListing({
123
+ id: parsedParams.data.id,
124
+ decision: status,
125
+ decidedBy: tenant.userId,
126
+ siteId: tenant.workspaceId,
127
+ });
128
+ if (!decisionResult.ok) {
129
+ if (decisionResult.error.kind === "not_found") return notFound(reply);
130
+ return invalidBody(reply, decisionResult.error.message);
131
+ }
132
+ if (decisionResult.changed) {
133
+ await sendListingDecisionEmail(decisionResult.value, status);
134
+ }
135
+ return sendValidated(reply, projectListingSchema, decisionResult.value, "patch");
136
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * GET /v1/project-listings/public?siteKey=
3
+ *
4
+ * The anonymous page-render read (#4998 AC4, reshaped by #5033). Every
5
+ * `active` row for the site, ordered: the three newest by `decidedAt` among
6
+ * them first, then the rest by `waitlistCount` desc, then `decidedAt` desc.
7
+ * A row whose window has elapsed comes back with a COMPUTED `closed` status —
8
+ * no job runs, nothing is stored, and a consumer partitions its shelves on
9
+ * what it is handed. `featured` surfaces only where the site's
10
+ * `SiteListingConfig` enables it. `founderEmail` never leaves this route —
11
+ * `projectListingPublicSchema` (`.omit({founderEmail})`) is what the response
12
+ * is re-parsed against before send.
13
+ */
14
+ import {
15
+ projectListingPublicListResponseSchema,
16
+ publicSiteKeySchema,
17
+ } from "@working-theory/validation";
18
+ import type { FastifyPluginAsync } from "fastify";
19
+ import { z } from "zod";
20
+
21
+ import { siteIdForPublicKey } from "../../services/listing-config.js";
22
+ import { publicProjectListings } from "../../services/project-listings.js";
23
+
24
+ import { sendValidated } from "./respond.js";
25
+
26
+ const querySchema = z.object({ siteKey: publicSiteKeySchema });
27
+
28
+ export const publicProjectListingsRoute: FastifyPluginAsync = async (fastify) => {
29
+ fastify.get("/v1/project-listings/public", async (request, reply) => {
30
+ const parsedQuery = querySchema.safeParse(request.query);
31
+ if (!parsedQuery.success) {
32
+ return reply.status(400).send({
33
+ ok: false,
34
+ error: { code: "validation_failed", message: "Invalid siteKey" },
35
+ });
36
+ }
37
+
38
+ // #5048 D2 — one identifier for the whole mountable contract. The shelves
39
+ // and the application form are configured with the same public key, so a
40
+ // site never has to hold two different names for itself.
41
+ const siteId = await siteIdForPublicKey(parsedQuery.data.siteKey);
42
+ if (siteId === null) {
43
+ return reply.status(400).send({
44
+ ok: false,
45
+ error: { code: "unknown_site_key", message: "Unknown site key" },
46
+ });
47
+ }
48
+
49
+ const data = await publicProjectListings(siteId);
50
+ return sendValidated(reply, projectListingPublicListResponseSchema, data, "public");
51
+ });
52
+ };