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
@@ -4,6 +4,7 @@
4
4
  // Chrome extract — Nav + Footer from @working-theory/ui (#1451)
5
5
  // Shell/Chrome composition — Nav + Footer wrap via @working-theory/templates MarketingShell (#2820)
6
6
  // Session-aware Nav actions — Sign in / Account + Sign out from the server-read session (#4984)
7
+ // Auth route group — the marketing chrome is dropped on the five (auth) paths (#5963)
7
8
  // Site-declared theme — `_site.mdx` `theme:` block → token overlay → CSS vars
8
9
  //
9
10
  // Sits under app/[locale]/ and sets the <html lang> attribute to the
@@ -25,8 +26,10 @@ import "../globals.css";
25
26
  import { Telemetry, VerificationMeta } from "@working-theory/analytics";
26
27
  import { MarketingShell } from "@working-theory/templates/shells";
27
28
  import { themeToCssVarsBothModes } from "@working-theory/theme";
28
- import { isRtl } from "@working-theory/ui";
29
+ import { Link, isRtl, localeHref } from "@working-theory/ui";
30
+ import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
29
31
  import {
32
+ Banner,
30
33
  Footer,
31
34
  Nav,
32
35
  type NavAction,
@@ -46,9 +49,11 @@ import type { ReactElement, ReactNode } from "react";
46
49
  import { auth } from "../../auth";
47
50
  import { GoogleAnalytics } from "../../components/google-analytics";
48
51
  import { SignOutTrigger } from "../../components/sign-out-trigger";
52
+ import { isAuthPath } from "../../lib/auth-routes";
49
53
  import { navItems } from "../../lib/nav-config";
50
54
  import { getOgImageUrl } from "../../lib/og";
51
55
  import { SITE_NAME, siteLogo } from "../../lib/site-brand";
56
+ import { siteChrome } from "../../lib/site-chrome";
52
57
  import { loadSiteBrandTokens } from "../../lib/site-theme";
53
58
  import { resolveBaseUrl } from "../../src/lib/routes";
54
59
 
@@ -57,6 +62,12 @@ import { resolveBaseUrl } from "../../src/lib/routes";
57
62
  const DEFAULT_LOCALE = "en";
58
63
  const THEME_COOKIE_NAME =
59
64
  process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme";
65
+ /**
66
+ * The cookie the consent island persists the visitor's choice under (#5557).
67
+ * Passed to Banner explicitly so the server-side read below and the island's
68
+ * client-side write name the same cookie by construction.
69
+ */
70
+ const CONSENT_COOKIE_NAME = "ailk-consent";
60
71
 
61
72
  type ThemeMode = "light" | "dark";
62
73
 
@@ -73,7 +84,9 @@ type ThemeMode = "light" | "dark";
73
84
  * has to satisfy the type: one minimal fields-step and a dead `action`.
74
85
  */
75
86
  const NAV_CLIENT_ACTION_FLOW: NavActionFlow["flow"] = {
76
- steps: [{ id: "noop", fields: [{ name: "noop", label: "noop", type: "text" }] }],
87
+ steps: [
88
+ { id: "noop", fields: [{ name: "noop", label: "noop", type: "text" }] },
89
+ ],
77
90
  action: "#",
78
91
  };
79
92
 
@@ -100,6 +113,26 @@ async function resolveThemeMode(): Promise<ThemeMode> {
100
113
  }
101
114
  }
102
115
 
