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
@@ -1,471 +0,0 @@
1
- /**
2
- * Payment-link MANAGEMENT route — emitted by `scaffold-commerce`'s
3
- * surface-selection branch (SKILL.md § Surface-selection branch, Step 10e-ii,
4
- * FEAT-023 U3). Three Fastify handlers: list, create (from a `lookupKey`),
5
- * archive/restore (by id).
6
- *
7
- * Instantiated into the consumer's own `apps/api/src/routes/payment-links/`,
8
- * alongside `stripe-config.ts` (Step 10d) — the SAME home as the checkout
9
- * sibling, and for the same two reasons:
10
- * 1. `architecture.yaml`: `apps/web` MAY NOT import `packages/stripe-client`
11
- * ("billing is org-scoped and server-side via apps/api"). Stripe secrets
12
- * never enter the Next.js app.
13
- * 2. `apps/api` is where the auth middleware this route depends on LIVES.
14
- * (An earlier revision of this template was a Next.js App Router handler.
15
- * It shipped with NO authentication at all — AILK#3744, a P0. A management
16
- * surface mounted in a runtime that has no auth preHandler in front of it
17
- * is a hole waiting to happen; this one was.)
18
- *
19
- * ── Auth posture — AUTHENTICATED + SITE-ADMIN ONLY (the opposite of checkout) ─
20
- * This is a MANAGEMENT surface: it LISTS, CREATES, and ARCHIVES payment
21
- * objects. The checkout sibling (`checkout-route.template.ts`) is deliberately
22
- * public — it is a storefront "buy" action that can only ever create one new
23
- * Checkout Session for one allowlisted product. Its own header says it plainly:
24
- * "A management surface (list/update/archive payment objects) is a DIFFERENT
25
- * shape and MUST be authenticated". This is that surface. Do not copy the
26
- * checkout route's public posture here.
27
- *
28
- * Without authentication, an anonymous caller could:
29
- * - GET -> read every Payment Link on the site (ids, buy URLs, active flags)
30
- * - PATCH -> archive them; iterating the GET output deactivates EVERY link:
31
- * total denial of revenue, in two curl calls
32
- * - POST -> mint Payment Links against the merchant's own Stripe account
33
- *
34
- * ── Entitlement is NOT authorization (the AILK#3744 reasoning error) ──────────
35
- * `assertCommerceEntitled()` reads the SITE's license from env. It returns the
36
- * IDENTICAL answer for every request, anonymous or not — it answers "does this
37
- * DEPLOYMENT have commerce?", never "is this CALLER allowed to manage payment
38
- * links?". It is a correct check and it fails closed, but it is the WRONG AXIS,
39
- * so failing closed buys nothing on its own. It is kept below, LAYERED UNDER
40
- * the identity gate — never as a substitute for it.
41
- *
42
- * ── Registration — LOAD-BEARING; the route is fail-closed if you get it wrong ─
43
- * Register INSIDE the authenticated scope, so `authPreHandlerPlugin` decorates
44
- * `request.userId` (and applies its same-origin CSRF check) before any handler
45
- * here runs:
46
- *
47
- * await fastify.register(async (authed) => {
48
- * authed.register(authPreHandlerPlugin, { frontendOrigin: config.frontendUrl });
49
- * authed.get("/api/payment-links", listPaymentLinksHandler);
50
- * authed.post("/api/payment-links", createPaymentLinkHandler);
51
- * authed.patch("/api/payment-links", updatePaymentLinkHandler);
52
- * });
53
- *
54
- * If you mis-register these OUTSIDE that scope, `request.userId` is `undefined`
55
- * and every handler answers 401 — `requireSiteAdmin()` re-checks it rather than
56
- * assuming the preHandler ran. That belt-and-suspenders re-check is the reason
57
- * a mis-registration is a broken feature instead of an open door.
58
- *
59
- * ── CSRF ─────────────────────────────────────────────────────────────────────
60
- * Inherited from the auth scope: `apps/api/src/middleware/auth.ts` requires a
61
- * same-origin `Origin` header on every cookie-authenticated non-GET request
62
- * (`isOriginAllowed`). No custom token scheme is needed — but that protection
63
- * exists ONLY inside the auth scope, which is the second reason the
64
- * registration above is load-bearing.
65
- *
66
- * ── Tenancy ──────────────────────────────────────────────────────────────────
67
- * Deliberately NOT registered inside `tenantScope`, and the org is deliberately
68
- * NOT read from `request.tenant`. This surface manages the SITE OWNER's own
69
- * Stripe catalog: `packages/stripe-client` is single-tenant (one Stripe account
70
- * per deployment), a Stripe Payment Link carries no organization binding, and
71
- * no local table maps a payment link to an org. The multi-tenant module's
72
- * org-scoped billing is a SEPARATE surface (`apps/api/src/routes/billing/`,
73
- * which DOES derive its org from `request.tenant.organizationId`, and whose
74
- * `apps/workspace` consumer may not import `stripe-client` at all).
75
- *
76
- * Cross-account access is therefore structurally impossible rather than
77
- * checked: Stripe object ids are scoped to the account whose key made the call,
78
- * so a payment-link id from any other Stripe account comes back
79
- * `resource_missing` — mapped below to the SAME 404 shape as a simply-unknown
80
- * id, so the response is never an enumeration oracle.
81
- *
82
- * The correct authorization axis for this surface is therefore the SITE role,
83
- * not the org membership: `User.role` in {"admin","owner"}
84
- * (`database/auth/schema.prisma` — a per-deployment String, distinct from the
85
- * org-membership `Role` enum used by the multi-tenant module).
86
- *
87
- * ── Test-mode-only ───────────────────────────────────────────────────────────
88
- * Carried in twice, belt-and-suspenders: `loadStripeSurfaceConfig()` guards
89
- * this surface's entry point; `@working-theory/stripe-client`'s
90
- * `getStripeClient()`/`assertSandboxKey()` guards the SDK call itself. This
91
- * route deliberately does NOT hand-roll its own live-key prefix check — it
92
- * defers ENTIRELY to the shared guard so it inherits that guard's coverage (and
93
- * any future fix to it) rather than risking a second, divergent implementation.
94
- *
95
- * Error mapping:
96
- * 401 unauthorized — no authenticated caller (anonymous, or the
97
- * route mis-registered outside the auth scope)
98
- * 403 forbidden — authenticated, but not a site admin/owner
99
- * 403 not-entitled — the deployment lacks `feature:commerce`
100
- * 400 invalid-input — malformed body (bad lookupKey / link id)
101
- * 404 not-found — no such payment link IN THIS ACCOUNT
102
- * 500 config-error — Stripe/site env missing or misconfigured
103
- * 500 internal-error — the role lookup itself failed (fail-closed)
104
- * 502 payment-link-unavailable — Stripe SDK threw an unexpected error
105
- */
106
- import { checkLicense } from "@working-theory/license";
107
- import {
108
- createPaymentLink,
109
- listPaymentLinks,
110
- StripeConfigError,
111
- UnknownProductCodeError,
112
- updatePaymentLink,
113
- } from "@working-theory/stripe-client";
114
- import type { FastifyReply, FastifyRequest } from "fastify";
115
-
116
- // The consumer's own Prisma singleton (`apps/api/src/lib/prisma.ts`) — the same
117
- // client the auth middleware uses to resolve sessions.
118
- import { prisma } from "../../lib/prisma.js";
119
-
120
- // Sibling file instantiated at Step 10d — same directory as this route.
121
- import { loadStripeSurfaceConfig } from "./stripe-config.js";
122
-
123
- // ── Authorization ─────────────────────────────────────────────────────────────
124
-
125
- /**
126
- * The site roles allowed to manage the payment catalog.
127
- *
128
- * `User.role` is a per-deployment String ("user" | "admin" | "owner"). On a
129
- * site with open signup, "authenticated" is NOT a meaningful bar — it is "the
130
- * whole internet with an extra step" — so authentication alone would not close
131
- * AILK#3744. The role check is what makes this an authorization control.
132
- */
133
- const SITE_ADMIN_ROLES: ReadonlySet<string> = new Set(["admin", "owner"]);
134
-
135
- const PAYMENT_LINK_ID_REGEX = /^plink_[A-Za-z0-9_]{1,100}$/;
136
- const LOOKUP_KEY_REGEX = /^[A-Za-z0-9._-]{1,200}$/;
137
-
138
- function errorResponse(
139
- reply: FastifyReply,
140
- status: number,
141
- code: string,
142
- message: string,
143
- ): FastifyReply {
144
- return reply.status(status).send({ ok: false, error: { code, message } });
145
- }
146
-
147
- /**
148
- * The gate. Returns `true` only for an authenticated caller whose CURRENT role
149
- * — re-read from the database on every request — is a site admin or owner.
150
- * Every other path replies and returns `false`; the caller MUST bail out.
151
- *
152
- * Call it as the FIRST statement of every handler:
153
- *
154
- * if (!(await requireSiteAdmin(request, reply))) return;
155
- *
156
- * Three properties are load-bearing:
157
- *
158
- * 1. The role is re-read from the DB by the caller's id — never trusted off
159
- * the request. A stale session belonging to a user who has since been
160
- * demoted loses access on their very next request.
161
- * 2. It FAILS CLOSED. A throw while resolving the role DENIES (500). It never
162
- * falls through to allow — the classic catch-and-continue that turns a
163
- * transient DB blip into an open door.
164
- * 3. It re-checks `request.userId` rather than assuming the auth preHandler
165
- * ran, so a MIS-REGISTRATION (routes mounted outside the auth scope) is a
166
- * 401 — a broken feature, not an open door.
167
- */
168
- export async function requireSiteAdmin(
169
- request: FastifyRequest,
170
- reply: FastifyReply,
171
- ): Promise<boolean> {
172
- const userId = request.userId;
173
- if (!userId) {
174
- // Anonymous — or mis-registered outside the auth scope. Same answer.
175
- errorResponse(
176
- reply,
177
- 401,
178
- "unauthorized",
179
- "Authentication is required.",
180
- );
181
- return false;
182
- }
183
-
184
- let role: string | undefined;
185
- try {
186
- const user = await prisma.user.findUnique({
187
- where: { id: userId },
188
- select: { role: true },
189
- });
190
- role = user?.role;
191
- } catch (err) {
192
- // FAIL CLOSED. Never `catch {}` into the happy path.
193
- request.log.error(
194
- {
195
- method: request.method,
196
- path: request.url,
197
- message: err instanceof Error ? err.message : "unknown",
198
- },
199
- "[payment-links] role resolution failed; DENYING",
200
- );
201
- errorResponse(
202
- reply,
203
- 500,
204
- "internal-error",
205
- "Could not verify your permissions.",
206
- );
207
- return false;
208
- }
209
-
210
- // A missing user row (deleted account, stale session) has no role -> denied.
211
- if (!role || !SITE_ADMIN_ROLES.has(role)) {
212
- request.log.warn(
213
- { method: request.method, path: request.url, status: 403 },
214
- "[payment-links] caller is not a site admin; refused",
215
- );
216
- errorResponse(
217
- reply,
218
- 403,
219
- "forbidden",
220
- "Managing payment links requires a site admin role.",
221
- );
222
- return false;
223
- }
224
-
225
- return true;
226
- }
227
-
228
- // ── Entitlement (layered UNDER authorization — never a substitute for it) ─────
229
-
230
- /**
231
- * Resolves the deployment's license and enforces the `feature:commerce`
232
- * entitlement. Fails closed: no license, an expired license, a revoked license,
233
- * or a license without the `commerce` feature are all treated as NOT entitled.
234
- *
235
- * This answers "does this DEPLOYMENT have commerce?" — a DIFFERENT question
236
- * from "may this CALLER manage payment links?", which is `requireSiteAdmin`'s
237
- * job. Both are enforced, in that order.
238
- */
239
- async function assertCommerceEntitled(): Promise<void> {
240
- const result = await checkLicense();
241
-
242
- // A revoked license is treated as feature-less — the feature check below then
243
- // throws, same as an absent license.
244
- const features: readonly string[] = result.revoked ? [] : result.features;
245
-
246
- // Expiry first, then the feature claim — the same order, and the same two
247
- // conditions, the paid entitlement helper applies. The third thing that
248
- // helper does is assert the feature's verification MODE against the license
249
- // manifest; `commerce` is declared `verification: static`, so that assert is
250
- // a no-op pass for this call, and the manifest is a paid-tier package an
251
- // emitted OSS route may not reach. Both throws below are caught by
252
- // `preflight()` and mapped to the SAME 403 not-entitled response, so the
253
- // error type is not observable at the route boundary.
254
- //
255
- // `LicenseResult.expiresAt` is `Date | null`; normalise anyway so a licence
256
- // resolver that yields `undefined` reads as "no expiry" rather than throwing.
257
- const expiresAt = result.expiresAt ?? null;
258
- if (expiresAt !== null && expiresAt.getTime() <= Date.now()) {
259
- throw new Error("This site's license has expired.");
260
- }
261
- if (!features.includes("commerce")) {
262
- throw new Error("This site's license does not include the commerce feature.");
263
- }
264
- }
265
-
266
- /**
267
- * Runs the entitlement + Stripe-config preflight shared by all three handlers.
268
- * Returns `true` when the request may proceed to a Stripe call.
269
- *
270
- * Callers MUST have cleared `requireSiteAdmin` first — this function performs
271
- * no identity check of its own.
272
- */
273
- async function preflight(
274
- request: FastifyRequest,
275
- reply: FastifyReply,
276
- ): Promise<boolean> {
277
- try {
278
- await assertCommerceEntitled();
279
- } catch {
280
- errorResponse(
281
- reply,
282
- 403,
283
- "not-entitled",
284
- "This site is not entitled to the commerce feature.",
285
- );
286
- return false;
287
- }
288
-
289
- try {
290
- loadStripeSurfaceConfig();
291
- } catch (err) {
292
- request.log.error(
293
- {
294
- method: request.method,
295
- path: request.url,
296
- message: err instanceof Error ? err.message : "unknown",
297
- },
298
- "[payment-links] config error",
299
- );
300
- errorResponse(
301
- reply,
302
- 500,
303
- "config-error",
304
- "Stripe is misconfigured for this site.",
305
- );
306
- return false;
307
- }
308
-
309
- return true;
310
- }
311
-
312
- /**
313
- * Stripe raises `resource_missing` for an id that does not exist IN THIS
314
- * ACCOUNT — which is also what a payment-link id belonging to a DIFFERENT
315
- * Stripe account looks like, since object ids are account-scoped. Both map to
316
- * the same 404, so the response cannot be used to enumerate ids.
317
- */
318
- function isResourceMissing(err: unknown): boolean {
319
- return (
320
- typeof err === "object" &&
321
- err !== null &&
322
- (err as { code?: unknown }).code === "resource_missing"
323
- );
324
- }
325
-
326
- /** Shared Stripe error mapping. Never echoes caller input back. */
327
- function stripeErrorResponse(
328
- request: FastifyRequest,
329
- reply: FastifyReply,
330
- err: unknown,
331
- ): FastifyReply {
332
- if (isResourceMissing(err)) {
333
- return errorResponse(reply, 404, "not-found", "No such payment link.");
334
- }
335
- if (err instanceof StripeConfigError) {
336
- return errorResponse(
337
- reply,
338
- 500,
339
- "config-error",
340
- "Payment links are misconfigured.",
341
- );
342
- }
343
- request.log.error(
344
- {
345
- method: request.method,
346
- path: request.url,
347
- errName: err instanceof Error ? err.name : "unknown",
348
- },
349
- "[payment-links] stripe error; payment-link-unavailable",
350
- );
351
- return errorResponse(
352
- reply,
353
- 502,
354
- "payment-link-unavailable",
355
- "Payment links are temporarily unavailable.",
356
- );
357
- }
358
-
359
- // ── Handlers ──────────────────────────────────────────────────────────────────
360
-
361
- /** GET /api/payment-links[?active=true|false] — list payment links. */
362
- export async function listPaymentLinksHandler(
363
- request: FastifyRequest,
364
- reply: FastifyReply,
365
- ): Promise<void> {
366
- if (!(await requireSiteAdmin(request, reply))) return;
367
- if (!(await preflight(request, reply))) return;
368
-
369
- const query = request.query as { active?: string } | undefined;
370
- const activeParam = query?.active;
371
- const active = activeParam === undefined ? undefined : activeParam === "true";
372
-
373
- try {
374
- const { paymentLinks } = await listPaymentLinks({ active });
375
- reply.send({ ok: true, data: { paymentLinks } });
376
- } catch (err) {
377
- stripeErrorResponse(request, reply, err);
378
- }
379
- }
380
-
381
- /** POST /api/payment-links — create a Payment Link from a lookupKey. */
382
- export async function createPaymentLinkHandler(
383
- request: FastifyRequest,
384
- reply: FastifyReply,
385
- ): Promise<void> {
386
- if (!(await requireSiteAdmin(request, reply))) return;
387
- if (!(await preflight(request, reply))) return;
388
-
389
- const body = request.body as
390
- | { lookupKey?: unknown; quantity?: unknown }
391
- | undefined;
392
- if (
393
- !body ||
394
- typeof body.lookupKey !== "string" ||
395
- !LOOKUP_KEY_REGEX.test(body.lookupKey)
396
- ) {
397
- errorResponse(
398
- reply,
399
- 400,
400
- "invalid-input",
401
- "lookupKey is required and must match /^[A-Za-z0-9._-]{1,200}$/.",
402
- );
403
- return;
404
- }
405
-
406
- const quantity =
407
- typeof body.quantity === "number" &&
408
- Number.isInteger(body.quantity) &&
409
- body.quantity >= 1 &&
410
- body.quantity <= 999
411
- ? body.quantity
412
- : undefined;
413
-
414
- try {
415
- const link = await createPaymentLink({
416
- lookupKey: body.lookupKey,
417
- quantity,
418
- });
419
- reply.status(201).send({ ok: true, data: link });
420
- } catch (err) {
421
- if (err instanceof UnknownProductCodeError) {
422
- // Do NOT echo the lookupKey back to the caller.
423
- errorResponse(
424
- reply,
425
- 404,
426
- "unknown-lookup-key",
427
- "No active price exists for that product.",
428
- );
429
- return;
430
- }
431
- stripeErrorResponse(request, reply, err);
432
- }
433
- }
434
-
435
- /** PATCH /api/payment-links — archive/restore a Payment Link by id. */
436
- export async function updatePaymentLinkHandler(
437
- request: FastifyRequest,
438
- reply: FastifyReply,
439
- ): Promise<void> {
440
- if (!(await requireSiteAdmin(request, reply))) return;
441
- if (!(await preflight(request, reply))) return;
442
-
443
- const body = request.body as
444
- | { paymentLinkId?: unknown; active?: unknown }
445
- | undefined;
446
- if (
447
- !body ||
448
- typeof body.paymentLinkId !== "string" ||
449
- !PAYMENT_LINK_ID_REGEX.test(body.paymentLinkId) ||
450
- typeof body.active !== "boolean"
451
- ) {
452
- // Shape-checked BEFORE any Stripe call. Does not echo the input back.
453
- errorResponse(
454
- reply,
455
- 400,
456
- "invalid-input",
457
- "paymentLinkId (plink_...) and active (boolean) are required.",
458
- );
459
- return;
460
- }
461
-
462
- try {
463
- const link = await updatePaymentLink({
464
- paymentLinkId: body.paymentLinkId,
465
- active: body.active,
466
- });
467
- reply.send({ ok: true, data: link });
468
- } catch (err) {
469
- stripeErrorResponse(request, reply, err);
470
- }
471
- }