create-ailk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +12248 -0
  3. package/component-catalog.md +193 -68
  4. package/dist/cli.js +29 -1
  5. package/dist/derive-architecture.js +5 -2
  6. package/dist/lib/api-boot-assertion.d.ts +46 -0
  7. package/dist/lib/api-boot-assertion.js +70 -0
  8. package/dist/lib/extract-module-bundle.d.ts +50 -2
  9. package/dist/lib/extract-module-bundle.js +223 -15
  10. package/dist/lib/fetch-guard.d.ts +28 -0
  11. package/dist/lib/fetch-guard.js +101 -0
  12. package/dist/lib/fetch-module.js +4 -0
  13. package/dist/lib/fetch-template.js +4 -0
  14. package/dist/lib/import-graph.d.ts +29 -0
  15. package/dist/lib/import-graph.js +140 -0
  16. package/dist/module-architecture.d.ts +60 -8
  17. package/dist/module-architecture.js +587 -242
  18. package/dist/parse-args.js +6 -2
  19. package/dist/programmatic.d.ts +10 -0
  20. package/dist/programmatic.js +134 -19
  21. package/dist/site-chrome.d.ts +80 -0
  22. package/dist/site-chrome.js +135 -0
  23. package/dist/surfaces.d.ts +21 -6
  24. package/dist/surfaces.js +209 -36
  25. package/dist/template-manifest.d.ts +88 -3
  26. package/dist/template-manifest.js +281 -29
  27. package/dist/template-names.d.ts +1 -1
  28. package/dist/template-names.js +2 -0
  29. package/package.json +10 -2
  30. package/site-templates/_base/ailk-template.json +6 -0
  31. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
  34. package/site-templates/marketing-site/ailk-template.json +11 -0
  35. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  36. package/site-templates/marketing-site/provenance.json +4 -0
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
  39. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
  41. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
  42. package/site-templates/minimalism-site/ailk-template.json +22 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  44. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  49. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  50. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  51. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  53. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  54. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  55. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  56. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  57. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
  58. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  60. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  65. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  68. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  69. package/site-templates/minimalism-site/provenance.json +4 -0
  70. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  71. package/site-templates/saas-product/ailk-template.json +16 -0
  72. package/site-templates/saas-product/provenance.json +10 -0
  73. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  74. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  75. package/site-templates/simple-service/ailk-template.json +10 -0
  76. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  77. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  78. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  79. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  80. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  81. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  82. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  83. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  84. package/site-templates/simple-service/provenance.json +4 -0
  85. package/templates/.claude/skills/README.md +16 -25
  86. package/templates/.env.example +102 -12
  87. package/templates/.github/workflows/ci.yml +48 -0
  88. package/templates/README.md +3 -1
  89. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  90. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  91. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  92. package/templates/apps/api/.env.example +13 -0
  93. package/templates/apps/api/CLAUDE.md +29 -2
  94. package/templates/apps/api/eslint.config.mjs +69 -0
  95. package/templates/apps/api/jest.config.cjs +3 -0
  96. package/templates/apps/api/package.json +7 -2
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  98. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
  99. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  100. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  101. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  102. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
  103. package/templates/apps/api/src/config/index.ts +62 -0
  104. package/templates/apps/api/src/config/modules.ts +7 -3
  105. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
  106. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  107. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  108. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  109. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  110. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  111. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  112. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  113. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  114. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  115. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  116. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  117. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  118. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  119. package/templates/apps/api/src/lib/site.ts +208 -9
  120. package/templates/apps/api/src/lib/stripe.ts +66 -0
  121. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
  122. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
  123. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  124. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  125. package/templates/apps/api/src/openapi/spec.ts +543 -2
  126. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  127. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  128. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  129. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  130. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  131. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  132. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  133. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  134. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  135. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  136. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  137. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  138. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  139. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  140. package/templates/apps/api/src/routes/content/index.ts +13 -73
  141. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  142. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  143. package/templates/apps/api/src/routes/content/search.ts +129 -0
  144. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  145. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  146. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  147. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  148. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  149. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  150. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  151. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  152. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  153. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  154. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  155. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  156. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  157. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  158. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  159. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  160. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  161. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  162. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  163. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  164. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  165. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  166. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  167. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  168. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  169. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  170. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  171. package/templates/apps/api/src/server.ts +521 -55
  172. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  173. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  174. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  175. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  176. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  177. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  178. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  179. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  180. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  181. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  182. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  183. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  184. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  185. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  186. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  187. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  188. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  189. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  190. package/templates/apps/api/src/services/audit-log.ts +668 -0
  191. package/templates/apps/api/src/services/consent.ts +18 -0
  192. package/templates/apps/api/src/services/default-tenant.ts +300 -0
  193. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  194. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  195. package/templates/apps/api/src/services/entitlements.ts +80 -0
  196. package/templates/apps/api/src/services/lead-promotion.ts +160 -0
  197. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  198. package/templates/apps/api/src/services/leads-inbox.ts +284 -0
  199. package/templates/apps/api/src/services/listing-config.ts +27 -9
  200. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  201. package/templates/apps/api/src/services/project-listings.ts +201 -6
  202. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  203. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  204. package/templates/apps/api/src/services/revalidation.ts +173 -0
  205. package/templates/apps/api/src/services/verified-email.ts +71 -0
  206. package/templates/apps/api/src/vercel-handler.ts +75 -35
  207. package/templates/apps/mcp/.env.example +17 -0
  208. package/templates/apps/mcp/CLAUDE.md +58 -25
  209. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  210. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  211. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  212. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  213. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  214. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  215. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  216. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  217. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  218. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  219. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  220. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  221. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  222. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  223. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  224. package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
  225. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  226. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  227. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  228. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  229. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  230. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  231. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  232. package/templates/apps/mcp/eslint.config.mjs +69 -0
  233. package/templates/apps/mcp/jest.config.cjs +13 -0
  234. package/templates/apps/mcp/package.json +8 -2
  235. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  236. package/templates/apps/mcp/src/config/modules.ts +10 -5
  237. package/templates/apps/mcp/src/http-transport.ts +102 -29
  238. package/templates/apps/mcp/src/index.ts +73 -11
  239. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  240. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  241. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  242. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  243. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  244. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  245. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  246. package/templates/apps/mcp/src/server.ts +61 -37
  247. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  248. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  249. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  250. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  251. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  252. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  253. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  254. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  255. package/templates/apps/mcp/src/tools/index.ts +9 -7
  256. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  257. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  258. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  259. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  260. package/templates/apps/web/.env.example +74 -8
  261. package/templates/apps/web/CLAUDE.md +46 -0
  262. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  263. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  264. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  265. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  266. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  267. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  268. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  269. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  270. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  271. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  272. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  273. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  274. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  275. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  276. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  277. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  278. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  279. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  280. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  281. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
  282. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
  283. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  284. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  285. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  286. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  287. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  288. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  289. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  290. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  291. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  292. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  293. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  294. package/templates/apps/web/app/[locale]/layout.tsx +308 -65
  295. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  296. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  297. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  298. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  299. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  300. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  301. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  302. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  303. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  304. package/templates/apps/web/auth.ts +407 -8
  305. package/templates/apps/web/components/google-analytics.tsx +18 -1
  306. package/templates/apps/web/eslint.config.mjs +69 -0
  307. package/templates/apps/web/jest.config.cjs +15 -19
  308. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  309. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  310. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  311. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
  312. package/templates/apps/web/lib/auth-email.ts +318 -0
  313. package/templates/apps/web/lib/auth-env.ts +85 -0
  314. package/templates/apps/web/lib/auth-routes.ts +55 -0
  315. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  316. package/templates/apps/web/lib/nav-config.ts +27 -7
  317. package/templates/apps/web/lib/server-api.ts +1 -1
  318. package/templates/apps/web/lib/site-chrome.ts +29 -0
  319. package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
  320. package/templates/apps/web/messages/en.json +216 -2
  321. package/templates/apps/web/next.config.mjs +20 -0
  322. package/templates/apps/web/package.json +8 -3
  323. package/templates/apps/web/sentry.client.config.ts +131 -0
  324. package/templates/apps/web/sentry.edge.config.ts +72 -0
  325. package/templates/apps/web/sentry.server.config.ts +72 -0
  326. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  327. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  328. package/templates/apps/web/src/lib/routes.ts +13 -18
  329. package/templates/brand_assets/README.md +24 -0
  330. package/templates/content/en/AboutPage/about.mdx +160 -15
  331. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  332. package/templates/content/en/ContactPage/contact.mdx +90 -13
  333. package/templates/content/en/FAQPage/faq.mdx +62 -22
  334. package/templates/content/en/HomePage/home.mdx +244 -20
  335. package/templates/content/en/LandingPage/blog.mdx +83 -0
  336. package/templates/content/en/LandingPage/features.mdx +103 -0
  337. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  338. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  339. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  340. package/templates/database/CHANGELOG.md +149 -0
  341. package/templates/database/README.md +10 -3
  342. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  343. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  344. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  345. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  346. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  347. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  348. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  349. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  350. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  351. package/templates/database/__tests__/seed.test.ts +1 -1
  352. package/templates/database/__tests__/tenancy.test.ts +141 -0
  353. package/templates/database/config/schema.prisma +79 -2
  354. package/templates/database/content/schema.prisma +87 -11
  355. package/templates/database/eslint.config.mjs +69 -0
  356. package/templates/database/inbox/schema.prisma +452 -9
  357. package/templates/database/jest.config.cjs +5 -3
  358. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  359. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  360. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  361. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  362. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  363. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  364. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  365. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  366. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  367. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  368. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  369. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  370. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  371. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  372. package/templates/database/package.json +7 -3
  373. package/templates/database/seed.ts +1 -1
  374. package/templates/database/tenancy.ts +86 -1
  375. package/templates/database/tsconfig.json +1 -0
  376. package/templates/package.json +1 -14
  377. package/versions.json +52 -0
  378. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  379. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  380. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  381. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  382. package/templates/apps/api/src/routes/content/create.ts +0 -115
  383. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  384. package/templates/apps/api/src/routes/content/update.ts +0 -112
  385. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  386. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  387. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  388. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  389. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  390. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  391. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  392. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  393. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  394. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  395. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
  396. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @file project-listing-consent-at.db.test.ts
