create-ailk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +12 -5
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +78 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -0,0 +1,221 @@
1
+ // oss: false
2
+ /**
3
+ * @file me-route-precedence.test.ts
4
+ *
5
+ * The regression net for the one structural risk in #5295: the static
6
+ * `/v1/project-listings/me` and the parametric `/v1/project-listings/:id` share
7
+ * a prefix and are registered in DIFFERENT scopes — `/me` in `server.ts`'s
8
+ * `authScope`, `/:id` inside the encapsulated `tenantScope` behind
9
+ * `tenantPreHandlerPlugin`.
10
+ *
11
+ * `me.test.ts` registers `/me` alone, so nothing there would catch a
12
+ * registration reorder that shadowed `/me` or weakened the tenant guard on
13
+ * `/:id`. The security review for this change verified the behaviour by hand and
14
+ * named the missing net; this file is that net.
15
+ *
16
+ * It asserts three things in ONE Fastify instance with the REAL preHandlers:
17
+ * 1. `/v1/project-listings/me` reaches the `/me` handler — static beats
18
+ * parametric — and runs auth ONLY, no tenant guard.
19
+ * 2. `/v1/project-listings/<id>` still reaches the tenant handler WITH the
20
+ * tenant guard: registering a static sibling in the outer scope does not
21
+ * weaken it. A member below the `owner|admin` floor is still refused.
22
+ * 3. Registering both raises no duplicate-route error.
23
+ *
24
+ * This file is `oss: false` because it imports the multi-tenant slice
25
+ * (`middleware/tenant.js`, `./get.js`), the same reason `tenant-isolation.test.ts`
26
+ * is.
27
+ */
28
+ import Fastify from "fastify";
29
+ import type { FastifyInstance } from "fastify";
30
+
31
+ jest.mock("@working-theory/email", () => ({
32
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
33
+ }));
34
+ jest.mock("@working-theory/license", () => ({
35
+ checkLicense: jest.fn().mockResolvedValue({
36
+ tier: "free",
37
+ plan: null,
38
+ features: [],
39
+ expiresAt: null,
40
+ revoked: false,
41
+ }),
42
+ }));
43
+
44
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
45
+ jest.mock("../../../lib/prisma.js", () => ({
46
+ prisma: {
47
+ session: { findUnique: jest.fn() },
48
+ apiToken: {
49
+ findUnique: jest.fn(),
50
+ update: jest.fn().mockResolvedValue(undefined),
51
+ },
52
+ user: { findUnique: jest.fn() },
53
+ workspace: { findUnique: jest.fn(), count: jest.fn() },
54
+ membership: { findUnique: jest.fn() },
55
+ projectListing: { findMany: jest.fn(), findFirst: jest.fn() },
56
+ waitlistScore: { count: jest.fn() },
57
+ },
58
+ }));
59
+
60
+ import { prisma } from "../../../lib/prisma.js";
61
+ import { authPreHandlerPlugin } from "../../../middleware/auth.js";
62
+ import { tenantPreHandlerPlugin } from "../../../middleware/tenant.js";
63
+ import { projectListingMeRoute, projectListingTenantRoutes } from "../index.js";
64
+
65
+ const FRONTEND = "https://app.example.com";
66
+ const FUTURE = new Date(Date.now() + 60 * 60 * 1000);
67
+
68
+ const ORG = "org_00000000-0000-7000-8000-000000000001";
69
+ const WS = "wksp_00000000-0000-7000-8000-000000000001";
70
+ const LISTING_ID = "prjl_00000000-0000-7000-8000-0000000000d1";
71
+
72
+ /** `owner` clears the floor; `member` does not. Both are real members of ORG. */
73
+ const SESSION_ROWS: Record<string, Record<string, unknown>> = {
74
+ cookie_owner: {
75
+ id: "sess_owner",
76
+ userId: "owner-user",
77
+ expiresAt: FUTURE,
78
+ activeOrganizationId: ORG,
79
+ activeWorkspaceId: WS,
80
+ },
81
+ cookie_member: {
82
+ id: "sess_member",
83
+ userId: "member-user",
84
+ expiresAt: FUTURE,
85
+ activeOrganizationId: ORG,
86
+ activeWorkspaceId: WS,
87
+ },
88
+ };
89
+
90
+ const MEMBERSHIPS: Record<string, string> = {
91
+ "owner-user": "owner",
92
+ "member-user": "member",
93
+ };
94
+
95
+ async function buildApp(): Promise<FastifyInstance> {
96
+ // The exact nesting server.ts uses: /me in the OUTER auth scope, the tenant
97
+ // routes in an INNER scope behind the tenant preHandler.
98
+ const app = Fastify();
99
+ await app.register(async (authScope) => {
100
+ await authScope.register(authPreHandlerPlugin, { frontendOrigin: FRONTEND });
101
+ await authScope.register(projectListingMeRoute);
102
+ await authScope.register(async (tenantScope) => {
103
+ await tenantScope.register(tenantPreHandlerPlugin);
104
+ await tenantScope.register(projectListingTenantRoutes);
105
+ });
106
+ });
107
+ await app.ready();
108
+ return app;
109
+ }
110
+
111
+ const cookie = (token: string) => ({ cookie: `better-auth.session_token=${token}` });
112
+
113
+ let app: FastifyInstance;
114
+
115
+ beforeEach(async () => {
116
+ jest.clearAllMocks();
117
+ (prisma.session.findUnique as jest.Mock).mockImplementation(
118
+ ({ where }: { where: { token?: string } }) =>
119
+ Promise.resolve(where.token ? (SESSION_ROWS[where.token] ?? null) : null),
120
+ );
121
+ (prisma.apiToken.findUnique as jest.Mock).mockResolvedValue(null);
122
+ (prisma.user.findUnique as jest.Mock).mockImplementation(({ where }: { where: { id: string } }) =>
123
+ Promise.resolve({ id: where.id, email: `${where.id}@example.com`, emailVerified: true }),
124
+ );
125
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({ id: WS, organizationId: ORG });
126
+ // The composite key is `organizationId_userId` — the order `tenant-context.ts`
127
+ // actually queries with.
128
+ (prisma.membership.findUnique as jest.Mock).mockImplementation(
129
+ ({ where }: { where: { organizationId_userId?: { userId: string } } }) => {
130
+ const userId = where.organizationId_userId?.userId;
131
+ const role = userId ? MEMBERSHIPS[userId] : undefined;
132
+ return Promise.resolve(role ? { role } : null);
133
+ },
134
+ );
135
+ // The /me read returns nothing; this file is about routing, not payloads.
136
+ (prisma.projectListing.findMany as jest.Mock).mockResolvedValue([]);
137
+ (prisma.projectListing.findFirst as jest.Mock).mockResolvedValue(null);
138
+ app = await buildApp();
139
+ });
140
+
141
+ afterEach(async () => {
142
+ await app.close();
143
+ });
144
+
145
+ describe("route precedence: static /me beside parametric /:id in one instance", () => {
146
+ it("registering both raises no duplicate-route error", async () => {
147
+ // buildApp() already awaited app.ready(); reaching here is the assertion.
148
+ const tree = app.printRoutes();
149
+ expect(tree).toContain("me");
150
+ expect(tree).toContain(":id");
151
+ });
152
+
153
+ it("/v1/project-listings/me reaches the /me handler, not the parametric one", async () => {
154
+ const res = await app.inject({
155
+ method: "GET",
156
+ url: "/v1/project-listings/me",
157
+ headers: cookie("cookie_owner"),
158
+ });
159
+
160
+ // The /me handler answers its own 200 + empty list. The parametric handler
161
+ // would have answered 400 `validation_failed` — "me" is not a listing id.
162
+ expect(res.statusCode).toBe(200);
163
+ expect(res.json()).toEqual({ ok: true, data: [] });
164
+ // And it read through the owner link, not by id.
165
+ expect(prisma.projectListing.findMany).toHaveBeenCalledWith(
166
+ expect.objectContaining({ where: { ownerUserId: "owner-user" } }),
167
+ );
168
+ expect(prisma.projectListing.findFirst).not.toHaveBeenCalled();
169
+ });
170
+
171
+ it("/me runs auth ONLY — a caller BELOW the owner|admin floor still gets their own rows", async () => {
172
+ const res = await app.inject({
173
+ method: "GET",
174
+ url: "/v1/project-listings/me",
175
+ headers: cookie("cookie_member"),
176
+ });
177
+
178
+ // A `member` is refused by the tenant routes and allowed here: /me is not
179
+ // behind the floor, and it does not need to be — the owner link is the gate.
180
+ expect(res.statusCode).toBe(200);
181
+ expect(res.json()).toEqual({ ok: true, data: [] });
182
+ });
183
+
184
+ it("the parametric /:id still runs the tenant guard — the static sibling does not weaken it", async () => {
185
+ const ownerRes = await app.inject({
186
+ method: "GET",
187
+ url: `/v1/project-listings/${LISTING_ID}`,
188
+ headers: cookie("cookie_owner"),
189
+ });
190
+ // Owner clears the floor and reaches the service, which finds no row.
191
+ expect(ownerRes.statusCode).toBe(404);
192
+
193
+ const memberRes = await app.inject({
194
+ method: "GET",
195
+ url: `/v1/project-listings/${LISTING_ID}`,
196
+ headers: cookie("cookie_member"),
197
+ });
198
+ // Member is still refused BEFORE any read — the floor is intact.
199
+ expect(memberRes.statusCode).toBe(403);
200
+ });
201
+
202
+ it("the tenant LIST route is unaffected and still refuses a member", async () => {
203
+ const res = await app.inject({
204
+ method: "GET",
205
+ url: "/v1/project-listings",
206
+ headers: cookie("cookie_member"),
207
+ });
208
+
209
+ expect(res.statusCode).toBe(403);
210
+ });
211
+
212
+ it("an unauthenticated caller is refused on both paths", async () => {
213
+ const meRes = await app.inject({ method: "GET", url: "/v1/project-listings/me" });
214
+ const idRes = await app.inject({ method: "GET", url: `/v1/project-listings/${LISTING_ID}` });
215
+
216
+ expect(meRes.statusCode).toBe(401);
217
+ expect(idRes.statusCode).toBe(401);
218
+ expect(prisma.projectListing.findMany).not.toHaveBeenCalled();
219
+ expect(prisma.projectListing.findFirst).not.toHaveBeenCalled();
220
+ });
221
+ });
@@ -0,0 +1,468 @@
1
+ /**
2
+ * @file me.test.ts
3
+ *
4
+ * THE ACCEPTANCE GATE for the founder's own listings read (#5295).
5
+ * Registration mirrors server.ts: REAL `authPreHandlerPlugin ->
6
+ * projectListingMeRoute` (auth only, NO tenant guard), mocked `lib/prisma.js`.
7
+ *
8
+ * Proves the security properties the owner link exists for:
9
+ * - two users sharing a `founderEmail` value never see each other's rows;
10
+ * - a row whose owner link was never set is invisible to the person whose
11
+ * address it carries — the read never falls back to an email comparison;
12
+ * - the `where` clause carries `ownerUserId` and nothing resembling an email
13
+ * (a clause carrying one is treated as a contract violation and returns
14
+ * nothing, the `flow-checkouts` test's own discipline);
15
+ * - a signed-in caller who owns nothing gets `200 []`, never a 403;
16
+ * - `PROJECT_LISTING_ROLES` is untouched and this route never consults it.
17
+ */
18
+ import Fastify from "fastify";
19
+ import type { FastifyInstance } from "fastify";
20
+ import { projectListingMeResponseSchema } from "@working-theory/validation";
21
+
22
+ // `services/project-listings.ts` imports `routeLead` from `lead-routing.js` at
23
+ // module scope, which transitively imports `@working-theory/email` and
24
+ // `@working-theory/license` — mocked here purely so that import chain resolves,
25
+ // mirroring `tenant-isolation.test.ts`'s discipline.
26
+ jest.mock("@working-theory/email", () => ({
27
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
28
+ }));
29
+ jest.mock("@working-theory/license", () => ({
30
+ checkLicense: jest.fn().mockResolvedValue({
31
+ tier: "free",
32
+ plan: null,
33
+ features: [],
34
+ expiresAt: null,
35
+ revoked: false,
36
+ }),
37
+ }));
38
+
39
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
40
+ jest.mock("../../../lib/prisma.js", () => ({
41
+ prisma: {
42
+ session: { findUnique: jest.fn() },
43
+ apiToken: {
44
+ findUnique: jest.fn(),
45
+ update: jest.fn().mockResolvedValue(undefined),
46
+ },
47
+ user: { findUnique: jest.fn() },
48
+ projectListing: { findMany: jest.fn() },
49
+ waitlistScore: { count: jest.fn() },
50
+ },
51
+ }));
52
+
53
+ import { prisma } from "../../../lib/prisma.js";
54
+ import { authPreHandlerPlugin } from "../../../middleware/auth.js";
55
+ import { PROJECT_LISTING_ROLES } from "../../../services/project-listings.js";
56
+ import { projectListingMeRoute } from "../index.js";
57
+
58
+ const FRONTEND = "https://app.example.com";
59
+ const FUTURE = new Date(Date.now() + 60 * 60 * 1000);
60
+
61
+ const SITE_A = "wksp_00000000-0000-7000-8000-000000000001";
62
+ const SITE_B = "wksp_00000000-0000-7000-8000-000000000002";
63
+
64
+ /**
65
+ * `ada` and `bruno` are TWO DIFFERENT PEOPLE who typed the SAME address into
66
+ * the anonymous application form. That is the whole point: `founderEmail` is
67
+ * not an identity, and nothing may derive ownership from it.
68
+ */
69
+ const SHARED_EMAIL = "founder@example.com";
70
+
71
+ const USERS: Record<string, { id: string; email: string; emailVerified: boolean }> = {
72
+ ada: { id: "ada", email: SHARED_EMAIL, emailVerified: true },
73
+ bruno: { id: "bruno", email: SHARED_EMAIL, emailVerified: true },
74
+ // Signed in, verified, owns nothing at all.
75
+ cleo: { id: "cleo", email: "cleo@example.com", emailVerified: true },
76
+ // Signed in with an address that was never verified, and whose own listing
77
+ // WAS linked earlier. See the test that covers this case.
78
+ dane: { id: "dane", email: "dane@example.com", emailVerified: false },
79
+ };
80
+
81
+ const SESSION_ROWS: Record<string, Record<string, unknown>> = {
82
+ cookie_ada: { id: "sess_ada", userId: "ada", expiresAt: FUTURE, activeOrganizationId: null, activeWorkspaceId: null },
83
+ cookie_bruno: { id: "sess_bruno", userId: "bruno", expiresAt: FUTURE, activeOrganizationId: null, activeWorkspaceId: null },
84
+ cookie_cleo: { id: "sess_cleo", userId: "cleo", expiresAt: FUTURE, activeOrganizationId: null, activeWorkspaceId: null },
85
+ cookie_dane: { id: "sess_dane", userId: "dane", expiresAt: FUTURE, activeOrganizationId: null, activeWorkspaceId: null },
86
+ };
87
+
88
+ type ListingFixture = {
89
+ id: string;
90
+ siteId: string;
91
+ founderEmail: string;
92
+ ownerUserId: string | null;
93
+ experimentKey: string | null;
94
+ createdAt: Date;
95
+ };
96
+
97
+ /** A complete stored row at `PROJECT_LISTING_SELECT`'s shape. */
98
+ function row(fixture: ListingFixture) {
99
+ return {
100
+ id: fixture.id,
101
+ siteId: fixture.siteId,
102
+ leadId: `lead_${fixture.id}`,
103
+ slug: fixture.id.replace("prjl_", ""),
104
+ name: "Atlas",
105
+ oneLiner: "A one-liner.",
106
+ problem: "A problem.",
107
+ url: "https://atlas.example.com",
108
+ product: "live-some-users",
109
+ commitment: "full-time",
110
+ teamComposition: "just-me",
111
+ faqs: null,
112
+ anythingElse: null,
113
+ address: "1 Main St",
114
+ addressLine1: null,
115
+ addressLine2: null,
116
+ addressCity: null,
117
+ addressRegion: null,
118
+ addressPostalCode: null,
119
+ addressCountry: null,
120
+ questions: null,
121
+ siteAnswers: null,
122
+ windowDays: 30,
123
+ founderName: "A Founder",
124
+ founderEmail: fixture.founderEmail,
125
+ founderCompany: null,
126
+ headline: "Atlas",
127
+ logoMarkUrl: null,
128
+ logoWordmarkUrl: null,
129
+ featured: false,
130
+ status: "requested",
131
+ consentAt: fixture.createdAt,
132
+ decidedAt: null,
133
+ decidedBy: "operator@example.com",
134
+ issueUrl: "https://github.com/example/repo/issues/1",
135
+ experimentKey: fixture.experimentKey,
136
+ windowEndsAt: null,
137
+ createdAt: fixture.createdAt,
138
+ updatedAt: fixture.createdAt,
139
+ };
140
+ }
141
+
142
+ const LISTINGS: ListingFixture[] = [
143
+ // ada's own, linked to ada.
144
+ {
145
+ id: "prjl_ada_one",
146
+ siteId: SITE_A,
147
+ founderEmail: SHARED_EMAIL,
148
+ ownerUserId: "ada",
149
+ experimentKey: "atlas-waitlist",
150
+ createdAt: new Date("2026-09-01T00:00:00.000Z"),
151
+ },
152
+ // ada again, NEWER, and in a SECOND workspace of ada's — proves the per-site
153
+ // waitlistCount resolution and the newest-first ordering.
154
+ {
155
+ id: "prjl_ada_two",
156
+ siteId: SITE_B,
157
+ founderEmail: SHARED_EMAIL,
158
+ ownerUserId: "ada",
159
+ experimentKey: "atlas-two-waitlist",
160
+ createdAt: new Date("2026-09-05T00:00:00.000Z"),
161
+ },
162
+ // bruno's own — SAME founderEmail as ada's rows, different person.
163
+ {
164
+ id: "prjl_bruno_one",
165
+ siteId: SITE_A,
166
+ founderEmail: SHARED_EMAIL,
167
+ ownerUserId: "bruno",
168
+ experimentKey: null,
169
+ createdAt: new Date("2026-09-03T00:00:00.000Z"),
170
+ },
171
+ // Carries the shared address and was NEVER linked. Invisible to everyone.
172
+ {
173
+ id: "prjl_unlinked",
174
+ siteId: SITE_A,
175
+ founderEmail: SHARED_EMAIL,
176
+ ownerUserId: null,
177
+ experimentKey: null,
178
+ createdAt: new Date("2026-09-04T00:00:00.000Z"),
179
+ },
180
+ // dane's own, linked while dane's address was still verified.
181
+ {
182
+ id: "prjl_dane_one",
183
+ siteId: SITE_A,
184
+ founderEmail: "dane@example.com",
185
+ ownerUserId: "dane",
186
+ experimentKey: null,
187
+ createdAt: new Date("2026-09-02T00:00:00.000Z"),
188
+ },
189
+ ];
190
+
191
+ type ListingWhere = Record<string, unknown> & { ownerUserId?: unknown };
192
+
193
+ const WAITLIST_COUNTS: Record<string, Record<string, number>> = {
194
+ [SITE_A]: { "atlas-waitlist": 7 },
195
+ [SITE_B]: { "atlas-two-waitlist": 3 },
196
+ };
197
+
198
+ function installFixtureMocks(): void {
199
+ (prisma.session.findUnique as jest.Mock).mockImplementation(
200
+ ({ where }: { where: { token?: string } }) =>
201
+ Promise.resolve(where.token ? (SESSION_ROWS[where.token] ?? null) : null),
202
+ );
203
+ (prisma.apiToken.findUnique as jest.Mock).mockResolvedValue(null);
204
+ (prisma.user.findUnique as jest.Mock).mockImplementation(({ where }: { where: { id: string } }) =>
205
+ Promise.resolve(USERS[where.id] ?? null),
206
+ );
207
+
208
+ /**
209
+ * The owner link is the ONLY join the service may use, and this mock is
210
+ * written to FAIL on a regression rather than to absorb one.
211
+ *
212
+ * Two earlier, weaker forms were rejected during review, both of which let a
213
+ * regression keep the security tests green:
214
+ *
215
+ * - `expect(where).not.toHaveProperty("founderEmail")` inspects TOP-LEVEL
216
+ * keys only, so an `OR`-wrapped email fallback
217
+ * (`{ OR: [{ ownerUserId }, { founderEmail }] }`) sails straight past it —
218
+ * and a mock that then filtered on `where.ownerUserId` returns `[]` for
219
+ * that shape, which looks like "nothing leaked".
220
+ * - Filtering on `where.ownerUserId` when it is `undefined` also returns
221
+ * `[]`, while Prisma compiles an `undefined` filter to no filter at all
222
+ * (`WHERE 1=1`) and the REAL read would be unscoped.
223
+ *
224
+ * So: any key other than `ownerUserId` THROWS, an `ownerUserId` that is not a
225
+ * string THROWS, and the filter is exact equality on it. A widened read now
226
+ * fails as a thrown error rather than as a quietly empty result.
227
+ */
228
+ (prisma.projectListing.findMany as jest.Mock).mockImplementation(
229
+ ({ where }: { where: ListingWhere }) => {
230
+ const keys = Object.keys(where);
231
+ if (keys.length !== 1 || keys[0] !== "ownerUserId") {
232
+ throw new Error(
233
+ `the /me read must scope on ownerUserId and nothing else; got where keys [${keys.join(", ")}]`,
234
+ );
235
+ }
236
+ if (typeof where.ownerUserId !== "string") {
237
+ throw new Error(
238
+ "ownerUserId must be a concrete string; Prisma compiles an undefined filter to no filter, leaving the read unscoped",
239
+ );
240
+ }
241
+ const ownerUserId = where.ownerUserId;
242
+ return Promise.resolve(
243
+ LISTINGS.filter((l) => l.ownerUserId === ownerUserId)
244
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
245
+ .map(row),
246
+ );
247
+ },
248
+ );
249
+
250
+ (prisma.waitlistScore.count as jest.Mock).mockImplementation(
251
+ ({ where }: { where: { siteId: string; experimentKey: string } }) =>
252
+ Promise.resolve(WAITLIST_COUNTS[where.siteId]?.[where.experimentKey] ?? 0),
253
+ );
254
+ }
255
+
256
+ async function buildApp(): Promise<FastifyInstance> {
257
+ const app = Fastify();
258
+ await app.register(async (authScope) => {
259
+ await authScope.register(authPreHandlerPlugin, { frontendOrigin: FRONTEND });
260
+ await authScope.register(projectListingMeRoute);
261
+ });
262
+ await app.ready();
263
+ return app;
264
+ }
265
+
266
+ const cookie = (token: string) => ({ cookie: `better-auth.session_token=${token}` });
267
+ const URL_ME = "/v1/project-listings/me";
268
+
269
+ const listingWhere = (call = 0) =>
270
+ (prisma.projectListing.findMany as jest.Mock).mock.calls[call][0].where as ListingWhere;
271
+
272
+ let app: FastifyInstance;
273
+
274
+ beforeEach(async () => {
275
+ jest.clearAllMocks();
276
+ installFixtureMocks();
277
+ app = await buildApp();
278
+ });
279
+
280
+ afterEach(async () => {
281
+ await app.close();
282
+ });
283
+
284
+ describe("GET /v1/project-listings/me — auth", () => {
285
+ it("no session cookie and no bearer → the preHandler's existing 401; no listing read", async () => {
286
+ const res = await app.inject({ method: "GET", url: URL_ME });
287
+
288
+ expect(res.statusCode).toBe(401);
289
+ expect(prisma.projectListing.findMany).not.toHaveBeenCalled();
290
+ });
291
+
292
+ it("an expired / unknown cookie → 401", async () => {
293
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_nobody") });
294
+
295
+ expect(res.statusCode).toBe(401);
296
+ expect(prisma.projectListing.findMany).not.toHaveBeenCalled();
297
+ });
298
+ });
299
+
300
+ describe("GET /v1/project-listings/me — the owner link is the only join", () => {
301
+ /** SECURITY PROPERTY: two users sharing a founderEmail value. */
302
+ it("two users sharing a founderEmail never see each other's listings", async () => {
303
+ const adaRes = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
304
+ const brunoRes = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_bruno") });
305
+
306
+ expect(adaRes.statusCode).toBe(200);
307
+ expect(brunoRes.statusCode).toBe(200);
308
+
309
+ const adaIds = (adaRes.json().data as Array<{ id: string }>).map((r) => r.id);
310
+ const brunoIds = (brunoRes.json().data as Array<{ id: string }>).map((r) => r.id);
311
+
312
+ // ada sees her two rows, newest first, and NOT bruno's.
313
+ expect(adaIds).toEqual(["prjl_ada_two", "prjl_ada_one"]);
314
+ // bruno sees his one row, and NOT ada's — though every row in play carries
315
+ // the identical founderEmail.
316
+ expect(brunoIds).toEqual(["prjl_bruno_one"]);
317
+
318
+ expect(adaIds).not.toContain("prjl_bruno_one");
319
+ expect(brunoIds).not.toContain("prjl_ada_one");
320
+ expect(brunoIds).not.toContain("prjl_ada_two");
321
+ // And neither of them reaches the un-linked row carrying the same address.
322
+ expect([...adaIds, ...brunoIds]).not.toContain("prjl_unlinked");
323
+ });
324
+
325
+ /** SECURITY PROPERTY: no read-time email fallback. */
326
+ it("a listing carrying the caller's own address but NO owner link is never returned", async () => {
327
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
328
+
329
+ const ids = (res.json().data as Array<{ id: string }>).map((r) => r.id);
330
+ expect(ids).not.toContain("prjl_unlinked");
331
+ });
332
+
333
+ it("the where clause is the owner link and carries nothing email-shaped", async () => {
334
+ await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
335
+
336
+ expect(listingWhere()).toEqual({ ownerUserId: "ada" });
337
+ });
338
+
339
+ it("a query parameter is never read — ?email= and ?ownerUserId= change nothing", async () => {
340
+ const res = await app.inject({
341
+ method: "GET",
342
+ url: `${URL_ME}?email=${encodeURIComponent(SHARED_EMAIL)}&ownerUserId=bruno`,
343
+ headers: cookie("cookie_ada"),
344
+ });
345
+
346
+ expect(res.statusCode).toBe(200);
347
+ // Still ada's own rows, scoped by the SESSION's user id.
348
+ expect(listingWhere()).toEqual({ ownerUserId: "ada" });
349
+ expect((res.json().data as Array<{ id: string }>).map((r) => r.id)).toEqual([
350
+ "prjl_ada_two",
351
+ "prjl_ada_one",
352
+ ]);
353
+ });
354
+ });
355
+
356
+ describe("GET /v1/project-listings/me — the empty case is not a refusal", () => {
357
+ /** SECURITY / UX PROPERTY: owning nothing is an empty list, never a 403. */
358
+ it("a signed-in caller who owns no listing gets 200 with an empty list, not a 403", async () => {
359
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_cleo") });
360
+
361
+ expect(res.statusCode).toBe(200);
362
+ expect(res.json()).toEqual({ ok: true, data: [] });
363
+ // Specifically NOT the tenant routes' 403 — cleo is legitimately signed in.
364
+ expect(res.statusCode).not.toBe(403);
365
+ });
366
+
367
+ it("no waitlist count is read when the caller owns nothing", async () => {
368
+ await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_cleo") });
369
+
370
+ expect(prisma.waitlistScore.count).not.toHaveBeenCalled();
371
+ });
372
+ });
373
+
374
+ describe("GET /v1/project-listings/me — the response shape", () => {
375
+ it("every row parses under projectListingMeResponseSchema", async () => {
376
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
377
+
378
+ expect(res.statusCode).toBe(200);
379
+ const parsed = projectListingMeResponseSchema.safeParse(res.json().data);
380
+ expect(parsed.success).toBe(true);
381
+ });
382
+
383
+ it("the five internal references never reach the founder", async () => {
384
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
385
+
386
+ for (const listing of res.json().data as Array<Record<string, unknown>>) {
387
+ expect(listing).not.toHaveProperty("siteId");
388
+ expect(listing).not.toHaveProperty("leadId");
389
+ expect(listing).not.toHaveProperty("decidedBy");
390
+ expect(listing).not.toHaveProperty("issueUrl");
391
+ expect(listing).not.toHaveProperty("experimentKey");
392
+ }
393
+ // No workspace id reaches the wire anywhere in the body.
394
+ expect(res.body).not.toContain(SITE_A);
395
+ expect(res.body).not.toContain(SITE_B);
396
+ });
397
+
398
+ it("waitlistCount is resolved per site, not all under one site id", async () => {
399
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
400
+
401
+ const byId = Object.fromEntries(
402
+ (res.json().data as Array<{ id: string; waitlistCount: number }>).map((r) => [
403
+ r.id,
404
+ r.waitlistCount,
405
+ ]),
406
+ );
407
+ // prjl_ada_one's experiment lives at SITE_A (7); prjl_ada_two's at SITE_B (3).
408
+ // One shared site id would have reported 0 for one of them.
409
+ expect(byId["prjl_ada_one"]).toBe(7);
410
+ expect(byId["prjl_ada_two"]).toBe(3);
411
+ expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
412
+ where: { siteId: SITE_A, experimentKey: "atlas-waitlist" },
413
+ });
414
+ expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
415
+ where: { siteId: SITE_B, experimentKey: "atlas-two-waitlist" },
416
+ });
417
+ });
418
+
419
+ it("the founder's own email is echoed back to the founder who wrote it", async () => {
420
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_bruno") });
421
+
422
+ expect((res.json().data as Array<{ founderEmail: string }>)[0]?.founderEmail).toBe(
423
+ SHARED_EMAIL,
424
+ );
425
+ });
426
+ });
427
+
428
+ describe("GET /v1/project-listings/me — the read-time verification posture", () => {
429
+ /**
430
+ * A caller whose address is no longer verified still reads the rows their
431
+ * link already covers. This is deliberate, and it is the one place this route
432
+ * diverges from `/v1/flow-checkouts/me`, which re-checks `emailVerified`.
433
+ *
434
+ * The re-check is load-bearing on `/v1/waitlist-signups/me`, where the match
435
+ * IS an email, and it is defence in depth on `/v1/flow-checkouts/me`. Here it
436
+ * would be neither: the link is the proof, it was only ever issuable under
437
+ * verification, and refusing on a later unverified address would hide rows
438
+ * the caller already proved they own (changing your email address is not a
439
+ * reason to lose your own submissions). An unverified address still cannot
440
+ * CREATE a link — `listing-promotion.ts` refuses before any listing read.
441
+ */
442
+ it("a caller whose address is no longer verified still reads the rows their link already covers", async () => {
443
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_dane") });
444
+
445
+ expect(res.statusCode).toBe(200);
446
+ expect((res.json().data as Array<{ id: string }>).map((r) => r.id)).toEqual([
447
+ "prjl_dane_one",
448
+ ]);
449
+ expect(listingWhere()).toEqual({ ownerUserId: "dane" });
450
+ });
451
+ });
452
+
453
+ describe("the existing operator role floor is untouched", () => {
454
+ it("PROJECT_LISTING_ROLES is still exactly owner + admin", () => {
455
+ expect([...PROJECT_LISTING_ROLES].sort()).toEqual(["admin", "owner"]);
456
+ expect(PROJECT_LISTING_ROLES.size).toBe(2);
457
+ });
458
+
459
+ it("the /me route never consults the tenant context or the role floor", async () => {
460
+ // The app this suite builds registers NO tenant preHandler at all, so
461
+ // `request.tenant` is undefined for every request above. That every test in
462
+ // this file answers 200 is the proof: a route that consulted the floor
463
+ // would have answered 401/403 instead.
464
+ const res = await app.inject({ method: "GET", url: URL_ME, headers: cookie("cookie_ada") });
465
+
466
+ expect(res.statusCode).toBe(200);
467
+ });
468
+ });