create-ailk 0.5.0 → 0.6.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 (299) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/lib/api-boot-assertion.d.ts +46 -0
  5. package/dist/lib/api-boot-assertion.js +70 -0
  6. package/dist/lib/import-graph.d.ts +29 -0
  7. package/dist/lib/import-graph.js +140 -0
  8. package/dist/module-architecture.d.ts +1 -0
  9. package/dist/module-architecture.js +253 -127
  10. package/dist/surfaces.js +105 -7
  11. package/dist/template-manifest.d.ts +33 -0
  12. package/dist/template-manifest.js +99 -16
  13. package/dist/template-names.d.ts +1 -1
  14. package/dist/template-names.js +1 -0
  15. package/package.json +3 -2
  16. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  17. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  18. package/site-templates/marketing-site/ailk-template.json +1 -1
  19. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  20. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  21. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  22. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  23. package/site-templates/minimalism-site/ailk-template.json +2 -1
  24. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  25. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  26. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  27. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  28. package/site-templates/simple-service/ailk-template.json +10 -0
  29. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  30. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  31. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  32. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  33. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  34. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  35. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  36. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  37. package/site-templates/simple-service/provenance.json +4 -0
  38. package/templates/.env.example +72 -7
  39. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  40. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  41. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  42. package/templates/apps/api/.env.example +13 -0
  43. package/templates/apps/api/CLAUDE.md +26 -2
  44. package/templates/apps/api/jest.config.cjs +3 -0
  45. package/templates/apps/api/package.json +1 -0
  46. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  47. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  48. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  49. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  50. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  51. package/templates/apps/api/src/config/index.ts +62 -0
  52. package/templates/apps/api/src/config/modules.ts +7 -3
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  54. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  55. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  56. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  57. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  58. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  59. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  60. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  61. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  62. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  63. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  64. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  65. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  66. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  67. package/templates/apps/api/src/lib/site.ts +208 -9
  68. package/templates/apps/api/src/lib/stripe.ts +66 -0
  69. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  70. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  71. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  72. package/templates/apps/api/src/openapi/spec.ts +543 -2
  73. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  74. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  75. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  76. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  77. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  78. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  80. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  81. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  82. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  83. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  84. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  85. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  87. package/templates/apps/api/src/routes/content/index.ts +13 -73
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  89. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  90. package/templates/apps/api/src/routes/content/search.ts +129 -0
  91. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  92. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  93. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  94. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  95. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  96. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  97. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  98. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  99. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  100. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  101. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  102. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  103. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  104. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  105. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  106. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  111. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  112. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  113. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  114. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  115. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  116. package/templates/apps/api/src/server.ts +439 -49
  117. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  118. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  119. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  120. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  121. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  122. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  123. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  124. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  125. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  126. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  127. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  128. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  129. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  130. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  131. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  132. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  133. package/templates/apps/api/src/services/audit-log.ts +668 -0
  134. package/templates/apps/api/src/services/consent.ts +18 -0
  135. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  136. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  137. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  138. package/templates/apps/api/src/services/entitlements.ts +80 -0
  139. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  140. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  141. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  142. package/templates/apps/api/src/services/listing-config.ts +27 -9
  143. package/templates/apps/api/src/services/project-listings.ts +201 -6
  144. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  145. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  146. package/templates/apps/api/src/services/revalidation.ts +173 -0
  147. package/templates/apps/api/src/services/verified-email.ts +71 -0
  148. package/templates/apps/api/src/vercel-handler.ts +75 -35
  149. package/templates/apps/mcp/.env.example +17 -0
  150. package/templates/apps/mcp/CLAUDE.md +57 -24
  151. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  152. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  153. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  154. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  155. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  156. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  157. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  158. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  159. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  160. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  161. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  162. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  163. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  164. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  165. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  166. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  167. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  168. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  169. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  170. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  171. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  172. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  173. package/templates/apps/mcp/jest.config.cjs +13 -0
  174. package/templates/apps/mcp/package.json +2 -0
  175. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  176. package/templates/apps/mcp/src/config/modules.ts +10 -5
  177. package/templates/apps/mcp/src/http-transport.ts +102 -29
  178. package/templates/apps/mcp/src/index.ts +73 -11
  179. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  180. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  181. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  182. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  183. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  184. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  185. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  186. package/templates/apps/mcp/src/server.ts +59 -35
  187. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  188. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  189. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  190. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  191. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  192. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  193. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  194. package/templates/apps/mcp/src/tools/index.ts +6 -4
  195. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  196. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  197. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  198. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  199. package/templates/apps/web/.env.example +71 -5
  200. package/templates/apps/web/CLAUDE.md +44 -0
  201. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  202. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  203. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  204. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  205. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  206. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  207. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  208. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  209. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  210. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  211. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  212. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  213. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  214. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  215. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  216. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  217. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  218. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  219. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  220. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  221. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  222. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  223. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  224. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  225. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  226. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  227. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  228. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  229. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  230. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  232. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  233. package/templates/apps/web/auth.ts +407 -8
  234. package/templates/apps/web/jest.config.cjs +8 -4
  235. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  236. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  237. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  238. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  239. package/templates/apps/web/lib/auth-email.ts +318 -0
  240. package/templates/apps/web/lib/auth-env.ts +85 -0
  241. package/templates/apps/web/lib/auth-routes.ts +55 -0
  242. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  243. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  244. package/templates/apps/web/messages/en.json +187 -0
  245. package/templates/apps/web/package.json +2 -1
  246. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  247. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  248. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  249. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  250. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  251. package/templates/database/CHANGELOG.md +142 -0
  252. package/templates/database/README.md +10 -3
  253. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  254. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  255. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  256. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  257. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  258. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  259. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  260. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  261. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  262. package/templates/database/__tests__/tenancy.test.ts +141 -0
  263. package/templates/database/config/schema.prisma +79 -2
  264. package/templates/database/content/schema.prisma +87 -11
  265. package/templates/database/inbox/schema.prisma +452 -9
  266. package/templates/database/jest.config.cjs +5 -3
  267. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  268. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  269. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  270. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  271. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  272. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  273. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  274. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  275. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  276. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  277. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  278. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  279. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  280. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  281. package/templates/database/package.json +1 -1
  282. package/templates/database/tenancy.ts +86 -1
  283. package/templates/database/tsconfig.json +1 -0
  284. package/versions.json +34 -34
  285. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  286. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  287. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  288. package/templates/apps/api/src/routes/content/create.ts +0 -115
  289. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  290. package/templates/apps/api/src/routes/content/update.ts +0 -112
  291. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  292. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  293. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  294. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  295. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  296. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  297. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  298. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  299. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * POST /api/webhooks/stripe — org-scoped billing path (#3601, spec AC2/AC4)
3
+ *
4
+ * Covers the Organization billing-link branch keyed on
5
+ * metadata.organizationId (spec D2): a checkout with organizationId links the
6
+ * Organization (real org AND individual-as-default-org), idempotently; a
7
+ * checkout WITHOUT organizationId takes the User path and never touches the
8
+ * Organization (single-tenant, degrade-clean regression floor).
9
+ *
10
+ * Companion to stripe.test.ts (the User-path tests, which stay unmodified —
11
+ * AC4 regression floor).
12
+ */
13
+ import type { FastifyInstance } from "fastify";
14
+ import Fastify from "fastify";
15
+
16
+ // ── Mocks (mirror stripe.test.ts) ────────────────────────────────────────────
17
+
18
+ const mockTrack = jest.fn().mockResolvedValue(undefined);
19
+ // Commerce claim (D-64, #5796): the receiver is a base-tree primitive gated on
20
+ // the JWT `commerce` claim, so every case in this file is a COMMERCE-CLAIMED
21
+ // deployment — which is what each one was already asserting before the gate
22
+ // existed. The unclaimed matrix lives in
23
+ // `src/__tests__/commerce-claim-registration.test.ts`.
24
+ jest.mock("@working-theory/license", () => ({
25
+ checkLicense: jest.fn().mockResolvedValue({
26
+ tier: "pro",
27
+ plan: "pro-monthly",
28
+ features: ["commerce"],
29
+ expiresAt: null,
30
+ revoked: false,
31
+ }),
32
+ }));
33
+
34
+ jest.mock("@working-theory/analytics/server", () => ({
35
+ track: (...args: unknown[]) => mockTrack(...args),
36
+ }));
37
+
38
+ // eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); __mocks__/prisma.ts is the repo-standard test double for this boundary.
39
+ jest.mock("../../../lib/prisma.js");
40
+
41
+ const mockVerifyWebhookSignature = jest.fn();
42
+ class MockStripeWebhookSignatureError extends Error {
43
+ constructor(message: string) {
44
+ super(message);
45
+ this.name = "StripeWebhookSignatureError";
46
+ }
47
+ }
48
+ jest.mock("@working-theory/stripe-client", () => ({
49
+ verifyWebhookSignature: (...args: unknown[]) =>
50
+ mockVerifyWebhookSignature(...args),
51
+ StripeWebhookSignatureError: MockStripeWebhookSignatureError,
52
+ assertSandboxKey: jest.fn(),
53
+ createCheckoutSession: jest.fn(),
54
+ getStripeClient: jest.fn(),
55
+ STRIPE_API_VERSION: "2024-06-20",
56
+ StripeConfigError: class StripeConfigError extends Error {},
57
+ UnknownProductCodeError: class UnknownProductCodeError extends Error {},
58
+ WEBHOOK_TOLERANCE_SECONDS: 300,
59
+ }));
60
+
61
+ import * as configModule from "../../../config/index.js";
62
+ import { prisma } from "../../../lib/prisma.js";
63
+ import { __resetIdempotencyStore } from "../../../lib/webhook-idempotency.js";
64
+ import { stripeWebhookHandler } from "../stripe.js";
65
+
66
+ // The sentinel default Organization id (orgs-of-one billing anchor).
67
+ const DEFAULT_ORG_ID = "org_00000000-0000-7000-8000-000000000001";
68
+ const REAL_ORG_ID = "org_00000000-0000-7000-8000-0000000000ab";
69
+
70
+ const VALID_BODY = Buffer.from(JSON.stringify({ type: "test" }));
71
+ const VALID_SIGNATURE = "t=1234,v1=abc";
72
+
73
+ async function buildTestServer(): Promise<FastifyInstance> {
74
+ const server = Fastify({ logger: false });
75
+ await server.register(async (scope) => {
76
+ scope.addContentTypeParser(
77
+ "application/json",
78
+ { parseAs: "buffer" },
79
+ (_req, body, done) => done(null, body),
80
+ );
81
+ scope.post("/v1/webhooks/stripe", stripeWebhookHandler);
82
+ });
83
+ await server.ready();
84
+ return server;
85
+ }
86
+
87
+ function checkoutEvent(
88
+ id: string,
89
+ metadata: Record<string, string> | null,
90
+ customer = "cus_test_org_123",
91
+ ) {
92
+ return {
93
+ id,
94
+ type: "checkout.session.completed",
95
+ data: {
96
+ object: {
97
+ id: "cs_test_org",
98
+ amount_total: 9900,
99
+ currency: "usd",
100
+ customer,
101
+ customer_details: { email: "org-admin@example.com" },
102
+ metadata,
103
+ },
104
+ },
105
+ };
106
+ }
107
+
108
+ async function fire(server: FastifyInstance) {
109
+ return server.inject({
110
+ method: "POST",
111
+ url: "/v1/webhooks/stripe",
112
+ payload: VALID_BODY,
113
+ headers: {
114
+ "content-type": "application/json",
115
+ "stripe-signature": VALID_SIGNATURE,
116
+ },
117
+ });
118
+ }
119
+
120
+ describe("POST /api/webhooks/stripe — org-scoped billing (#3601)", () => {
121
+ let server: FastifyInstance;
122
+
123
+ beforeEach(async () => {
124
+ jest.clearAllMocks();
125
+ __resetIdempotencyStore();
126
+ jest.replaceProperty(configModule, "config", {
127
+ ...configModule.config,
128
+ STRIPE_WEBHOOK_SECRET: "whsec_test_secret",
129
+ });
130
+ server = await buildTestServer();
131
+ });
132
+
133
+ afterEach(async () => {
134
+ await server.close();
135
+ jest.restoreAllMocks();
136
+ });
137
+
138
+ // AC2 — a real org checkout links the Organization by id; replay is a no-op.
139
+ it("links the Organization on checkout with metadata.organizationId, idempotent on replay", async () => {
140
+ const customerId = "cus_test_org_123";
141
+
142
+ // First fire: no prior composite link → update the target org.
143
+ (prisma.organization.findFirst as jest.Mock).mockResolvedValueOnce(null);
144
+ (prisma.organization.update as jest.Mock).mockResolvedValueOnce({
145
+ id: REAL_ORG_ID,
146
+ billingProvider: "stripe",
147
+ billingCustomerId: customerId,
148
+ });
149
+
150
+ mockVerifyWebhookSignature.mockReturnValue(
151
+ checkoutEvent("evt_org_001", { organizationId: REAL_ORG_ID }, customerId),
152
+ );
153
+ const r1 = await fire(server);
154
+ expect(r1.statusCode).toBe(200);
155
+ expect(prisma.organization.update).toHaveBeenCalledTimes(1);
156
+ expect(prisma.organization.update).toHaveBeenCalledWith(
157
+ expect.objectContaining({
158
+ where: { id: REAL_ORG_ID },
159
+ data: expect.objectContaining({
160
+ billingProvider: "stripe",
161
+ billingCustomerId: customerId,
162
+ }),
163
+ }),
164
+ );
165
+ // The User path is never taken for an org checkout.
166
+ expect(prisma.user.update).not.toHaveBeenCalled();
167
+
168
+ // Replay the SAME event id → idempotency store short-circuits, no 2nd write.
169
+ mockVerifyWebhookSignature.mockReturnValue(
170
+ checkoutEvent("evt_org_001", { organizationId: REAL_ORG_ID }, customerId),
171
+ );
172
+ const r2 = await fire(server);
173
+ expect(r2.statusCode).toBe(200);
174
+ expect(prisma.organization.update).toHaveBeenCalledTimes(1);
175
+ });
176
+
177
+ // AC2 — typed-composite match already pointing at this org → no-op (a fresh
178
+ // event id, but the customer is already linked to the target org).
179
+ it("is a no-op when the org is already linked to this customer (typed-composite match)", async () => {
180
+ const customerId = "cus_already_linked";
181
+ (prisma.organization.findFirst as jest.Mock).mockResolvedValueOnce({
182
+ id: REAL_ORG_ID,
183
+ billingProvider: "stripe",
184
+ billingCustomerId: customerId,
185
+ });
186
+
187
+ mockVerifyWebhookSignature.mockReturnValue(
188
+ checkoutEvent("evt_org_002", { organizationId: REAL_ORG_ID }, customerId),
189
+ );
190
+ const r = await fire(server);
191
+ expect(r.statusCode).toBe(200);
192
+ expect(prisma.organization.update).not.toHaveBeenCalled();
193
+ });
194
+
195
+ // AC2 — an individual is billed on their sentinel default Organization
196
+ // (orgs-of-one): the SAME org path, targeting DEFAULT_ORG_ID.
197
+ it("bills an individual on their sentinel default Organization (orgs-of-one)", async () => {
198
+ const customerId = "cus_individual_xyz";
199
+ (prisma.organization.findFirst as jest.Mock).mockResolvedValueOnce(null);
200
+ (prisma.organization.update as jest.Mock).mockResolvedValueOnce({
201
+ id: DEFAULT_ORG_ID,
202
+ billingProvider: "stripe",
203
+ billingCustomerId: customerId,
204
+ });
205
+
206
+ mockVerifyWebhookSignature.mockReturnValue(
207
+ checkoutEvent(
208
+ "evt_org_003",
209
+ { organizationId: DEFAULT_ORG_ID },
210
+ customerId,
211
+ ),
212
+ );
213
+ const r = await fire(server);
214
+ expect(r.statusCode).toBe(200);
215
+ expect(prisma.organization.update).toHaveBeenCalledWith(
216
+ expect.objectContaining({ where: { id: DEFAULT_ORG_ID } }),
217
+ );
218
+ expect(prisma.user.update).not.toHaveBeenCalled();
219
+ });
220
+
221
+ // AC4 — a checkout WITHOUT organizationId takes the User path unchanged; the
222
+ // Organization is never touched (single-tenant degrade-clean regression).
223
+ it("takes the User path (never touches Organization) when no organizationId is present", async () => {
224
+ const customerId = "cus_single_tenant";
225
+ (prisma.user.findFirst as jest.Mock).mockResolvedValueOnce(null);
226
+ (prisma.user.findUnique as jest.Mock).mockResolvedValueOnce({
227
+ id: "usr_st",
228
+ email: "org-admin@example.com",
229
+ billingProvider: null,
230
+ billingCustomerId: null,
231
+ });
232
+ (prisma.user.update as jest.Mock).mockResolvedValueOnce({ id: "usr_st" });
233
+
234
+ mockVerifyWebhookSignature.mockReturnValue(
235
+ checkoutEvent("evt_st_004", { productId: "pro" }, customerId),
236
+ );
237
+ const r = await fire(server);
238
+ expect(r.statusCode).toBe(200);
239
+ expect(prisma.user.update).toHaveBeenCalledTimes(1);
240
+ expect(prisma.organization.findFirst).not.toHaveBeenCalled();
241
+ expect(prisma.organization.update).not.toHaveBeenCalled();
242
+ });
243
+
244
+ // customer.created also routes on metadata.organizationId.
245
+ it("links the Organization on customer.created with metadata.organizationId", async () => {
246
+ const customerId = "cus_created_org";
247
+ (prisma.organization.findFirst as jest.Mock).mockResolvedValueOnce(null);
248
+ (prisma.organization.update as jest.Mock).mockResolvedValueOnce({
249
+ id: REAL_ORG_ID,
250
+ });
251
+
252
+ mockVerifyWebhookSignature.mockReturnValue({
253
+ id: "evt_cust_005",
254
+ type: "customer.created",
255
+ data: {
256
+ object: {
257
+ id: customerId,
258
+ email: "org-admin@example.com",
259
+ metadata: { organizationId: REAL_ORG_ID },
260
+ },
261
+ },
262
+ });
263
+ const r = await fire(server);
264
+ expect(r.statusCode).toBe(200);
265
+ expect(prisma.organization.update).toHaveBeenCalledWith(
266
+ expect.objectContaining({ where: { id: REAL_ORG_ID } }),
267
+ );
268
+ expect(prisma.user.update).not.toHaveBeenCalled();
269
+ });
270
+
271
+ // A DB failure on the org link never throws (200 still returned).
272
+ it("returns 200 even when the org billing update throws", async () => {
273
+ (prisma.organization.findFirst as jest.Mock).mockResolvedValueOnce(null);
274
+ (prisma.organization.update as jest.Mock).mockRejectedValueOnce(
275
+ new Error("db unreachable"),
276
+ );
277
+
278
+ mockVerifyWebhookSignature.mockReturnValue(
279
+ checkoutEvent("evt_org_006", { organizationId: REAL_ORG_ID }),
280
+ );
281
+ const r = await fire(server);
282
+ expect(r.statusCode).toBe(200);
283
+ expect(JSON.parse(r.body)).toEqual({ received: true });
284
+ });
285
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Stripe webhook rate-limit — per-IP cap on the webhook endpoint.
3
+ *
4
+ * The webhook is a public endpoint. Without a rate limit, an attacker can
5
+ * trigger CPU-intensive signature verification at unbounded rate (DoS) or
6
+ * pin our log infrastructure with garbage payloads. We allow 100 req/min
7
+ * per IP — well above Stripe's actual retry cadence (~ a few per hour for
8
+ * a healthy account) but tight enough to make abuse unattractive.
9
+ *
10
+ * The limit applies regardless of signature validity — we don't want to
11
+ * permit unlimited *invalid* requests either, since those still consume CPU.
12
+ */
13
+ import type { FastifyInstance } from "fastify";
14
+ import Fastify from "fastify";
15
+
16
+ // ── Mocks ────────────────────────────────────────────────────────────────────
17
+
18
+ const mockConfig: { STRIPE_WEBHOOK_SECRET: string | undefined } = {
19
+ STRIPE_WEBHOOK_SECRET: "whsec_test_secret",
20
+ };
21
+ // eslint-disable-next-line no-restricted-syntax -- mirrors stripe.test.ts mock pattern
22
+ jest.mock("../../../config/index.js", () => ({
23
+ get config() {
24
+ return mockConfig;
25
+ },
26
+ }));
27
+
28
+ // Commerce claim (D-64, #5796): the receiver is a base-tree primitive gated on
29
+ // the JWT `commerce` claim, so every case in this file is a COMMERCE-CLAIMED
30
+ // deployment — which is what each one was already asserting before the gate
31
+ // existed. The unclaimed matrix lives in
32
+ // `src/__tests__/commerce-claim-registration.test.ts`.
33
+ jest.mock("@working-theory/license", () => ({
34
+ checkLicense: jest.fn().mockResolvedValue({
35
+ tier: "pro",
36
+ plan: "pro-monthly",
37
+ features: ["commerce"],
38
+ expiresAt: null,
39
+ revoked: false,
40
+ }),
41
+ }));
42
+
43
+ jest.mock("@working-theory/analytics/server", () => ({
44
+ track: jest.fn().mockResolvedValue(undefined),
45
+ }));
46
+
47
+ // Mock prisma so customer.created billing upsert doesn't throw
48
+ // eslint-disable-next-line no-restricted-syntax -- mirrors stripe.test.ts mock pattern
49
+ jest.mock("../../../lib/prisma.js");
50
+
51
+ const mockVerifyWebhookSignature = jest.fn();
52
+ class MockStripeWebhookSignatureError extends Error {
53
+ constructor(message: string) {
54
+ super(message);
55
+ this.name = "StripeWebhookSignatureError";
56
+ }
57
+ }
58
+ // eslint-disable-next-line no-restricted-syntax -- mirrors stripe.test.ts mock pattern
59
+ jest.mock("../../../lib/stripe.js", () => ({
60
+ verifyWebhookSignature: (...args: unknown[]) =>
61
+ mockVerifyWebhookSignature(...args),
62
+ StripeWebhookSignatureError: MockStripeWebhookSignatureError,
63
+ }));
64
+
65
+ import { webhooksRoutes } from "../index.js";
66
+ import { __resetIdempotencyStore } from "../../../lib/webhook-idempotency.js";
67
+
68
+ // ── Helpers ──────────────────────────────────────────────────────────────────
69
+
70
+ const VALID_BODY = Buffer.from(JSON.stringify({ type: "test" }));
71
+ const VALID_SIGNATURE = "t=1234,v1=abc";
72
+
73
+ async function buildServerWithIp(ip: string): Promise<FastifyInstance> {
74
+ const server = Fastify({ logger: false });
75
+
76
+ // Stub req.ip so we can simulate distinct callers reliably (light.my.request
77
+ // sends localhost by default which makes per-IP testing brittle).
78
+ server.addHook("onRequest", async (req) => {
79
+ Object.defineProperty(req, "ip", { value: ip, configurable: true });
80
+ });
81
+
82
+ await server.register(async (scope) => {
83
+ scope.addContentTypeParser(
84
+ "application/json",
85
+ { parseAs: "buffer" },
86
+ (_req, body, done) => {
87
+ done(null, body);
88
+ },
89
+ );
90
+ await scope.register(webhooksRoutes);
91
+ });
92
+
93
+ await server.ready();
94
+ return server;
95
+ }
96
+
97
+ // ── Tests ────────────────────────────────────────────────────────────────────
98
+
99
+ describe("POST /api/webhooks/stripe — rate limit", () => {
100
+ beforeEach(() => {
101
+ jest.clearAllMocks();
102
+ __resetIdempotencyStore();
103
+ mockConfig.STRIPE_WEBHOOK_SECRET = "whsec_test_secret";
104
+ // Each request gets a unique event.id so idempotency doesn't short-circuit
105
+ let counter = 0;
106
+ mockVerifyWebhookSignature.mockImplementation(() => ({
107
+ id: `evt_rate_${(counter++).toString().padStart(12, "0")}`,
108
+ type: "customer.created",
109
+ data: { object: {} },
110
+ }));
111
+ });
112
+
113
+ it("allows requests up to the per-minute cap", async () => {
114
+ const server = await buildServerWithIp("10.10.10.1");
115
+
116
+ try {
117
+ // 100 requests within the cap should all succeed
118
+ for (let i = 0; i < 100; i++) {
119
+ const response = await server.inject({
120
+ method: "POST",
121
+ url: "/v1/webhooks/stripe",
122
+ payload: VALID_BODY,
123
+ headers: {
124
+ "content-type": "application/json",
125
+ "stripe-signature": VALID_SIGNATURE,
126
+ },
127
+ });
128
+ expect(response.statusCode).toBe(200);
129
+ }
130
+ } finally {
131
+ await server.close();
132
+ }
133
+ });
134
+
135
+ it("returns 429 on the 101st request within the window", async () => {
136
+ const server = await buildServerWithIp("10.10.10.2");
137
+
138
+ try {
139
+ for (let i = 0; i < 100; i++) {
140
+ await server.inject({
141
+ method: "POST",
142
+ url: "/v1/webhooks/stripe",
143
+ payload: VALID_BODY,
144
+ headers: {
145
+ "content-type": "application/json",
146
+ "stripe-signature": VALID_SIGNATURE,
147
+ },
148
+ });
149
+ }
150
+
151
+ const overLimit = await server.inject({
152
+ method: "POST",
153
+ url: "/v1/webhooks/stripe",
154
+ payload: VALID_BODY,
155
+ headers: {
156
+ "content-type": "application/json",
157
+ "stripe-signature": VALID_SIGNATURE,
158
+ },
159
+ });
160
+
161
+ expect(overLimit.statusCode).toBe(429);
162
+ } finally {
163
+ await server.close();
164
+ }
165
+ });
166
+
167
+ it("tracks rate-limit buckets per IP independently", async () => {
168
+ const serverA = await buildServerWithIp("10.10.10.3");
169
+ const serverB = await buildServerWithIp("10.10.10.4");
170
+
171
+ try {
172
+ // Saturate server A
173
+ for (let i = 0; i < 100; i++) {
174
+ await serverA.inject({
175
+ method: "POST",
176
+ url: "/v1/webhooks/stripe",
177
+ payload: VALID_BODY,
178
+ headers: {
179
+ "content-type": "application/json",
180
+ "stripe-signature": VALID_SIGNATURE,
181
+ },
182
+ });
183
+ }
184
+
185
+ // A is over the cap
186
+ const aOverLimit = await serverA.inject({
187
+ method: "POST",
188
+ url: "/v1/webhooks/stripe",
189
+ payload: VALID_BODY,
190
+ headers: {
191
+ "content-type": "application/json",
192
+ "stripe-signature": VALID_SIGNATURE,
193
+ },
194
+ });
195
+ expect(aOverLimit.statusCode).toBe(429);
196
+
197
+ // B still has its full budget
198
+ const bFirst = await serverB.inject({
199
+ method: "POST",
200
+ url: "/v1/webhooks/stripe",
201
+ payload: VALID_BODY,
202
+ headers: {
203
+ "content-type": "application/json",
204
+ "stripe-signature": VALID_SIGNATURE,
205
+ },
206
+ });
207
+ expect(bFirst.statusCode).toBe(200);
208
+ } finally {
209
+ await serverA.close();
210
+ await serverB.close();
211
+ }
212
+ });
213
+ });