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