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
@@ -56,6 +56,15 @@ export interface ConsentWriteClient {
56
56
 
57
57
  interface ConsentGrantRow {
58
58
  siteId: string;
59
+ /**
60
+ * #6374 — the tenancy fence's isolation key, equal to `siteId` by
61
+ * construction. Non-optional on purpose: `consent_grants.workspace_id` is
62
+ * NOT NULL as of
63
+ * database/migrations/20260922120000_tenancy_fence, so a row builder that
64
+ * forgot it would fail at runtime; declaring it required here makes the
65
+ * type checker say so instead.
66
+ */
67
+ workspaceId: string;
59
68
  subjectType: string;
60
69
  subjectId: string;
61
70
  purposeId: string;
@@ -169,6 +178,12 @@ export async function recordConsent(
169
178
  const { statement, locale } = resolveConsentStatement(purpose, input.locale);
170
179
  rows.push({
171
180
  siteId: input.siteId,
181
+ // #6374 — the isolation key, equal to `siteId` by construction.
182
+ // `input.siteId` is the caller's already-resolved site id; the
183
+ // `consent_grants_workspace_site_agree` CHECK makes a disagreement
184
+ // unstorable, and `consent_grants_workspace_id_format` rejects a raw
185
+ // alias loudly. Never read from a request body (AC-6).
186
+ workspaceId: input.siteId,
172
187
  subjectType: input.subjectType,
173
188
  subjectId: input.subjectId,
174
189
  purposeId,
@@ -222,6 +237,9 @@ export async function recordLegacyConsent(
222
237
  data: [
223
238
  {
224
239
  siteId: input.siteId,
240
+ // #6374 — the isolation key, equal to `siteId` by construction; the
241
+ // same stamp the D10 path's row builder above applies.
242
+ workspaceId: input.siteId,
225
243
  subjectType: input.subjectType,
226
244
  subjectId: input.subjectId,
227
245
  purposeId: purpose.id,
@@ -38,13 +38,15 @@
38
38
  */
39
39
  import {
40
40
  DEFAULT_ORG_ID,
41
- DEFAULT_WORKSPACE_ID,
42
41
  OPERATOR_MEMBERSHIP_ID,
42
+ resolveDefaultWorkspaceRow as resolveDefaultWorkspaceRowShared,
43
43
  } from "@working-theory/database/tenancy";
44
- import { isSentinel, type Id } from "@working-theory/ids";
44
+ import type { Id } from "@working-theory/ids";
45
45
 
46
46
  import { prisma } from "../lib/prisma.js";
47
47
 
48
+ import { findVerifiedEmail } from "./verified-email.js";
49
+
48
50
  export type MembershipRole = "owner" | "admin" | "member";
49
51
 
50
52
  /**
@@ -93,9 +95,11 @@ export function isUniqueViolation(error: unknown): boolean {
93
95
  * multi-tenant state (`count > 1`).
94
96
  *
95
97
  * When `AILK_OPERATOR_EMAIL` is set (spec D4 — trimmed + lowercased before
96
- * comparison), the bind is attempted ONLY for the caller whose `User.email`
97
- * matches it; every other caller returns without writing and is denied
98
- * downstream. An empty/whitespace-only `AILK_OPERATOR_EMAIL` normalizes to
98
+ * comparison), the bind is attempted ONLY for the caller whose VERIFIED
99
+ * `User.email` matches it (#5701 AC-2 — the read goes through
100
+ * `findVerifiedEmail`, so an unverified address is refused here and not only
101
+ * at the base-build entry above); every other caller returns without writing
102
+ * and is denied downstream. An empty/whitespace-only `AILK_OPERATOR_EMAIL` normalizes to
99
103
  * `""`, which is falsy — treated as unset (pure TOFU), so a blank pin can
100
104
  * never "match" a blank `User.email`. When unset, the bind is pure TOFU —
101
105
  * first caller wins.
@@ -103,11 +107,18 @@ export function isUniqueViolation(error: unknown): boolean {
103
107
  async function bindOperatorMembership(userId: Id<"user">): Promise<void> {
104
108
  const pinnedEmail = process.env.AILK_OPERATOR_EMAIL?.trim().toLowerCase();
105
109
  if (pinnedEmail) {
106
- const user = await prisma.user.findUnique({
107
- where: { id: userId },
108
- select: { email: true },
109
- });
110
- if (!user || user.email.trim().toLowerCase() !== pinnedEmail) {
110
+ // The verified-email read is `services/verified-email.ts` (#5701 D4) — the
111
+ // same one `resolveDefaultTenantForOperator` uses. It returns the address
112
+ // only when `emailVerified === true`, so an address that merely CLAIMS to
113
+ // be the pin can no longer mint `OPERATOR_MEMBERSHIP_ID`. Both refusals
114
+ // (`user_not_found`, `email_unverified`) take the branch's existing early
115
+ // return — the caller is then unbound and denied by the
116
+ // `membership.findUnique` that follows.
117
+ const verified = await findVerifiedEmail(userId);
118
+ if (!verified.ok) {
119
+ return;
120
+ }
121
+ if (verified.value.email.trim().toLowerCase() !== pinnedEmail) {
111
122
  return;
112
123
  }
113
124
  }
@@ -185,23 +196,60 @@ export async function resolveDefaultTenantForOperator(
185
196
  if (!pinnedEmail) {
186
197
  return null;
187
198
  }
188
- const user = await prisma.user.findUnique({
189
- where: { id: userId },
190
- select: { email: true, emailVerified: true },
191
- });
192
- // `emailVerified` is load-bearing wherever this codebase joins an identity
193
- // to data by email — the respondent reads (`services/waitlist-signups.ts`,
194
- // `services/flow-checkouts.ts`) refuse before any Lead read for the same
195
- // reason. An unverified address is an unproven claim to the pinned one.
196
- if (!user || !user.emailVerified) {
199
+ // The verified-email read is `services/verified-email.ts` (#5701 D4) — the
200
+ // one this and the four other email-keyed paths share. It returns the
201
+ // address only when `emailVerified === true`, which is what makes a match
202
+ // against the pin a proven claim rather than an asserted one.
203
+ const verified = await findVerifiedEmail(userId);
204
+ if (!verified.ok) {
197
205
  return null;
198
206
  }
199
- if (user.email.trim().toLowerCase() !== pinnedEmail) {
207
+ if (verified.value.email.trim().toLowerCase() !== pinnedEmail) {
200
208
  return null;
201
209
  }
202
210
  return resolveDefaultTenant(userId);
203
211
  }
204
212
 
213
+ /**
214
+ * The DEPLOYMENT half of the default resolution, extracted so it exists once
215
+ * (#5852 D2). Returns the sole, well-known default Workspace row, or `null`
216
+ * the instant the deployment is not unambiguously one-workspace — a second
217
+ * Workspace exists, none does, or the sole one is not the seeded default.
218
+ *
219
+ * Carries no authentication: it answers "is this deployment one-workspace, and
220
+ * which workspace is it", nothing about the caller. `resolveDefaultTenant`
221
+ * adds the authentication half on top; `services/public-site-resolver.ts`
222
+ * (unauthenticated public reads) uses the deployment half alone. One
223
+ * predicate, reached two ways — never two predicates.
224
+ *
225
+ * The predicate itself is `@working-theory/database/tenancy`'s
226
+ * `resolveDefaultWorkspaceRow` (#5876 spec D2/D3) — this is now a thin
227
+ * pass-through of `apps/api`'s own `prisma` singleton, kept as a named local
228
+ * function (rather than re-exporting the shared one directly) so every
229
+ * existing caller — this file's own `resolveDefaultWorkspaceId` and
230
+ * `resolveDefaultTenant`, and `services/public-site-resolver.ts`'s dynamic
231
+ * import of the wrapper — is untouched. There is exactly one implementation
232
+ * of the one-workspace predicate in the repository (NFR-7); apps/mcp's own
233
+ * resolution (`apps/mcp/src/lib/public-workspace.ts`) delegates to the same
234
+ * shared export.
235
+ */
236
+ async function resolveDefaultWorkspaceRow(): Promise<{
237
+ id: string;
238
+ organizationId: string;
239
+ } | null> {
240
+ return resolveDefaultWorkspaceRowShared(prisma);
241
+ }
242
+
243
+ /**
244
+ * The one-workspace deployment predicate, as a workspace id (#5852 D2).
245
+ * `null` means the deployment is not unambiguously one-workspace, and every
246
+ * caller fails closed on it.
247
+ */
248
+ export async function resolveDefaultWorkspaceId(): Promise<Id<"workspace"> | null> {
249
+ const workspace = await resolveDefaultWorkspaceRow();
250
+ return workspace ? (workspace.id as Id<"workspace">) : null;
251
+ }
252
+
205
253
  /**
206
254
  * Resolves the default single-tenant scope when the deployment is
207
255
  * unambiguously single-tenant — the default Workspace (`DEFAULT_WORKSPACE_ID`)
@@ -226,17 +274,8 @@ export async function resolveDefaultTenantForOperator(
226
274
  export async function resolveDefaultTenant(
227
275
  userId: Id<"user">,
228
276
  ): Promise<TenantContext | null> {
229
- const workspaceCount = await prisma.workspace.count();
230
- if (workspaceCount !== 1) {
231
- return null;
232
- }
233
- const workspace = await prisma.workspace.findUnique({
234
- where: { id: DEFAULT_WORKSPACE_ID },
235
- select: { id: true, organizationId: true },
236
- });
237
- // The default workspace must hang off the sentinel default Organization
238
- // (ADR-0031 rule 7): keyed on the reserved-range predicate, not a literal.
239
- if (!workspace || !isSentinel(workspace.organizationId)) {
277
+ const workspace = await resolveDefaultWorkspaceRow();
278
+ if (!workspace) {
240
279
  return null;
241
280
  }
242
281
  await bindOperatorMembership(userId);
@@ -45,13 +45,30 @@
45
45
  * PII boundary: logs carry only `{ siteId, flowSlug, sessionId, outcome }` —
46
46
  * never email/asset-key/copy values (mirrors flow-engine.ts's + abandon-
47
47
  * email.ts's boundary).
48
+ *
49
+ * #6310 D1/D2/D4/D5/D15 — a non-`playbook:` `assetKey` now resolves against
50
+ * the workspace-scoped `Asset` store FIRST, via the named boundary
51
+ * `lib/deliverable-asset-store.ts` (never a raw `prisma.asset` call here —
52
+ * that access is banned by architecture.yaml's tenant-scope
53
+ * `forbidden_pattern`), falling back to the LEGACY `DeliverableAsset` store
54
+ * on a miss (D3 — existing rows keep delivering, unmigrated). The workspace
55
+ * is read from the magnet's own `FlowDefinition.workspaceId`, ONE
56
+ * `findUnique` issued only on this branch (never for a `playbook:` key, and
57
+ * never before the `no_entry` exit) — a `null`/absent value skips the
58
+ * `Asset` lookup entirely and falls straight to the legacy arm, never
59
+ * guessing a workspace (D5, mirroring #6155 D3's posture). The grant is
60
+ * minted with `workspaceId` ONLY on the `Asset` arm — the same value that
61
+ * authorized the resolution — restoring the download route's serve-time
62
+ * tenancy re-check for this new grant class (D15).
48
63
  */
49
64
 
50
65
  import { sendDeliverableEmail } from "@working-theory/email";
51
66
  import { logger } from "@working-theory/observability";
52
67
  import { flowConfigSchema, type FlowConfig } from "@working-theory/validation";
53
68
 
69
+ import { findDeliverableAssetIdByKey } from "../lib/deliverable-asset-store.js";
54
70
  import { prisma } from "../lib/prisma.js";
71
+ import { resolveWorkspaceIdForAlias } from "../lib/site.js";
55
72
 
56
73
  // ─── Types ────────────────────────────────────────────────────────────────────
57
74
 
@@ -194,6 +211,15 @@ async function stampOutcome(
194
211
  now: Date,
195
212
  ): Promise<void> {
196
213
  try {
214
+ // fence: global-key — `DeliverableGrant.id`, the capability token itself
215
+ // (#3961 D5: the grant IS the token). Every caller reaches this line
216
+ // inside an already-scoped frame — `fulfillDeliverable` has just MINTED
217
+ // this id under the session's workspace, and `resendDeliverableGrant` has
218
+ // just read the row back by the same opaque id — so the id is the row's
219
+ // own primary key, not a tenant selector, and a `workspaceId` term here
220
+ // could only re-state what the mint already fenced. It would also stop
221
+ // stamping LEGACY grants, whose `workspaceId` is NULL by the #6254 § 10.1
222
+ // Q2 carve-out, turning a post-mint audit stamp into a silent no-op.
197
223
  await prisma.deliverableGrant.update({
198
224
  where: { id: grantId },
199
225
  data: { sendStatus: outcome, sendAt: now },
@@ -312,9 +338,17 @@ export async function fulfillDeliverable(
312
338
  const playbookKey = playbookExperimentKey(entry.assetKey);
313
339
 
314
340
  let assetId: string;
341
+ // #6310 D15 — written ONLY on the Asset arm; stays undefined (no column
342
+ // written) for a playbook: or legacy dasset_ grant.
343
+ let grantWorkspaceId: string | undefined;
344
+
315
345
  if (playbookKey !== undefined) {
346
+ // #6257 — `siteId` is the session's own, resolved by the route that
347
+ // opened it (#6254/#6374 AC-7), so `workspaceId` rides beside it: a lead
348
+ // id and an experiment key alone would select a row whose tenant the
349
+ // query never stated.
316
350
  const instance = await prisma.playbookInstance.findFirst({
317
- where: { siteId, leadId, experimentKey: playbookKey },
351
+ where: { siteId, workspaceId: siteId, leadId, experimentKey: playbookKey },
318
352
  select: { id: true },
319
353
  });
320
354
  if (!instance) {
@@ -323,24 +357,75 @@ export async function fulfillDeliverable(
323
357
  }
324
358
  assetId = entry.assetKey;
325
359
  } else {
326
- // A missing row (config references an un-uploaded key) → same
327
- // skip-and-log posture (D3).
328
- const asset = (await prisma.deliverableAsset.findUnique({
329
- where: { siteId_key: { siteId, key: entry.assetKey } },
330
- })) as DeliverableAssetRow | null;
331
-
332
- if (!asset) {
333
- logOutcome(logCtx, "asset_missing");
334
- return;
360
+ // #6310 D4 — resolve the workspace from the magnet's own live
361
+ // FlowDefinition, ONE query, issued only on this branch. D5 — a null or
362
+ // absent workspaceId skips the Asset lookup entirely; it is never
363
+ // guessed.
364
+ const definition = (await prisma.flowDefinition.findUnique({
365
+ where: { siteId_slug: { siteId, slug: flowSlug } },
366
+ select: { workspaceId: true },
367
+ })) as { workspaceId: string | null } | null;
368
+ const workspaceId = definition?.workspaceId ?? null;
369
+
370
+ const resolvedAssetId =
371
+ workspaceId !== null
372
+ ? await findDeliverableAssetIdByKey(workspaceId, entry.assetKey)
373
+ : null;
374
+
375
+ if (resolvedAssetId !== null && workspaceId !== null) {
376
+ // #6310 D2 — Asset resolves first; D15 — the grant carries the SAME
377
+ // workspaceId that authorized this resolution.
378
+ assetId = resolvedAssetId;
379
+ grantWorkspaceId = workspaceId;
380
+ } else {
381
+ // #6310 D2/D3 — legacy fallback. A missing row (config references an
382
+ // un-uploaded key) → same skip-and-log posture as before.
383
+ // #6257 — the LEGACY store's read, scoped by the same resolved
384
+ // workspace id the `Asset` arm above already resolves through
385
+ // `lib/deliverable-asset-store.ts`. The compound `[siteId, key]` names
386
+ // one row; `workspaceId` is what makes the tenant the QUERY's statement.
387
+ const asset = (await prisma.deliverableAsset.findUnique({
388
+ where: { siteId_key: { siteId, key: entry.assetKey }, workspaceId: siteId },
389
+ })) as DeliverableAssetRow | null;
390
+
391
+ if (!asset) {
392
+ logOutcome(logCtx, "asset_missing");
393
+ return;
394
+ }
395
+ assetId = asset.id;
335
396
  }
336
- assetId = asset.id;
337
397
  }
338
398
 
339
399
  // D5: the grant IS the token — minted before the send is attempted so a
340
400
  // later email-config gap or send failure never wastes it (a later
341
401
  // manual/operator resend can reuse the same grant id).
342
402
  const grant = (await prisma.deliverableGrant.create({
343
- data: { siteId, assetId, leadId, sessionId },
403
+ data: {
404
+ siteId,
405
+ assetId,
406
+ leadId,
407
+ sessionId,
408
+ // #6374 D4 / AC-8 — the SECOND DeliverableGrant mint site. The column
409
+ // stays NULLABLE for LEGACY rows (NULL is the serve path's fail-closed
410
+ // sentinel, #6254 § 10.1 Q2) and the migration touches none of them —
411
+ // but a grant minted HERE always knows its workspace, so it is never
412
+ // left to fail closed.
413
+ //
414
+ // `grantWorkspaceId` is #6310 D15's value, resolved from the magnet's
415
+ // own FlowDefinition on the `asset_` arm; that read is scoped by
416
+ // `siteId`, and the keystone's `flow_definitions_workspace_site_agree`
417
+ // CHECK makes `workspaceId = siteId` on that row, so the two are the
418
+ // SAME value and the `??` is a fallback for the legacy `dasset_` and
419
+ // virtual `playbook:` arms rather than a choice between two scopes.
420
+ // The new `deliverable_grants_workspace_site_agree` CHECK would reject
421
+ // a disagreement loudly either way.
422
+ //
423
+ // This does NOT change which bytes the download route serves: that
424
+ // route branches on the `assetId` PREFIX, and consults
425
+ // `grant.workspaceId` only on the `asset_` arm, where this value is
426
+ // what it already was.
427
+ workspaceId: grantWorkspaceId ?? siteId,
428
+ },
344
429
  select: { id: true },
345
430
  })) as { id: string };
346
431
 
@@ -400,6 +485,13 @@ type FlowSessionConfigRow = {
400
485
  export async function resendDeliverableGrant(
401
486
  grantId: string,
402
487
  ): Promise<ResendGrantResult> {
488
+ // fence: global-key — `DeliverableGrant.id`, the capability token itself
489
+ // (#3961 D5). This is the resolver: the operator hands the
490
+ // `deliverable:resend` CLI one opaque grant id and nothing else, and the
491
+ // WORKSPACE is what this read produces (`grant.siteId`, carried into
492
+ // `sendAndStamp` below), so a tenant term here would have to come from the
493
+ // answer it is computing. Holding the id is the capability, exactly as it
494
+ // is for the download route this resend re-sends a link to.
403
495
  const grant = (await prisma.deliverableGrant.findUnique({
404
496
  where: { id: grantId },
405
497
  select: { id: true, siteId: true, leadId: true, sessionId: true },
@@ -514,9 +606,45 @@ type DeliverableGrantListRow = {
514
606
  export async function listSendlessGrants(
515
607
  opts: ListSendlessGrantsOptions,
516
608
  ): Promise<SendlessGrantRow[]> {
609
+ // #6257 — `opts.siteId` is the one value in this service that arrives as a
610
+ // RAW ALIAS: the `deliverable:resend` CLI passes `--site` through verbatim.
611
+ // It is resolved here, once, the way `dunning-policy.ts` resolves its own
612
+ // parameter — an alias that names no workspace lists nothing rather than
613
+ // falling back to the unscoped read it used to make.
614
+ const workspaceId = await resolveWorkspaceIdForAlias(opts.siteId);
615
+ if (workspaceId === null) {
616
+ logger.warn(
617
+ { siteId: opts.siteId, outcome: "site_unresolved" },
618
+ "[deliverable-fulfillment] --site names no workspace — listing nothing",
619
+ );
620
+ return [];
621
+ }
622
+
623
+ // #6257 — LEGACY grants are NOT in this list, and the narrowing is stated
624
+ // rather than left for an operator to discover. Two terms exclude them: the
625
+ // `workspaceId` filter below (their column is NULL by the #6254 § 10.1 Q2
626
+ // carve-out) and the resolved `siteId` (the keystone migration deliberately
627
+ // does NOT normalize `deliverable_grants.site_id` — migration.sql:1059 — so
628
+ // a legacy row on a `"default"`-aliased deployment still carries the alias).
629
+ //
630
+ // This is a genuine TIGHTENING beyond the serve path, not parity with it:
631
+ // `routes/deliverables/index.ts` consults `grant.workspaceId` only on the
632
+ // `asset_` arm, so a legacy `dasset_` or `playbook:` grant still downloads.
633
+ // The fence forces it — AC-2 forbids keying this read on the raw alias the
634
+ // CLI was given, and a legacy row carries no workspace key to resolve — so
635
+ // the list is narrower by construction and the recovery path for such a row
636
+ // is `deliverable:resend --grant <id>`, which is unaffected.
637
+ logger.info(
638
+ { siteId: workspaceId, outcome: "legacy_grants_excluded" },
639
+ "[deliverable-fulfillment] grants with no workspace key are NOT listed — resend one by id with --grant",
640
+ );
641
+
517
642
  const rows = (await prisma.deliverableGrant.findMany({
518
643
  where: {
519
- siteId: opts.siteId,
644
+ siteId: workspaceId,
645
+ // #6257 — the fence term. See the note above for what it excludes and
646
+ // why that is a tightening rather than serve-path parity.
647
+ workspaceId,
520
648
  sendStatus: null,
521
649
  ...(opts.since !== undefined && { createdAt: { gte: opts.since } }),
522
650
  },