116
+ /**
117
+ * Whether this visitor has already accepted or declined cookies (#5557).
118
+ *
119
+ * Read on the server, not left to the island alone: `BannerConsentIsland`
120
+ * initialises its `consented` state from `document.cookie`, which makes the
121
+ * CLIENT render null while the SERVER (no `document`) has already streamed
122
+ * the banner — React then leaves that server markup in place, un-hydrated, so
123
+ * a returning visitor sees a dead banner whose buttons do nothing. Deciding
124
+ * here means a decided visitor gets no banner markup at all, and a fresh one
125
+ * gets the live island that hides itself on Accept / Reject.
126
+ */
127
+ async function hasConsentChoice(): Promise<boolean> {
128
+ try {
129
+ const cookieStore = await cookies();
130
+ return cookieStore.get(CONSENT_COOKIE_NAME)?.value !== undefined;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
103
136
  /**
104
137
  * Blocking inline script that runs synchronously before any styled element.
105
138
  *
@@ -177,6 +210,7 @@ export default async function LocaleLayout({
177
210
  const { locale } = await params;
178
211
  const messages = await getMessages();
179
212
  const themeMode = await resolveThemeMode();
213
+ const consentDecided = await hasConsentChoice();
180
214
  // Server-side session read (#4984) — the same `auth.api.getSession` idiom
181
215
  // the `(authed)` segment guard uses. Null when signed out or auth is unset.
182
216
  const session = await auth.api.getSession({ headers: await headers() });
@@ -187,12 +221,26 @@ export default async function LocaleLayout({
187
221
  const navLabels = {
188
222
  items: {
189
223
  home: tNav("home"),
224
+ // The Free marketing-site chrome (#5469 D5): the Product flyout's three
225
+ // entries + descriptions, the top-level items, and the Get started CTA.
226
+ product: tNav("product"),
227
+ howItWorks: tNav("howItWorks"),
228
+ howItWorksDesc: tNav("howItWorksDesc"),
229
+ features: tNav("features"),
230
+ featuresDesc: tNav("featuresDesc"),
231
+ faq: tNav("faq"),
232
+ faqDesc: tNav("faqDesc"),
233
+ pricing: tNav("pricing"),
234
+ about: tNav("about"),
235
+ getStarted: tNav("getStarted"),
190
236
  docs: tNav("docs"),
191
237
  blog: tNav("blog"),
192
- // The scheduling module's booking page (#4633). Present in the map
193
- // unconditionally, like `docs` and `blog`: the map is a label lookup, and
194
- // an entry for a surface this site did not scaffold is never reached —
195
- // `nav-config.ts` only lists surfaces the declared architecture carries.
238
+ // The scheduling module's booking page, /meet (#4633; renamed from the
239
+ // schedule/ route by #5563 — the label key itself keeps its name, D1
240
+ // R2: it names copy, not a route). Present in the map unconditionally,
241
+ // like `docs` and `blog`: the map is a label lookup, and an entry for
242
+ // a surface this site did not scaffold is never reached — `nav-config.ts`
243
+ // only lists surfaces the declared architecture carries.
196
244
  schedule: tNav("schedule"),
197
245
  // Session-aware action labels (#4984). `account` carries the signed-in
198
246
  // user's display name (name, else email) so the Nav shows WHO is signed
@@ -207,8 +255,13 @@ export default async function LocaleLayout({
207
255
  closeMenu: tNav("closeMenu"),
208
256
  };
209
257
  const navOverflowLabel = tNav("moreLinks");
258
+ // Cookie-consent copy (#5557). The `consent` namespace ships in every
259
+ // scaffold's messages file; the Banner below is UI-only — OSS ships no
260
+ // enforcement, the callbacks are the hook for @working-theory/consent.
261
+ const tConsent = await getTranslations({ locale, namespace: "consent" });
210
262
 
211
- // Right-side action cluster (#4984). Signed out → a single outline-strong
263
+ // Right-side action cluster (#4984). Signed out → the Free template's pair
264
+ // (#5469 D5): a primary "Get started" → /contact beside the outline-strong
212
265
  // "Sign in" link (the operator's auth-pair idiom, #4370, MINUS Sign up —
213
266
  // this site offers no sign-up flow). Signed in → the user's name (a
214
267
  // non-navigating label) + an outline-strong "Sign out"; both authored as
@@ -224,7 +277,10 @@ export default async function LocaleLayout({
224
277
  variant: "outline-strong",
225
278
  },
226
279
  ]
227
- : [{ labelKey: "signIn", path: "/sign-in", variant: "outline-strong" }];
280
+ : [
281
+ { labelKey: "getStarted", path: "/contact", variant: "primary" },
282
+ { labelKey: "signIn", path: "/sign-in", variant: "outline-strong" },
283
+ ];
228
284
 
229
285
  // Only compose the flow seam when there is a flow action to compose. This
230
286
  // layout is a Server Component and `renderFlowTrigger` is a function, so
@@ -237,8 +293,79 @@ export default async function LocaleLayout({
237
293
  // this is the caller half of that contract.
238
294
  const hasFlowAction = navActions.some((action) => action.kind === "flow");
239
295
 
240
- // Shared brand mark injected into both Nav and Footer chrome slots.
296
+ // Shared brand mark injected into both Nav and Footer chrome slots. The
297
+ // mark comes from site-brand.tsx (the light/dark pair the /sign-in card
298
+ // also renders); the Nav adds the shipped wordmark beside it so BrandLogo
299
+ // composes mark + wordmark on desktop and mark-only below `md` (#5469 D5).
241
300
  const brandLogo = siteLogo();
301
+ const brandWordmark = {
302
+ src: "/wordmark.svg",
303
+ alt: SITE_NAME,
304
+ width: 140,
305
+ height: 16,
306
+ };
307
+
308
+ // The locales the footer's language switcher offers — the same list the
309
+ // middleware and hreflang alternates read (NEXT_PUBLIC_ENABLED_LOCALES).
310
+ const enabledLocales = readLocales();
311
+
312
+ // The announcement bar is HOME-ONLY (#5469 D5): its route gate matches the
313
+ // pathname literally, and under next-intl's `as-needed` prefixing the home
314
+ // path is `/` for the default locale and `/<locale>` for every other one.
315
+ const homePath = locale === DEFAULT_LOCALE ? "/" : `/${locale}`;
316
+
317
+ // Deck home (#5612 D3): `siteChrome.home === "deck"` only ever applies
318
+ // `snap="mandatory"` + `deckHero` on the locale-root route — every other
319
+ // path renders byte-identically regardless of the knob. `x-pathname` is the
320
+ // same header `generateMetadata` above reads (forwarded by middleware.ts);
321
+ // Next.js's `headers()` is request-scoped and cheap to call again here.
322
+ const pathname = (await headers()).get("x-pathname") ?? homePath;
323
+ const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
324
+
325
+ // The (auth) route group's chrome branch (#5963).
326
+ //
327
+ // A route group's layout nests INSIDE its parent segment's layout, so
328
+ // `(auth)/layout.tsx` renders BETWEEN the Nav and the Footer below — a group
329
+ // changes which siblings share a layout, it does not let a child escape an
330
+ // ancestor. The only place the marketing frame can be dropped for those five
331
+ // paths is here, where it is composed.
332
+ //
333
+ // Fail-safe direction: `isAuthPath()` answers false for an absent
334
+ // `x-pathname`, so a missing header can never strip a marketing page's
335
+ // chrome; the worst case is an auth page rendered WITH chrome, which the
336
+ // group's own suite catches. The marketing tree is NOT restructured into a
337
+ // `(marketing)` group — a ~35-directory move for a five-page delta (spec D1).
338
+ const isAuthRoute = isAuthPath(pathname, enabledLocales);
339
+
340
+ // Placeholder social handles + copyright (#5469 D5): `{{COMPANY_HANDLE}}` /
341
+ // `{{COMPANY_NAME}}` are create-ailk markers substituted at scaffold time
342
+ // (packages/create-ailk/src/programmatic.ts), so a customer's site never
343
+ // carries the kit vendor's own identity here. The year is computed, never
344
+ // typed, so the placeholder does not go stale.
345
+ const socialLinks = [
346
+ { platform: "facebook", href: "https://facebook.com/{{COMPANY_HANDLE}}" },
347
+ { platform: "instagram", href: "https://instagram.com/{{COMPANY_HANDLE}}" },
348
+ { platform: "x", href: "https://x.com/{{COMPANY_HANDLE}}" },
349
+ {
350
+ platform: "linkedin",
351
+ href: "https://linkedin.com/company/{{COMPANY_HANDLE}}",
352
+ },
353
+ { platform: "youtube", href: "https://youtube.com/@{{COMPANY_HANDLE}}" },
354
+ ] as const;
355
+ const copyrightText = `© ${new Date().getFullYear()} {{COMPANY_NAME}}`;
356
+
357
+ // Footer legal links (#5612 D3). The centered variant folds in FAQ — the
358
+ // roster's third always-scaffolded core route (`/faq`, alongside `/privacy`
359
+ // and `/terms`) — since a mark-only nav (its usual pairing, #5589) has
360
+ // nowhere else on the page linking it. The default `columns` footer keeps
361
+ // exactly today's two-entry list, byte-identical.
362
+ const legalLinks = [
363
+ { label: "Privacy Policy", href: "/privacy" },
364
+ { label: "Terms of Service", href: "/terms" },
365
+ ...(siteChrome.footer === "centered"
366
+ ? [{ label: navLabels.items.faq, href: "/faq" }]
367
+ : []),
368
+ ];
242
369
 
243
370
  // The site's own declared colours, read from `content/_site.mdx`'s `theme:`
244
371
  // block and translated from that customer-facing vocabulary onto a token
@@ -257,7 +384,11 @@ export default async function LocaleLayout({
257
384
  );
258
385
 
259
386
  return (
260
- <html lang={locale} dir={isRtl(locale) ? "rtl" : "ltr"} data-theme={themeMode}>
387
+ <html
388
+ lang={locale}
389
+ dir={isRtl(locale) ? "rtl" : "ltr"}
390
+ data-theme={themeMode}
391
+ >
261
392
  <head>
262
393
  {/* Flash-prevention: runs synchronously before any styled element */}
263
394
  <script
@@ -271,10 +402,24 @@ export default async function LocaleLayout({
271
402
  />
272
403
  {/* Favicons — single brand mark (theme-independent), PNG fallbacks, ICO legacy */}
273
404
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
274
- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
275
- <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
405
+ <link
406
+ rel="icon"
407
+ type="image/png"
408
+ sizes="32x32"
409
+ href="/favicon-32x32.png"
410
+ />
411
+ <link
412
+ rel="icon"
413
+ type="image/png"
414
+ sizes="16x16"
415
+ href="/favicon-16x16.png"
416
+ />
276
417
  <link rel="shortcut icon" href="/favicon.ico" />
277
- <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
418
+ <link
419
+ rel="apple-touch-icon"
420
+ sizes="180x180"
421
+ href="/apple-touch-icon.png"
422
+ />
278
423
  <link rel="manifest" href="/site.webmanifest" />
279
424
  <GoogleAnalytics />
280
425
  <VerificationMeta />
@@ -292,58 +437,156 @@ export default async function LocaleLayout({
292
437
  * emitted unconditionally in OSS forks; paying customers install
293
438
  * @working-theory/branding and gate it in their own layout.
294
439
  */}
295
- <MarketingShell
296
- contentOwnsMain
297
- nav={
298
- <Nav
299
- locale={locale}
300
- labels={navLabels}
301
- navItems={navItems}
302
- siteName={SITE_NAME}
303
- logo={brandLogo}
304
- overflowLabel={navOverflowLabel}
305
- actions={navActions}
306
- {...(hasFlowAction
307
- ? {
308
- renderFlowTrigger: (action, trigger) =>
309
- action.labelKey === "signOut" ? (
310
- <SignOutTrigger>{trigger}</SignOutTrigger>
311
- ) : (
312
- <span
313
- className="text-body-sm text-(--color-text-muted)"
314
- data-testid="nav-account-name"
315
- >
316
- {navLabels.items.account}
317
- </span>
318
- ),
319
- }
320
- : {})}
321
- />
322
- }
323
- footer={
324
- <Footer
325
- locale={locale}
326
- itemLabels={navLabels.items}
327
- navItems={navItems}
328
- siteName={SITE_NAME}
329
- logo={brandLogo}
330
- // #4157 — deliberately left as a literal "Built with AI Launch
331
- // Kit" / https://ailaunchkit.ai, not SITE_NAME: this credits
332
- // the OSS TEMPLATE this site is built with (a "powered by"
333
- // link, unconditional in OSS forks per the comment above),
334
- // never the customer's own identity, so it isn't the vendor-
335
- // brand-as-default defect #4157 fixes. See
336
- // docs/specs/4157-scaffold-brand-defaults-spec.md § Design
337
- // Decisions for the fuller reasoning.
338
- attribution={{
339
- label: "Built with AI Launch Kit",
340
- href: "https://ailaunchkit.ai",
341
- }}
342
- />
343
- }
344
- >
345
- {children}
346
- </MarketingShell>
440
+ {isAuthRoute ? (
441
+ /*
442
+ * The (auth) branch (#5963): the five auth paths render their own
443
+ * `(auth)/layout.tsx` — AuthShell — with no Nav and no marketing
444
+ * Footer above or below it. Everything outside this ternary (the
445
+ * document, the flash-prevention script, the theme vars, the
446
+ * favicons, NextIntlClientProvider, the consent Banner and
447
+ * Telemetry) is shared by both branches and written once.
448
+ */
449
+ children
450
+ ) : (
451
+ <MarketingShell
452
+ contentOwnsMain
453
+ gutters="plain"
454
+ bannerPlacement="above-nav"
455
+ {...(siteChrome.announcementBanner
456
+ ? {
457
+ banner: (
458
+ <Banner
459
+ variant="contrast"
460
+ placement="above-nav"
461
+ message="Medium length banner heading with link goes here"
462
+ action={{
463
+ label: "Link",
464
+ href: "/pricing",
465
+ kind: "link",
466
+ }}
467
+ routes={{ include: [homePath] }}
468
+ dismissible={false}
469
+ locale={locale}
470
+ />
471
+ ),
472
+ }
473
+ : {})}
474
+ {...(isDeckHome
475
+ ? { snap: "mandatory" as const, deckHero: true }
476
+ : {})}
477
+ nav={
478
+ <Nav
479
+ locale={locale}
480
+ labels={navLabels}
481
+ navItems={siteChrome.nav === "mark-only" ? [] : navItems}
482
+ // Deck home (#5697): the two forwards that join #5426 D4 to a
483
+ // real deck. `sticky` pins the bar — MarketingShell renders
484
+ // {nav} as a plain child of the scroll container, so under
485
+ // `scroll-snap-type: y mandatory` the browser snaps slide 1 to
486
+ // the top on load and the unpinned bar is gone before the
487
+ // reader touches anything. `deckMorph` is what puts BrandLogo's
488
+ // `.ailk-deck-brand-mark` / `.ailk-deck-brand-wordmark` hooks in
489
+ // the document for the D4 CSS to key off; it requires both
490
+ // `logo` and `wordmark` (Nav.tsx:344), which the layout already
491
+ // passes above. Scoped to the SAME `isDeckHome` gate
492
+ // MarketingShell's snap/deckHero use, so a non-deck route's
493
+ // render is byte-identical to before this branch existed.
494
+ {...(isDeckHome ? { sticky: true, deckMorph: true } : {})}
495
+ siteName={SITE_NAME}
496
+ showThemeToggle={false}
497
+ logo={brandLogo}
498
+ wordmark={brandWordmark}
499
+ overflowLabel={navOverflowLabel}
500
+ actions={navActions}
501
+ {...(hasFlowAction
502
+ ? {
503
+ renderFlowTrigger: (action, trigger) =>
504
+ action.labelKey === "signOut" ? (
505
+ <SignOutTrigger>{trigger}</SignOutTrigger>
506
+ ) : (
507
+ <span
508
+ className="text-body-sm text-(--color-text-muted)"
509
+ data-testid="nav-account-name"
510
+ >
511
+ {navLabels.items.account}
512
+ </span>
513
+ ),
514
+ }
515
+ : {})}
516
+ />
517
+ }
518
+ footer={
519
+ <Footer
520
+ locale={locale}
521
+ itemLabels={navLabels.items}
522
+ navItems={navItems}
523
+ siteName={SITE_NAME}
524
+ logo={brandLogo}
525
+ social={[...socialLinks]}
526
+ {...(siteChrome.footer === "centered"
527
+ ? { layout: "centered" as const }
528
+ : {})}
529
+ showThemeToggle
530
+ copyrightText={copyrightText}
531
+ legalLinks={legalLinks}
532
+ languageSwitcher={
533
+ <LocaleSwitcherNav
534
+ current={locale}
535
+ locales={enabledLocales}
536
+ pathname={pathname}
537
+ />
538
+ }
539
+ // #4157 — deliberately left as a literal "Built by Working
540
+ // Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
541
+ // the OSS TEMPLATE this site is built with (a "powered by"
542
+ // link, unconditional in OSS forks per the comment above),
543
+ // never the customer's own identity, so it isn't the vendor-
544
+ // brand-as-default defect #4157 fixes. See
545
+ // docs/specs/4157-scaffold-brand-defaults-spec.md § Design
546
+ // Decisions for the fuller reasoning.
547
+ attribution={{
548
+ label: "Built by Working Theory Labs",
549
+ href: "https://ailaunchkit.ai",
550
+ }}
551
+ />
552
+ }
553
+ >
554
+ {children}
555
+ </MarketingShell>
556
+ )}
557
+ {/*
558
+ * Cookie consent (#5557). Bottom-pinned `info` Banner in consent
559
+ * mode: `secondaryAction` makes Banner compose BannerConsentIsland,
560
+ * which reads/writes the `ailk-consent` cookie itself — a fresh
561
+ * visitor sees it, an accepted/declined one does not, on every
562
+ * page. Out of the shell's flow (placement `bottom` is a fixed
563
+ * overlay), so accepting causes no layout shift.
564
+ */}
565
+ {consentDecided ? null : (
566
+ <Banner
567
+ variant="info"
568
+ placement="bottom"
569
+ consentCookieName={CONSENT_COOKIE_NAME}
570
+ title={tConsent("title")}
571
+ message={
572
+ <>
573
+ {tConsent("body")}{" "}
574
+ <Link
575
+ href={localeHref(locale, "/privacy")}
576
+ variant="unstyled"
577
+ className="font-semibold text-current underline underline-offset-4"
578
+ >
579
+ {tConsent("policyLinkLabel")}
580
+ </Link>
581
+ </>
582
+ }
583
+ dismissible={false}
584
+ acceptLabel={tConsent("acceptLabel")}
585
+ secondaryAction={{ label: tConsent("rejectLabel") }}
586
+ rejectLabel={tConsent("rejectLabel")}
587
+ locale={locale}
588
+ />
589
+ )}
347
590
  <Telemetry />
348
591
  </NextIntlClientProvider>
349
592
  </body>
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tests for /[locale]/purchase/cancel (#6084).
3
+ *
4
+ * D4 — reads nothing. AC-2 — states nothing was charged, links to
5
+ * `/services`, noindex, every string from the `purchase` namespace.
6
+ */
7
+ import React from "react";
8
+ import { render } from "@testing-library/react";
9
+
10
+ jest.mock("next/navigation", () => ({
11
+ __esModule: true,
12
+ notFound: jest.fn(() => {
13
+ throw new Error("NEXT_NOT_FOUND");
14
+ }),
15
+ }));
16
+
17
+ const requestedKeys: string[] = [];
18
+ jest.mock("next-intl/server", () => ({
19
+ __esModule: true,
20
+ getTranslations: jest.fn().mockResolvedValue((key: string) => {
21
+ requestedKeys.push(key);
22
+ return `copy:${key}`;
23
+ }),
24
+ }));
25
+
26
+ const capturedSections: unknown[] = [];
27
+ jest.mock("@working-theory/ui/sections", () => ({
28
+ __esModule: true,
29
+ SectionRenderer: ({ sections }: { sections: unknown[] }) => {
30
+ capturedSections.splice(0, capturedSections.length, ...sections);
31
+ return React.createElement("div", {
32
+ "data-testid": "section-renderer",
33
+ "data-section-count": String(sections.length),
34
+ });
35
+ },
36
+ }));
37
+
38
+ const mockGetSession = jest.fn();
39
+ jest.mock("@working-theory/api-client", () => ({
40
+ __esModule: true,
41
+ checkout: { getSession: (...args: unknown[]) => mockGetSession(...args) },
42
+ }));
43
+
44
+ // Mocks the REAL boundary `resolveCommerceClaim()` itself calls
45
+ // (`checkLicense` from `@working-theory/license`), not the internal wrapper
46
+ // module — matching the eslint "no internal-module mocks" rule (#507).
47
+ const mockCheckLicense = jest.fn();
48
+ jest.mock("@working-theory/license", () => ({
49
+ __esModule: true,
50
+ checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
51
+ }));
52
+
53
+ // `../../../../src/lib/commerce-sections` wraps a lazy, webpack-ignored
54
+ // `import()` of the paid `@working-theory/ui-commerce` package — mocked
55
+ // directly, mirroring `../__tests__/success.test.tsx`'s identical mock.
56
+ const mockEnsureCommerceSectionsRegistered = jest.fn();
57
+ // eslint-disable-next-line no-restricted-syntax -- see the identical exception in success.test.tsx
58
+ jest.mock("../../../../src/lib/commerce-sections", () => ({
59
+ __esModule: true,
60
+ ensureCommerceSectionsRegistered: () =>
61
+ mockEnsureCommerceSectionsRegistered(),
62
+ }));
63
+
64
+ import { notFound } from "next/navigation";
65
+
66
+ import Page, { generateMetadata } from "../cancel/page";
67
+
68
+ const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
69
+
70
+ function sectionTypes(): string[] {
71
+ return capturedSections.map((s) => (s as { type: string }).type);
72
+ }
73
+
74
+ async function renderPage() {
75
+ const element = await Page({ params: Promise.resolve({ locale: "en" }) });
76
+ return render(element);
77
+ }
78
+
79
+ describe("/purchase/cancel", () => {
80
+ beforeEach(() => {
81
+ capturedSections.length = 0;
82
+ requestedKeys.length = 0;
83
+ mockGetSession.mockReset();
84
+ mockCheckLicense.mockReset();
85
+ mockCheckLicense.mockResolvedValue({ revoked: false, features: ["commerce"] });
86
+ mockNotFound.mockClear();
87
+ mockEnsureCommerceSectionsRegistered.mockReset();
88
+ mockEnsureCommerceSectionsRegistered.mockResolvedValue(true);
89
+ });
90
+
91
+ it("renders payment-receipt(outcome:cancel) → cta, with a primary link to /services (AC-2, D5)", async () => {
92
+ await renderPage();
93
+
94
+ expect(sectionTypes()).toEqual(["payment-receipt", "cta"]);
95
+ const receipt = capturedSections[0] as {
96
+ data: Record<string, unknown>;
97
+ };
98
+ expect(receipt.data.outcome).toBe("cancel");
99
+ const cta = capturedSections[1] as { data: Record<string, unknown> };
100
+ expect((cta.data.primaryCta as { href: string }).href).toBe("/services");
101
+ });
102
+
103
+ it("falls back to content-block instead of payment-receipt when @working-theory/ui-commerce is not installed", async () => {
104
+ mockEnsureCommerceSectionsRegistered.mockResolvedValue(false);
105
+
106
+ await renderPage();
107
+
108
+ expect(sectionTypes()).toEqual(["content-block", "cta"]);
109
+ const fallback = capturedSections[0] as {
110
+ data: { headline: string; body: string };
111
+ };
112
+ expect(fallback.data.headline).toBe("copy:cancel.title");
113
+ expect(fallback.data.body).toContain("copy:cancel.message");
114
+ });
115
+
116
+ it("reads NOTHING — no API call at all (D4)", async () => {
117
+ await renderPage();
118
+ expect(mockGetSession).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("takes every string from the purchase namespace (D4/AC-2)", async () => {
122
+ await renderPage();
123
+ expect(requestedKeys.length).toBeGreaterThan(0);
124
+ expect(requestedKeys.every((k) => k.startsWith("cancel."))).toBe(true);
125
+ });
126
+
127
+ it("is not indexable", async () => {
128
+ const metadata = await generateMetadata({
129
+ params: Promise.resolve({ locale: "en" }),
130
+ });
131
+ expect(metadata.robots).toEqual({ index: false, follow: true });
132
+ });
133
+
134
+ it("carries no data-outcome=success and no PaymentComplete string — a cancel is not a payment claim", async () => {
135
+ await renderPage();
136
+ const serialized = JSON.stringify(capturedSections);
137
+ expect(serialized).not.toContain('"outcome":"success"');
138
+ expect(serialized).not.toContain("PaymentComplete");
139
+ });
140
+
141
+ // ── T-16 / D12 — the request-time commerce-claim gate ───────────────────
142
+
143
+ it("answers notFound() when the commerce claim is NOT held (D12)", async () => {
144
+ mockCheckLicense.mockResolvedValue({ revoked: false, features: [] });
145
+
146
+ await expect(renderPage()).rejects.toThrow("NEXT_NOT_FOUND");
147
+ expect(mockNotFound).toHaveBeenCalledTimes(1);
148
+ });
149
+
150
+ it("fail-closed: answers notFound() when the licence read THROWS", async () => {
151
+ mockCheckLicense.mockRejectedValue(new Error("licence read blew up"));
152
+
153
+ await expect(renderPage()).rejects.toThrow();
154
+ });
155
+
156
+ it("renders normally when the commerce claim IS held", async () => {
157
+ mockCheckLicense.mockResolvedValue({ revoked: false, features: ["commerce"] });
158
+ await renderPage();
159
+ expect(mockNotFound).not.toHaveBeenCalled();
160
+ expect(sectionTypes()).toEqual(["payment-receipt", "cta"]);
161
+ });
162
+ });