create-ailk 0.1.1 → 0.3.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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -0,0 +1,86 @@
1
+ /**
2
+ * NOT marked `oss: false` (D-48/#5366 escalation, mirrors the original
3
+ * multi-tenant/#5088 card's own conclusion about this exact file, lines
4
+ * 556-571 of module-architecture.ts): `./index.ts` in this directory is a
5
+ * MIXED barrel that statically imports this file, and `server.ts` reaches
6
+ * `./index.ts` via a plain (non-slice) literal `import()` for multi-tenant's
7
+ * tenant-scoped routes — a specifier `tsc` always resolves, module selection
8
+ * or not. Stripping this file leaves that barrel with a dangling import on
9
+ * every tree. Registration stays gated (`isModuleEnabled("signal-collectors")`
10
+ * in server.ts); only the source file itself must ship unconditionally.
11
+ *
12
+ * GET /v1/project-listings/me
13
+ *
14
+ * The FOUNDER'S OWN listings read (#5295) — the `/v1/flow-checkouts/me` and
15
+ * `/v1/waitlist-signups/me` posture, for `ProjectListing`.
16
+ *
17
+ * Registered INSIDE `server.ts`'s `authScope` (auth only — NOT the inner
18
+ * `tenantScope`): a founder is not a workspace member, and the tenant guard is
19
+ * default-deny, so a founder reaching the tenant-scoped list route gets a 403
20
+ * for their own submission. That is the problem this route exists to solve, and
21
+ * it is solved by ADDING this route rather than by relaxing
22
+ * `PROJECT_LISTING_ROLES` — the operator floor on
23
+ * `GET|PATCH /v1/project-listings[/:id]` is unchanged and still `owner|admin`.
24
+ *
25
+ * Identity is `request.userId` ONLY. The route declares no query schema and
26
+ * reads no query or body key — an `email` parameter is never parsed and never
27
+ * read — and the service joins on the owner link and nothing else
28
+ * (`services/project-listings.ts`'s `listMyProjectListings`: `ownerUserId ===
29
+ * userId`, never a `founderEmail` comparison at read time).
30
+ *
31
+ * Route precedence: this static path is registered alongside the parametric
32
+ * `/v1/project-listings/:id` in the tenant scope. Fastify's router prefers a
33
+ * static segment over a parametric one, so `/me` reaches this handler, exactly
34
+ * as the already-shipped static `/v1/project-listings/public` coexists with the
35
+ * same parametric route.
36
+ *
37
+ * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
38
+ * `{ ok: false, error: { code, message } }`, real HTTP status codes:
39
+ * 401 unauthorized · 500 internal_error (the 200 body failed its own
40
+ * contract schema — a drift is never a leak). A signed-in caller who owns
41
+ * NOTHING is a 200 with an empty list, never a 403: they are legitimately
42
+ * signed in and simply own no listing yet.
43
+ *
44
+ * API↔MCP parity: identity-bound (the session IS the identity; the OSS MCP
45
+ * surface is identity-blind) — no MCP twin (#3591 exception, the same shape as
46
+ * `/v1/flow-checkouts/me` and `/v1/waitlist-signups/me`).
47
+ */
48
+ import { projectListingMeResponseSchema } from "@working-theory/validation";
49
+ import type { FastifyPluginAsync, FastifyReply } from "fastify";
50
+
51
+ import { listMyProjectListings } from "../../services/project-listings.js";
52
+
53
+ import { sendValidated } from "./respond.js";
54
+
55
+ function unauthorized(reply: FastifyReply) {
56
+ return reply.status(401).send({
57
+ ok: false,
58
+ error: { code: "unauthorized", message: "Authentication required" },
59
+ });
60
+ }
61
+
62
+ export const projectListingMeRoute: FastifyPluginAsync = async (fastify) => {
63
+ fastify.get("/v1/project-listings/me", async (request, reply) => {
64
+ const userId = request.userId;
65
+ if (!userId) return unauthorized(reply);
66
+
67
+ const listings = await listMyProjectListings(userId);
68
+
69
+ // The omission is explicit at the send site rather than hidden in a schema
70
+ // strip, the idiom `patch-draft.ts` uses: `projectListingDraftSchema` is
71
+ // `.strict()`, so a key that stops being dropped here becomes a 500 instead
72
+ // of quietly shipping an internal reference to the founder.
73
+ const data = listings.map(
74
+ ({
75
+ siteId: _siteId,
76
+ leadId: _leadId,
77
+ decidedBy: _decidedBy,
78
+ issueUrl: _issueUrl,
79
+ experimentKey: _experimentKey,
80
+ ...mine
81
+ }) => mine,
82
+ );
83
+
84
+ return sendValidated(reply, projectListingMeResponseSchema, data, "me");
85
+ });
86
+ };
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
3
+ * escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
4
+ * reaches by a literal, always-resolved `import()`; this file ships
5
+ * unconditionally, registration stays gated).
6
+ *
2
7
  * PATCH /v1/project-listings/drafts/:id
