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,166 @@
1
+ /**
2
+ * @file listing-config.test.ts
3
+ * @description Unit tests for the per-site project-listings config reads
4
+ * (#5033) under the tenancy fence (#6257, Stage 2 unit C2).
5
+ *
6
+ * This suite is NEW. `listing-config.ts`'s only coverage lived under
7
+ * `apps/api/src/routes/**\/__tests__/`, which this unit must not touch — so
8
+ * the two reads it owns are pinned here instead, at the service boundary
9
+ * where the fence is applied.
10
+ *
11
+ * Both reads run on the ANONYMOUS path, with no authenticated tenant:
12
+ *
13
+ * - `listingConfigFor(siteId)` is handed an ALREADY-RESOLVED workspace id
14
+ * by every caller (a listing row's own `siteId`, a tenant's
15
+ * `workspaceId`, or `siteIdForPublicKey`'s return), so its read is
16
+ * scoped by it.
17
+ * - `siteIdForPublicKey(publicKey)` is the query that RESOLVES the tenant.
18
+ * It is the documented global-key exemption (its call site carries the
19
+ * marker comment): it is keyed on a
20
+ * site's public listing key, which names one row across all workspaces,
21
+ * and a tenant term would be the very thing it exists to produce.
22
+ *
23
+ * External dependency boundaries mocked:
24
+ * - @working-theory/database (via moduleNameMapper → prisma-client.js mock;
25
+ * prisma singleton via __mocks__/prisma.ts)
26
+ * - @working-theory/observability logger
27
+ */
28
+
29
+ // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock (Jest requires explicit call for local files)
30
+ jest.mock("../../lib/prisma.js");
31
+
32
+ const mockLoggerWarn = jest.fn();
33
+ const mockLoggerError = jest.fn();
34
+ jest.mock("@working-theory/observability", () => ({
35
+ logger: {
36
+ info: jest.fn(),
37
+ warn: (...args: unknown[]) => mockLoggerWarn(...args),
38
+ error: (...args: unknown[]) => mockLoggerError(...args),
39
+ debug: jest.fn(),
40
+ },
41
+ }));
42
+
43
+ import { SITE_LISTING_CONFIG_DEFAULTS } from "@working-theory/validation";
44
+
45
+ import { prisma } from "../../lib/prisma.js";
46
+ import { __resetSiteWorkspaceCacheForTests } from "../../lib/site.js";
47
+ import { listingConfigFor, siteIdForPublicKey } from "../listing-config";
48
+
49
+ const SITE_ID = "wksp_00000000-0000-7000-8000-0000000000a1";
50
+ const OTHER_SITE_ID = "wksp_00000000-0000-7000-8000-0000000000ff";
51
+ const PUBLIC_KEY = "pk_listing_abcdef";
52
+
53
+ const priorPublicKey = process.env["AILK_LISTING_PUBLIC_KEY"];
54
+ const priorSiteId = process.env["AILK_SITE_ID"];
55
+
56
+ beforeEach(() => {
57
+ jest.clearAllMocks();
58
+ __resetSiteWorkspaceCacheForTests();
59
+ delete process.env["AILK_LISTING_PUBLIC_KEY"];
60
+ delete process.env["AILK_SITE_ID"];
61
+ });
62
+
63
+ afterAll(() => {
64
+ if (priorPublicKey === undefined) delete process.env["AILK_LISTING_PUBLIC_KEY"];
65
+ else process.env["AILK_LISTING_PUBLIC_KEY"] = priorPublicKey;
66
+ if (priorSiteId === undefined) delete process.env["AILK_SITE_ID"];
67
+ else process.env["AILK_SITE_ID"] = priorSiteId;
68
+ });
69
+
70
+ // ─── listingConfigFor — the fenced read ────────────────────────────────────
71
+
72
+ describe("listingConfigFor", () => {
73
+ it("scopes the config read by the workspace id it was handed (#6257 AC-1/AC-2)", async () => {
74
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue({
75
+ data: { maxWindowDays: 60, defaultWindowDays: 21 },
76
+ });
77
+
78
+ await listingConfigFor(SITE_ID);
79
+
80
+ expect(prisma.siteListingConfig.findUnique).toHaveBeenCalledTimes(1);
81
+ const { where } = (prisma.siteListingConfig.findUnique as jest.Mock).mock.calls[0][0];
82
+ expect(where.workspaceId).toBe(SITE_ID);
83
+ expect(where.siteId).toBe(SITE_ID);
84
+ });
85
+
86
+ it("reads a DIFFERENT site under that site's own workspace id, never a remembered one", async () => {
87
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue(null);
88
+
89
+ await listingConfigFor(SITE_ID);
90
+ await listingConfigFor(OTHER_SITE_ID);
91
+
92
+ const calls = (prisma.siteListingConfig.findUnique as jest.Mock).mock.calls;
93
+ expect(calls[0][0].where.workspaceId).toBe(SITE_ID);
94
+ expect(calls[1][0].where.workspaceId).toBe(OTHER_SITE_ID);
95
+ });
96
+
97
+ it("a site with no row gets the platform defaults", async () => {
98
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue(null);
99
+
100
+ await expect(listingConfigFor(SITE_ID)).resolves.toEqual(
101
+ SITE_LISTING_CONFIG_DEFAULTS,
102
+ );
103
+ });
104
+
105
+ it("a stored blob that fails its schema gets the platform defaults, logged at ERROR", async () => {
106
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue({
107
+ data: { maxWindowDays: "not-a-number" },
108
+ });
109
+
110
+ await expect(listingConfigFor(SITE_ID)).resolves.toEqual(
111
+ SITE_LISTING_CONFIG_DEFAULTS,
112
+ );
113
+ expect(mockLoggerError).toHaveBeenCalledWith(
114
+ expect.objectContaining({ siteId: SITE_ID }),
115
+ expect.stringContaining("PLATFORM defaults"),
116
+ );
117
+ });
118
+ });
119
+
120
+ // ─── siteIdForPublicKey — the documented global-key exemption ──────────────
121
+
122
+ describe("siteIdForPublicKey", () => {
123
+ it("is keyed on the public listing key ALONE — the query that resolves the tenant carries no tenant term", async () => {
124
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue({
125
+ siteId: SITE_ID,
126
+ });
127
+
128
+ await expect(siteIdForPublicKey(PUBLIC_KEY)).resolves.toBe(SITE_ID);
129
+
130
+ const { where } = (prisma.siteListingConfig.findUnique as jest.Mock).mock.calls[0][0];
131
+ expect(where).toEqual({ publicKey: PUBLIC_KEY });
132
+ expect(where.workspaceId).toBeUndefined();
133
+ });
134
+
135
+ it("an unmatched key resolves to null rather than falling through to the env path", async () => {
136
+ process.env["AILK_LISTING_PUBLIC_KEY"] = "pk_env_key";
137
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue(null);
138
+
139
+ await expect(siteIdForPublicKey("pk_typo")).resolves.toBeNull();
140
+ });
141
+
142
+ it("the env single-tenant path returns the RESOLVED workspace id, never the raw alias", async () => {
143
+ process.env["AILK_LISTING_PUBLIC_KEY"] = PUBLIC_KEY;
144
+ process.env["AILK_SITE_ID"] = SITE_ID;
145
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue(null);
146
+ // Keyed on the REQUESTED id — a blanket resolve would make a fail-closed
147
+ // assertion impossible (#6374's own test convention).
148
+ (prisma.workspace.findUnique as jest.Mock).mockImplementation(
149
+ async ({ where }: { where: { id: string } }) =>
150
+ where.id === SITE_ID
151
+ ? { id: SITE_ID, organizationId: "org_00000000-0000-7000-8000-000000000001" }
152
+ : null,
153
+ );
154
+
155
+ await expect(siteIdForPublicKey(PUBLIC_KEY)).resolves.toBe(SITE_ID);
156
+ });
157
+
158
+ it("fails closed when the env alias names no workspace", async () => {
159
+ process.env["AILK_LISTING_PUBLIC_KEY"] = PUBLIC_KEY;
160
+ process.env["AILK_SITE_ID"] = "no-such-workspace";
161
+ (prisma.siteListingConfig.findUnique as jest.Mock).mockResolvedValue(null);
162
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
163
+
164
+ await expect(siteIdForPublicKey(PUBLIC_KEY)).resolves.toBeNull();
165
+ });
166
+ });
@@ -0,0 +1,154 @@
1
+ // oss: false
2
+ /**
3
+ * @file project-listings.fence.test.ts
4
+ *
5
+ * #6258 AC-6 — `attachWaitlistCounts` (`services/project-listings.ts:425-443`)
6
+ * is one of the nine fenced reads that gain a `workspaceId` term beside their
7
+ * existing `siteId` one. This suite is that read's per-`where` proof.
8
+ *
9
+ * Why a NEW file rather than an addition to an existing suite: this service's
10
+ * only coverage today lives under `apps/api/src/routes/project-listings/__tests__/`,
11
+ * a path under the advisory route floor this unit keeps its diff out of
12
+ * (spec § 8.3). The `where` lives in the service, so the service is where it
13
+ * is proven.
14
+ */
15
+ import { listProjectListings } from "../project-listings.js";
16
+
17
+ // Boundary mocks — `project-listings.ts`'s import chain reaches the Resend
18
+ // client and the licence check; neither is exercised by the read under test.
19
+ jest.mock("@working-theory/email", () => ({
20
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
21
+ sendDeliverableEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
22
+ }));
23
+
24
+ jest.mock("@working-theory/license", () => ({
25
+ checkLicense: jest.fn().mockResolvedValue({
26
+ tier: "free",
27
+ plan: null,
28
+ features: [],
29
+ expiresAt: null,
30
+ revoked: false,
31
+ }),
32
+ }));
33
+
34
+ // eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
35
+ jest.mock("../../lib/prisma.js", () => ({
36
+ prisma: {
37
+ projectListing: { findMany: jest.fn() },
38
+ waitlistScore: { count: jest.fn() },
39
+ },
40
+ }));
41
+
42
+ import { prisma } from "../../lib/prisma.js";
43
+
44
+ const SITE = "wksp_00000000-0000-7000-8000-000000000001";
45
+ const EXPERIMENT_A = "ailk-founder-diagnostic";
46
+ const EXPERIMENT_B = "ailk-agency-waitlist";
47
+
48
+ /**
49
+ * A listing row carrying `workspaceId` equal to its `siteId` — exactly what
50
+ * the `project_listings_workspace_site_agree` CHECK guarantees in the
51
+ * database (#6258 D11).
52
+ */
53
+ function listingRow(id: string, experimentKey: string | null): Record<string, unknown> {
54
+ return {
55
+ id,
56
+ siteId: SITE,
57
+ workspaceId: SITE,
58
+ leadId: null,
59
+ slug: `slug-${id}`,
60
+ name: "Acme",
61
+ headline: null,
62
+ oneLiner: null,
63
+ problem: null,
64
+ solution: null,
65
+ url: null,
66
+ product: null,
67
+ commitment: null,
68
+ teamComposition: null,
69
+ faqs: null,
70
+ anythingElse: null,
71
+ address: null,
72
+ addressLine1: null,
73
+ addressLine2: null,
74
+ addressCity: null,
75
+ addressRegion: null,
76
+ addressPostalCode: null,
77
+ addressCountry: null,
78
+ questions: null,
79
+ siteAnswers: null,
80
+ windowDays: null,
81
+ founderName: null,
82
+ founderFirstName: null,
83
+ founderLastName: null,
84
+ founderEmail: null,
85
+ founderPhone: null,
86
+ founderCompany: null,
87
+ emailVerifiedAt: null,
88
+ logoMarkUrl: null,
89
+ logoWordmarkUrl: null,
90
+ featured: false,
91
+ status: "active",
92
+ consentAt: new Date("2026-09-01T00:00:00.000Z"),
93
+ decidedAt: null,
94
+ decidedBy: null,
95
+ issueUrl: null,
96
+ experimentKey,
97
+ windowEndsAt: new Date("2099-01-01T00:00:00.000Z"),
98
+ createdAt: new Date("2026-09-01T00:00:00.000Z"),
99
+ updatedAt: new Date("2026-09-01T00:00:00.000Z"),
100
+ };
101
+ }
102
+
103
+ beforeEach(() => {
104
+ jest.clearAllMocks();
105
+ (prisma.waitlistScore.count as jest.Mock).mockResolvedValue(3);
106
+ });
107
+
108
+ describe("attachWaitlistCounts — the fenced count filters on workspaceId beside siteId (#6258 AC-6)", () => {
109
+ it("prisma.waitlistScore.count's where carries workspaceId === the resolved scope id, by value", async () => {
110
+ (prisma.projectListing.findMany as jest.Mock).mockResolvedValue([
111
+ listingRow("plst_a", EXPERIMENT_A),
112
+ ]);
113
+
114
+ await listProjectListings(SITE);
115
+
116
+ expect((prisma.waitlistScore.count as jest.Mock).mock.calls[0]![0]!.where).toEqual({
117
+ siteId: SITE,
118
+ workspaceId: SITE,
119
+ experimentKey: EXPERIMENT_A,
120
+ });
121
+ });
122
+
123
+ it("every DISTINCT experimentKey gets the same two scope terms — one count per key, no more", async () => {
124
+ (prisma.projectListing.findMany as jest.Mock).mockResolvedValue([
125
+ listingRow("plst_a", EXPERIMENT_A),
126
+ listingRow("plst_b", EXPERIMENT_B),
127
+ listingRow("plst_c", EXPERIMENT_A),
128
+ ]);
129
+
130
+ await listProjectListings(SITE);
131
+
132
+ expect(prisma.waitlistScore.count).toHaveBeenCalledTimes(2);
133
+ const wheres = (prisma.waitlistScore.count as jest.Mock).mock.calls.map(
134
+ (call) => call[0]!.where as Record<string, unknown>,
135
+ );
136
+ for (const where of wheres) {
137
+ expect(where.siteId).toBe(SITE);
138
+ expect(where.workspaceId).toBe(SITE);
139
+ }
140
+ expect(wheres.map((w) => w.experimentKey).sort()).toEqual(
141
+ [EXPERIMENT_A, EXPERIMENT_B].sort(),
142
+ );
143
+ });
144
+
145
+ it("no listing carries an experimentKey → no fenced read is issued at all", async () => {
146
+ (prisma.projectListing.findMany as jest.Mock).mockResolvedValue([
147
+ listingRow("plst_a", null),
148
+ ]);
149
+
150
+ await listProjectListings(SITE);
151
+
152
+ expect(prisma.waitlistScore.count).not.toHaveBeenCalled();
153
+ });
154
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @file public-site-resolver.test.ts
3
+ *
4
+ * Unit tests for `resolvePublicWorkspace(request)` (#5852, roadmap D4 read
5
+ * half). Database-free: the module-level `prisma` singleton is faked via the
6
+ * repo's manual-mock convention (`jest.mock("../../lib/prisma.js")` →
7
+ * `apps/api/src/lib/__mocks__/prisma.ts`), so every case runs in the ordinary
8
+ * workspace Jest run with no `DATABASE_URL`.
9
+ *
10
+ * The four arms under test: no-op off the database adapter (`mdx`,
11
+ * `projection`), resolved on a one-workspace deployment, and fail-closed on
12
+ * every ambiguity (zero, two, or a sole non-default workspace).
13
+ */
14
+
15
+ // 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.
16
+ jest.mock("../../lib/prisma.js");
17
+
18
+ import {
19
+ DEFAULT_ORG_ID,
20
+ DEFAULT_WORKSPACE_ID,
21
+ } from "@working-theory/database/tenancy";
22
+ import type { FastifyRequest } from "fastify";
23
+
24
+ import { prisma } from "../../lib/prisma.js";
25
+ import { resolvePublicWorkspace } from "../public-site-resolver.js";
26
+
27
+ // The resolver reads nothing off the request today (#5852 D1), so the stub is
28
+ // an empty object asserted at the parameter's type rather than a fixture.
29
+ const request = {} as FastifyRequest;
30
+
31
+ /** A one-workspace deployment seeded with the well-known default. */
32
+ function seedOneWorkspaceDeployment(): void {
33
+ (prisma.workspace.count as jest.Mock).mockResolvedValue(1);
34
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
35
+ id: DEFAULT_WORKSPACE_ID,
36
+ organizationId: DEFAULT_ORG_ID,
37
+ });
38
+ }
39
+
40
+ describe("resolvePublicWorkspace", () => {
41
+ const savedAdapter = process.env.CONTENT_ADAPTER;
42
+
43
+ beforeEach(() => {
44
+ jest.clearAllMocks();
45
+ });
46
+
47
+ afterEach(() => {
48
+ if (savedAdapter === undefined) {
49
+ delete process.env.CONTENT_ADAPTER;
50
+ } else {
51
+ process.env.CONTENT_ADAPTER = savedAdapter;
52
+ }
53
+ });
54
+
55
+ describe("off the database adapter, it is a no-op (D4)", () => {
56
+ it("returns not-required under the default mdx adapter", async () => {
57
+ delete process.env.CONTENT_ADAPTER;
58
+
59
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
60
+ kind: "not-required",
61
+ });
62
+ });
63
+
64
+ it("returns not-required under the projection adapter", async () => {
65
+ process.env.CONTENT_ADAPTER = "projection";
66
+
67
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
68
+ kind: "not-required",
69
+ });
70
+ });
71
+
72
+ it("issues no tenancy query at all on the mdx arm", async () => {
73
+ delete process.env.CONTENT_ADAPTER;
74
+
75
+ await resolvePublicWorkspace(request);
76
+
77
+ expect(prisma.workspace.count).not.toHaveBeenCalled();
78
+ expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
79
+ });
80
+ });
81
+
82
+ describe("under CONTENT_ADAPTER=database", () => {
83
+ beforeEach(() => {
84
+ process.env.CONTENT_ADAPTER = "database";
85
+ });
86
+
87
+ it("resolves the seeded default on a one-workspace deployment", async () => {
88
+ seedOneWorkspaceDeployment();
89
+
90
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
91
+ kind: "resolved",
92
+ workspaceId: DEFAULT_WORKSPACE_ID,
93
+ });
94
+ });
95
+
96
+ it("is unresolved when the deployment has no workspace", async () => {
97
+ (prisma.workspace.count as jest.Mock).mockResolvedValue(0);
98
+
99
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
100
+ kind: "unresolved",
101
+ reason: "ambiguous",
102
+ });
103
+ });
104
+
105
+ it("is unresolved when the deployment has two workspaces", async () => {
106
+ (prisma.workspace.count as jest.Mock).mockResolvedValue(2);
107
+
108
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
109
+ kind: "unresolved",
110
+ reason: "ambiguous",
111
+ });
112
+ });
113
+
114
+ it("is unresolved when the sole workspace is not the seeded default", async () => {
115
+ (prisma.workspace.count as jest.Mock).mockResolvedValue(1);
116
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
117
+
118
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
119
+ kind: "unresolved",
120
+ reason: "ambiguous",
121
+ });
122
+ });
123
+
124
+ it("is unresolved when the sole workspace hangs off a non-sentinel org", async () => {
125
+ (prisma.workspace.count as jest.Mock).mockResolvedValue(1);
126
+ (prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
127
+ id: DEFAULT_WORKSPACE_ID,
128
+ organizationId: "org_01930000-0000-7000-8000-00000000beef",
129
+ });
130
+
131
+ await expect(resolvePublicWorkspace(request)).resolves.toEqual({
132
+ kind: "unresolved",
133
+ reason: "ambiguous",
134
+ });
135
+ });
136
+ });
137
+ });