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
@@ -72,7 +72,13 @@ describe("auth preHandler — cookie path", () => {
72
72
  // The signature suffix must be stripped before the Session lookup.
73
73
  expect(prisma.session.findUnique).toHaveBeenCalledWith({
74
74
  where: { token: "sess_tok_abc" },
75
- select: { userId: true, expiresAt: true },
75
+ select: {
76
+ id: true,
77
+ userId: true,
78
+ expiresAt: true,
79
+ activeOrganizationId: true,
80
+ activeWorkspaceId: true,
81
+ },
76
82
  });
77
83
  });
78
84
 
@@ -14,9 +14,16 @@
14
14
  */
15
15
 
16
16
  import Fastify from "fastify";
17
- import { rateLimitMiddleware, __resetBuckets } from "../rate-limit.js";
18
-
19
- function buildApp(opts: { perIpPerMinute?: number; windowMs?: number } = {}) {
17
+ import {
18
+ rateLimitMiddleware,
19
+ __resetBuckets,
20
+ __bucketCount,
21
+ DEFAULT_PER_IP_PER_MINUTE_STRICT,
22
+ } from "../rate-limit.js";
23
+
24
+ function buildApp(
25
+ opts: { perIpPerMinute?: number; windowMs?: number; maxBuckets?: number } = {},
26
+ ) {
20
27
  const app = Fastify({ trustProxy: true });
21
28
  app.register(rateLimitMiddleware, opts);
22
29
  app.get("/test", async () => ({ ok: true }));
@@ -159,6 +166,48 @@ describe("per-IP rate limit middleware", () => {
159
166
  expect(ipB.statusCode).toBe(200);
160
167
  });
161
168
 
169
+ it("derives independent buckets from the ORIGINATING address when two visitors share a proxy hop (#4730)", async () => {
170
+ // Simulates a consumer site's Next.js server action forwarding the
171
+ // visitor's real IP as x-forwarded-for, with this API's own edge then
172
+ // appending the calling web function's shared egress IP as a second
173
+ // hop — the exact multi-hop shape a consumer double-hop produces
174
+ // (apps/web/app/[locale]/flows/[slug]/flow.actions.ts). With
175
+ // trustProxy:true, req.ip resolves to the LEFTMOST (originating)
176
+ // address, so two visitors sharing the same egress hop still get
177
+ // independent buckets — this is the failure #4730 reports: before the
178
+ // fix, ALL visitors behind one web function's egress IP shared ONE
179
+ // bucket.
180
+ app = buildApp({ perIpPerMinute: 1 });
181
+ await app.ready();
182
+ const SHARED_EGRESS = "9.9.9.9";
183
+
184
+ const visitorA = await app.inject({
185
+ method: "GET",
186
+ url: "/test",
187
+ headers: { "x-forwarded-for": `20.0.0.1, ${SHARED_EGRESS}` },
188
+ });
189
+ expect(visitorA.statusCode).toBe(200);
190
+
191
+ // Visitor A's own second request, still behind the shared egress hop,
192
+ // correctly trips ITS OWN (now-exhausted) bucket.
193
+ const visitorAOverLimit = await app.inject({
194
+ method: "GET",
195
+ url: "/test",
196
+ headers: { "x-forwarded-for": `20.0.0.1, ${SHARED_EGRESS}` },
197
+ });
198
+ expect(visitorAOverLimit.statusCode).toBe(429);
199
+
200
+ // Visitor B, behind the SAME shared egress hop, still has a fresh
201
+ // budget — the two are bucketed on the leftmost (visitor) address, not
202
+ // the shared egress IP.
203
+ const visitorB = await app.inject({
204
+ method: "GET",
205
+ url: "/test",
206
+ headers: { "x-forwarded-for": `20.0.0.2, ${SHARED_EGRESS}` },
207
+ });
208
+ expect(visitorB.statusCode).toBe(200);
209
+ });
210
+
162
211
  it("respects custom perIpPerMinute option over env/default", async () => {
163
212
  app = buildApp({ perIpPerMinute: 3 });
164
213
  await app.ready();
@@ -183,7 +232,7 @@ describe("per-IP rate limit middleware", () => {
183
232
  });
184
233
 
185
234
  it("reads perIpPerMinute from AILK_RATE_LIMIT_PER_IP_PER_MINUTE env var when option is absent", async () => {
186
- // Cover `Number(process.env.AILK_RATE_LIMIT_PER_IP_PER_MINUTE) || 10` left-side truthy arm
235
+ // Cover `Number(process.env.AILK_RATE_LIMIT_PER_IP_PER_MINUTE) || 20` left-side truthy arm
187
236
  // Build the app without a perIpPerMinute option so the env var path is taken
188
237
  const prev = process.env.AILK_RATE_LIMIT_PER_IP_PER_MINUTE;
189
238
  process.env.AILK_RATE_LIMIT_PER_IP_PER_MINUTE = "2";
@@ -289,4 +338,42 @@ describe("per-IP rate limit middleware", () => {
289
338
  // Cleanup fired — the route still responds normally.
290
339
  expect(res.statusCode).toBe(200);
291
340
  });
341
+
342
+ it("caps total distinct buckets at maxBuckets, evicting the oldest-inserted entry on overflow (#5111/#4780 AC-3)", async () => {
343
+ // A caller minting a fresh identity every request (e.g. a spoofed
344
+ // X-Forwarded-For under trustProxy:true) must not grow the in-house
345
+ // buckets Map without bound — independent of the EXISTING time-based
346
+ // eviction (which only fires on stale/expired windows). This test
347
+ // never lets any window expire, so ONLY the new entry-count cap can
348
+ // be responsible for keeping distinct-bucket growth bounded. A small
349
+ // maxBuckets is passed explicitly so the test doesn't need thousands
350
+ // of injected requests to reach the real production default.
351
+ const maxBuckets = 3;
352
+ app = buildApp({ perIpPerMinute: 200, maxBuckets });
353
+ await app.ready();
354
+
355
+ for (let i = 0; i < maxBuckets; i++) {
356
+ const res = await app.inject({
357
+ method: "GET",
358
+ url: "/test",
359
+ headers: { "x-forwarded-for": `172.16.0.${i}` },
360
+ });
361
+ expect(res.statusCode).toBe(200);
362
+ expect(__bucketCount()).toBe(i + 1);
363
+ }
364
+
365
+ // A 4th distinct identity must not push the store past maxBuckets —
366
+ // the oldest-inserted bucket is evicted to make room.
367
+ const overflowRes = await app.inject({
368
+ method: "GET",
369
+ url: "/test",
370
+ headers: { "x-forwarded-for": "203.0.113.99" },
371
+ });
372
+ expect(overflowRes.statusCode).toBe(200);
373
+ expect(__bucketCount()).toBe(maxBuckets);
374
+ });
375
+
376
+ it("exposes DEFAULT_PER_IP_PER_MINUTE_STRICT as the pre-#4730 per-scope default (10/min)", () => {
377
+ expect(DEFAULT_PER_IP_PER_MINUTE_STRICT).toBe(10);
378
+ });
292
379
  });
@@ -29,6 +29,7 @@
29
29
  import { createHash } from "node:crypto";
30
30
 
31
31
  import { getClock } from "@working-theory/common";
32
+ import type { Id } from "@working-theory/ids";
32
33
  import type {
33
34
  FastifyPluginCallback,
34
35
  FastifyReply,
@@ -50,10 +51,25 @@ const UNAUTHORIZED = { error: "unauthorized" } as const;
50
51
 
51
52
  declare module "fastify" {
52
53
  interface FastifyRequest {
53
- userId?: string;
54
+ userId?: Id<"user">;
55
+ sessionTenant?: SessionTenant;
54
56
  }
55
57
  }
56
58
 
59
+ /**
60
+ * Active tenant context carried by a cookie session (multi-tenant module).
61
+ * Set ONLY on the cookie path — bearer tokens carry no workspace binding yet
62
+ * (#3591 tracks tenant-scoped agent auth), so bearer-authenticated requests
63
+ * are denied by the tenant guard (default-deny), never silently granted a
64
+ * tenant. `sessionRowId` is the Session PK, used by the workspace-switch
65
+ * service to update the active tenant server-side.
66
+ */
67
+ export type SessionTenant = {
68
+ sessionRowId: Id<"session">;
69
+ activeOrganizationId: Id<"organization"> | null;
70
+ activeWorkspaceId: Id<"workspace"> | null;
71
+ };
72
+
57
73
  /**
58
74
  * Hash an opaque bearer-token string the same way `tokens.ts` does at create
59
75
  * time. Centralised here so the lookup contract stays identical.
@@ -114,10 +130,11 @@ function isOriginAllowed(
114
130
  if (method === "GET" || method === "HEAD" || method === "OPTIONS")
115
131
  return true;
116
132
  if (!frontendOrigin) {
117
- // No FRONTEND_URL configured accept any origin. The single-tenant OSS
118
- // default trusts the deployment to set FRONTEND_URL; mis-configuration
119
- // is a deploy-time concern, not a runtime auth failure.
120
- return true;
133
+ // No FRONTEND_URL configured: fail OPEN in dev/test (zero-config OSS
134
+ // boot), fail CLOSED in production (security review M1 — with the
135
+ // multi-tenant module's state-changing routes, an unset origin must not
136
+ // silently disable the CSRF check in a real deployment).
137
+ return process.env.NODE_ENV !== "production";
121
138
  }
122
139
  const origin = request.headers.origin;
123
140
  if (!origin) return false;
@@ -125,8 +142,11 @@ function isOriginAllowed(
125
142
  }
126
143
 
127
144
  type SessionRow = {
145
+ id: string;
128
146
  userId: string;
129
147
  expiresAt: Date;
148
+ activeOrganizationId: string | null;
149
+ activeWorkspaceId: string | null;
130
150
  };
131
151
 
132
152
  async function resolveCookieSession(
@@ -134,7 +154,13 @@ async function resolveCookieSession(
134
154
  ): Promise<SessionRow | undefined> {
135
155
  const row = await prisma.session.findUnique({
136
156
  where: { token: cookieToken },
137
- select: { userId: true, expiresAt: true },
157
+ select: {
158
+ id: true,
159
+ userId: true,
160
+ expiresAt: true,
161
+ activeOrganizationId: true,
162
+ activeWorkspaceId: true,
163
+ },
138
164
  });
139
165
  if (!row) return undefined;
140
166
  return row;
@@ -187,7 +213,15 @@ export function makeAuthPreHandler(opts: AuthPreHandlerOptions = {}) {
187
213
  reply.code(401).send(UNAUTHORIZED);
188
214
  return;
189
215
  }
190
- request.userId = session.userId;
216
+ // Brand assertion at the Prisma read edge (ADR-0031 rule 5): the DB
217
+ // CHECK constraints guarantee these columns hold well-formed
218
+ // prefixed ids, so the compile-time brand is asserted once here.
219
+ request.userId = session.userId as Id<"user">;
220
+ request.sessionTenant = {
221
+ sessionRowId: session.id as Id<"session">,
222
+ activeOrganizationId: session.activeOrganizationId as Id<"organization"> | null,
223
+ activeWorkspaceId: session.activeWorkspaceId as Id<"workspace"> | null,
224
+ };
191
225
  return;
192
226
  }
193
227
  // Cookie present but invalid / expired → 401 (no silent bearer fallback).
@@ -210,7 +244,7 @@ export function makeAuthPreHandler(opts: AuthPreHandlerOptions = {}) {
210
244
  reply.code(401).send(UNAUTHORIZED);
211
245
  return;
212
246
  }
213
- request.userId = row.userId;
247
+ request.userId = row.userId as Id<"user">;
214
248
  // Best-effort: bump lastUsedAt. Failure here MUST NOT 401 — the caller
215
249
  // is already authenticated. Swallow the error and continue.
216
250
  prisma.apiToken
@@ -237,6 +271,7 @@ export const authPreHandlerPlugin: FastifyPluginCallback<
237
271
  > = (fastify, opts, done) => {
238
272
  const handler = makeAuthPreHandler(opts);
239
273
  fastify.decorateRequest("userId", undefined);
274
+ fastify.decorateRequest("sessionTenant", undefined);
240
275
  fastify.addHook("preHandler", handler);
241
276
  done();
242
277
  };
@@ -14,10 +14,37 @@
14
14
  * IP cardinality). No setInterval is started until the middleware is registered,
15
15
  * keeping the module import-side-effect-free.
16
16
  *
17
+ * Entry-count cap (#5111/#4780 AC-3): the eviction above is time-based only —
18
+ * it removes buckets whose WINDOW has expired, never bounding the TOTAL
19
+ * distinct-identity count within a still-open window. Once `trustProxy: true`
20
+ * made `req.ip` a caller-influenced value (see below), a caller minting a
21
+ * fresh identity on every request (e.g. varying `X-Forwarded-For`) can grow
22
+ * `buckets` without bound for the lifetime of one isolate. `MAX_BUCKETS`
23
+ * caps total entries independent of the time-based eviction: once at
24
+ * capacity, a genuinely new identity evicts the single oldest-inserted entry
25
+ * (`Map` iteration order is insertion order) before inserting itself — O(1)
26
+ * amortized, no behavior change for any caller under the cap.
27
+ *
17
28
  * Trust-proxy boundary: this middleware reads req.ip, which Fastify populates
18
29
  * from the socket address and any trusted proxy headers (X-Forwarded-For) per
19
30
  * the server's own trustProxy config. This middleware does NOT parse
20
- * X-Forwarded-For itself.
31
+ * X-Forwarded-For itself. `server.ts` enables `trustProxy: true`, so req.ip
32
+ * resolves to the leftmost (originating) address of a multi-hop
33
+ * X-Forwarded-For chain.
34
+ *
35
+ * Consumer double-hop caveat (#4730): when a consumer site's own Next.js
36
+ * server action calls one of the rate-limited scopes below (flows/leads/aeo/
37
+ * schedule) on a visitor's behalf, `req.ip` would otherwise reflect the
38
+ * calling web function's shared egress IP, not the visitor's — every visitor
39
+ * of that consumer site would collapse into one bucket. The fix lives on the
40
+ * caller's side: `@working-theory/api-client`'s `post()`/`get()` accept an
41
+ * `init.headers` override, and the flows server action
42
+ * (`apps/web/app/[locale]/flows/[slug]/flow.actions.ts`) forwards the
43
+ * visitor's own `x-forwarded-for` (read from ITS OWN incoming request via
44
+ * Next's `headers()`) as an explicit `x-forwarded-for` header on the outbound
45
+ * call — appending to, not replacing, whatever hop this API's own edge adds,
46
+ * so trustProxy's leftmost-address resolution still lands on the real
47
+ * visitor.
21
48
  */
22
49
 
23
50
  import { getClock } from "@working-theory/common";
@@ -28,12 +55,25 @@ import type {
28
55
  } from "fastify";
29
56
 
30
57
  export type RateLimitOptions = {
31
- /** Requests allowed per IP per window. Default: AILK_RATE_LIMIT_PER_IP_PER_MINUTE env var ?? 10 */
58
+ /**
59
+ * Requests allowed per IP per window.
60
+ * Default: AILK_RATE_LIMIT_PER_IP_PER_MINUTE env var ?? 20 (#4730 — sized
61
+ * to cover a full flow: the longest shipped flow, ailk-founder-diagnostic,
62
+ * is 13 steps + 1 GET, plus headroom for an invisible aeo_score
63
+ * submission and one legitimate resubmit; see apps/api/src/routes/flows/
64
+ * README.md).
65
+ */
32
66
  perIpPerMinute?: number;
33
67
  /** Window duration in milliseconds. Default: 60_000 */
34
68
  windowMs?: number;
35
69
  /** Extract the IP key from a request. Default: req.ip */
36
70
  ipFromRequest?: (req: FastifyRequest) => string;
71
+ /**
72
+ * Maximum distinct entries `buckets` may hold at once, independent of the
73
+ * time-based eviction. Default: `MAX_BUCKETS`. Exposed for tests only —
74
+ * no production caller overrides this.
75
+ */
76
+ maxBuckets?: number;
37
77
  };
38
78
 
39
79
  type Bucket = {
@@ -43,6 +83,21 @@ type Bucket = {
43
83
 
44
84
  const RATE_LIMITED = "rate_limited" as const;
45
85
 
86
+ /**
87
+ * The pre-#4730 per-scope default (10/min) — leads/aeo/schedule's own
88
+ * `perIpPerMinute` (#5111, DEF-004). `flowsScope` keeps the wider
89
+ * env-var-driven default below; it is NOT built from this constant.
90
+ */
91
+ export const DEFAULT_PER_IP_PER_MINUTE_STRICT = 10;
92
+
93
+ /**
94
+ * Default cap on `buckets`' total distinct entries (#5111/#4780 AC-3). Sized
95
+ * generously above any plausible legitimate per-minute visitor cardinality
96
+ * for this product's anonymous scopes, while still bounding worst-case
97
+ * memory growth under an unbounded-identity-churn attack.
98
+ */
99
+ export const MAX_BUCKETS = 10_000;
100
+
46
101
  // Module-scoped bucket store. One entry per unique IP.
47
102
  const buckets = new Map<string, Bucket>();
48
103
 
@@ -68,6 +123,14 @@ export function __resetBuckets(): void {
68
123
  requestsSinceLastClean = 0;
69
124
  }
70
125
 
126
+ /**
127
+ * Test-only accessor for the current distinct-bucket count. Not intended for
128
+ * production use.
129
+ */
130
+ export function __bucketCount(): number {
131
+ return buckets.size;
132
+ }
133
+
71
134
  // Break Fastify's plugin encapsulation so the onRequest hook propagates to
72
135
  // parent and sibling scopes. This is the manual equivalent of fastify-plugin's
73
136
  // `skip-override` mechanism — documented in Fastify's plugin guide.
@@ -79,9 +142,10 @@ export const rateLimitMiddleware: FastifyPluginCallback<RateLimitOptions> = (
79
142
  ) => {
80
143
  const {
81
144
  perIpPerMinute = Number(process.env.AILK_RATE_LIMIT_PER_IP_PER_MINUTE) ||
82
- 10,
145
+ 20,
83
146
  windowMs = 60_000,
84
147
  ipFromRequest = (req: FastifyRequest) => req.ip,
148
+ maxBuckets = MAX_BUCKETS,
85
149
  } = opts;
86
150
 
87
151
  fastify.addHook(
@@ -98,6 +162,17 @@ export const rateLimitMiddleware: FastifyPluginCallback<RateLimitOptions> = (
98
162
  let bucket = buckets.get(ip);
99
163
 
100
164
  if (!bucket || now - bucket.windowStartMs >= windowMs) {
165
+ // A genuinely new distinct identity (not a window roll on an
166
+ // existing bucket) at capacity evicts the single oldest-inserted
167
+ // entry first — independent of the time-based eviction above,
168
+ // which only removes EXPIRED windows and may leave `buckets` at or
169
+ // above `maxBuckets` when every entry is still within its window.
170
+ if (!buckets.has(ip) && buckets.size >= maxBuckets) {
171
+ const oldestKey = buckets.keys().next().value;
172
+ if (oldestKey !== undefined) {
173
+ buckets.delete(oldestKey);
174
+ }
175
+ }
101
176
  bucket = { count: 0, windowStartMs: now };
102
177
  buckets.set(ip, bucket);
103
178
  }
@@ -0,0 +1,99 @@
1
+ // oss: false
2
+ /**
3
+ * Tenant guard preHandler (multi-tenant module — FEAT-042).
4
+ *
5
+ * Runs AFTER the dual-mode auth preHandler inside the workspace route scope.
6
+ * Default-deny: resolves the caller's active tenant via
7
+ * `services/tenant-context.ts#resolveActiveTenant` and rejects everything
8
+ * that is not an explicit grant:
9
+ *
10
+ * - bearer-authenticated requests (no workspace binding yet — #3591) → 403
11
+ * - cookie sessions with no / stale active workspace → 403
12
+ * - revoked membership → 403
13
+ * - URL workspaceId ≠ the session's active workspace → 403
14
+ * (compared BEFORE any lookup of the target — no tenant enumeration)
15
+ *
16
+ * On grant it decorates `request.tenant` (a `TenantContext`); route handlers
17
+ * pass that to `tenantDb(tenant)` so every domain query is workspace-scoped.
18
+ * Routes whose target is a session/artifact id (no workspaceId param) rely on
19
+ * the scope layer's `findFirst({ id, workspaceId })` — a foreign target is a
20
+ * 404, indistinguishable from a missing one.
21
+ */
22
+ import type {
23
+ FastifyPluginCallback,
24
+ FastifyReply,
25
+ FastifyRequest,
26
+ } from "fastify";
27
+
28
+ import {
29
+ resolveActiveTenant,
30
+ type TenantContext,
31
+ } from "../services/tenant-context.js";
32
+
33
+ declare module "fastify" {
34
+ interface FastifyRequest {
35
+ tenant?: TenantContext;
36
+ }
37
+ }
38
+
39
+ const FORBIDDEN = {
40
+ ok: false,
41
+ error: { code: "forbidden", message: "Forbidden" },
42
+ } as const;
43
+
44
+ /**
45
+ * Resolve + attach the active tenant, or reply 403. Registered as a hook in
46
+ * the workspace route scope (after auth), so no tenant-scoped handler runs
47
+ * without a granted TenantContext.
48
+ */
49
+ export async function tenantPreHandler(
50
+ request: FastifyRequest,
51
+ reply: FastifyReply,
52
+ ): Promise<void> {
53
+ const userId = request.userId;
54
+ if (!userId) {
55
+ reply.code(401).send({
56
+ ok: false,
57
+ error: { code: "unauthorized", message: "Authentication required" },
58
+ });
59
+ return;
60
+ }
61
+
62
+ const resolved = await resolveActiveTenant({
63
+ userId,
64
+ sessionTenant: request.sessionTenant,
65
+ });
66
+ if (!resolved.ok) {
67
+ reply.code(403).send(FORBIDDEN);
68
+ return;
69
+ }
70
+
71
+ // URL-scoped routes: the target workspace MUST be the session's active
72
+ // workspace. Compared before any target lookup — a foreign (or nonexistent)
73
+ // workspaceId gets the same 403.
74
+ const params = request.params as { workspaceId?: string } | undefined;
75
+ if (params?.workspaceId && params.workspaceId !== resolved.value.workspaceId) {
76
+ reply.code(403).send(FORBIDDEN);
77
+ return;
78
+ }
79
+
80
+ request.tenant = resolved.value;
81
+ }
82
+
83
+ /**
84
+ * Plugin form, mirroring `authPreHandlerPlugin`: skip-override so the hook
85
+ * propagates to sibling route plugins registered in the same scope.
86
+ */
87
+ export const tenantPreHandlerPlugin: FastifyPluginCallback = (
88
+ fastify,
89
+ _opts,
90
+ done,
91
+ ) => {
92
+ fastify.decorateRequest("tenant", undefined);
93
+ fastify.addHook("preHandler", tenantPreHandler);
94
+ done();
95
+ };
96
+
97
+ (tenantPreHandlerPlugin as FastifyPluginCallback & { [key: symbol]: boolean })[
98
+ Symbol.for("skip-override")
99
+ ] = true;
@@ -126,4 +126,96 @@ describe("GET /api/openapi.json", () => {
126
126
  spec.paths["/v1/content/{type}"].get.responses["400"],
127
127
  ).toMatchObject({ $ref: "#/components/responses/BadRequest" });
128
128
  });
129
+
130
+ describe("lead-write routes (#4991)", () => {
131
+ const leadPaths: Array<{
132
+ path: string;
133
+ operationId: string;
134
+ requiredFields: string[];
135
+ }> = [
136
+ {
137
+ path: "/v1/leads/submit",
138
+ operationId: "submitLead",
139
+ requiredFields: ["siteId", "source", "email", "payload"],
140
+ },
141
+ {
142
+ path: "/v1/leads/subscribe",
143
+ operationId: "subscribeLead",
144
+ requiredFields: ["siteId", "email", "source"],
145
+ },
146
+ {
147
+ path: "/v1/leads/callback",
148
+ operationId: "requestCallback",
149
+ requiredFields: ["siteId", "name", "phone", "payload"],
150
+ },
151
+ ];
152
+
153
+ it.each(leadPaths)(
154
+ "documents $path as a POST with a matching operationId",
155
+ async ({ path, operationId }) => {
156
+ const res = await app.inject({
157
+ method: "GET",
158
+ url: "/v1/openapi.json",
159
+ });
160
+
161
+ const spec = JSON.parse(res.body);
162
+ expect(spec.paths[path].post.operationId).toBe(operationId);
163
+ },
164
+ );
165
+
166
+ it.each(leadPaths)(
167
+ "documents $path's requestBody with the Zod schema's required keys",
168
+ async ({ path, requiredFields }) => {
169
+ const res = await app.inject({
170
+ method: "GET",
171
+ url: "/v1/openapi.json",
172
+ });
173
+
174
+ const spec = JSON.parse(res.body);
175
+ const requestBodySchema =
176
+ spec.paths[path].post.requestBody.content["application/json"]
177
+ .schema;
178
+ const resolved =
179
+ "$ref" in requestBodySchema
180
+ ? resolveRef(spec, requestBodySchema.$ref)
181
+ : requestBodySchema;
182
+
183
+ expect(resolved.required.slice().sort()).toEqual(
184
+ requiredFields.slice().sort(),
185
+ );
186
+ },
187
+ );
188
+
189
+ it.each(leadPaths)(
190
+ "documents 201, 400, and 503 responses for $path",
191
+ async ({ path }) => {
192
+ const res = await app.inject({
193
+ method: "GET",
194
+ url: "/v1/openapi.json",
195
+ });
196
+
197
+ const spec = JSON.parse(res.body);
198
+ const responses = spec.paths[path].post.responses;
199
+ expect(responses["201"]).toBeDefined();
200
+ expect(responses["400"]).toBeDefined();
201
+ expect(responses["503"]).toBeDefined();
202
+ },
203
+ );
204
+
205
+ it("no longer describes the API as read-only", async () => {
206
+ const res = await app.inject({
207
+ method: "GET",
208
+ url: "/v1/openapi.json",
209
+ });
210
+
211
+ const spec = JSON.parse(res.body);
212
+ expect(spec.info.description).not.toMatch(/Read-only/i);
213
+ });
214
+ });
129
215
  });
216
+
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test helper over a raw JSON document
218
+ function resolveRef(spec: any, ref: string): any {
219
+ const path = ref.replace(/^#\//, "").split("/");
220
+ return path.reduce((node, key) => node[key], spec);
221
+ }