3
8
  *
4
9
  * `patch` — the save-as-you-go write (#5036 D1). Anonymous, token-gated,
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
3
+ * escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
4
+ * reaches by a literal, always-resolved `import()`; this file ships
5
+ * unconditionally, registration stays gated).
6
+ *
2
7
  * GET /v1/project-listings/public?siteKey=
3
8
  *
4
9
  * The anonymous page-render read (#4998 AC4, reshaped by #5033). Every
@@ -1,4 +1,15 @@
1
1
  /**
2
+ * NOT marked `oss: false` (D-48/#5366 escalation, reversing this pass's
3
+ * own D6 read — see `./me.ts`'s header comment): `sendValidated` is imported
4
+ * by `./me.ts`, `./patch-draft.ts` and `./public.ts`, each of which must ship
5
+ * unconditionally because `./index.ts`'s multi-tenant literal `import()`
6
+ * forces it. A file imported by an always-shipping file cannot itself be
7
+ * excluded — this is the ORIGINAL multi-tenant/#5088 card's own conclusion
8
+ * about this exact file (module-architecture.ts lines 569-571), which D6's
9
+ * "every other consumer is Pro Plus" premise doesn't override: two of its
10
+ * three non-tenant consumers are these three always-shipping files, not a
11
+ * gated one.
12
+ *
2
13
  * Response-schema validation for the project-listings routes (#4998),
3
14
  * mirroring `waitlist-experiments/respond.ts`'s discipline: response schemas
4
15
  * are contract-only unless re-parsed against their own `.strict()` schema
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
3
+ * escalation): imported only by `./patch-draft.ts` and `./submit.ts`, both
4
+ * of which ship unconditionally (`./index.ts`'s multi-tenant literal
5
+ * `import()`), so this file cannot be excluded either.
6
+ *
2
7
  * The resume token's transport (#5036 D1).
3
8
  *
4
9
  * `start` hands the client an opaque token; `patch` and `submit` present it
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
3
+ * escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
4
+ * reaches by a literal, always-resolved `import()`; this file ships
5
+ * unconditionally, registration stays gated).
6
+ *
2
7
  * POST /v1/project-listings
3
8
  *
4
9
  * `start` — step one of the application (#5036 D1). Anonymous, registered in
@@ -37,6 +42,24 @@
37
42
  * PII-safe logging (mirrors `routes/leads/index.ts`): routing metadata only —
38
43
  * never founderEmail, founderName, or the resume token.
39
44
  *
45
+ * SENDS THE RESUME EMAIL (#5406), fire-and-forget, after the row exists. The
46
+ * 201 below is never delayed or failed by it: `sendListingResumeLink` returns
47
+ * `void` and swallows everything, exactly as `sendListingDecisionEmail`'s
48
+ * `fileIssueOnApprove` does on the approve path. The email is OPT-IN — a site
49
+ * whose `SiteListingConfig` carries no `resumeEmail.fromAddress` gets none, so
50
+ * a site sending its own today does not start sending two — and the request
51
+ * and response shapes of this hard-floor route are unchanged.
52
+ *
53
+ * THE EMAIL IS ALSO THE ONLY CARRIER OF THE VERIFICATION NONCE (#5407).
54
+ * `startProjectListingDraft` mints it, stores only its digest, and returns it
55
+ * as a SIBLING of the result — never inside the value parsed below, because
56
+ * `projectListingStartResponseSchema` is this route's hard-floor contract and
57
+ * carries `id` and `resumeToken` and nothing else. The nonce reaches
58
+ * `sendListingResumeLink` from a local variable and goes nowhere else: not
59
+ * into the response, not into the log line above it. That is precisely what
60
+ * makes `email_verified_at` mean something the resume token alone could not
61
+ * prove — see `verifyProjectListingDraftEmail`.
62
+ *
40
63
  * The response uses the API's own `{ ok, data }` / `{ ok, error }` envelope
41
64
  * (`apps/api/CLAUDE.md`) on BOTH the success and the failure path — the
42
65
  * anonymous create it replaces answered bare on success and enveloped on
@@ -49,6 +72,7 @@ import {
49
72
  import type { FastifyPluginAsync } from "fastify";
50
73
 
51
74
  import { siteIdForPublicKey } from "../../services/listing-config.js";
75
+ import { sendListingResumeLink } from "../../services/project-listing-resume-email.js";
52
76
  import { startProjectListingDraft } from "../../services/project-listings.js";
53
77
 
54
78
  export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
@@ -83,12 +107,26 @@ export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
83
107
  }
84
108
 
85
109
  const result = await startProjectListingDraft(siteId, parsed.data);
110
+ // #5275 — a `consents` map declining a REQUIRED purpose contradicts the
111
+ // body's own `consent: true`; no row was created.
112
+ if (!result.ok) {
113
+ return reply.status(400).send({
114
+ ok: false,
115
+ error: { code: result.error.kind, message: result.error.message },
116
+ });
117
+ }
86
118
 
87
119
  request.log.info(
88
120
  { siteId, listingId: result.id, ip: request.ip },
89
121
  "project listing draft started",
90
122
  );
91
123
 
124
+ // #5406 — the founder's way back to this draft. NOT awaited: the 201 must
125
+ // not wait on Resend, and must not fail if Resend does. Both credentials
126
+ // are handed over here and neither is ever logged — they ride the link
127
+ // inside the email, and the #5407 nonce rides NOTHING ELSE anywhere.
128
+ sendListingResumeLink(result.id, result.resumeToken, result.verifyNonce);
129
+
92
130
  const body = projectListingStartResponseSchema.parse({
93
131
  id: result.id,
94
132
  resumeToken: result.resumeToken,
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * NOT marked `oss: false` — see `./me.ts`'s header comment (D-48/#5366
3
+ * escalation: `./index.ts` is a mixed barrel multi-tenant's `server.ts`
4
+ * reaches by a literal, always-resolved `import()`; this file ships
5
+ * unconditionally, registration stays gated).
6
+ *
2
7
  * POST /v1/project-listings/drafts/:id/submit
3
8
  *
4
9
  * `submit` — the moment an application stops being a draft (#5036 D1).
@@ -0,0 +1,150 @@
1
+ /**
2
+ * POST /v1/project-listings/drafts/:id/verify-email
3
+ *
4
+ * Records that the founder proved control of `founderEmail`, by opening the
5
+ * resume link emailed at step one (#5407). Anonymous, credential-gated,
6
+ * registered in the same rate-limited `leadsScope` as the other three
7
+ * anonymous listing routes.
8
+ *
9
+ * WHY THE ROUTE EXISTS. The resume email (#5406) tells the founder to "verify
10
+ * your email and continue", and until this route nothing made that sentence
11
+ * true: `ListingApplicationFlow` rehydrated from `?draft=&token=` entirely in
12
+ * the browser and touched the server only on the NEXT advance — so a founder
13
+ * who opened the link and closed the tab left no record, and the operator had
14
+ * no way to tell a proved address from a typed one at review (samhenry spec
15
+ * 316 D6b).
16
+ *
17
+ * WHAT IT PROVES. That somebody who can read mail at `founderEmail` opened
18
+ * this link — evidence about the ADDRESS, not about who is driving the draft
19
+ * (`verifyProjectListingDraftEmail` carries the full note). It takes
20
+ * TWO credentials and only the second carries that: the `Authorization:
21
+ * Bearer` resume token says "you opened this draft" (it is in `start`'s own
22
+ * 201 body, so anyone who posted the form has it), and the `verifyNonce` in
23
+ * the body says "you read the email" — it is minted at `start`, digested onto
24
+ * the row, and handed to nothing but the resume-email builder, reaching the
25
+ * outside world only as the `?verify=` parameter inside the message. Keying
26
+ * this route on the resume token ALONE was the first design and was
27
+ * forgeable in two HTTP calls; `verifyProjectListingDraftEmail` carries the
28
+ * full note.
29
+ *
30
+ * It still says nothing about WHO the founder is. `ownerUserId` (#5295) is
31
+ * the column that carries an identity claim.
32
+ *
33
+ * A SITE THAT SENDS NO RESUME EMAIL CAN NEVER VERIFY, and that is correct:
34
+ * the nonce is minted on every draft but delivered only by the email, so
35
+ * where `SiteListingConfig` carries no `resumeEmail` the column stays null
36
+ * forever — nobody has read anything at that address.
37
+ *
38
+ * IDEMPOTENT, AND WRITE-ONCE. The service stamps by compare-and-swap on
39
+ * `emailVerifiedAt: null`, so a founder who opens the link five times is
40
+ * stamped once and every call answers with that FIRST stamp.
41
+ *
42
+ * THE ERROR SURFACE IS `patch`'s AND `submit`'s, deliberately unchanged: a
43
+ * malformed id or a body that is not `{ verifyNonce }` is `400
44
+ * validation_failed`, a missing or malformed `Authorization: Bearer` is `401
45
+ * unauthorized`, and a WRONG token, a WRONG nonce, a credential bound to
46
+ * another listing, an unknown id and an already-submitted row are all one
47
+ * `404 not_found`. The 400s describe the REQUEST's shape and reveal nothing
48
+ * about any row; everything that depends on a row is the single 404. An
49
+ * anonymous surface must not distinguish "exists but not yours" from "does
50
+ * not exist", and this route least of all — a distinguishable answer would
51
+ * make it both an oracle for which drafts exist and a guessing channel for
52
+ * the nonce.
53
+ *
54
+ * THE NONCE TRAVELS IN THE BODY, not the query string, for the reason
55
+ * `resume-token.ts` gives about the resume token: a query parameter lands in
56
+ * access logs, proxy logs and `Referer` headers. The claim is about THIS hop
57
+ * only, and it is worth being exact about that — the nonce's DELIVERY hop is
58
+ * itself a query parameter on the consuming site's URL, so it lands in that
59
+ * site's own access logs exactly as the resume token does. What the body
60
+ * placement buys is that the API's logs and any proxy between the browser and
61
+ * this server never see it. The resume token keeps its `Authorization:
62
+ * Bearer` header. Neither credential is ever logged and neither is ever
63
+ * returned.
64
+ */
65
+ import { zProjectListingId } from "@working-theory/ids";
66
+ import { projectListingVerifyEmailResponseSchema } from "@working-theory/validation";
67
+ import type { FastifyPluginAsync } from "fastify";
68
+ import { z } from "zod";
69
+
70
+ import { verifyProjectListingDraftEmail } from "../../services/project-listings.js";
71
+
72
+ import { sendValidated } from "./respond.js";
73
+ import { resumeTokenFrom } from "./resume-token.js";
74
+
75
+ const paramsSchema = z.object({ id: zProjectListingId });
76
+
77
+ /**
78
+ * The nonce, as the emailed link carried it. Bounded so an oversized body is
79
+ * refused before any digest is computed; the value is 256 bits of CSPRNG
80
+ * output in base64url, which is 43 characters.
81
+ */
82
+ const bodySchema = z.object({ verifyNonce: z.string().min(1).max(256) }).strict();
83
+
84
+ export const verifyProjectListingDraftEmailRoute: FastifyPluginAsync = async (
85
+ fastify,
86
+ ) => {
87
+ fastify.post(
88
+ "/v1/project-listings/drafts/:id/verify-email",
89
+ async (request, reply) => {
90
+ const parsedParams = paramsSchema.safeParse(request.params);
91
+ if (!parsedParams.success) {
92
+ return reply.status(400).send({
93
+ ok: false,
94
+ error: { code: "validation_failed", message: "Invalid project listing id" },
95
+ });
96
+ }
97
+
98
+ const resumeToken = resumeTokenFrom(request);
99
+ if (resumeToken === null) {
100
+ return reply.status(401).send({
101
+ ok: false,
102
+ error: { code: "unauthorized", message: "A resume token is required" },
103
+ });
104
+ }
105
+
106
+ // The message names the FIELD, never whether the value would have
107
+ // matched — that distinction is the 404's, below.
108
+ const parsedBody = bodySchema.safeParse(request.body);
109
+ if (!parsedBody.success) {
110
+ return reply.status(400).send({
111
+ ok: false,
112
+ error: {
113
+ code: "validation_failed",
114
+ message: "A verification nonce is required",
115
+ },
116
+ });
117
+ }
118
+
119
+ const result = await verifyProjectListingDraftEmail(
120
+ parsedParams.data.id,
121
+ resumeToken,
122
+ parsedBody.data.verifyNonce,
123
+ );
124
+ if (!result.ok) {
125
+ return reply.status(404).send({
126
+ ok: false,
127
+ error: { code: "not_found", message: "Draft not found" },
128
+ });
129
+ }
130
+
131
+ // Routing metadata only, the same PII boundary `start` documents: the
132
+ // listing id and the caller's ip, never the founder's email and never
133
+ // the token.
134
+ request.log.info(
135
+ { listingId: result.value.id, ip: request.ip },
136
+ "project listing draft email verified",
137
+ );
138
+
139
+ return sendValidated(
140
+ reply,
141
+ projectListingVerifyEmailResponseSchema,
142
+ {
143
+ id: result.value.id,
144
+ emailVerifiedAt: result.value.emailVerifiedAt.toISOString(),
145
+ },
146
+ "verify-email",
147
+ );
148
+ },
149
+ );
150
+ };