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,807 +0,0 @@
1
- ---
2
- name: scaffold-commerce
3
- description: Wire up commerce on an AILK consumer site — either an external link to an existing storefront (Amazon, Etsy, Shopify, eBay, Gumroad, custom) via a `<MarketplaceCTA>` button, or direct payments via Stripe (test-mode only at v1). Use when the user says "let me sell something", "add a buy button", "wire up Stripe", "set up payments", "I have an Amazon store", or otherwise wants commerce wiring on their AILK site. Two-path branching — picks link vs direct first, never assumes Stripe.
4
- ---
5
-
6
- # scaffold-commerce
7
-
8
- Add commerce to an AILK site. The first question is always: **external
9
- link to an existing storefront, or direct payments on this site?** Both
10
- paths are conversational and emit minimal, consumer-owned source.
11
-
12
- This skill is the canonical "sell stuff" entry point. Pricing UI itself
13
- is `scaffold-page` (#362)'s lane (you scaffold a `/pricing` page with
14
- that skill, then wire `lookup_key`s into it). Production webhook setup
15
- is `provision-config` (#367)'s lane.
16
-
17
- ## When to use
18
-
19
- The user wants to sell something — physical goods, digital goods,
20
- subscriptions, services, courses, anything with a price. Triggers:
21
-
22
- - "let me sell something" / "I want to sell"
23
- - "add a buy button" / "add a buy now"
24
- - "wire up Stripe" / "set up payments"
25
- - "I have an Amazon / Etsy / Shopify / eBay / Gumroad store, link it"
26
- - "what's the simplest way to charge for this?"
27
-
28
- Skip this skill when:
29
-
30
- - The user wants to _design_ a pricing page (lay out tiers, copy,
31
- comparison table) — that's `scaffold-page` (#362) emitting a Product
32
- page. Wire `lookup_key`s afterwards from this skill's output.
33
- - The user wants to set up production Stripe (live mode, real webhooks).
34
- Hard-blocked at v1 — sandbox-only is hook-enforced globally. Defer
35
- with the same answer regardless: "v1 is test-mode only; production
36
- setup is `provision-config` (#367)'s lane."
37
- - The user is on a non-Stripe payment processor (Lemon Squeezy, Paddle,
38
- Gumroad-as-payment-processor). v1 only supports Stripe for direct
39
- payments. Acknowledge and stop; future PRs will add `--provider`
40
- branches.
41
-
42
- ## The first question (always asked)
43
-
44
- Greet the consumer with a clear two-path branch:
45
-
46
- > "Two ways to sell on your AILK site:
47
- >
48
- > **Link to an existing store** — you already sell on Amazon, Etsy,
49
- > Shopify, eBay, Gumroad, or your own storefront. We add a 'Buy on
50
- > {marketplace}' button that links there.
51
- >
52
- > **Direct payments via Stripe** — you don't have a storefront yet, or
53
- > you want checkout on your own site. We create products in your
54
- > Stripe (test mode), wire env vars, and point you at the next steps.
55
- >
56
- > Which one?"
57
-
58
- Wait for the consumer's choice. Then run the matching path.
59
-
60
- ## Link path
61
-
62
- The link path emits source the consumer owns — no env wiring, no API
63
- calls, no `@working-theory/ui` import.
64
-
65
- ### Step 1 — Collect the inputs
66
-
67
- Ask conversationally for:
68
-
69
- 1. **URL** — the storefront URL. Validate it parses as a URL. Don't
70
- accept plain text; prompt again if input fails.
71
- 2. **Button text** — defaults to "Buy on {marketplace}" where
72
- `{marketplace}` is parsed from the URL hostname (e.g.,
73
- `amazon.com` → "Amazon"). Consumer can override.
74
- 3. **Optional `iconSrc`** — a path to an SVG/PNG of the marketplace
75
- logo, e.g., `/logos/amazon.svg`. Optional — clean text-only buttons
76
- are fine. If the consumer wants logos, point them at
77
- `references/marketplace-brand-pages.md` for sourcing.
78
-
79
- ### Step 2 — Emit the route template
80
-
81
- Use `templates/marketplace-cta-route.tsx.tmpl` as the starting source.
82
- The template defines `<MarketplaceCTA>` **inline** at the top of the
83
- emitted route file (not bundled in `@working-theory/ui`, not imported from a
84
- shared package). The consumer owns the component and can customize it.
85
-
86
- The emitted file path is the consumer's call — typical placement is
87
- `app/buy/page.tsx` for a dedicated landing, or inserted into an
88
- existing page (e.g., `app/products/[slug]/page.tsx`) as a `<MarketplaceCTA>`
89
- usage. Ask which.
90
-
91
- ### Step 3 — Confirm and write
92
-
93
- Show the consumer the emitted file diff (or new file if standalone)
94
- in a single confirmation step. After approval, write.
95
-
96
- ### Step 4 — No env wiring, no follow-up files
97
-
98
- The link path is self-contained. Nothing to add to `.env.local`. No
99
- references file write (the marketplace brand pages cheat-sheet ships
100
- in the skill folder; the skill points at it but doesn't copy it into
101
- the consumer's repo).
102
-
103
- ## Direct payments path (Stripe, test-mode only at v1)
104
-
105
- The Stripe path uses the **Stripe MCP** (configured in the AILK
106
- consumer scaffold's `.claude/settings.json`) to create products with
107
- consumer-derived `lookup_key`s, writes test-mode env vars, and
108
- surfaces created keys for the consumer to grep when wiring pricing UI.
109
-
110
- ### Step 1 — Defense-in-depth live-key check
111
-
112
- Before any other action:
113
-
114
- 1. **Env detection.** Scan the consumer's `.env.local`, `.env`,
115
- `.env.development`, and any `.env.example` for `sk_live_` or
116
- `pk_live_`. If any match, refuse with:
117
-
118
- > "AILK is sandbox-only at v1. I see a `sk_live_*` or `pk_live_*`
119
- > value in your env files — replace it with a test-mode key from
120
- > <https://dashboard.stripe.com/test/apikeys> before continuing.
121
- > Production setup is `provision-config` (#367)'s lane and is
122
- > deferred at v1."
123
-
124
- Stop. No Stripe API calls fire.
125
-
126
- 2. **Paste detection.** If the consumer pastes a `sk_live_` or
127
- `pk_live_` value during the flow, refuse with the same message
128
- and stop.
129
-
130
- This double-check is required by the brief and matches the global
131
- hook-enforced sandbox-only constraint.
132
-
133
- ### Step 2 — Collect product inputs
134
-
135
- Ask the consumer to describe each product they want to sell. Per
136
- product:
137
-
138
- - **Name** — e.g., "Pro plan", "Pizza", "Online course".
139
- - **Price + interval** — e.g., "$29/month", "$15 one-time", "$199/year".
140
- - **Optional features bullet list** — surfaced in the dashboard, not
141
- required for the API call.
142
-
143
- Loop until the consumer says "that's all" or equivalent.
144
-
145
- ### Step 3 — Derive `lookup_key`s
146
-
147
- For each product, derive a stable, human-readable `lookup_key`:
148
-
149
- - Lowercase the name, replace spaces with underscores.
150
- - Append `_monthly` / `_yearly` / `_onetime` based on interval.
151
- - Examples:
152
- - "Pro plan, $29/month" → `pro_monthly`
153
- - "Annual subscription, $199/year" → `annual_yearly`
154
- - "Pizza, $15 one-time" → `pizza_onetime`
155
-
156
- Show the derivations to the consumer and offer to override per-product
157
- before any API call.
158
-
159
- ### Step 4 — Conflict handling (per product)
160
-
161
- For each product, before creating it, query the consumer's Stripe
162
- account for an existing product with the same `lookup_key` (use the
163
- Stripe MCP — `stripe.products.list({lookup_keys: [...]})` or equivalent).
164
-
165
- If one exists, **show + ask** (do not silently skip, do not refuse):
166
-
167
- ```
168
- A product with lookup_key `pro_monthly` already exists:
169
- Existing: "Pro plan" — $29/month — created 2026-04-15
170
- Proposed: "Pro plan" — $29/month — new
171
-
172
- What would you like to do?
173
- - Keep existing (no change)
174
- - Overwrite amounts (update price, keep lookup_key)
175
- - Pick a different lookup_key (e.g., `pro_monthly_v2`)
176
- ```
177
-
178
- Wait for choice. Apply.
179
-
180
- ### Step 5 — Create products + prices in Stripe
181
-
182
- For each product (after conflict resolution), use the Stripe MCP:
183
-
184
- 1. **Create product:** `stripe.products.create({ name, ... })`.
185
- 2. **Create price:** `stripe.prices.create({ product: <id>, unit_amount, currency: 'usd', recurring: { interval } | undefined, lookup_key })`.
186
-
187
- Surface created IDs in chat as you go so the consumer can verify in
188
- their Stripe dashboard alongside the run.
189
-
190
- ### Step 6 — Write `.env.local`
191
-
192
- After all products are created, write to the consumer's `.env.local`:
193
-
194
- ```
195
- STRIPE_SECRET_KEY=sk_test_...
196
- STRIPE_PUBLISHABLE_KEY=pk_test_...
197
- STRIPE_WEBHOOK_SECRET=
198
- ```
199
-
200
- Use the test-mode keys from the consumer's Stripe account (the
201
- consumer pastes these from
202
- <https://dashboard.stripe.com/test/apikeys>). Leave
203
- `STRIPE_WEBHOOK_SECRET` empty — Step 7 fills it.
204
-
205
- ### Step 7 — Webhook setup (dev-only)
206
-
207
- Tell the consumer to run, in a separate terminal:
208
-
209
- ```bash
210
- stripe listen --forward-to localhost:3000/api/webhooks/stripe
211
- ```
212
-
213
- The CLI prints a signing secret (`whsec_...`). Have the consumer paste
214
- it into `.env.local` as `STRIPE_WEBHOOK_SECRET`.
215
-
216
- Note: production webhook setup is **deferred to v2** — the consumer
217
- will register a permanent webhook endpoint in Stripe's dashboard
218
- during deploy. Point at `provision-config` (#367) or future deploy
219
- skill for the production handoff. Do not attempt to register a
220
- production webhook from this skill.
221
-
222
- ### Step 8 — Write `references/lookup-keys.md`
223
-
224
- Use `references/lookup-keys-template.md` as the source. Instantiate
225
- with the products created this session:
226
-
227
- ```markdown
228
- # Lookup Keys — generated by scaffold-commerce
229
-
230
- | Product | Lookup key | Amount | Interval |
231
- | -------- | ------------- | ------ | -------- |
232
- | Pro plan | `pro_monthly` | $29.00 | monthly |
233
- | ... | ... | ... | ... |
234
-
235
- When wiring a pricing page (`scaffold-page` (#362) on a `Product` slug),
236
- import or reference these lookup keys. Stripe lookup keys are stable
237
- across price recreates — safe to hard-code in your app.
238
- ```
239
-
240
- The consumer's filled-in `lookup-keys.md` lives in their repo (e.g.,
241
- project root or a `docs/` folder) — point them at `scaffold-page`
242
- (#362) for the pricing page that consumes these keys.
243
-
244
- ### Step 9 — Closing summary
245
-
246
- Print:
247
-
248
- - Number of products created (with `lookup_key`s).
249
- - Path to `references/lookup-keys.md`.
250
- - Reminder: webhook setup is dev-only at v1; production webhook is
251
- `provision-config` (#367)'s lane.
252
- - Pointer at `scaffold-page` (#362) for wiring pricing UI.
253
- - Reminder: sandbox-only at v1. Live keys are hook-blocked globally.
254
-
255
- ### Step 10 — Surface-selection branch (`feature:commerce` only, FEAT-023 U1)
256
-
257
- This step is **additive** — it runs after Step 9's closing summary and
258
- never changes anything above it. A site without the `commerce`
259
- entitlement gets exactly today's flow (Steps 1–9) and stops there.
260
-
261
- **10a — Fail-closed entitlement check.** Determine whether this site is
262
- entitled to `feature:commerce` (the license-manifest feature added in
263
- FEAT-023 U1 — `commerce` is included in the `pro` / `pro_plus` /
264
- `enterprise` tiers, absent from `free`):
265
-
266
- - If the consumer's project has a resolvable license — a
267
- `WT_LICENSE_KEY` (or equivalent) env var resolving to a
268
- `@working-theory/license` `License` — call
269
- `hasFeature("commerce", license)` (`@working-theory/license-client`,
270
- static-mode: sync, no network). `true` → entitled; proceed to 10b.
271
- - **Any other case is NOT entitled — fail closed:** no license
272
- configured, the license lacks `commerce`, or `hasFeature` throws.
273
- Do nothing further. The consumer already has the full link path +
274
- product-creation flow from Steps 1–9; there is no additional
275
- action or message needed.
276
-
277
- Do not attempt to determine entitlement by asking the consumer their
278
- plan conversationally — the check is against the resolved license, not
279
- a verbal claim.
280
-
281
- **10b — Offer the surface-selection branch.** For an entitled site,
282
- ask:
283
-
284
- > "Want to scaffold a payment surface for these products? Pick one (or
285
- > 'skip' for now):
286
- >
287
- > - **Checkout** — a hosted or embedded checkout flow (one-time or
288
- > subscription).
289
- > - **Pay-by-link** — a shareable payment link, no page to build.
290
- > - **Invoice** — send + track invoices with a status badge.
291
- > - **Subscription** — recurring billing with a status badge.
292
- > - **Portal** — a self-serve billing-management page for customers."
293
-
294
- **10c — Re-run the live-key check.** Before dispatching to any surface
295
- scaffolder, re-run Step 1's live-key check (env scan + paste
296
- detection) — belt-and-suspenders, since surface scaffolding may run in
297
- a session well after the original product-creation pass.
298
-
299
- **10d — Instantiate the shared config.** Copy
300
- `templates/stripe-config.template.ts` into the consumer's `apps/api`
301
- (or the surface's own route directory) as the shared
302
- `loadStripeSurfaceConfig()` the surface's route handler calls at
303
- request time. This carries the test-mode-only guard
304
- (`assertSandboxKey`, `@working-theory/stripe-client`) into the new
305
- surface entry point — the same belt-and-suspenders guard
306
- `getStripeClient()` already applies, so a live key is refused here too,
307
- not only at the shared Stripe client singleton.
308
-
309
- **10e — Dispatch to the matching surface scaffolder.** **Checkout
310
- (one-time)** (10e-i, FEAT-023 U2), **Pay by link** (10e-ii, FEAT-023 U3),
311
- **Invoice** (10g, FEAT-023 U4), **Subscription** (10h, FEAT-023 U5), and
312
- **Portal** (10i, FEAT-023 U6) are all available inline. If a future choice's
313
- matching skill is not installed, say so plainly:
314
-
315
- > "Payment-surface scaffolding for `<choice>` isn't available in this
316
- > AILK version yet — the foundation (entitlement gate + shared
317
- > config + UI primitives) is in place; the surface itself ships in a
318
- > later update."
319
-
320
- Do not attempt to hand-build the surface yourself when its scaffolder
321
- is missing — that would bypass the recomposition method (§8: capability
322
- checklist → `packages/ui` primitives, never a verbatim UI lift) every
323
- FEAT-023 UI surface must follow.
324
-
325
- **10e-i — Checkout (one-time), FEAT-023 U2.** When the consumer picks
326
- **Checkout** and wants a one-time payment (not a subscription — v1 only
327
- scaffolds one-time; acknowledge and stop if the consumer wants
328
- recurring billing, that's a future `mode: "subscription"` branch):
329
-
330
- 1. **Pick the product(s).** Offer the `lookup_key`s already created this
331
- session (Step 3/5) as the checkout's product choices. If none were
332
- created yet, run Steps 2–5 first (or ask for at least one product).
333
- 2. **Instantiate the route.** Copy
334
- `templates/checkout-route.template.ts` next to the already-instantiated
335
- `stripe-config.ts` (Step 10d) in the consumer's `apps/api/src/routes/checkout/`
336
- (checkout creation is server-side only — `apps/web` may not import
337
- `@working-theory/stripe-client`, architecture.yaml). Register its exported
338
- `oneTimeCheckoutHandler` on a `POST` route, e.g.
339
- `POST /api/checkout/one-time`, SCOPED BEHIND the shared per-IP rate-limit
340
- middleware (`apps/api/src/middleware/rate-limit.ts`, already present in
341
- every consumer's `apps/api` — the same one `server.ts` wires around the
342
- anonymous "leads" routes). This is a public, unauthenticated, single-purpose
343
- session-creation endpoint by design (see the template's own header comment)
344
- — the rate limit is its abuse guard against unbounded session-creation /
345
- card-testing attempts; do not register it unscoped. Ask the consumer to
346
- confirm the route path if their `apps/api` layout differs from the default.
347
-
348
- **Two edits are MANDATORY as you instantiate it. The route is fail-closed
349
- until both are done — every request 404s or 500s. This is deliberate: a
350
- checkout route that is wrong in either respect is exploitable by an
351
- anonymous internet caller.**
352
-
353
- **(a) Pin the purchasable-product allowlist.** Replace the placeholder in
354
- the route's `ALLOWED_LOOKUP_KEYS` with EXACTLY the `lookup_key`s the
355
- consumer selected at step 1 — nothing else:
356
-
357
- ```ts
358
- const ALLOWED_LOOKUP_KEYS: ReadonlySet<string> = new Set<string>([
359
- "pizza_onetime", // ← the step-1 selection, verbatim
360
- "combo_onetime",
361
- ]);
362
- ```
363
-
364
- Why this is not optional: without it, the route resolves ANY
365
- caller-supplied `lookupKey` against the merchant's ENTIRE Stripe account.
366
- A merchant with a public `pizza_onetime` ($15) and a private
367
- `enterprise_annual_90pct_off` in the same account is one guessed lookup
368
- key away from selling the discounted price to an anonymous `curl`; any
369
- `$0` comp/test price becomes a free-fulfillment endpoint. Pin the
370
- products this surface sells — never the account. Do not offer to
371
- "allow all keys", and do not read the allowlist from an env var or the
372
- request: it is a source-pinned constant.
373
-
374
- **(b) Set `FRONTEND_URL`.** The route locks every `successUrl` /
375
- `cancelUrl` to the site's own origin, read from `FRONTEND_URL` (the same
376
- env var the consumer's `apps/api` already uses for CORS —
377
- `apps/api/src/config/index.ts`). Confirm it is set for each environment
378
- (dev / preview / prod). If it is unset the route returns
379
- `500 config-error` and refuses every checkout — fail-closed by design.
380
- Without the origin lock, an anonymous caller can mint a REAL
381
- `checkout.stripe.com` session for the real merchant whose success page is
382
- an attacker's — the victim pays on Stripe's real domain and lands on the
383
- attacker's "receipt".
384
-
385
- 3. **Compose the checkout page.** In the consumer's `apps/web`, create a
386
- page that renders `CheckoutForm` (`@working-theory/ui`) with the
387
- session's products as `CheckoutProduct[]` (`lookupKey` + `name` + price).
388
- Wire `onCheckout` to POST to the Step-2 route with `{ lookupKey,
389
- successUrl, cancelUrl }` and, on a `201` response, set
390
- `window.location.href` to the returned `url`. Pass `testMode` (always
391
- `true` at v1 — sandbox-only). Ask which page path to use (e.g.
392
- `app/checkout/page.tsx`), mirroring the link path's "ask which"
393
- convention. Send the redirects as RELATIVE PATHS (`/checkout/success`,
394
- `/checkout/cancel`): the route composes them onto the site's own origin
395
- server-side. A cross-origin absolute URL is refused with
396
- `400 invalid-redirect-url`.
397
- 4. **Compose the success/cancel pages.** Create two pages (e.g.
398
- `app/checkout/success/page.tsx`, `app/checkout/cancel/page.tsx`) at the
399
- `successUrl`/`cancelUrl` the checkout page passed, each rendering
400
- `PaymentResult` (`@working-theory/ui`) with `outcome="success"` /
401
- `outcome="cancel"` respectively and the consumer's own copy.
402
-
403
- **Tell the consumer, in these words: the success page is a DISPLAY-ONLY
404
- terminal state; it is NEVER proof of payment.** Anyone can navigate
405
- straight to `/checkout/success` and be shown "Payment successful". So
406
- never hang fulfillment, an entitlement grant, a license issue, or a file
407
- download off that page. Real fulfillment must be driven by a verified
408
- Stripe webhook (signature-checked) or a server-side
409
- `stripe.checkout.sessions.retrieve(id)` whose `payment_status === "paid"`
410
- — neither of which this v1 surface scaffolds. If the consumer asks for
411
- fulfillment, that is the next unit of work, not a tweak to this page.
412
-
413
- 5. **Surface the session id.** After the flow is wired, remind the
414
- consumer that each created Checkout Session's id is logged (truncated,
415
- no PII) by the route for verification in
416
- <https://dashboard.stripe.com/test/payments>.
417
- 6. **Closing note.** Confirm the surface is reachable only because
418
- `feature:commerce` resolved entitled (10a) — a non-entitled site never
419
- sees this branch. Note that this entitlement check is SCAFFOLD-TIME only:
420
- the emitted route reads no license at runtime, so a lapsed
421
- `feature:commerce` does not switch the endpoint off.
422
-
423
- Remind the consumer this is test-mode only. Be precise about what the
424
- live-key guard actually covers: `assertSandboxKey` (10d's
425
- `loadStripeSurfaceConfig`, and the shared Stripe client singleton) is an
426
- ALLOW-LIST as of AILK#3734 — it accepts only `sk_test_`, `pk_test_` and
427
- `rk_test_`, and refuses everything else. A Stripe **restricted** live key
428
- (`rk_live_…`) — the key type Stripe steers server-side integrations toward
429
- — IS refused, along with any live prefix Stripe introduces later, because
430
- it is absent from the allow-list rather than named on a ban-list.
431
- So: "only recognized test-mode keys are accepted", not "these particular
432
- live prefixes are blocked". Tell the consumer to use a test-mode key and to
433
- keep live keys out of the environment entirely anyway — a working guard is
434
- not a reason to point a live key at a non-production deployment.
435
-
436
- Finally, confirm all three abuse guards are actually in place: the
437
- rate-limit middleware is wired (step 2 — an unauthenticated checkout route
438
- without it is a card-testing vector), `ALLOWED_LOOKUP_KEYS` is pinned to
439
- the real selection (step 2a), and `FRONTEND_URL` is set for every
440
- environment (step 2b).
441
-
442
- **10e-ii — Pay-by-link (payment-link management), FEAT-023 U3.** When
443
- the consumer picks **Pay by link**, instantiate
444
- `templates/payment-link-route.template.ts` next to the already-instantiated
445
- `stripe-config.ts` (Step 10d) in the consumer's
446
- `apps/api/src/routes/payment-links/` — server-side only, same reason as
447
- checkout (`apps/web` may not import `@working-theory/stripe-client`,
448
- architecture.yaml).
449
-
450
- This is a **MANAGEMENT** surface (list / create / archive payment objects),
451
- NOT a storefront "buy" action — so its auth posture is the OPPOSITE of the
452
- checkout route's. Register its three exported handlers INSIDE the
453
- authenticated scope, so `authPreHandlerPlugin` decorates `request.userId`
454
- (and applies its same-origin CSRF check) before any handler runs:
455
-
456
- ```ts
457
- await fastify.register(async (authed) => {
458
- authed.register(authPreHandlerPlugin, { frontendOrigin: config.frontendUrl });
459
- authed.get("/api/payment-links", listPaymentLinksHandler);
460
- authed.post("/api/payment-links", createPaymentLinkHandler);
461
- authed.patch("/api/payment-links", updatePaymentLinkHandler);
462
- });
463
- ```
464
-
465
- The registration is **load-bearing** — do not copy the checkout route's
466
- public, rate-limited registration here:
467
-
468
- 1. **Authentication + authorization.** Each handler calls `requireSiteAdmin()`
469
- FIRST — before the entitlement check and before any Stripe call. It
470
- re-reads `User.role` from the database and admits only `admin` / `owner`.
471
- Authentication alone is not the bar: on a site with open signup, "any
472
- logged-in user" is "the whole internet with an extra step".
473
- 2. **Entitlement is NOT authorization.** The `feature:commerce` check answers
474
- "does this DEPLOYMENT have commerce?", never "may this CALLER manage
475
- payment links?" — it reads the site's env license and returns the identical
476
- answer to an anonymous caller. It is retained, layered UNDER the identity
477
- gate, never as a substitute for it. (Shipping this surface with ONLY the
478
- entitlement gate was AILK#3744: a P0 that let an anonymous caller list every
479
- payment link, archive them all — total denial of revenue — and mint new ones
480
- against the merchant's Stripe account.)
481
- 3. **CSRF** is inherited from the auth scope's same-origin `Origin` check
482
- (`apps/api/src/middleware/auth.ts`), which exists ONLY inside that scope.
483
- 4. **Fail-closed by construction.** Mis-register the routes outside the auth
484
- scope and `request.userId` is `undefined`, so every handler answers 401 — a
485
- broken feature, never an open door.
486
- 5. **Tenancy.** Deliberately NOT `tenantScope`. This surface manages the SITE
487
- OWNER's single Stripe catalog (`packages/stripe-client` is single-tenant; a
488
- Stripe Payment Link carries no org binding). Cross-account access is
489
- structurally impossible — Stripe object ids are account-scoped, so a foreign
490
- id returns `resource_missing`, which the route maps to the same 404 as an
491
- unknown id (no enumeration oracle). The multi-tenant module's org-scoped
492
- billing is a SEPARATE surface (`apps/api/src/routes/billing/`), which DOES
493
- derive its org from `request.tenant.organizationId`.
494
-
495
- Test-mode-only caveats are identical to checkout's (step 10e-i) — state the
496
- `assertSandboxKey` allow-list (AILK#3734): only recognized test-mode prefixes
497
- are accepted, so every live key is refused by construction.
498
-
499
- **10f — Shared UI primitives.** Every surface scaffolder composes from
500
- `@working-theory/ui`'s `MoneyAmount` (primitive), `PaymentStatusBadge`
501
- (block), and `StripeActionButton` (block) — built once in FEAT-023 U1
502
- so no surface hand-rolls its own amount formatting, status chip, or
503
- checkout/portal button.
504
-
505
- **10g — Invoice surface, shipped (FEAT-023 U4).** When the consumer picks
506
- "Invoice" at 10b, this surface IS available (unlike the still-unshipped
507
- choices in 10e). Invoice management is an ADMIN action, never a customer-
508
- facing or anonymous one (a security review of the sibling U3 pay-by-link
509
- template found the entitlement check alone is NOT authentication — see
510
- `docs/research/commerce-template-audit/invoices.md` finding 7):
511
-
512
- 1. Instantiate `templates/invoice-route.template.ts` into the consumer's
513
- `apps/api` — mirrors 10d's `stripe-config.template.ts` instantiation. It
514
- exports a Fastify plugin (`invoiceRoutes`: `GET` list/retrieve, `POST`
515
- create-draft, `POST .../send` finalize+send) built on
516
- `@working-theory/stripe-client`'s `createInvoice` / `sendInvoice` /
517
- `listInvoices` / `retrieveInvoice`.
518
- 2. **Register it inside the TENANT-GUARDED scope `apps/api`'s
519
- `billingRoutes` use** — the inner `tenantScope` in `server.ts` that
520
- registers `tenantPreHandlerPlugin` (itself nested inside `authScope`) —
521
- e.g. `await tenantScope.register(invoiceRoutes);`. **NOT** the outer
522
- `authScope`. This is REQUIRED, not optional, and it is a SECURITY
523
- boundary, not a style preference:
524
- - `authPreHandlerPlugin` (inherited from the enclosing `authScope`) gives
525
- every handler an authenticated caller plus the cookie path's same-origin
526
- CSRF check. The template then requires `User.role` in {`admin`,`owner`},
527
- so anonymous and plain-authenticated callers are refused.
528
- - `tenantPreHandlerPlugin` (default-deny) populates `request.tenant`, so
529
- the org is **server-derived** and never client-supplied — the same
530
- property `routes/billing/read.ts` relies on. Registering in `authScope`
531
- instead leaves `request.tenant` undefined, and the template then fails
532
- **closed** (403) rather than serving an unscoped admin surface.
533
- Tell the consumer explicitly that this route is admin-only, and that a
534
- Bearer-token caller is denied (a bearer token carries no workspace
535
- binding) — this is a cookie-session admin-UI surface.
536
- 3. Point the consumer at `@working-theory/ui`'s `InvoiceList` (block),
537
- `InvoiceDetail` (panel), and `InvoiceForm` (block) to compose the
538
- admin-facing management page — `InvoiceForm` posts directly to the
539
- instantiated route; `InvoiceList`/`InvoiceDetail` render already-fetched
540
- invoice records (reusing `PaymentStatusBadge` for status and
541
- `MoneyAmount` for amounts, per 10f).
542
- 4. Re-run 10c's live-key check before instantiating (already covered by
543
- this step's ordering, restated for clarity). The guard has no remaining
544
- gap: AILK#3734 converted `assertSandboxKey` to an allow-list, so a Stripe
545
- RESTRICTED key is refused like every other non-test-mode key. Finding 10
546
- in the audit log records the historical deny-list gap and is marked
547
- resolved there.
548
- 5. **Multi-tenant module — the surface REFUSES, it does not warn.** v1 has
549
- ONE deployment-wide `STRIPE_SECRET_KEY` and no per-organization Stripe
550
- customer namespace, so an invoice carries no org attribution and a
551
- caller-supplied `invoiceId`/`customerId` cannot be honestly checked
552
- against the caller's org (findings 7 + 9 in the audit log). The template's
553
- `requireSingleTenantScope` guard therefore admits **only the deployment's
554
- own operator org** — the fixed single-tenant sentinel `DEFAULT_ORG_ID`,
555
- which an organization created through the multi-tenant module never
556
- carries — and **hard-refuses every other org's admin with a 403
557
- `tenancy_unsupported`**, before any Stripe call. That is what stops one
558
- tenant's admin reading another tenant's customer PII (names, emails,
559
- amounts, line items) out of the shared Stripe account. Tell the consumer
560
- plainly: **invoice management is an OPERATOR surface at v1, not a
561
- per-tenant self-service one.** The operator can still see every invoice in
562
- the deployment-wide Stripe account — they hold the `STRIPE_SECRET_KEY` and
563
- can read the same data from the Stripe dashboard, so this is not an
564
- escalation — but per-tenant invoicing needs a per-org Stripe Connect /
565
- customer-namespace model (tracked follow-up).
566
-
567
- **`User.role` is DEPLOYMENT-OPERATOR-ONLY, and must not be widened to reach
568
- this surface.** The `requireSiteAdmin` gate keys on `User.role` — a
569
- per-deployment String (`"user" | "admin" | "owner"`), DISTINCT from the
570
- org-membership `Role` enum the multi-tenant module uses. Do not grant
571
- `User.role = "admin"` to an organization's admins as a way of giving them
572
- invoice access: that is a site-wide operator grant, not an org-scoped one,
573
- and until per-org Stripe namespacing exists it would turn the tenancy
574
- refusal above into a cross-tenant PII leak (names, emails, amounts, line
575
- items). The correct answer to "our org admins need invoices" at v1 is that
576
- the surface does not support it yet — not a role change.
577
-
578
- **10h — Subscription surface, shipped (FEAT-023 U5).** When the consumer picks
579
- "Subscription" at 10b, this surface IS available. It splits into TWO handlers
580
- with DELIBERATELY OPPOSITE auth postures — the same split checkout (public)
581
- vs invoice (admin) already established:
582
-
583
- 1. Instantiate `templates/subscription-route.template.ts` into the consumer's
584
- `apps/api` — mirrors 10d's `stripe-config.template.ts` instantiation. It
585
- exports `subscribeHandler` (a standalone Fastify handler) and
586
- `subscriptionRoutes` (a Fastify plugin), built on
587
- `@working-theory/stripe-client`'s `createCheckoutSession` (`mode:
588
- "subscription"`, pinned) and `retrieveSubscriptionStatus`.
589
- 2. **Register `subscribeHandler` PUBLICLY, rate-limited — mirrors 10e-i's
590
- checkout registration exactly.** It is a storefront "subscribe" action
591
- (the recurring-price analog of the one-time checkout route): pin
592
- `ALLOWED_LOOKUP_KEYS` to the RECURRING lookup_key(s) the consumer selected
593
- (step 1's selection, verbatim — never "allow all"), confirm `FRONTEND_URL`
594
- is set (the same origin-lock 10e-i requires), and scope it behind the
595
- shared per-IP rate-limit middleware. Both edits are MANDATORY — the route
596
- is fail-closed until they're done.
597
- 3. **Register `subscriptionRoutes` inside the SAME tenant-guarded scope
598
- 10g's `invoiceRoutes` uses** — e.g. `await tenantScope.register(subscriptionRoutes);`.
599
- NOT the outer `authScope`, and NOT the same public scope as
600
- `subscribeHandler`. This is a READ of a specific subscription's status —
601
- a MANAGEMENT-shaped action, so it inherits 10g's admin + tenancy guards
602
- verbatim (`requireSiteAdmin` + `requireSingleTenantScope`, the fixed
603
- single-tenant sentinel `DEFAULT_ORG_ID`). See the template's own header
604
- comment for the full registration snippet.
605
- 4. Point the consumer at `@working-theory/ui`'s `PricingTable` (section) and
606
- `SubscriptionStatus` (block) to compose the subscribe + status pages —
607
- `PricingTable`'s `onSubscribe` POSTs to step 2's route + redirects to the
608
- returned Stripe Checkout URL (mirrors `CheckoutForm`'s `onCheckout`
609
- shape); `SubscriptionStatus` renders an already-fetched status (step 3's
610
- route) via `PaymentStatusBadge` (reused from U1) plus a portal "Manage"
611
- action pointing at the U6 billing-portal surface — this surface never
612
- builds self-service plan-change/cancel itself.
613
- 5. Re-run 10c's live-key check before instantiating. State the CURRENT
614
- behaviour to the consumer: `assertSandboxKey` is an ALLOW-LIST as of
615
- AILK#3734 — only `sk_test_`, `pk_test_` and `rk_test_` are accepted, so
616
- every live key including a restricted one is refused. No surface header
617
- carries the pre-#3734 deny-list caveat any more (AILK#3765 retired the
618
- last of them); if you find one, it is stale — fix it rather than repeat it.
619
- 6. **Multi-tenant module — same refusal as invoices, same reason.** v1 has
620
- ONE deployment-wide `STRIPE_SECRET_KEY` and no per-organization Stripe
621
- customer namespace, so a subscription carries no org attribution.
622
- `subscriptionRoutes`'s `requireSingleTenantScope` guard therefore admits
623
- ONLY the deployment's own operator org and hard-refuses every other org's
624
- admin with a 403 `tenancy_unsupported`, before any Stripe call. Tell the
625
- consumer plainly: subscription-status reading is an OPERATOR surface at
626
- v1, not a per-tenant self-service one — the same disclosed limitation as
627
- 10g's invoice surface (see
628
- `docs/research/commerce-template-audit/subscriptions.md`).
629
- 7. **No subscription webhook is scaffolded at v1** — the status read is
630
- retrieve-on-view by the subscription's own Stripe id (mirrors 10g's
631
- `?invoiceId=` shape with `?subscriptionId=`), not webhook-driven. If a
632
- future need requires webhook-driven status (e.g. proactive
633
- past-due notices), that is a new unit of work, not a tweak to this
634
- surface.
635
-
636
- **10i — Portal surface, shipped (FEAT-023 U6).** When the consumer picks
637
- "Portal" at 10b, this surface IS available. It is the smallest FEAT-023
638
- surface (spec §1) — AILK owns only the entry, Stripe hosts the portal
639
- itself:
640
-
641
- 1. Instantiate `templates/portal-route.template.ts` into the consumer's
642
- `apps/api` — mirrors 10d's `stripe-config.template.ts` instantiation. It
643
- exports `portalRoutes` (a Fastify plugin: a single `POST
644
- /v1/commerce/portal-session`), built on
645
- `@working-theory/stripe-client`'s `createBillingPortalSession`.
646
- 2. **Register `portalRoutes` inside the SAME tenant-guarded scope 10g's
647
- `invoiceRoutes` uses** — e.g. `await tenantScope.register(portalRoutes);`.
648
- NOT the outer `authScope`. Starting a billing-portal session hands the
649
- caller a live link into a customer's full billing surface, so — like
650
- 10h's `subscriptionRoutes` status read — this is a MANAGEMENT-shaped
651
- action, never public/anonymous: it inherits 10g's admin + tenancy guards
652
- verbatim (`requireSiteAdmin` + `requireSingleTenantScope`, the fixed
653
- single-tenant sentinel `DEFAULT_ORG_ID`). See the template's own header
654
- comment for the full registration snippet.
655
- 3. Point the consumer at `@working-theory/ui`'s `PortalEntry` (block) to
656
- compose a standalone billing-management entry point, AND wire it as the
657
- target of 10h's `SubscriptionStatus` `manageAction` (the U5↔U6 seam,
658
- spec § design decision) — both `PortalEntry`'s `onManage` and
659
- `SubscriptionStatus`'s `manageAction.onManage` POST to step 1's route
660
- with `{ customerId }` and, on a `200` response, set
661
- `window.location.href` to the returned `url`. Pass `testMode` (always
662
- `true` at v1 — sandbox-only).
663
- 4. Re-run 10c's live-key check before instantiating. Note the guard's
664
- current state (AILK#3734, now an allow-list) — restate the CURRENT state
665
- to the consumer, same as 10h.
666
- 5. **Multi-tenant module — same refusal as invoices/subscriptions, same
667
- reason.** v1 has ONE deployment-wide `STRIPE_SECRET_KEY` and no
668
- per-organization Stripe customer namespace, so a `customerId` carries no
669
- org attribution. `portalRoutes`'s `requireSingleTenantScope` guard
670
- therefore admits ONLY the deployment's own operator org and hard-refuses
671
- every other org's admin with a 403 `tenancy_unsupported`, before any
672
- Stripe call. Tell the consumer plainly: billing-portal sessions are an
673
- OPERATOR surface at v1, not a per-tenant self-service one — the same
674
- disclosed limitation as 10g/10h (see
675
- `docs/research/commerce-template-audit/portal.md`).
676
- 6. **The return URL is fixed, server-derived, never caller-supplied** — a
677
- single `PORTAL_RETURN_PATH` composed from `FRONTEND_URL` (the template's
678
- own `SCAFFOLD-TIME EDIT` if the consumer's account/billing page lives
679
- elsewhere), mirroring `apps/api/src/routes/billing/portal.ts`'s existing
680
- convention. There is no caller-chosen destination to validate, unlike
681
- checkout's `successUrl`/`cancelUrl`.
682
- 7. **No portal-session webhook is scaffolded at v1** — mint-on-demand by
683
- the caller-supplied `customerId`, not webhook-driven, mirroring 10h's
684
- escalation-boundary posture.
685
-
686
- ## Output style
687
-
688
- - **Two questions, two paths.** Don't lead with Stripe assumptions.
689
- - **Live-key checks happen first** for the Stripe path. Always.
690
- - **Show + ask on conflicts.** Never silently skip or auto-overwrite.
691
- - **Source the consumer owns.** The link path emits inline source, not
692
- an `@working-theory/ui` import. The consumer can edit any of it post-emit.
693
- - **`lookup_key` derivations are visible.** Show the derivation; let
694
- the consumer override before any API call.
695
-
696
- ## What this skill does NOT do
697
-
698
- - Live-mode Stripe anything. Hook-blocked globally; SKILL refuses.
699
- - Bundle marketplace logos (Amazon, Etsy, Shopify, eBay, Gumroad).
700
- AILK doesn't ship those SVGs (trademark-redistribution concern). The
701
- consumer drops their own SVG into `public/logos/` and passes
702
- `iconSrc` to `<MarketplaceCTA>`. See
703
- `references/marketplace-brand-pages.md` for official brand-kit
704
- sources.
705
- - Build a pricing page. That's `scaffold-page` (#362).
706
- - Register a production webhook endpoint. That's `provision-config`
707
- (#367) or future deploy skill.
708
- - Use `pricebook.ts` or env-var price mapping. AILK uses
709
- `lookup_key`s as the source of truth — they're stable across price
710
- recreates.
711
- - Auto-invoke other skills. Always points at the next skill by name;
712
- consumer drives.
713
- - Guess entitlement conversationally. The `feature:commerce`
714
- surface-selection branch (Step 10) checks the resolved license via
715
- `hasFeature("commerce", license)` — never a verbal "I'm on Pro"
716
- claim. No resolvable license → not entitled (fail-closed).
717
- - Build a payment surface itself for a choice OTHER than checkout
718
- (U3–U6). This skill's Step 10 checks entitlement, offers the choice,
719
- and dispatches — the surface scaffolders own the actual page/route.
720
- Checkout (one-time) is the one exception: Step 10e-i wires it inline
721
- (FEAT-023 U2).
722
-
723
- ## References
724
-
725
- - `references/marketplace-brand-pages.md` — links to official brand
726
- kits (Amazon, Etsy, Shopify, eBay, Gumroad). Used by the link path
727
- when the consumer wants logos. AILK does not bundle the SVGs.
728
- - `references/lookup-keys-template.md` — template the skill instantiates
729
- per session, lists every product the consumer created with its
730
- `lookup_key` and price.
731
- - `templates/marketplace-cta-route.tsx.tmpl` — emitted route source
732
- the link path writes. Includes the inline `<MarketplaceCTA>`
733
- component definition (not a separate package).
734
- - `templates/stripe-config.template.ts` — shared Stripe surface config
735
- (env contract + `assertSandboxKey` test-mode guard) every U2–U6
736
- surface scaffolder instantiates (Step 10d, FEAT-023 U1).
737
- - `templates/checkout-route.template.ts` — the one-time checkout POST
738
- route (`mode: "payment"`, `stripe-client/checkout.ts`), instantiated
739
- by Step 10e-i (FEAT-023 U2).
740
- - `templates/payment-link-route.template.ts` — the payment-link
741
- MANAGEMENT route (list / create / archive), instantiated by Step
742
- 10e-ii (FEAT-023 U3). Authenticated + site-admin only, registered
743
- inside the auth scope — see 10e-ii; its posture is the opposite of
744
- the public checkout route's.
745
- - `templates/invoice-route.template.ts` — the Invoice surface's admin-only
746
- Fastify plugin (create/send/list/retrieve), instantiated at Step 10g
747
- (FEAT-023 U4, shipped). Registration inside `apps/api`'s tenant-guarded
748
- scope is REQUIRED — see the template's own header comment.
749
- - `templates/subscription-route.template.ts` — the Subscription surface's
750
- `subscribeHandler` (public, rate-limited) + `subscriptionRoutes` (admin +
751
- tenant-guarded status read), instantiated at Step 10h (FEAT-023 U5,
752
- shipped). The two handlers have OPPOSITE auth postures — see the
753
- template's own header comment for both registration points.
754
- - `templates/portal-route.template.ts` — the Portal surface's admin +
755
- tenant-guarded `portalRoutes` (mints a billing-portal session for a
756
- caller-supplied `customerId`), instantiated at Step 10i (FEAT-023 U6,
757
- shipped). Registration inside `apps/api`'s tenant-guarded scope is
758
- REQUIRED — see the template's own header comment.
759
-
760
- ## Coordination
761
-
762
- - **`scaffold-page` (#362)** — pricing UI. After this skill creates
763
- products and surfaces `lookup_key`s, the consumer scaffolds a
764
- `/pricing` page that consumes them.
765
- - **`provision-config` (#367)** — production webhook setup, deploy
766
- config, persistent leads (related infra). v1 is dev-mode only;
767
- prod webhook lives there.
768
- - **Stripe MCP** — used for product/price/lookup_key creation. Should
769
- already be configured in `.claude/settings.json` from the consumer
770
- scaffold. If missing, surface a clear "Stripe MCP not configured"
771
- error and stop; do not invent the configuration.
772
- - **`@working-theory/stripe-client`** — wrapper for Stripe SDK. The skill does
773
- not import this directly (the skill is a prompt/template, not
774
- runtime code). The consumer's `apps/api/routes/checkout/sessions.ts`
775
- (generic primitive) and the Step-10e-i-instantiated
776
- `checkout-route.template.ts` (one-time surface) use it.
777
- `assertSandboxKey` is reused by `stripe-config.template.ts`.
778
- - **`@working-theory/license-client`** — `hasFeature("commerce", license)`
779
- is the entitlement read Step 10a consults (static-mode, sync, no
780
- network). The skill does not import this directly either — it
781
- describes the check a consumer's own license-aware tooling performs.
782
- - **FEAT-023 U2–U6 (checkout · pay-by-link · invoice · subscription ·
783
- portal)** — the five payment surfaces Step 10e dispatches to. Checkout
784
- (one-time) shipped in U2 (10e-i); pay-by-link shipped in U3 (10e-ii);
785
- invoice shipped in U4 (10g); subscription shipped in U5 (10h); portal
786
- shipped in U6 (10i).
787
- - **FEAT-023 U4 (invoice)** — shipped (Step 10g). `@working-theory/stripe-client`'s
788
- `invoice.ts` (create/send/list/retrieve) + `@working-theory/ui`'s
789
- `InvoiceList`/`InvoiceDetail`/`InvoiceForm` compose the surface; the
790
- route template lives at `templates/invoice-route.template.ts`.
791
- - **FEAT-023 U5 (subscription)** — shipped (Step 10h).
792
- `@working-theory/stripe-client`'s `checkout.ts` (`createCheckoutSession`
793
- `mode: "subscription"` + `retrieveSubscriptionStatus`) +
794
- `@working-theory/ui`'s `PricingTable`/`SubscriptionStatus` compose the
795
- surface; the route template lives at
796
- `templates/subscription-route.template.ts`.
797
- - **FEAT-023 U6 (portal)** — shipped (Step 10i). `@working-theory/stripe-client`'s
798
- `billing-portal.ts` (`createBillingPortalSession`) + `@working-theory/ui`'s
799
- `PortalEntry` (reuses U1's `StripeActionButton`) compose the surface — the
800
- target of U5's `SubscriptionStatus` "Manage" action; the route template
801
- lives at `templates/portal-route.template.ts`.
802
- - **`@working-theory/ui`'s `MoneyAmount` / `PaymentStatusBadge` /
803
- `StripeActionButton`** — the shared presentational components every
804
- U2–U6 surface composes (Step 10f, FEAT-023 U1).
805
- - **`@working-theory/ui`'s `CheckoutForm` / `PaymentResult`** — the
806
- one-time checkout surface's own capability-bearing blocks (FEAT-023
807
- U2), composed at Step 10e-i steps 3–4.