3
+ *
4
+ * LIVE-DATABASE proof that `ProjectListing.consentAt` round-trips through
5
+ * the real Prisma Client with timezone information preserved (#6171).
6
+ * Requires DATABASE_URL pointing at a Postgres with the migrations applied
7
+ * (`prisma migrate deploy`) — the whole suite SKIPS when DATABASE_URL is
8
+ * unset, keeping the workspace Jest run DB-free (the repo convention; see
9
+ * `prefixed-ids.db.test.ts` / `appointment.db.test.ts`).
10
+ *
11
+ * The bug this guards: `database/inbox/schema.prisma`'s `consentAt` field
12
+ * declared `DateTime @default(now()) @map("consent_at")` with no native-type
13
+ * annotation, so Prisma treated it as `TIMESTAMP(3)` (no time zone) by
14
+ * default, while its migration
15
+ * (`20260909180000_listing_draft_and_fields/migration.sql`) created the
16
+ * actual column as `TIMESTAMPTZ` — deliberately, since it records a legal
17
+ * consent timestamp. The fix adds `@db.Timestamptz(3)` so the schema's
18
+ * declared type agrees with what the column actually is.
19
+ *
20
+ * Spawns `project-listing-consent-probe.ts` via `tsx` (a real ESM process)
21
+ * rather than importing the generated client directly, for the same reason
22
+ * `appointment.db.test.ts` does: the generated client is ESM-only
23
+ * (`import.meta.url`) and cannot load under this package's CJS `ts-jest`
24
+ * transform.
25
+ */
26
+ import { spawnSync } from "node:child_process";
27
+ import { existsSync, readFileSync } from "node:fs";
28
+ import { join } from "node:path";
29
+
30
+ const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
31
+
32
+ const DATABASE_DIR = join(__dirname, "..");
33
+ const PROBE_SCRIPT = join(
34
+ __dirname,
35
+ "fixtures",
36
+ "project-listing-consent-probe.ts",
37
+ );
38
+
39
+ // Runs with no DATABASE_URL required — a deterministic guard so the schema
40
+ // declaration can't silently regress even in a DB-free run (the live-DB
41
+ // round trip below still runs whenever DATABASE_URL is set).
42
+ describe("ProjectListing.consentAt — schema declares the real column type (#6171)", () => {
43
+ it("carries @db.Timestamptz(3), matching the TIMESTAMPTZ column created by 20260909180000_listing_draft_and_fields", () => {
44
+ const schema = readFileSync(
45
+ join(DATABASE_DIR, "inbox", "schema.prisma"),
46
+ "utf-8",
47
+ );
48
+ const fieldLine = schema
49
+ .split("\n")
50
+ .find((line) => /^\s*consentAt\s+DateTime\b/.test(line));
51
+ expect(fieldLine).toBeDefined();
52
+ expect(fieldLine).toMatch(/@db\.Timestamptz\(3\)/);
53
+ });
54
+ });
55
+
56
+ /** Walk up from DATABASE_DIR to find node_modules/.bin/tsx (worktree-safe —
57
+ * mirrors `appointment.db.test.ts`'s own resolution). */
58
+ function findTsxBin(): string {
59
+ let dir = DATABASE_DIR;
60
+ for (let i = 0; i < 5; i++) {
61
+ const candidate = join(dir, "node_modules", ".bin", "tsx");
62
+ if (existsSync(candidate)) return candidate;
63
+ dir = join(dir, "..");
64
+ }
65
+ throw new Error(`tsx binary not found searching up from ${DATABASE_DIR}.`);
66
+ }
67
+
68
+ describeDb(
69
+ "ProjectListing.consentAt — live DB round trip via prisma.projectListing (#6171)",
70
+ () => {
71
+ it("preserves the exact instant through create() + a fresh findUniqueOrThrow(), regardless of session timezone", () => {
72
+ const tsxBin = findTsxBin();
73
+ const result = spawnSync(tsxBin, [PROBE_SCRIPT], {
74
+ cwd: DATABASE_DIR,
75
+ encoding: "utf-8",
76
+ env: process.env,
77
+ });
78
+
79
+ // Exit status + a successful stdout parse is the full signal (see
80
+ // appointment.db.test.ts for why this, rather than empty stderr, is
81
+ // the right assertion — a stray Node/tsx diagnostic must not fail an
82
+ // otherwise-successful run).
83
+ if (result.status !== 0) {
84
+ throw new Error(
85
+ `probe exited ${result.status}: ${result.stderr || "(no stderr)"}`,
86
+ );
87
+ }
88
+
89
+ const probe = JSON.parse(result.stdout) as {
90
+ writtenIso: string;
91
+ createdConsentAtIso: string;
92
+ rereadConsentAtIso: string;
93
+ liveColumnDataType: string | null;
94
+ };
95
+
96
+ // The instant `create()` returns must be exactly what was written —
97
+ // not shifted by the pinned Asia/Kolkata (+05:30) session timezone.
98
+ expect(probe.createdConsentAtIso).toBe(probe.writtenIso);
99
+
100
+ // A SECOND, independent read (a fresh findUniqueOrThrow, not the
101
+ // object create() already returned) must agree too — the read path,
102
+ // not just the write path, preserves the instant.
103
+ expect(probe.rereadConsentAtIso).toBe(probe.writtenIso);
104
+
105
+ // Ground truth: the live column really is timezone-aware, which is
106
+ // what makes the schema's `@db.Timestamptz(3)` annotation correct
107
+ // rather than merely quiet.
108
+ expect(probe.liveColumnDataType).toBe("timestamp with time zone");
109
+ });
110
+ },
111
+ );
@@ -7,7 +7,7 @@
7
7
  * Prisma is injected as a lightweight jest-mock stub typed as
8
8
  * `DefaultWorkspacePrismaClient` — no live Postgres, mirroring the DB-free
9
9
  * convention in database/__tests__/user-cascade.test.ts and the
10
- * `createAuth({ prisma })` DI pattern in apps-paid/workspace/__tests__.
10
+ * `createAuth({ prisma })` DI pattern in apps-paid/admin/__tests__.
11
11
  */
12
12
 
13
13
  import {
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @file tenancy.test.ts
3
+ * @description Unit coverage for `resolveDefaultWorkspaceRow` — the shared
4
+ * one-workspace deployment predicate (#5876 spec D2) — over a structural
5
+ * `WorkspacePredicateClient` stub. No `@prisma/client` import anywhere in
6
+ * this file or in `../tenancy.ts`; the four arms below are the same ones
7
+ * `apps/api/src/services/__tests__/public-site-resolver.test.ts` and
8
+ * `apps/api/src/middleware/__tests__/default-tenant.test.ts` already pin for
9
+ * the re-pointed caller — this suite is the predicate's OWN coverage.
10
+ *
11
+ * `resolveSentinelWorkspaceRow` (#6155 spec D2 rule 2) is the sibling
12
+ * sentinel-EXISTENCE predicate, covered below on the SAME three-workspace
13
+ * mock that `resolveDefaultWorkspaceRow` refuses — the pair is what proves
14
+ * the two questions ("is this deployment single-tenant" vs "does the
15
+ * sentinel workspace exist") stay separate rather than re-merging.
16
+ */
17
+
18
+ import {
19
+ DEFAULT_ORG_ID,
20
+ DEFAULT_WORKSPACE_ID,
21
+ resolveDefaultWorkspaceRow,
22
+ resolveSentinelWorkspaceRow,
23
+ type WorkspacePredicateClient,
24
+ } from "../tenancy";
25
+
26
+ const NON_SENTINEL_ORG_ID = "org_01930000-0000-7bcd-8000-000000000002";
27
+
28
+ function makeStub(
29
+ count: number,
30
+ found: { organizationId: string } | null,
31
+ ): WorkspacePredicateClient {
32
+ return {
33
+ workspace: {
34
+ count: jest.fn().mockResolvedValue(count),
35
+ findUnique: jest.fn().mockResolvedValue(found),
36
+ },
37
+ };
38
+ }
39
+
40
+ describe("resolveDefaultWorkspaceRow", () => {
41
+ it("returns null when zero workspaces exist (count() === 0)", async () => {
42
+ const stub = makeStub(0, null);
43
+
44
+ const result = await resolveDefaultWorkspaceRow(stub);
45
+
46
+ expect(result).toBeNull();
47
+ // The deployment is ambiguous at the count check alone — findUnique must
48
+ // never run.
49
+ expect(stub.workspace.findUnique).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it("returns null when two or more workspaces exist (count() === 2)", async () => {
53
+ const stub = makeStub(2, null);
54
+
55
+ const result = await resolveDefaultWorkspaceRow(stub);
56
+
57
+ expect(result).toBeNull();
58
+ expect(stub.workspace.findUnique).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("returns null when the sole workspace hangs off a non-sentinel Organization", async () => {
62
+ const stub = makeStub(1, { organizationId: NON_SENTINEL_ORG_ID });
63
+
64
+ const result = await resolveDefaultWorkspaceRow(stub);
65
+
66
+ expect(result).toBeNull();
67
+ });
68
+
69
+ it("returns null when the sole workspace is not found at the default id", async () => {
70
+ const stub = makeStub(1, null);
71
+
72
+ const result = await resolveDefaultWorkspaceRow(stub);
73
+
74
+ expect(result).toBeNull();
75
+ });
76
+
77
+ it("returns the row when the sole workspace is the well-known default hanging off the sentinel Organization", async () => {
78
+ const stub = makeStub(1, { organizationId: DEFAULT_ORG_ID });
79
+
80
+ const result = await resolveDefaultWorkspaceRow(stub);
81
+
82
+ expect(result).toEqual({
83
+ id: DEFAULT_WORKSPACE_ID,
84
+ organizationId: DEFAULT_ORG_ID,
85
+ });
86
+ expect(stub.workspace.findUnique).toHaveBeenCalledWith({
87
+ where: { id: DEFAULT_WORKSPACE_ID },
88
+ });
89
+ });
90
+
91
+ it("returns null on a THREE-workspace deployment even though the sentinel workspace exists — the count gate refuses it before findUnique ever runs", async () => {
92
+ // A correctly-configured multi-workspace deployment: the sentinel is one
93
+ // of three, but resolveDefaultWorkspaceRow answers "is this deployment
94
+ // unambiguously single-tenant", not "does the sentinel exist" — #6155
95
+ // spec D2/R1, the exact case resolveSentinelWorkspaceRow exists for.
96
+ const stub = makeStub(3, { organizationId: DEFAULT_ORG_ID });
97
+
98
+ const result = await resolveDefaultWorkspaceRow(stub);
99
+
100
+ expect(result).toBeNull();
101
+ expect(stub.workspace.findUnique).not.toHaveBeenCalled();
102
+ });
103
+ });
104
+
105
+ describe("resolveSentinelWorkspaceRow", () => {
106
+ it("resolves the sentinel workspace on a THREE-workspace deployment — no count() gate", async () => {
107
+ // The SAME three-workspace mock resolveDefaultWorkspaceRow refuses above
108
+ // — this is the pair that keeps the two questions separate (#6155 D2
109
+ // rule 2): resolveDefaultWorkspaceRow answers "is this deployment
110
+ // single-tenant" (no); resolveSentinelWorkspaceRow answers "does the
111
+ // seeded sentinel workspace exist" (yes), and it never calls count().
112
+ const stub = makeStub(3, { organizationId: DEFAULT_ORG_ID });
113
+
114
+ const result = await resolveSentinelWorkspaceRow(stub);
115
+
116
+ expect(result).toEqual({
117
+ id: DEFAULT_WORKSPACE_ID,
118
+ organizationId: DEFAULT_ORG_ID,
119
+ });
120
+ expect(stub.workspace.count).not.toHaveBeenCalled();
121
+ expect(stub.workspace.findUnique).toHaveBeenCalledWith({
122
+ where: { id: DEFAULT_WORKSPACE_ID },
123
+ });
124
+ });
125
+
126
+ it("returns null when no workspace carries the sentinel id", async () => {
127
+ const stub = makeStub(3, null);
128
+
129
+ const result = await resolveSentinelWorkspaceRow(stub);
130
+
131
+ expect(result).toBeNull();
132
+ });
133
+
134
+ it("returns null when the id resolves but hangs off a non-sentinel Organization", async () => {
135
+ const stub = makeStub(3, { organizationId: NON_SENTINEL_ORG_ID });
136
+
137
+ const result = await resolveSentinelWorkspaceRow(stub);
138
+
139
+ expect(result).toBeNull();
140
+ });
141
+ });
@@ -9,17 +9,45 @@
9
9
  // invariant #1). Cross-namespace integrity + cascade are the service layer's
10
10
  // (ADR-0009 D3), delivered by the Phase 4 workspace-scoped write surface. Every
11
11
  // model here is fenced by the architecture.yaml tenant-scope forbidden-pattern.
12
+ //
13
+ // SiteSetting and SiteListingConfig also carry the #6374 tenancy fence. The
14
+ // placement rule stated in database/inbox/schema.prisma's header applies
15
+ // here verbatim: `workspaceId` goes on the line immediately after `siteId`
16
+ // (immediately after `id` where there is no `siteId`), and no comment
17
+ // written for it contains a closing curly, because the drift guard's
18
+ // model-body collector stops at the first one.
12
19
 
13
20
  /// SiteSetting - Key-value settings for the customer site
14
21
  /// Use for: feature flags, custom config, cached Platform data
15
22
  model SiteSetting {
16
23
  id String @id @default(dbgenerated("('siteset_'::text || (uuidv7())::text)")) @db.Text
17
- key String @unique // "feature_flags", "cached_config", "custom_theme"
24
+ // #6374 D3 — the tenancy fence, in the shape this model's ABSENT site_id
25
+ // forces (operator disposition, 2026-09-20). This table is keyed BY the
26
+ // workspace: `key` loses its global @unique and the pair becomes unique
27
+ // instead, so two sellers may each hold a `feature_flags` row. Existing
28
+ // rows backfill to the SENTINEL workspace — a pre-fence row belonged to
29
+ // "the site", which is exactly what the alias `default` resolves to.
30
+ //
31
+ // The `key` TEXT itself does NOT change. `dunning-policy.ts` builds
32
+ // `commerce.dunning_policy:` + siteId from its UNRESOLVED siteId
33
+ // parameter, because this table never had a site_id column to scope by;
34
+ // the backfill leaves every key verbatim, so the read keeps building the
35
+ // key the same way and adds workspaceId from the resolver. Rebuilding the
36
+ // key from the RESOLVED id would match no row and silently hand every
37
+ // seller the built-in default policy.
38
+ //
39
+ // DO NOT MOVE this field — see the fence-fan-out note in
40
+ // database/inbox/schema.prisma's header: placement immediately after id is
41
+ // load-bearing, not cosmetic.
42
+ workspaceId String @map("workspace_id")
43
+ key String // "feature_flags", "cached_config", "custom_theme"
18
44
  value Json
19
45
 
20
46
  createdAt DateTime @default(now()) @map("created_at")
21
47
  updatedAt DateTime @updatedAt @map("updated_at")
22
48
 
49
+ @@unique([workspaceId, key])
50
+ @@index([workspaceId])
23
51
  @@map("site_settings")
24
52
  }
25
53
 
@@ -50,7 +78,22 @@ model WorkspaceTheme {
50
78
  @@map("workspace_themes")
51
79
  }
52
80
 
53
- /// WorkspaceSiteIdentity — per-workspace SiteIdentitySchema blob (#3641)
81
+ /// WorkspaceSiteIdentity — per-workspace SiteIdentitySchema blob (#3641).
82
+ ///
83
+ /// #6254 AC-2 — carries `@@unique([id, workspaceId])`, a named Postgres
84
+ /// unique constraint over a pair already unique today (`id` is the PK, so
85
+ /// the pair is trivially unique too) — redundant now, load-bearing the
86
+ /// moment `workspaceId` ever drops its own `@unique` above (§2.3 of
87
+ /// docs/specs/6254-the-composite-fk-pattern-workspacesiteidentity-spec.md).
88
+ /// It is NOT the target of a Prisma `@relation` from the inbox-namespace
89
+ /// fenced models (Lead, ProjectListing, FlowDefinition, FlowSession):
90
+ /// ADR-0009 invariant #1 forbids a cross-namespace relation (this model is
91
+ /// `config`, they are `inbox`), and `site_id` never holds this model's
92
+ /// `siteid_<uuidv7>` id — it holds the WORKSPACE id, so a real FK would be
93
+ /// unsatisfiable for every existing row. Those four models enforce
94
+ /// site<->workspace agreement instead via a same-table equality CHECK
95
+ /// (`<table>_workspace_site_agree`) — see database/inbox/schema.prisma and
96
+ /// docs/database/namespaces.md.
54
97
  model WorkspaceSiteIdentity {
55
98
  id String @id @default(dbgenerated("('siteid_'::text || (uuidv7())::text)")) @db.Text
56
99
  workspaceId String @unique @map("workspace_id")
@@ -58,6 +101,7 @@ model WorkspaceSiteIdentity {
58
101
  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
59
102
  updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
60
103
 
104
+ @@unique([id, workspaceId])
61
105
  @@map("workspace_site_identities")
62
106
  }
63
107
 
@@ -72,6 +116,26 @@ model WorkspaceSEO {
72
116
  @@map("workspace_seos")
73
117
  }
74
118
 
119
+ /// WorkspaceMeetings — per-workspace MeetingsSchema blob: the operator's
120
+ /// enabled-slug gate for meeting types (#6183 S9b, D1). Structurally
121
+ /// identical to its three siblings above — same `@unique` key, same
122
+ /// Json-column convention, validation at the API layer. The field-level
123
+ /// `@map`s are load-bearing (not merely style, unlike the siblings' own
124
+ /// comment implies): `prisma validate` accepts a model without them, and
125
+ /// only `pnpm check:prisma-migration-drift` catches the resulting
126
+ /// camelCase/snake_case mismatch against a hand-written migration — the
127
+ /// exact class that shipped a P2022 on every `Appointment.noShow` write
128
+ /// (#6162). Keep every `@map` in lockstep with the migration's column names.
129
+ model WorkspaceMeetings {
130
+ id String @id @default(dbgenerated("('meet_'::text || (uuidv7())::text)")) @db.Text
131
+ workspaceId String @unique @map("workspace_id")
132
+ data Json
133
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
134
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
135
+
136
+ @@map("workspace_meetings")
137
+ }
138
+
75
139
  /// SiteListingConfig — per-SITE project-listings config blob (#5033).
76
140
  ///
77
141
  /// The fourth site-config model, and the one keyed on `siteId` rather than
@@ -90,6 +154,18 @@ model WorkspaceSEO {
90
154
  model SiteListingConfig {
91
155
  id String @id @default(dbgenerated("('sltcfg_'::text || (uuidv7())::text)")) @db.Text
92
156
  siteId String @unique @map("site_id")
157
+ // #6374 — the tenancy fence, extended to this model (spec
158
+ // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
159
+ // Prisma relation directive (ADR-0009 invariant #1), and
160
+ // site_listing_configs_workspace_site_agree CHECK (workspace_id = site_id)
161
+ // — since siteId IS the resolved workspace id, Postgres itself rejects a
162
+ // row whose two columns disagree. Operator-seeded config: there is no
163
+ // create/upsert of this model in the tree, and its one read
164
+ // (apps/api/src/services/listing-config.ts) already resolves the alias.
165
+ // DO NOT MOVE this field — see the fence-fan-out note in
166
+ // database/inbox/schema.prisma's header: placement immediately after
167
+ // siteId is load-bearing, not cosmetic.
168
+ workspaceId String @map("workspace_id")
93
169
 
94
170
  /// The site's PUBLIC site key (#5048 D2) — the addressing token an anonymous
95
171
  /// application carries in place of a siteId, so the server resolves the site
@@ -106,6 +182,7 @@ model SiteListingConfig {
106
182
  createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
107
183
  updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
108
184
 
185
+ @@index([workspaceId])
109
186
  @@map("site_listing_configs")
110
187
  }
111
188
 
@@ -1,5 +1,5 @@
1
1
  // content namespace — chat + artifact substrate (AgentSession, ConversationEvent,
2
- // SessionReadReceipt, Artifact, ArtifactVersion)
2
+ // SessionReadReceipt, Artifact, ArtifactVersion, Asset)
3
3
  //
4
4
  // Backs the event-sourced conversation transport + artifact persistence in
5
5
  // docs/research/ploy-capture/BACKEND-REQUIREMENTS.md (§1 transport R-T1..R-T7,
@@ -75,21 +75,50 @@ model SessionReadReceipt {
75
75
  @@map("session_read_receipts")
76
76
  }
77
77
 
78
- /// Artifact — a session's produced output (first type: text/document, #3846).
78
+ /// Artifact — a piece of the WORKSPACE's versioned content (#3846, generalized
79
+ /// by #5800 / roadmap D1). Originally "a session's produced output"; a session
80
+ /// is now PROVENANCE, not ownership — `sessionId` is optional and `SetNull`, so
81
+ /// deleting the conversation that produced a page leaves the page standing.
82
+ ///
83
+ /// `contentType` is the discriminator the content store reads: `text/page` is
84
+ /// what the editor writes, `text/document` the legacy chat artifact. A page's
85
+ /// public identity is `(workspaceId, contentType, locale, slug)`; `slug` stays
86
+ /// NULL until publish, and NULLs never collide in a Postgres unique index, so a
87
+ /// workspace may hold any number of unpublished drafts.
88
+ ///
89
+ /// `publishedVersionId` is a plain pointer at the ArtifactVersion the public
90
+ /// read serves — deliberately NOT a relation (a FK back into `versions` would
91
+ /// make the delete order of an artifact's own history circular), and
92
+ /// deliberately NOT `ArtifactVersion.isCurrent`, which is the DRAFT TIP and is
93
+ /// never the publish mechanism. Saving a draft moves `isCurrent`; only publish
94
+ /// moves this pointer.
95
+ ///
96
+ /// `type` is the LEGACY discriminator. It predates `contentType` and is still
97
+ /// read by apps/api/src/routes/workspaces/sessions.ts (the session-artifacts
98
+ /// route, a hard-floor surface), so #5800 added `contentType` beside it and
99
+ /// backfilled it from `type` rather than renaming the column out from under a
100
+ /// live consumer. New code reads `contentType`; retiring `type` belongs to the
101
+ /// card that moves that route.
79
102
  model Artifact {
80
- id String @id @default(dbgenerated("('artif_'::text || (uuidv7())::text)")) @db.Text
81
- sessionId String @map("session_id")
82
- workspaceId String @map("workspace_id")
83
- type String @default("text/document")
84
- title String
85
- createdAt DateTime @default(now()) @map("created_at")
86
- updatedAt DateTime @updatedAt @map("updated_at")
87
-
88
- session AgentSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
103
+ id String @id @default(dbgenerated("('artif_'::text || (uuidv7())::text)")) @db.Text
104
+ sessionId String? @map("session_id")
105
+ workspaceId String @map("workspace_id")
106
+ type String @default("text/document")
107
+ contentType String @default("text/document") @map("content_type")
108
+ locale String @default("en")
109
+ slug String?
110
+ title String
111
+ publishedVersionId String? @map("published_version_id")
112
+ createdAt DateTime @default(now()) @map("created_at")
113
+ updatedAt DateTime @updatedAt @map("updated_at")
114
+
115
+ session AgentSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
89
116
  versions ArtifactVersion[]
90
117
 
118
+ @@unique([workspaceId, contentType, locale, slug])
91
119
  @@index([sessionId])
92
120
  @@index([workspaceId])
121
+ @@index([workspaceId, contentType, updatedAt])
93
122
  @@map("artifacts")
94
123
  }
95
124
 
@@ -110,6 +139,53 @@ model ArtifactVersion {
110
139
  @@map("artifact_versions")
111
140
  }
112
141
 
142
+ /// Asset — a workspace's uploaded binary (#5800 / roadmap D7), following the
143
+ /// `DeliverableAsset` precedent in the inbox namespace: the bytes live in the
144
+ /// row, not in object storage. That precedent was deliberate — no new
145
+ /// infrastructure — and only the resolver changes if blob storage arrives later.
146
+ ///
147
+ /// `workspaceId` is a plain String stable id with no @relation (ADR 0009
148
+ /// invariant 1), and `(workspaceId, key)` is the addressable identity. Ids are
149
+ /// UUIDv7 (ADR 0031), so a re-upload is a NEW id and a served asset can be
150
+ /// cached immutably. The upload route itself is #5802's scope, not this model's.
151
+ ///
152
+ /// Since #6310: this is also the store a magnet's `deliverables.assets[].assetKey`
153
+ /// resolves against FIRST (by `(workspaceId, key)`), ahead of the legacy
154
+ /// `DeliverableAsset` fallback (database/inbox/schema.prisma) — read through
155
+ /// the scoped boundary in apps/api/src/lib/deliverable-asset-store.ts, never
156
+ /// a raw query.
157
+ model Asset {
158
+ id String @id @default(dbgenerated("('asset_'::text || (uuidv7())::text)")) @db.Text
159
+ workspaceId String @map("workspace_id")
160
+ /// The caller-chosen name an artifact's content references the asset by.
161
+ key String
162
+ /// Download filename (Content-Disposition). Stored RAW; sanitize at SERVE
163
+ /// time, exactly as the DeliverableAsset download route does.
164
+ filename String
165
+ contentType String @map("content_type")
166
+ bytes Bytes
167
+ size Int
168
+ /// Owner-authored display name, shown above the filename in the lightbox
169
+ /// (#6092, S4/D10). NULL means the owner has not written one — distinct from
170
+ /// an empty string, which means they cleared it. Never backfilled from
171
+ /// `filename`: the two are different facts (demo.html:782 renders both).
172
+ title String?
173
+ /// Owner-authored longer description. Same NULL-vs-empty contract as `title`.
174
+ description String?
175
+ /// Owner-controlled tag SLUGS (`ad-creative`, `workspace-memory`, …), never
176
+ /// the human labels — the label is presentation, resolved by the surface from
177
+ /// its own vocabulary (demo.html:358 `ATAGS`, :794 the Docs set). Free-form
178
+ /// by design: the asset vocabulary and the document vocabulary (D8/D9) ride
179
+ /// this one column, and the "fixed set" of D10 is a UI affordance, not a
180
+ /// database constraint (#6092, D2 of the spec).
181
+ tags String[] @default([])
182
+ createdAt DateTime @default(now()) @map("created_at")
183
+
184
+ @@unique([workspaceId, key])
185
+ @@index([workspaceId])
186
+ @@map("assets")
187
+ }
188
+
113
189
  // ============================================================================
114
190
  // Usage-billing metering (FEAT-047 P1.S1, #3709 — Epic #3704)
115
191
  //
@@ -0,0 +1,69 @@
1
+ // Self-contained ESLint flat config for this scaffolded package.
2
+ //
3
+ // It deliberately does NOT depend on @working-theory/eslint-config — that
4
+ // package is internal to the AI Launch Kit monorepo and is not published, so
5
+ // it cannot be a dependency of a scaffolded starter. Everything below comes
6
+ // from public npm packages already pinned as devDependencies (see
7
+ // package.json): @eslint/js, globals and typescript-eslint.
8
+ //
9
+ // Source file name (#5556): this file is checked in as `_eslint.config.mjs`,
10
+ // not `eslint.config.mjs` — build-templates.mjs renames it at snapshot time
11
+ // (renameScaffoldLintConfigs()). A file literally named `eslint.config.mjs`
12
+ // anywhere under this monorepo (even one this repo's own root eslint.config.ts
13
+ // lists in its `ignores`) is auto-discovered by ESLint's flat-config nested
14
+ // resolution as a competing config root, which breaks the monorepo's own
15
+ // lint runs (verified live: typescript-eslint's "multiple candidate
16
+ // TSConfigRootDirs" parser error across unrelated packages/create-ailk
17
+ // files). The underscore prefix keeps it invisible to that discovery while
18
+ // it lives in this checkout; only the snapshot in templates/ — which ships
19
+ // to the customer, not this repo — gets the real filename.
20
+ import js from "@eslint/js";
21
+ import globals from "globals";
22
+ import tseslint from "typescript-eslint";
23
+
24
+ export default tseslint.config(
25
+ {
26
+ ignores: [
27
+ "**/.next/**",
28
+ "**/dist/**",
29
+ "**/generated/**",
30
+ "**/node_modules/**",
31
+ ],
32
+ },
33
+ js.configs.recommended,
34
+ ...tseslint.configs.recommended,
35
+ {
36
+ languageOptions: {
37
+ globals: {
38
+ ...globals.node,
39
+ ...globals.browser,
40
+ ...globals.jest,
41
+ },
42
+ // Explicit, self-contained root — this scaffold config is never meant
43
+ // to combine with any sibling scaffold package's config (each ships
44
+ // standalone), and leaving it unset makes typescript-eslint's
45
+ // candidate-root disambiguation ambiguous the moment more than one
46
+ // scaffold package's config resolves in the same ESLint process (e.g.
47
+ // this monorepo's own `templates/` build, where all four exist
48
+ // side by side; a real customer scaffold never has that problem,
49
+ // since it only ever ships and lints ONE of these four at a time).
50
+ parserOptions: {
51
+ tsconfigRootDir: import.meta.dirname,
52
+ },
53
+ },
54
+ rules: {
55
+ // Downgraded rather than off, so both stay visible without failing a
56
+ // fresh scaffold's lint:
57
+ // - unused-vars: most of the source follows the underscore-prefix
58
+ // convention for intentionally-unused bindings, which this rule's
59
+ // default options don't recognize; a handful of test fixtures also
60
+ // carry genuinely-unused locals this minimal config doesn't try to
61
+ // triage file-by-file.
62
+ // - require-imports: CommonJS require() is the correct form in Jest
63
+ // config/mock files and in a few test-only dynamic re-require
64
+ // patterns.
65
+ "@typescript-eslint/no-unused-vars": "warn",
66
+ "@typescript-eslint/no-require-imports": "warn",
67
+ },
68
+ },
69
+ );