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
@@ -15,10 +15,30 @@
15
15
  * Telemetry failures are caught, logged, and do NOT change the response.
16
16
  *
17
17
  * Billing upsert (customer.created / checkout.session.completed):
18
- * Lookup priority:
18
+ * Billing-entity routing (org-scoped billing, #3601, spec D2): a checkout
19
+ * carrying metadata.organizationId links the Organization (multi-tenant
20
+ * module); absent → the existing User path (single-tenant), byte-for-byte.
21
+ *
22
+ * Organization path (metadata.organizationId present):
23
+ * 1. Typed composite match already pointing at this org → idempotent no-op
24
+ * 2. Else link the target Organization (by id) billing fields
25
+ *
26
+ * User path (no organizationId — unchanged):
19
27
  * 1. Typed composite match: (billingProvider="stripe", billingCustomerId=customer)
20
28
  * 2. Email fallback: User.findUnique({ where: { email } }) → update billing fields
21
29
  * 3. No match → 200 + warn log; no User row created (pre-signup state)
30
+ *
31
+ * Recommender purchase stamp (checkout.session.completed / .expired, #4838):
32
+ * A session carrying metadata.flowCheckoutId (the FlowCheckout buyer row
33
+ * #4841 opened it with) is stamped `completed` / `expired` by ONE guarded
34
+ * `updateMany` on `{ id, status: "created" }` — the durable idempotency key
35
+ * (D1): a replay, a restart between deliveries, or a late `expired` after a
36
+ * `completed` all match zero rows and log `stale_or_unknown`. Runs BESIDE the
37
+ * telemetry + billing tasks (D4), never instead of them; a charged/quoted
38
+ * mismatch is logged, never reconciled by overwrite (D2). Always 200. A
39
+ * `completed` session stamps only when `payment_status` is `"paid"` or
40
+ * `"no_payment_required"` (a 100%-off promo code, #4862) — anything else
41
+ * logs `outcome: "unpaid"` and leaves the row `created`.
22
42
  */
23
43
  import { track } from "@working-theory/analytics/server";
24
44
  import type { FastifyReply, FastifyRequest } from "fastify";
@@ -98,22 +118,30 @@ export const stripeWebhookHandler = async (
98
118
  const customer = event.data.object as {
99
119
  id: string;
100
120
  email?: string | null;
121
+ metadata?: Record<string, string> | null;
101
122
  };
102
- await upsertBillingIdentity(request, {
103
- customerId: customer.id,
104
- email: customer.email ?? null,
105
- eventIdPrefix,
106
- });
123
+ // Billing-entity routing (spec D2): a multi-tenant checkout stamps the
124
+ // target Organization id in metadata.organizationId (an individual → their
125
+ // sentinel default org). Present → link the Organization; absent → the
126
+ // existing User path, byte-for-byte (single-tenant, degrade-clean).
127
+ const organizationId = customer.metadata?.["organizationId"] ?? null;
128
+ if (organizationId) {
129
+ await upsertOrgBillingIdentity(request, {
130
+ organizationId,
131
+ customerId: customer.id,
132
+ eventIdPrefix,
133
+ });
134
+ } else {
135
+ await upsertBillingIdentity(request, {
136
+ customerId: customer.id,
137
+ email: customer.email ?? null,
138
+ eventIdPrefix,
139
+ });
140
+ }
107
141
  break;
108
142
  }
109
143
  case "checkout.session.completed": {
110
- const session = event.data.object as {
111
- amount_total?: number | null;
112
- currency?: string | null;
113
- customer?: string | null;
114
- customer_details?: { email?: string | null } | null;
115
- metadata?: Record<string, string> | null;
116
- };
144
+ const session = event.data.object as CheckoutSessionObject;
117
145
  const props = {
118
146
  amount: session.amount_total ?? undefined,
119
147
  currency: session.currency ?? undefined,
@@ -125,19 +153,47 @@ export const stripeWebhookHandler = async (
125
153
  eventIdPrefix,
126
154
  };
127
155
 
128
- // Telemetry and billing upsert are independent — run in parallel.
129
- await Promise.all([
130
- fireTelemetryWithTimeout(request, "purchase-completed", props),
131
- session.customer
132
- ? upsertBillingIdentity(request, {
156
+ // Billing-entity routing (spec D2): metadata.organizationId Organization
157
+ // path; absent → the existing User path, unchanged (single-tenant).
158
+ const organizationId = session.metadata?.["organizationId"] ?? null;
159
+ let billingTask: Promise<void> = Promise.resolve();
160
+ if (session.customer) {
161
+ billingTask = organizationId
162
+ ? upsertOrgBillingIdentity(request, {
163
+ organizationId,
133
164
  customerId: session.customer,
134
- email: session.customer_details?.email ?? null,
135
165
  eventIdPrefix,
136
166
  })
137
- : Promise.resolve(),
167
+ : upsertBillingIdentity(request, {
168
+ customerId: session.customer,
169
+ email: session.customer_details?.email ?? null,
170
+ eventIdPrefix,
171
+ });
172
+ }
173
+
174
+ // Telemetry, billing upsert and the recommender stamp are independent —
175
+ // run in parallel (the stamp is a no-op without metadata.flowCheckoutId).
176
+ await Promise.all([
177
+ fireTelemetryWithTimeout(request, "purchase-completed", props),
178
+ billingTask,
179
+ stampFlowCheckout(request, { session, eventIdPrefix, outcome: "completed" }),
138
180
  ]);
139
181
  break;
140
182
  }
183
+ case "checkout.session.expired": {
184
+ const session = event.data.object as CheckoutSessionObject;
185
+ if (!session.metadata?.["flowCheckoutId"]) {
186
+ // Not a recommender session — the same low-noise line the default
187
+ // arm emitted for this event before #4838 handled it.
188
+ request.log.info(
189
+ { eventType: event.type, eventIdPrefix },
190
+ "unhandled webhook event",
191
+ );
192
+ break;
193
+ }
194
+ await stampFlowCheckout(request, { session, eventIdPrefix, outcome: "expired" });
195
+ break;
196
+ }
141
197
  default:
142
198
  request.log.info(
143
199
  { eventType: event.type, eventIdPrefix },
@@ -149,6 +205,117 @@ export const stripeWebhookHandler = async (
149
205
  await reply.code(200).send({ received: true });
150
206
  };
151
207
 
208
+ type CheckoutSessionObject = {
209
+ id?: string;
210
+ payment_status?: string | null;
211
+ amount_total?: number | null;
212
+ currency?: string | null;
213
+ customer?: string | null;
214
+ customer_details?: { email?: string | null } | null;
215
+ metadata?: Record<string, string> | null;
216
+ };
217
+
218
+ /**
219
+ * Stamp the recommender's FlowCheckout buyer row from a terminal Checkout
220
+ * Session event (#4838).
221
+ *
222
+ * The `status: "created"` guard on the update IS the idempotency key (D1):
223
+ * `count === 1` → stamped; `count === 0` → the row is already terminal, or the
224
+ * id is unknown — a warning, never a retry (Stripe would re-fire telemetry +
225
+ * billing on eventual success). A `completed` session whose
226
+ * `payment_status` is not `"paid"` logs `unpaid` and leaves the row `created`. A charged amount that differs from the quoted
227
+ * `amountMinor` is logged with both integers and the stamp proceeds (D2). Log
228
+ * lines carry only `eventIdPrefix`, an 8-char `flowCheckoutIdPrefix`, an
229
+ * `outcome`, and the two integers — never the email, customer id, or permalink.
230
+ *
231
+ * Never throws — the handler's "always 200 for a verified event" contract.
232
+ */
233
+ async function stampFlowCheckout(
234
+ request: FastifyRequest,
235
+ {
236
+ session,
237
+ eventIdPrefix,
238
+ outcome,
239
+ }: { session: CheckoutSessionObject; eventIdPrefix: string; outcome: "completed" | "expired" },
240
+ ): Promise<void> {
241
+ const flowCheckoutId = session.metadata?.["flowCheckoutId"];
242
+ if (!flowCheckoutId) return;
243
+ const flowCheckoutIdPrefix = flowCheckoutId.substring(0, 8);
244
+
245
+ // A `completed` session is only a purchase once Stripe reports it paid, OR
246
+ // reports nothing was due (`no_payment_required` — a 100%-off promo code,
247
+ // #4862, still a Stripe-confirmed completion). A delayed-notification or
248
+ // genuinely unpaid completion leaves the row `created` (code + security
249
+ // review, PR #4855).
250
+ if (
251
+ outcome === "completed" &&
252
+ session.payment_status !== "paid" &&
253
+ session.payment_status !== "no_payment_required"
254
+ ) {
255
+ request.log.warn(
256
+ { eventIdPrefix, flowCheckoutIdPrefix, outcome: "unpaid" },
257
+ "flow checkout: completed session is not paid — not stamping",
258
+ );
259
+ return;
260
+ }
261
+
262
+ try {
263
+ if (outcome === "completed" && typeof session.amount_total === "number") {
264
+ const row = await prisma.flowCheckout.findUnique({
265
+ where: { id: flowCheckoutId },
266
+ select: { amountMinor: true },
267
+ });
268
+ if (row && row.amountMinor !== session.amount_total) {
269
+ request.log.warn(
270
+ {
271
+ eventIdPrefix,
272
+ flowCheckoutIdPrefix,
273
+ outcome: "amount_mismatch",
274
+ expected: row.amountMinor,
275
+ received: session.amount_total,
276
+ },
277
+ "flow checkout: charged amount differs from the quoted amount — stamping anyway",
278
+ );
279
+ }
280
+ }
281
+
282
+ const result = await prisma.flowCheckout.updateMany({
283
+ where: { id: flowCheckoutId, status: "created" },
284
+ data:
285
+ outcome === "completed"
286
+ ? {
287
+ status: "completed",
288
+ completedAt: new Date(),
289
+ stripeSessionId: session.id ?? null,
290
+ stripeCustomerId: session.customer ?? null,
291
+ }
292
+ : { status: "expired" },
293
+ });
294
+
295
+ if (result.count === 1) {
296
+ request.log.info(
297
+ { eventIdPrefix, flowCheckoutIdPrefix, outcome },
298
+ "flow checkout stamped",
299
+ );
300
+ } else {
301
+ request.log.warn(
302
+ { eventIdPrefix, flowCheckoutIdPrefix, outcome: "stale_or_unknown" },
303
+ "flow checkout: no created row matched — replay, late terminal event, or unknown id",
304
+ );
305
+ }
306
+ } catch (err) {
307
+ request.log.error(
308
+ {
309
+ eventIdPrefix,
310
+ flowCheckoutIdPrefix,
311
+ outcome: "stamp_failed",
312
+ errName: err instanceof Error ? err.name : "UnknownError",
313
+ },
314
+ "flow checkout stamp failed — continuing",
315
+ );
316
+ }
317
+ }
318
+
152
319
  /**
153
320
  * Link User.billingProvider + User.billingCustomerId for a Stripe customer.
154
321
  *
@@ -221,6 +388,67 @@ async function upsertBillingIdentity(
221
388
  }
222
389
  }
223
390
 
391
+ /**
392
+ * Link Organization.billingProvider + billingCustomerId for a Stripe customer
393
+ * (org-scoped billing, #3601, spec D2/D3). Called when a checkout carries
394
+ * metadata.organizationId — for a real org, or an individual's sentinel default
395
+ * Organization (orgs-of-one; AC2 — ONE billing path inside the module).
396
+ *
397
+ * Idempotent: a typed-composite match (billingProvider="stripe",
398
+ * billingCustomerId=customerId) already pointing at THIS org is a no-op on
399
+ * replay. Otherwise the target Organization (by id from checkout metadata) is
400
+ * linked.
401
+ *
402
+ * DB failures are caught and logged (never re-thrown) — a broken link must not
403
+ * make Stripe retry (which would re-fire analytics + billing on eventual
404
+ * success), matching the User-path contract above.
405
+ */
406
+ async function upsertOrgBillingIdentity(
407
+ request: FastifyRequest,
408
+ {
409
+ organizationId,
410
+ customerId,
411
+ eventIdPrefix,
412
+ }: { organizationId: string; customerId: string; eventIdPrefix: string },
413
+ ): Promise<void> {
414
+ try {
415
+ const existingByBilling = await prisma.organization.findFirst({
416
+ where: {
417
+ billingProvider: "stripe",
418
+ billingCustomerId: customerId,
419
+ },
420
+ });
421
+
422
+ if (existingByBilling && existingByBilling.id === organizationId) {
423
+ request.log.info(
424
+ { eventIdPrefix },
425
+ "org billing identity already linked — idempotent no-op",
426
+ );
427
+ return;
428
+ }
429
+
430
+ await prisma.organization.update({
431
+ where: { id: organizationId },
432
+ data: {
433
+ billingProvider: "stripe",
434
+ billingCustomerId: customerId,
435
+ },
436
+ });
437
+ request.log.info(
438
+ { eventIdPrefix },
439
+ "org billing identity linked from checkout metadata.organizationId",
440
+ );
441
+ } catch (err) {
442
+ request.log.error(
443
+ {
444
+ eventIdPrefix,
445
+ error: err instanceof Error ? err.message : String(err),
446
+ },
447
+ "org billing identity upsert failed — continuing",
448
+ );
449
+ }
450
+ }
451
+
224
452
  async function fireTelemetryWithTimeout(
225
453
  request: FastifyRequest,
226
454
  name: string,
@@ -0,0 +1,356 @@
1
+ // oss: false
2
+ /**
3
+ * @file config-tenant-isolation.test.ts
4
+ *
5
+ * THE ACCEPTANCE GATE for the workspace-scoped site-config write surface
6
+ * (STC-P4 / #3634): a member of Org A must not read or write Org B's site
7
+ * config (theme / identity / seo) by ANY route.
8
+ *
9
+ * Fixture: two orgs, alice = owner of org A (active wsA), bob = owner of org B
10
+ * (active wsB). The prisma mock HONORS where-clauses (workspace/membership
11
+ * compound key) and keeps a per-workspace in-memory config store so a PUT then
12
+ * GET round-trips — cross-tenant denials are additionally asserted by "config
13
+ * query never ran" (the guard 403s before any handler touches the model).
14
+ *
15
+ * Registration mirrors server.ts exactly: auth preHandler → nested scope
16
+ * { tenant guard → workspaceConfigRoutes }.
17
+ *
18
+ * SECURITY-SENSITIVE (tenant isolation for untrusted end-tenants) — see spec
19
+ * §5.3: this suite is the objective pass/fail record the mandatory
20
+ * `security-reviewer` verifies against.
21
+ */
22
+ import Fastify from "fastify";
23
+ import type { FastifyInstance } from "fastify";
24
+
25
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
26
+ jest.mock("../../../lib/prisma.js", () => ({
27
+ prisma: {
28
+ session: { findUnique: jest.fn(), update: jest.fn() },
29
+ apiToken: {
30
+ findUnique: jest.fn(),
31
+ update: jest.fn().mockResolvedValue(undefined),
32
+ },
33
+ workspace: { findUnique: jest.fn(), count: jest.fn() },
34
+ membership: { findUnique: jest.fn() },
35
+ workspaceTheme: {
36
+ findUnique: jest.fn(),
37
+ upsert: jest.fn(),
38
+ deleteMany: jest.fn(),
39
+ },
40
+ workspaceSiteIdentity: {
41
+ findUnique: jest.fn(),
42
+ upsert: jest.fn(),
43
+ deleteMany: jest.fn(),
44
+ },
45
+ workspaceSEO: {
46
+ findUnique: jest.fn(),
47
+ upsert: jest.fn(),
48
+ deleteMany: jest.fn(),
49
+ },
50
+ },
51
+ }));
52
+
53
+ import { prisma } from "../../../lib/prisma.js";
54
+ import { authPreHandlerPlugin } from "../../../middleware/auth.js";
55
+ import { tenantPreHandlerPlugin } from "../../../middleware/tenant.js";
56
+ import { workspaceConfigRoutes } from "../config.js";
57
+
58
+ const FRONTEND = "https://app.example.com";
59
+ const FUTURE = new Date(Date.now() + 60 * 60 * 1000);
60
+
61
+ const WS_A = "wksp_00000000-0000-7000-8000-0000000000aa";
62
+ const WS_B = "wksp_00000000-0000-7000-8000-0000000000bb";
63
+ const ORG_A = "org_00000000-0000-7000-8000-000000000aaa";
64
+ const ORG_B = "org_00000000-0000-7000-8000-000000000bbb";
65
+
66
+ // ── Fixture data ────────────────────────────────────────────────────────────
67
+
68
+ const WORKSPACES = [
69
+ { id: WS_A, organizationId: ORG_A },
70
+ { id: WS_B, organizationId: ORG_B },
71
+ ];
72
+
73
+ const MEMBERSHIPS = [
74
+ { organizationId: ORG_A, userId: "alice", role: "owner" },
75
+ { organizationId: ORG_B, userId: "bob", role: "owner" },
76
+ ];
77
+
78
+ const SESSION_ROWS: Record<string, Record<string, unknown>> = {
79
+ cookie_alice: {
80
+ id: "sess_alice",
81
+ userId: "alice",
82
+ expiresAt: FUTURE,
83
+ activeOrganizationId: ORG_A,
84
+ activeWorkspaceId: WS_A,
85
+ },
86
+ cookie_bob: {
87
+ id: "sess_bob",
88
+ userId: "bob",
89
+ expiresAt: FUTURE,
90
+ activeOrganizationId: ORG_B,
91
+ activeWorkspaceId: WS_B,
92
+ },
93
+ };
94
+
95
+ // ── Valid config bodies (schema-conformant) ─────────────────────────────────
96
+
97
+ const PALETTE = {
98
+ bg: "#ffffff",
99
+ bgMuted: "#f5f5f5",
100
+ surface: "#ffffff",
101
+ border: "#e5e5e5",
102
+ text: "#111111",
103
+ textMuted: "#666666",
104
+ primary: "#2563eb",
105
+ primaryFg: "#ffffff",
106
+ accent: "#9333ea",
107
+ accentFg: "#ffffff",
108
+ destructive: "#dc2626",
109
+ destructiveFg: "#ffffff",
110
+ };
111
+
112
+ const VALID_THEME = {
113
+ version: "1",
114
+ colors: { light: PALETTE, dark: PALETTE },
115
+ radius: { sm: "2px", md: "6px", lg: "12px" },
116
+ shadow: { sm: "0 1px 2px", md: "0 2px 4px", lg: "0 4px 8px" },
117
+ fonts: { display: "Inter", body: "Inter" },
118
+ lineHeight: { tight: "1.1", normal: "1.5", relaxed: "1.8" },
119
+ icon: { sm: "16px", md: "20px", lg: "24px" },
120
+ breakpoint: { sm: "640px", md: "768px", lg: "1024px", xl: "1280px" },
121
+ container: { sm: "640px", md: "768px", lg: "1024px", xl: "1280px" },
122
+ };
123
+
124
+ const VALID_IDENTITY = {
125
+ kind: "Organization",
126
+ name: "Acme",
127
+ url: "https://acme.example",
128
+ };
129
+
130
+ const VALID_SEO = { version: "1", description: "Acme site" };
131
+
132
+ const CONFIG_CASES = [
133
+ { type: "theme", model: "workspaceTheme", valid: VALID_THEME },
134
+ { type: "identity", model: "workspaceSiteIdentity", valid: VALID_IDENTITY },
135
+ { type: "seo", model: "workspaceSEO", valid: VALID_SEO },
136
+ ] as const;
137
+
138
+ // ── Where-clause-honoring mock wiring (stateful config store) ───────────────
139
+
140
+ type Where = Record<string, unknown>;
141
+
142
+ /** Per-model, per-workspaceId in-memory store so PUT→GET round-trips. */
143
+ function installFixtureMocks(): void {
144
+ (prisma.session.findUnique as jest.Mock).mockImplementation(
145
+ ({ where }: { where: { token?: string } }) =>
146
+ Promise.resolve(where.token ? (SESSION_ROWS[where.token] ?? null) : null),
147
+ );
148
+ (prisma.session.update as jest.Mock).mockResolvedValue({});
149
+
150
+ (prisma.workspace.findUnique as jest.Mock).mockImplementation(
151
+ ({ where }: { where: { id?: string } }) =>
152
+ Promise.resolve(WORKSPACES.find((w) => w.id === where.id) ?? null),
153
+ );
154
+ // Genuinely multi-tenant (2 workspaces) — the single-tenant default-resolution
155
+ // branch (count() === 1) must never fire, so an unbound session keeps denying.
156
+ (prisma.workspace.count as jest.Mock).mockResolvedValue(WORKSPACES.length);
157
+
158
+ (prisma.membership.findUnique as jest.Mock).mockImplementation(
159
+ ({
160
+ where,
161
+ }: {
162
+ where: {
163
+ organizationId_userId?: { organizationId: string; userId: string };
164
+ };
165
+ }) => {
166
+ const key = where.organizationId_userId;
167
+ if (!key) return Promise.resolve(null);
168
+ return Promise.resolve(
169
+ MEMBERSHIPS.find(
170
+ (m) =>
171
+ m.organizationId === key.organizationId && m.userId === key.userId,
172
+ ) ?? null,
173
+ );
174
+ },
175
+ );
176
+
177
+ for (const { model } of CONFIG_CASES) {
178
+ const store = new Map<
179
+ string,
180
+ { id: string; workspaceId: string; data: unknown }
181
+ >();
182
+ const delegate = prisma[model];
183
+ (delegate.findUnique as jest.Mock).mockImplementation(
184
+ ({ where }: { where: { workspaceId?: string } }) =>
185
+ Promise.resolve(
186
+ where.workspaceId ? (store.get(where.workspaceId) ?? null) : null,
187
+ ),
188
+ );
189
+ (delegate.upsert as jest.Mock).mockImplementation(
190
+ ({
191
+ where,
192
+ create,
193
+ update,
194
+ }: {
195
+ where: { workspaceId: string };
196
+ create: { workspaceId: string; data: unknown };
197
+ update: { data: unknown };
198
+ }) => {
199
+ const existing = store.get(where.workspaceId);
200
+ const row = existing
201
+ ? { ...existing, data: update.data }
202
+ : {
203
+ id: `${model}_row`,
204
+ workspaceId: create.workspaceId,
205
+ data: create.data,
206
+ };
207
+ store.set(where.workspaceId, row);
208
+ return Promise.resolve(row);
209
+ },
210
+ );
211
+ (delegate.deleteMany as jest.Mock).mockImplementation(
212
+ ({ where }: { where: { workspaceId?: string } }) => {
213
+ const had = where.workspaceId ? store.has(where.workspaceId) : false;
214
+ if (where.workspaceId) store.delete(where.workspaceId);
215
+ return Promise.resolve({ count: had ? 1 : 0 });
216
+ },
217
+ );
218
+ }
219
+ }
220
+
221
+ // ── App assembly (mirrors server.ts registration order) ─────────────────────
222
+
223
+ async function buildApp(): Promise<FastifyInstance> {
224
+ const app = Fastify();
225
+ app.register(authPreHandlerPlugin, { frontendOrigin: FRONTEND });
226
+ app.register(async (scope) => {
227
+ await scope.register(tenantPreHandlerPlugin);
228
+ await scope.register(workspaceConfigRoutes);
229
+ });
230
+ await app.ready();
231
+ return app;
232
+ }
233
+
234
+ function cookieHeaders(
235
+ token: string,
236
+ mutating = false,
237
+ ): Record<string, string> {
238
+ return {
239
+ cookie: `better-auth.session_token=${token}`,
240
+ ...(mutating ? { origin: FRONTEND } : {}),
241
+ };
242
+ }
243
+
244
+ describe("site-config tenant isolation — cross-org read/write denied on every config route", () => {
245
+ let app: FastifyInstance;
246
+
247
+ beforeEach(async () => {
248
+ jest.clearAllMocks();
249
+ installFixtureMocks();
250
+ app = await buildApp();
251
+ });
252
+
253
+ afterEach(async () => {
254
+ await app.close();
255
+ });
256
+
257
+ describe.each(CONFIG_CASES)("$type", ({ type, model, valid }) => {
258
+ const own = `/v1/workspaces/${WS_A}/${type}`;
259
+ const foreign = `/v1/workspaces/${WS_B}/${type}`;
260
+ // The mocked delegate for this config type — jest's `expect` accepts the
261
+ // mock fns directly (matching the sibling tenant-isolation.test.ts pattern).
262
+ const delegate = prisma[model];
263
+
264
+ // AC-1 — own-workspace write then read round-trips; server-side scope only.
265
+ it("PUT own workspace with a valid body → 200 and GET returns the stored blob", async () => {
266
+ const put = await app.inject({
267
+ method: "PUT",
268
+ url: own,
269
+ headers: cookieHeaders("cookie_alice", true),
270
+ payload: valid,
271
+ });
272
+ expect(put.statusCode).toBe(200);
273
+ expect(put.json().ok).toBe(true);
274
+ expect(put.json().data).toEqual(valid);
275
+ // The upsert is scoped to the resolved workspace, never a body-supplied id.
276
+ expect(delegate.upsert).toHaveBeenCalledWith(
277
+ expect.objectContaining({ where: { workspaceId: WS_A } }),
278
+ );
279
+
280
+ const get = await app.inject({
281
+ method: "GET",
282
+ url: own,
283
+ headers: cookieHeaders("cookie_alice"),
284
+ });
285
+ expect(get.statusCode).toBe(200);
286
+ expect(get.json().data).toEqual(valid);
287
+ });
288
+
289
+ // AC-4 — GET a not-yet-configured workspace → data: null (not 404).
290
+ it("GET an unconfigured workspace → { ok: true, data: null }", async () => {
291
+ const res = await app.inject({
292
+ method: "GET",
293
+ url: own,
294
+ headers: cookieHeaders("cookie_alice"),
295
+ });
296
+ expect(res.statusCode).toBe(200);
297
+ expect(res.json()).toEqual({ ok: true, data: null });
298
+ });
299
+
300
+ // AC-3 — invalid body → 400, no upsert.
301
+ it("PUT with a schema-invalid body → 400 validation_failed, no upsert runs", async () => {
302
+ const res = await app.inject({
303
+ method: "PUT",
304
+ url: own,
305
+ headers: cookieHeaders("cookie_alice", true),
306
+ payload: { bogus: true },
307
+ });
308
+ expect(res.statusCode).toBe(400);
309
+ expect(res.json().error.code).toBe("validation_failed");
310
+ expect(delegate.upsert).not.toHaveBeenCalled();
311
+ });
312
+
313
+ // AC-2 — cross-workspace READ denied, no config query runs.
314
+ it("GET a foreign workspace's config → 403, no config query runs", async () => {
315
+ const res = await app.inject({
316
+ method: "GET",
317
+ url: foreign,
318
+ headers: cookieHeaders("cookie_alice"),
319
+ });
320
+ expect(res.statusCode).toBe(403);
321
+ expect(delegate.findUnique).not.toHaveBeenCalled();
322
+ });
323
+
324
+ // AC-2 — cross-workspace WRITE denied, no config query runs.
325
+ it("PUT a foreign workspace's config → 403, no upsert runs", async () => {
326
+ const res = await app.inject({
327
+ method: "PUT",
328
+ url: foreign,
329
+ headers: cookieHeaders("cookie_alice", true),
330
+ payload: valid,
331
+ });
332
+ expect(res.statusCode).toBe(403);
333
+ expect(delegate.upsert).not.toHaveBeenCalled();
334
+ });
335
+
336
+ // AC-2 — unauthenticated → 401.
337
+ it("GET without a session → 401", async () => {
338
+ const res = await app.inject({ method: "GET", url: own });
339
+ expect(res.statusCode).toBe(401);
340
+ });
341
+
342
+ // Grant path works for the other tenant too.
343
+ it("the owning tenant of the other org CAN write its own config", async () => {
344
+ const res = await app.inject({
345
+ method: "PUT",
346
+ url: `/v1/workspaces/${WS_B}/${type}`,
347
+ headers: cookieHeaders("cookie_bob", true),
348
+ payload: valid,
349
+ });
350
+ expect(res.statusCode).toBe(200);
351
+ expect(delegate.upsert).toHaveBeenCalledWith(
352
+ expect.objectContaining({ where: { workspaceId: WS_B } }),
353
+ );
354
+ });
355
+ });
356
+ });