create-ailk 0.2.0 → 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 (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. 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
- }