create-ailk 0.5.0 → 0.6.1

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 (329) 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/cli.js +54 -5
  5. package/dist/copy.d.ts +14 -0
  6. package/dist/copy.js +38 -1
  7. package/dist/lib/api-boot-assertion.d.ts +46 -0
  8. package/dist/lib/api-boot-assertion.js +70 -0
  9. package/dist/lib/extract-module-bundle.d.ts +20 -1
  10. package/dist/lib/extract-module-bundle.js +64 -5
  11. package/dist/lib/import-graph.d.ts +29 -0
  12. package/dist/lib/import-graph.js +140 -0
  13. package/dist/lib/module-license-gate.d.ts +6 -1
  14. package/dist/lib/module-license-gate.js +6 -1
  15. package/dist/lib/pro-callout.js +1 -1
  16. package/dist/module-architecture.d.ts +1 -0
  17. package/dist/module-architecture.js +253 -127
  18. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  19. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  20. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  21. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  22. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  23. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  24. package/dist/programmatic.js +14 -10
  25. package/dist/surfaces.js +105 -7
  26. package/dist/template-manifest.d.ts +33 -0
  27. package/dist/template-manifest.js +99 -16
  28. package/dist/template-names.d.ts +1 -1
  29. package/dist/template-names.js +1 -0
  30. package/package.json +3 -2
  31. package/site-templates/marketing-site/DESIGN.md +148 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  34. package/site-templates/marketing-site/ailk-template.json +1 -1
  35. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  38. package/site-templates/minimalism-site/DESIGN.md +148 -0
  39. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  41. package/site-templates/minimalism-site/ailk-template.json +2 -1
  42. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  43. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  44. package/site-templates/saas-product/DESIGN.md +148 -0
  45. package/site-templates/simple-service/DESIGN.md +148 -0
  46. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  47. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  48. package/site-templates/simple-service/ailk-template.json +10 -0
  49. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  50. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  51. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  52. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  53. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  54. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  55. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  56. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  57. package/site-templates/simple-service/provenance.json +4 -0
  58. package/templates/.env.example +72 -7
  59. package/templates/CLAUDE.md +2 -3
  60. package/templates/DESIGN.md +155 -0
  61. package/templates/README.md +42 -8
  62. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  63. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  64. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  65. package/templates/apps/api/.env.example +13 -0
  66. package/templates/apps/api/CLAUDE.md +26 -2
  67. package/templates/apps/api/jest.config.cjs +3 -0
  68. package/templates/apps/api/package.json +1 -0
  69. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  70. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  71. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  72. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  73. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  74. package/templates/apps/api/src/config/index.ts +62 -0
  75. package/templates/apps/api/src/config/modules.ts +7 -3
  76. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  77. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  78. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  79. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  81. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  82. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  83. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  84. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  85. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  86. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  87. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  88. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  89. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  90. package/templates/apps/api/src/lib/site.ts +208 -9
  91. package/templates/apps/api/src/lib/stripe.ts +66 -0
  92. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  93. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  94. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  95. package/templates/apps/api/src/openapi/spec.ts +543 -2
  96. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  97. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  98. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  99. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  100. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  101. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  102. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  103. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  104. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  105. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  106. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  107. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  108. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  109. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  110. package/templates/apps/api/src/routes/content/index.ts +13 -73
  111. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  112. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  113. package/templates/apps/api/src/routes/content/search.ts +129 -0
  114. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  115. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  116. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  120. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  121. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  122. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  123. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  124. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  125. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  126. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  127. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  128. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  129. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  130. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  131. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  132. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  133. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  134. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  135. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  136. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  137. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  138. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  139. package/templates/apps/api/src/server.ts +439 -49
  140. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  141. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  142. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  143. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  144. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  145. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  146. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  147. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  148. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  149. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  150. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  151. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  152. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  153. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  154. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  155. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  156. package/templates/apps/api/src/services/audit-log.ts +668 -0
  157. package/templates/apps/api/src/services/consent.ts +18 -0
  158. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  159. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  160. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  161. package/templates/apps/api/src/services/entitlements.ts +80 -0
  162. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  163. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  164. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  165. package/templates/apps/api/src/services/listing-config.ts +27 -9
  166. package/templates/apps/api/src/services/project-listings.ts +201 -6
  167. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  168. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  169. package/templates/apps/api/src/services/revalidation.ts +173 -0
  170. package/templates/apps/api/src/services/verified-email.ts +71 -0
  171. package/templates/apps/api/src/vercel-handler.ts +75 -35
  172. package/templates/apps/mcp/.env.example +17 -0
  173. package/templates/apps/mcp/CLAUDE.md +57 -24
  174. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  175. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  176. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  177. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  178. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  179. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  180. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  181. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  182. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  183. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  184. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  185. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  186. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  187. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  188. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  189. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  190. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  191. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  192. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  193. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  194. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  195. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  196. package/templates/apps/mcp/jest.config.cjs +13 -0
  197. package/templates/apps/mcp/package.json +2 -0
  198. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  199. package/templates/apps/mcp/src/config/modules.ts +10 -5
  200. package/templates/apps/mcp/src/http-transport.ts +102 -29
  201. package/templates/apps/mcp/src/index.ts +73 -11
  202. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  203. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  204. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  205. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  206. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  207. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  208. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  209. package/templates/apps/mcp/src/server.ts +59 -35
  210. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  211. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  212. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  213. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  214. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  215. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  216. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  217. package/templates/apps/mcp/src/tools/index.ts +6 -4
  218. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  219. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  220. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  221. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  222. package/templates/apps/web/.env.example +71 -5
  223. package/templates/apps/web/CLAUDE.md +44 -0
  224. package/templates/apps/web/PRODUCT.md +86 -0
  225. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  226. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  227. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  228. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  229. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  230. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  231. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  232. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  233. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  234. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  235. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  236. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  237. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  238. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  239. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  240. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  241. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  242. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  243. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  244. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  245. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  246. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  247. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  248. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  249. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  250. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  251. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  252. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  253. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  254. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  255. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  256. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  257. package/templates/apps/web/auth.ts +407 -8
  258. package/templates/apps/web/jest.config.cjs +8 -4
  259. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  260. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  261. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  262. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  263. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  264. package/templates/apps/web/lib/auth-email.ts +318 -0
  265. package/templates/apps/web/lib/auth-env.ts +85 -0
  266. package/templates/apps/web/lib/auth-routes.ts +55 -0
  267. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  268. package/templates/apps/web/lib/site-brand.tsx +4 -8
  269. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  270. package/templates/apps/web/messages/en.json +187 -0
  271. package/templates/apps/web/middleware.ts +1 -1
  272. package/templates/apps/web/package.json +2 -1
  273. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  274. package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
  275. package/templates/content/_site.mdx +18 -3
  276. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  277. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  278. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  279. package/templates/database/CHANGELOG.md +149 -0
  280. package/templates/database/README.md +10 -3
  281. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  282. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  283. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  284. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  285. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  286. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  287. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  288. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  289. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  290. package/templates/database/__tests__/tenancy.test.ts +141 -0
  291. package/templates/database/config/schema.prisma +79 -2
  292. package/templates/database/content/schema.prisma +87 -11
  293. package/templates/database/inbox/schema.prisma +452 -9
  294. package/templates/database/jest.config.cjs +5 -3
  295. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  296. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  297. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  298. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  299. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  300. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  301. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  302. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  303. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  304. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  305. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  306. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  307. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  308. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  309. package/templates/database/package.json +3 -2
  310. package/templates/database/tenancy.ts +86 -1
  311. package/templates/database/tsconfig.json +1 -0
  312. package/versions.json +34 -34
  313. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  314. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  315. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  316. package/templates/apps/api/src/routes/content/create.ts +0 -115
  317. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  318. package/templates/apps/api/src/routes/content/update.ts +0 -112
  319. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  320. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  321. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  322. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  323. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  324. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  325. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  326. package/templates/apps/web/public/logo-dark.svg +0 -21
  327. package/templates/apps/web/public/logo-light.svg +0 -21
  328. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  329. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Unit tests for the commerce claim gate (D-64, #5796; round 2).
3
+ *
4
+ * Mocks @working-theory/license `checkLicense` and asserts the TRI-STATE
5
+ * contract (security review 2026-09-16, High; operator-disposed round 2):
6
+ *
7
+ * claimed — read successfully, features[] carries `commerce`
8
+ * unclaimed — read successfully and positively WITHOUT the claim, on a
9
+ * deployment that configures no licence at all
10
+ * undetermined — the read did not establish a "no": it threw, the licence
11
+ * is revoked, a licence IS configured yet resolves free
12
+ * (expired or otherwise unverifiable), or the licence storage
13
+ * file could not be READ at all (#5820)
14
+ *
15
+ * Route behaviour differs by what is at stake, which is why this is not one
16
+ * response: the checkout launcher answers the module-disabled 404 on both
17
+ * non-serving states (a checkout that cannot start loses nothing), while the
18
+ * webhook receiver holds an `undetermined` event with a retryable 503 rather
19
+ * than dropping a charge that already happened.
20
+ *
21
+ * The tier is deliberately varied against the claim: this gate reads
22
+ * `features`, never `tier` (D-64).
23
+ */
24
+
25
+ const mockCheckLicense = jest.fn();
26
+
27
+ // Only `checkLicense` is stubbed. `readStoredJwtState()` runs for real against
28
+ // a per-test fixture directory pointed at by `AILK_LICENSE_STORAGE_PATH`
29
+ // (#5820): the absent-vs-unreadable distinction IS the thing under test, and a
30
+ // stub of it would assert only that the stub was called — which is exactly how
31
+ // the previous "the storage read throws" case passed against a throw the
32
+ // package can never produce.
33
+ jest.mock("@working-theory/license", () => ({
34
+ ...jest.requireActual("@working-theory/license"),
35
+ checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
36
+ }));
37
+
38
+ import { promises as fs } from "fs";
39
+ import { tmpdir } from "os";
40
+ import { join } from "path";
41
+
42
+ import type { FastifyReply, FastifyRequest } from "fastify";
43
+
44
+ import {
45
+ resolveCommerceClaim,
46
+ requireCommerceClaim,
47
+ } from "../require-commerce-claim.js";
48
+
49
+ interface StubReply {
50
+ statusCalled: number | null;
51
+ sendCalled: unknown;
52
+ status: jest.MockedFunction<(code: number) => StubReply>;
53
+ send: jest.MockedFunction<(body: unknown) => StubReply>;
54
+ }
55
+
56
+ function makeReply(): StubReply {
57
+ const reply: StubReply = {
58
+ statusCalled: null,
59
+ sendCalled: undefined,
60
+ status: jest.fn(),
61
+ send: jest.fn(),
62
+ };
63
+ reply.status.mockImplementation((code: number) => {
64
+ reply.statusCalled = code;
65
+ return reply;
66
+ });
67
+ reply.send.mockImplementation((body: unknown) => {
68
+ reply.sendCalled = body;
69
+ return reply;
70
+ });
71
+ return reply;
72
+ }
73
+
74
+ const logWarn = jest.fn();
75
+
76
+ // The gate reads exactly one thing off the request — `log.warn`. Cast at one
77
+ // typed boundary, the same shape require-paid-tier.test.ts uses.
78
+ function makeRequest(): FastifyRequest {
79
+ // @ts-expect-error: a stub carrying only `log.warn`; a full FastifyRequest
80
+ // cannot be constructed without a live server instance in unit tests.
81
+ return { log: { warn: logWarn } };
82
+ }
83
+
84
+ const request = makeRequest();
85
+
86
+ function asReply(stub: StubReply): FastifyReply {
87
+ // @ts-expect-error: StubReply satisfies the status+send surface requireCommerceClaim uses;
88
+ // a full Fastify reply cannot be constructed without a live server instance in unit tests.
89
+ return stub;
90
+ }
91
+
92
+ function license(overrides: Record<string, unknown> = {}) {
93
+ return {
94
+ tier: "pro",
95
+ plan: "pro-monthly",
96
+ features: [],
97
+ expiresAt: null,
98
+ revoked: false,
99
+ ...overrides,
100
+ };
101
+ }
102
+
103
+ /** The shape `checkLicense()` returns for expired, invalid AND keyless alike. */
104
+ function freeResult() {
105
+ return license({ tier: "free", plan: null, features: [], revoked: false });
106
+ }
107
+
108
+ const ORIGINAL_LICENSE_KEY = process.env["AILK_LICENSE_KEY"];
109
+
110
+ /** A deployment that configures a licence (env key present). */
111
+ function withLicenceConfigured() {
112
+ // Presence is the whole test — the gate never parses this value, so it is
113
+ // deliberately not JWT-shaped (a realistic stub trips the secret scanner's
114
+ // entropy rule for no benefit).
115
+ process.env["AILK_LICENSE_KEY"] = "fixture";
116
+ }
117
+
118
+ /** A keyless deployment — no env key, and no storage file at the fixture path. */
119
+ function withNoLicenceConfigured() {
120
+ delete process.env["AILK_LICENSE_KEY"];
121
+ }
122
+
123
+ // ── The storage file, for real ──────────────────────────────────────────────
124
+ //
125
+ // Every test points `AILK_LICENSE_STORAGE_PATH` at a fresh temp path that does
126
+ // not exist, which is the `absent` state. The two cases below create the file
127
+ // the package will actually read.
128
+
129
+ let storageDir: string;
130
+ let storagePath: string;
131
+
132
+ /** A deployment licensed purely from the refresh storage file. */
133
+ async function withStoredLicence() {
134
+ await fs.writeFile(storagePath, "stored.jwt.token", "utf8");
135
+ }
136
+
137
+ /** A storage file that exists and cannot be read — a directory at its path. */
138
+ async function withUnreadableStorage() {
139
+ await fs.mkdir(storagePath);
140
+ }
141
+
142
+ beforeEach(async () => {
143
+ storageDir = await fs.mkdtemp(join(tmpdir(), "ailk-claim-gate-"));
144
+ storagePath = join(storageDir, ".ailk-license.jwt");
145
+ process.env["AILK_LICENSE_STORAGE_PATH"] = storagePath;
146
+ });
147
+
148
+ afterEach(async () => {
149
+ delete process.env["AILK_LICENSE_STORAGE_PATH"];
150
+ await fs.rm(storageDir, { recursive: true, force: true });
151
+ });
152
+
153
+ afterAll(() => {
154
+ if (ORIGINAL_LICENSE_KEY === undefined) delete process.env["AILK_LICENSE_KEY"];
155
+ else process.env["AILK_LICENSE_KEY"] = ORIGINAL_LICENSE_KEY;
156
+ });
157
+
158
+ describe("requireCommerceClaim (the checkout launcher's preHandler)", () => {
159
+ beforeEach(() => {
160
+ mockCheckLicense.mockReset();
161
+ logWarn.mockReset();
162
+ withNoLicenceConfigured();
163
+ });
164
+
165
+ it("404s the module-disabled body when features[] lacks 'commerce'", async () => {
166
+ mockCheckLicense.mockResolvedValue(freeResult());
167
+ const reply = makeReply();
168
+
169
+ await requireCommerceClaim(request, asReply(reply));
170
+
171
+ expect(reply.statusCalled).toBe(404);
172
+ expect(reply.sendCalled).toEqual({
173
+ error: "not_found",
174
+ message: "Not found",
175
+ });
176
+ });
177
+
178
+ it("404s a PAID tier that carries other features but not 'commerce'", async () => {
179
+ mockCheckLicense.mockResolvedValue(
180
+ license({ tier: "enterprise", features: ["docs", "multi-tenant"] }),
181
+ );
182
+ const reply = makeReply();
183
+
184
+ await requireCommerceClaim(request, asReply(reply));
185
+
186
+ expect(reply.statusCalled).toBe(404);
187
+ });
188
+
189
+ it("proceeds when features[] includes 'commerce'", async () => {
190
+ mockCheckLicense.mockResolvedValue(license({ features: ["commerce"] }));
191
+ const reply = makeReply();
192
+
193
+ await requireCommerceClaim(request, asReply(reply));
194
+
195
+ expect(reply.statusCalled).toBeNull();
196
+ expect(reply.send).not.toHaveBeenCalled();
197
+ });
198
+
199
+ // Round 2, D1: the checkout launcher answers the module-disabled response on
200
+ // BOTH non-serving states. A checkout that cannot be started loses nothing,
201
+ // so there is no reason to leak a "configured but unreadable" shape here —
202
+ // it is the webhook, where money has already moved, that holds with a 503.
203
+ it("404s — not 503s — on an undetermined read, because a checkout that cannot start loses nothing", async () => {
204
+ mockCheckLicense.mockRejectedValue(new Error("boom"));
205
+ const reply = makeReply();
206
+
207
+ await requireCommerceClaim(request, asReply(reply));
208
+
209
+ expect(reply.statusCalled).toBe(404);
210
+ expect(reply.sendCalled).toEqual({
211
+ error: "not_found",
212
+ message: "Not found",
213
+ });
214
+ });
215
+
216
+ it("404s a revoked license even while the claim is still in the JWT", async () => {
217
+ mockCheckLicense.mockResolvedValue(
218
+ license({ features: ["commerce"], revoked: true }),
219
+ );
220
+ const reply = makeReply();
221
+
222
+ await requireCommerceClaim(request, asReply(reply));
223
+
224
+ expect(reply.statusCalled).toBe(404);
225
+ });
226
+ });
227
+
228
+ describe("resolveCommerceClaim", () => {
229
+ beforeEach(() => {
230
+ mockCheckLicense.mockReset();
231
+ withNoLicenceConfigured();
232
+ });
233
+
234
+ it("is 'claimed' for a live license carrying the claim", async () => {
235
+ mockCheckLicense.mockResolvedValue(license({ features: ["commerce"] }));
236
+ await expect(resolveCommerceClaim()).resolves.toBe("claimed");
237
+ });
238
+
239
+ it("is 'unclaimed' for a license read successfully that lacks the claim", async () => {
240
+ mockCheckLicense.mockResolvedValue(license({ features: ["docs"] }));
241
+ await expect(resolveCommerceClaim()).resolves.toBe("unclaimed");
242
+ });
243
+
244
+ it("is 'unclaimed' on a KEYLESS build — no licence configured is a positive 'no'", async () => {
245
+ mockCheckLicense.mockResolvedValue(freeResult());
246
+ await expect(resolveCommerceClaim()).resolves.toBe("unclaimed");
247
+ });
248
+
249
+ // The storage-file path. `checkLicense()` resolves a licence from
250
+ // AILK_LICENSE_KEY *or* the refresh storage file, so a deployment can be
251
+ // fully licensed with no env key at all. An env-only "is a licence
252
+ // configured" test read that site as keyless, so its EXPIRED licence
253
+ // resolved `unclaimed` and its already-charged events were dropped — the
254
+ // same High the tri-state exists to prevent, one path over.
255
+ it("is 'undetermined' for an EXPIRED licence held only in the storage file", async () => {
256
+ delete process.env["AILK_LICENSE_KEY"];
257
+ mockCheckLicense.mockResolvedValue(freeResult());
258
+ await withStoredLicence();
259
+ await expect(resolveCommerceClaim()).resolves.toBe("undetermined");
260
+ });
261
+
262
+ it("is 'unclaimed' when neither the env key nor the storage file has a token", async () => {
263
+ delete process.env["AILK_LICENSE_KEY"];
264
+ mockCheckLicense.mockResolvedValue(freeResult());
265
+ await expect(resolveCommerceClaim()).resolves.toBe("unclaimed");
266
+ });
267
+
268
+ // #5820. Holding is the safe direction: a file that could not be READ is not
269
+ // evidence that a site is keyless, and dropping a signed Stripe event needs
270
+ // evidence. The previous version of this case stubbed `readStoredJwt()` into
271
+ // rejecting — a throw the package never produces, since it catches every
272
+ // error — so it passed while the real path acked 200 and lost the charge.
273
+ it("is 'undetermined' when the storage file exists and cannot be READ", async () => {
274
+ delete process.env["AILK_LICENSE_KEY"];
275
+ mockCheckLicense.mockResolvedValue(freeResult());
276
+ await withUnreadableStorage();
277
+ await expect(resolveCommerceClaim()).resolves.toBe("undetermined");
278
+ });
279
+
280
+ it("is 'undetermined' when the read itself throws", async () => {
281
+ mockCheckLicense.mockRejectedValue(new Error("boom"));
282
+ await expect(resolveCommerceClaim()).resolves.toBe("undetermined");
283
+ });
284
+
285
+ // Round 2, D1 (operator-disposed): a revoked licence is a PAYING commerce
286
+ // deployment inside its JWT window. Dropping its already-charged events is
287
+ // the same data loss the security review's High named, so it holds.
288
+ it("is 'undetermined' for a revoked license — a revoked site is still a live one mid-window", async () => {
289
+ mockCheckLicense.mockResolvedValue(
290
+ license({ features: ["commerce"], revoked: true }),
291
+ );
292
+ await expect(resolveCommerceClaim()).resolves.toBe("undetermined");
293
+ });
294
+
295
+ // Round 2, D1: an EXPIRED licence. `checkLicense()` collapses expired,
296
+ // invalid and keyless into one identical free result (check-license.ts
297
+ // returns FREE_RESULT for all three), so the only signal that separates
298
+ // "this site configured a licence that no longer verifies" from "this site
299
+ // never had one" is whether a licence is configured at all.
300
+ it("is 'undetermined' when a licence IS configured yet resolves free — expired or unverifiable", async () => {
301
+ withLicenceConfigured();
302
+ mockCheckLicense.mockResolvedValue(freeResult());
303
+ await expect(resolveCommerceClaim()).resolves.toBe("undetermined");
304
+ });
305
+
306
+ it("is 'claimed' for a configured licence that verifies and carries the claim", async () => {
307
+ withLicenceConfigured();
308
+ mockCheckLicense.mockResolvedValue(license({ features: ["commerce"] }));
309
+ await expect(resolveCommerceClaim()).resolves.toBe("claimed");
310
+ });
311
+
312
+ it("is 'unclaimed' for a configured licence that verifies to a paid tier without the claim", async () => {
313
+ withLicenceConfigured();
314
+ mockCheckLicense.mockResolvedValue(license({ features: ["docs"] }));
315
+ await expect(resolveCommerceClaim()).resolves.toBe("unclaimed");
316
+ });
317
+ });
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @file site.test.ts
3
+ * @description Coverage for the site-alias -> workspace resolution (#6155
4
+ * spec D2/D5/D6, AC-5): `resolveSiteWorkspaceId()`'s two-and-only-two
5
+ * resolution rule and `assertSiteAliasResolves()`'s startup refusal.
6
+ *
7
+ * Mocks the module-level `prisma` singleton via the repo's manual-mock
8
+ * convention (`apps/api/src/lib/__mocks__/prisma.ts`), the same shape
9
+ * `middleware/__tests__/default-tenant.test.ts` uses — this suite exercises
10
+ * the REAL `resolveSentinelWorkspaceRow` (`@working-theory/database/tenancy`,
11
+ * mapped straight to source by `jest.config.cjs`) against the mocked
12
+ * `prisma.workspace` delegate, not a re-implementation of it.
13
+ *
14
+ * `DATABASE_URL` is set globally by `src/__tests__/setup.ts`, so every case
15
+ * here runs past the D6 database-less carve-out and exercises the real
16
+ * resolution path. `CONTENT_ADAPTER` is pinned to `database` per-case below
17
+ * (#6228): the assertion's first arm now skips on any OTHER adapter, so a
18
+ * case that means to exercise the REAL resolution must say which adapter it
19
+ * is booting under rather than inherit whatever the ambient env holds.
20
+ */
21
+
22
+ // 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.
23
+ jest.mock("../prisma.js");
24
+
25
+ import { prisma } from "../prisma.js";
26
+
27
+ import {
28
+ __resetSiteWorkspaceCacheForTests,
29
+ assertSiteAliasResolves,
30
+ currentSiteId,
31
+ resolveSiteWorkspaceId,
32
+ } from "../site.js";
33
+
34
+ const DEFAULT_ORG_ID = "org_00000000-0000-7000-8000-000000000001";
35
+ const DEFAULT_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
36
+ const PINNED_WORKSPACE_ID = "wksp_01930000-0000-7000-8000-000000000042";
37
+ const NON_SENTINEL_ORG_ID = "org_01930000-0000-7bcd-8000-000000000002";
38
+
39
+ const ORIGINAL_AILK_SITE_ID = process.env.AILK_SITE_ID;
40
+ const ORIGINAL_CONTENT_ADAPTER = process.env.CONTENT_ADAPTER;
41
+
42
+ beforeEach(() => {
43
+ // #6228 arm 0 — every case in this suite is about the alias->workspace
44
+ // resolution itself, which only the database adapter reaches.
45
+ process.env.CONTENT_ADAPTER = "database";
46
+ });
47
+
48
+ afterEach(() => {
49
+ if (ORIGINAL_CONTENT_ADAPTER === undefined) {
50
+ delete process.env.CONTENT_ADAPTER;
51
+ } else {
52
+ process.env.CONTENT_ADAPTER = ORIGINAL_CONTENT_ADAPTER;
53
+ }
54
+ __resetSiteWorkspaceCacheForTests();
55
+ jest.clearAllMocks();
56
+ if (ORIGINAL_AILK_SITE_ID === undefined) {
57
+ delete process.env.AILK_SITE_ID;
58
+ } else {
59
+ process.env.AILK_SITE_ID = ORIGINAL_AILK_SITE_ID;
60
+ }
61
+ });
62
+
63
+ describe("resolveSiteWorkspaceId / assertSiteAliasResolves (#6155 D2/D5/D6)", () => {
64
+ it("case 1 — resolves a pinned alias to the matching workspace, and the assertion returns without throwing", async () => {
65
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
66
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
67
+ id: PINNED_WORKSPACE_ID,
68
+ organizationId: NON_SENTINEL_ORG_ID,
69
+ });
70
+
71
+ const resolved = await resolveSiteWorkspaceId();
72
+
73
+ expect(resolved).toBe(PINNED_WORKSPACE_ID);
74
+ expect(prisma.workspace.findUnique).toHaveBeenCalledWith({
75
+ where: { id: PINNED_WORKSPACE_ID },
76
+ });
77
+ await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
78
+ });
79
+
80
+ it("case 2 — the unset alias (the scaffold default) resolves to the seeded sentinel workspace (D2 rule 2)", async () => {
81
+ delete process.env.AILK_SITE_ID;
82
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
83
+ id: DEFAULT_WORKSPACE_ID,
84
+ organizationId: DEFAULT_ORG_ID,
85
+ });
86
+
87
+ const resolved = await resolveSiteWorkspaceId();
88
+
89
+ expect(resolved).toBe(DEFAULT_WORKSPACE_ID);
90
+ expect(prisma.workspace.findUnique).toHaveBeenCalledWith({
91
+ where: { id: DEFAULT_WORKSPACE_ID },
92
+ });
93
+ // resolveSentinelWorkspaceRow carries no workspace.count() gate (D2).
94
+ expect(prisma.workspace.count).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it("case 3 — a pinned alias naming no workspace does not resolve, and the assertion rejects naming both the alias and AILK_SITE_ID", async () => {
98
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
99
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
100
+
101
+ const resolved = await resolveSiteWorkspaceId();
102
+ expect(resolved).toBeNull();
103
+
104
+ await expect(assertSiteAliasResolves()).rejects.toThrow(
105
+ new RegExp(PINNED_WORKSPACE_ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
106
+ );
107
+ await expect(assertSiteAliasResolves()).rejects.toThrow(/AILK_SITE_ID/);
108
+ });
109
+
110
+ it("case 4 — memoized: a second resolveSiteWorkspaceId() call issues zero further Prisma queries", async () => {
111
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
112
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
113
+ id: PINNED_WORKSPACE_ID,
114
+ organizationId: NON_SENTINEL_ORG_ID,
115
+ });
116
+
117
+ const first = await resolveSiteWorkspaceId();
118
+ const second = await resolveSiteWorkspaceId();
119
+
120
+ expect(first).toBe(PINNED_WORKSPACE_ID);
121
+ expect(second).toBe(PINNED_WORKSPACE_ID);
122
+ expect(prisma.workspace.findUnique).toHaveBeenCalledTimes(1);
123
+ });
124
+
125
+ it("case 5 — the unset alias resolves to the sentinel on a MULTI-workspace deployment (the D2 rule-2 regression case)", async () => {
126
+ // The exact case resolveDefaultWorkspaceRow's workspace.count() gate
127
+ // would have refused — three workspaces exist, one of them the seeded
128
+ // sentinel, and AILK_SITE_ID is unpinned. resolveSentinelWorkspaceRow
129
+ // carries no count() gate, so this must still resolve.
130
+ delete process.env.AILK_SITE_ID;
131
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
132
+ id: DEFAULT_WORKSPACE_ID,
133
+ organizationId: DEFAULT_ORG_ID,
134
+ });
135
+
136
+ const resolved = await resolveSiteWorkspaceId();
137
+
138
+ expect(resolved).toBe(DEFAULT_WORKSPACE_ID);
139
+ expect(prisma.workspace.count).not.toHaveBeenCalled();
140
+ await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
141
+ });
142
+
143
+ it("case 6 — the unset alias does not resolve when the sentinel workspace is absent — the misconfiguration AC-5 asks to fail loudly", async () => {
144
+ delete process.env.AILK_SITE_ID;
145
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
146
+
147
+ const resolved = await resolveSiteWorkspaceId();
148
+ expect(resolved).toBeNull();
149
+
150
+ await expect(assertSiteAliasResolves()).rejects.toThrow(/AILK_SITE_ID/);
151
+ });
152
+
153
+ it("case 7 — a genuine thrown resolution error PROPAGATES and crashes the boot — it is NOT swallowed as a database-less carve-out", async () => {
154
+ // The code-review finding this case pins (#6155): the D6 carve-out is a
155
+ // presence check made BEFORE any query, never a catch around one that
156
+ // already ran. A reachable database that errors mid-query (a real
157
+ // connection failure, a permissions error) is exactly the
158
+ // misconfiguration class AC-5 exists to surface, so it must reject —
159
+ // not resolve silently the way the "cannot be attempted" cases do.
160
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
161
+ const connectionError = new Error("P1001: Can't reach database server");
162
+ (prisma.workspace.findUnique as jest.Mock).mockRejectedValue(
163
+ connectionError,
164
+ );
165
+
166
+ await expect(resolveSiteWorkspaceId()).rejects.toThrow(connectionError);
167
+ await expect(assertSiteAliasResolves()).rejects.toThrow(connectionError);
168
+ });
169
+
170
+ it("case 8 — prisma.workspace.findUnique missing entirely (a prisma double that never touches tenancy) is the ONLY case that skips without throwing", async () => {
171
+ // The exact shape that surfaced the D6 carve-out: a shared/global prisma
172
+ // test double (lib/__mocks__/prisma-client.js, and the many hand-rolled
173
+ // hoisted factories elsewhere) that declares no workspace delegate at
174
+ // all — the resolution could not even be ATTEMPTED. Distinguishes this
175
+ // from case 7's genuine mid-query failure: here findUnique is not a
176
+ // function at all, never called, never thrown from.
177
+ delete process.env.AILK_SITE_ID;
178
+ const original = prisma.workspace.findUnique;
179
+ // eslint-disable-next-line no-restricted-syntax -- deliberately simulating a prisma double that declares no `workspace.findUnique` delegate at all (the shape assertSiteAliasResolves()'s presence check exists to catch), restored in the finally block below.
180
+ (prisma.workspace as unknown as Record<string, unknown>).findUnique =
181
+ undefined;
182
+
183
+ try {
184
+ await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
185
+ } finally {
186
+ prisma.workspace.findUnique = original;
187
+ }
188
+ });
189
+
190
+ it("currentSiteId() stays synchronous and byte-identical for the env-set case (NFR1)", () => {
191
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
192
+ expect(currentSiteId()).toBe(PINNED_WORKSPACE_ID);
193
+ });
194
+
195
+ it("currentSiteId() stays synchronous and byte-identical for the env-unset case (NFR1)", () => {
196
+ delete process.env.AILK_SITE_ID;
197
+ expect(currentSiteId()).toBe("default");
198
+ });
199
+ });
200
+
201
+ describe("assertSiteAliasResolves — the #6228 content-adapter carve-out (arm 0)", () => {
202
+ it("skips the assertion on a non-database adapter, even with DATABASE_URL set and unreachable", async () => {
203
+ // The scaffold-boot shape: create-ailk's verify-scaffold.mjs sets a
204
+ // PLACEHOLDER DATABASE_URL (validateConfig checks presence only, Prisma
205
+ // connects lazily), so arm 1 does not fire. Before #6228 the generated
206
+ // MDX site crashed at boot on `Can't reach database server`.
207
+ process.env.CONTENT_ADAPTER = "mdx";
208
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
209
+ (prisma.workspace.findUnique as jest.Mock).mockRejectedValue(
210
+ new Error("Can't reach database server at 127.0.0.1:5432"),
211
+ );
212
+
213
+ await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
214
+ // The arm is a PRESENCE check made BEFORE any query — not a catch around
215
+ // one that already ran (the #6155 code-review finding this file's
216
+ // docblock records).
217
+ expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
218
+ });
219
+
220
+ it("skips on the projection adapter too — the arm is `not database`, not `is mdx`", async () => {
221
+ process.env.CONTENT_ADAPTER = "projection";
222
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
223
+
224
+ await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
225
+ expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
226
+ });
227
+
228
+ it("still throws under the database adapter when the alias names no workspace", async () => {
229
+ // AC-2: the arm narrows WHICH boots are asserted, never WHAT the
230
+ // assertion decides once it runs.
231
+ process.env.CONTENT_ADAPTER = "database";
232
+ process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
233
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
234
+
235
+ await expect(assertSiteAliasResolves()).rejects.toThrow(/AILK_SITE_ID/);
236
+ expect(prisma.workspace.findUnique).toHaveBeenCalled();
237
+ });
238
+ });
@@ -0,0 +1,75 @@
1
+ // oss: false
2
+ /**
3
+ * apps/api stripe shim — surface-drift smoke tests.
4
+ *
5
+ * Ensures apps/api/src/lib/stripe.ts re-exports exactly what
6
+ * @working-theory/stripe-client exports, with no surface drift.
7
+ */
8
+
9
+ jest.mock("@working-theory/stripe-client", () => ({
10
+ getStripeClient: jest.fn(),
11
+ assertSandboxKey: jest.fn(),
12
+ verifyWebhookSignature: jest.fn(),
13
+ createCheckoutSession: jest.fn(),
14
+ STRIPE_API_VERSION: "2023-10-16",
15
+ WEBHOOK_TOLERANCE_SECONDS: 300,
16
+ StripeConfigError: class StripeConfigError extends Error {},
17
+ StripeWebhookSignatureError: class StripeWebhookSignatureError extends Error {},
18
+ UnknownProductCodeError: class UnknownProductCodeError extends Error {},
19
+ }));
20
+
21
+ // Import the shim (not the wrapper directly)
22
+ import * as stripeShim from "../stripe.js";
23
+ import * as stripeClient from "@working-theory/stripe-client";
24
+
25
+ describe("apps/api/src/lib/stripe — surface re-exports", () => {
26
+ it("exports getStripeClient as a function", () => {
27
+ expect(typeof stripeShim.getStripeClient).toBe("function");
28
+ });
29
+
30
+ it("exports assertSandboxKey as a function", () => {
31
+ expect(typeof stripeShim.assertSandboxKey).toBe("function");
32
+ });
33
+
34
+ it("exports verifyWebhookSignature as a function", () => {
35
+ expect(typeof stripeShim.verifyWebhookSignature).toBe("function");
36
+ });
37
+
38
+ it("exports createCheckoutSession as a function", () => {
39
+ expect(typeof stripeShim.createCheckoutSession).toBe("function");
40
+ });
41
+
42
+ it("exports STRIPE_API_VERSION", () => {
43
+ expect(stripeShim.STRIPE_API_VERSION).toBe("2023-10-16");
44
+ });
45
+
46
+ it("exports WEBHOOK_TOLERANCE_SECONDS", () => {
47
+ expect(stripeShim.WEBHOOK_TOLERANCE_SECONDS).toBe(300);
48
+ });
49
+
50
+ it("exports StripeConfigError as a constructor producing Error instances", () => {
51
+ expect(typeof stripeShim.StripeConfigError).toBe("function");
52
+ expect(new stripeShim.StripeConfigError()).toBeInstanceOf(Error);
53
+ });
54
+
55
+ it("exports StripeWebhookSignatureError as a constructor producing Error instances", () => {
56
+ expect(typeof stripeShim.StripeWebhookSignatureError).toBe("function");
57
+ expect(new stripeShim.StripeWebhookSignatureError()).toBeInstanceOf(Error);
58
+ });
59
+
60
+ it("exports UnknownProductCodeError as a constructor producing Error instances", () => {
61
+ expect(typeof stripeShim.UnknownProductCodeError).toBe("function");
62
+ expect(new stripeShim.UnknownProductCodeError()).toBeInstanceOf(Error);
63
+ });
64
+
65
+ it("shim functions are the same references as @working-theory/stripe-client exports", () => {
66
+ expect(stripeShim.getStripeClient).toBe(stripeClient.getStripeClient);
67
+ expect(stripeShim.assertSandboxKey).toBe(stripeClient.assertSandboxKey);
68
+ expect(stripeShim.verifyWebhookSignature).toBe(
69
+ stripeClient.verifyWebhookSignature,
70
+ );
71
+ expect(stripeShim.createCheckoutSession).toBe(
72
+ stripeClient.createCheckoutSession,
73
+ );
74
+ });
75
+ });