create-ailk 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/cli.js +54 -5
  5. package/dist/copy.d.ts +14 -0
  6. package/dist/copy.js +38 -1
  7. package/dist/lib/api-boot-assertion.d.ts +46 -0
  8. package/dist/lib/api-boot-assertion.js +70 -0
  9. package/dist/lib/extract-module-bundle.d.ts +20 -1
  10. package/dist/lib/extract-module-bundle.js +64 -5
  11. package/dist/lib/import-graph.d.ts +29 -0
  12. package/dist/lib/import-graph.js +140 -0
  13. package/dist/lib/module-license-gate.d.ts +6 -1
  14. package/dist/lib/module-license-gate.js +6 -1
  15. package/dist/lib/pro-callout.js +1 -1
  16. package/dist/module-architecture.d.ts +1 -0
  17. package/dist/module-architecture.js +253 -127
  18. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  19. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  20. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  21. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  22. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  23. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  24. package/dist/programmatic.js +14 -10
  25. package/dist/surfaces.js +105 -7
  26. package/dist/template-manifest.d.ts +33 -0
  27. package/dist/template-manifest.js +99 -16
  28. package/dist/template-names.d.ts +1 -1
  29. package/dist/template-names.js +1 -0
  30. package/package.json +3 -2
  31. package/site-templates/marketing-site/DESIGN.md +148 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  34. package/site-templates/marketing-site/ailk-template.json +1 -1
  35. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  38. package/site-templates/minimalism-site/DESIGN.md +148 -0
  39. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  41. package/site-templates/minimalism-site/ailk-template.json +2 -1
  42. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  43. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  44. package/site-templates/saas-product/DESIGN.md +148 -0
  45. package/site-templates/simple-service/DESIGN.md +148 -0
  46. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  47. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  48. package/site-templates/simple-service/ailk-template.json +10 -0
  49. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  50. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  51. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  52. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  53. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  54. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  55. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  56. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  57. package/site-templates/simple-service/provenance.json +4 -0
  58. package/templates/.env.example +72 -7
  59. package/templates/CLAUDE.md +2 -3
  60. package/templates/DESIGN.md +155 -0
  61. package/templates/README.md +42 -8
  62. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  63. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  64. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  65. package/templates/apps/api/.env.example +13 -0
  66. package/templates/apps/api/CLAUDE.md +26 -2
  67. package/templates/apps/api/jest.config.cjs +3 -0
  68. package/templates/apps/api/package.json +1 -0
  69. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  70. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  71. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  72. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  73. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  74. package/templates/apps/api/src/config/index.ts +62 -0
  75. package/templates/apps/api/src/config/modules.ts +7 -3
  76. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  77. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  78. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  79. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  81. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  82. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  83. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  84. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  85. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  86. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  87. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  88. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  89. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  90. package/templates/apps/api/src/lib/site.ts +208 -9
  91. package/templates/apps/api/src/lib/stripe.ts +66 -0
  92. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  93. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  94. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  95. package/templates/apps/api/src/openapi/spec.ts +543 -2
  96. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  97. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  98. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  99. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  100. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  101. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  102. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  103. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  104. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  105. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  106. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  107. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  108. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  109. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  110. package/templates/apps/api/src/routes/content/index.ts +13 -73
  111. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  112. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  113. package/templates/apps/api/src/routes/content/search.ts +129 -0
  114. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  115. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  116. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  120. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  121. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  122. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  123. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  124. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  125. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  126. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  127. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  128. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  129. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  130. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  131. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  132. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  133. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  134. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  135. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  136. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  137. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  138. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  139. package/templates/apps/api/src/server.ts +439 -49
  140. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  141. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  142. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  143. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  144. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  145. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  146. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  147. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  148. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  149. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  150. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  151. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  152. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  153. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  154. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  155. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  156. package/templates/apps/api/src/services/audit-log.ts +668 -0
  157. package/templates/apps/api/src/services/consent.ts +18 -0
  158. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  159. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  160. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  161. package/templates/apps/api/src/services/entitlements.ts +80 -0
  162. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  163. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  164. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  165. package/templates/apps/api/src/services/listing-config.ts +27 -9
  166. package/templates/apps/api/src/services/project-listings.ts +201 -6
  167. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  168. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  169. package/templates/apps/api/src/services/revalidation.ts +173 -0
  170. package/templates/apps/api/src/services/verified-email.ts +71 -0
  171. package/templates/apps/api/src/vercel-handler.ts +75 -35
  172. package/templates/apps/mcp/.env.example +17 -0
  173. package/templates/apps/mcp/CLAUDE.md +57 -24
  174. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  175. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  176. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  177. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  178. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  179. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  180. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  181. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  182. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  183. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  184. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  185. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  186. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  187. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  188. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  189. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  190. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  191. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  192. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  193. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  194. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  195. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  196. package/templates/apps/mcp/jest.config.cjs +13 -0
  197. package/templates/apps/mcp/package.json +2 -0
  198. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  199. package/templates/apps/mcp/src/config/modules.ts +10 -5
  200. package/templates/apps/mcp/src/http-transport.ts +102 -29
  201. package/templates/apps/mcp/src/index.ts +73 -11
  202. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  203. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  204. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  205. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  206. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  207. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  208. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  209. package/templates/apps/mcp/src/server.ts +59 -35
  210. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  211. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  212. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  213. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  214. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  215. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  216. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  217. package/templates/apps/mcp/src/tools/index.ts +6 -4
  218. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  219. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  220. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  221. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  222. package/templates/apps/web/.env.example +71 -5
  223. package/templates/apps/web/CLAUDE.md +44 -0
  224. package/templates/apps/web/PRODUCT.md +86 -0
  225. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  226. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  227. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  228. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  229. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  230. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  231. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  232. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  233. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  234. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  235. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  236. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  237. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  238. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  239. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  240. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  241. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  242. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  243. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  244. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  245. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  246. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  247. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  248. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  249. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  250. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  251. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  252. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  253. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  254. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  255. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  256. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  257. package/templates/apps/web/auth.ts +407 -8
  258. package/templates/apps/web/jest.config.cjs +8 -4
  259. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  260. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  261. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  262. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  263. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  264. package/templates/apps/web/lib/auth-email.ts +318 -0
  265. package/templates/apps/web/lib/auth-env.ts +85 -0
  266. package/templates/apps/web/lib/auth-routes.ts +55 -0
  267. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  268. package/templates/apps/web/lib/site-brand.tsx +4 -8
  269. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  270. package/templates/apps/web/messages/en.json +187 -0
  271. package/templates/apps/web/middleware.ts +1 -1
  272. package/templates/apps/web/package.json +2 -1
  273. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  274. package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
  275. package/templates/content/_site.mdx +18 -3
  276. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  277. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  278. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  279. package/templates/database/CHANGELOG.md +149 -0
  280. package/templates/database/README.md +10 -3
  281. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  282. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  283. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  284. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  285. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  286. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  287. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  288. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  289. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  290. package/templates/database/__tests__/tenancy.test.ts +141 -0
  291. package/templates/database/config/schema.prisma +79 -2
  292. package/templates/database/content/schema.prisma +87 -11
  293. package/templates/database/inbox/schema.prisma +452 -9
  294. package/templates/database/jest.config.cjs +5 -3
  295. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  296. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  297. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  298. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  299. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  300. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  301. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  302. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  303. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  304. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  305. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  306. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  307. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  308. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  309. package/templates/database/package.json +3 -2
  310. package/templates/database/tenancy.ts +86 -1
  311. package/templates/database/tsconfig.json +1 -0
  312. package/versions.json +34 -34
  313. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  314. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  315. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  316. package/templates/apps/api/src/routes/content/create.ts +0 -115
  317. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  318. package/templates/apps/api/src/routes/content/update.ts +0 -112
  319. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  320. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  321. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  322. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  323. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  324. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  325. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  326. package/templates/apps/web/public/logo-dark.svg +0 -21
  327. package/templates/apps/web/public/logo-light.svg +0 -21
  328. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  329. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,58 @@
1
+ "use server";
2
+
3
+ /**
4
+ * account/actions.ts — the update-your-card banner's Server Action (#6038,
5
+ * D5): mints a Stripe billing-portal session on SUBMIT, never on render, and
6
+ * hands the browser straight to it via `redirect()`.
7
+ *
8
+ * The portal URL is never embedded in page data — it is created here, on
9
+ * click, and never rendered into HTML or a client bundle (D5's precise
10
+ * claim: `redirect()` DOES surface the target in the address bar/history/
11
+ * navigation telemetry for a Server Action, which is inherent to any hosted-
12
+ * portal handoff, not something this design avoids).
13
+ *
14
+ * CSRF is already handled: `forwardedCookieInit()` forwards the incoming
15
+ * request's real `Origin` alongside the cookie, and `apps/api`'s
16
+ * `authPreHandler` requires a matching `Origin` on this cookie-authenticated
17
+ * mutating request (D3.1/D5).
18
+ *
19
+ * `redirect()` throws `NEXT_REDIRECT` by design — called OUTSIDE the
20
+ * try/catch (mirrors `checkout.actions.ts`'s idiom) so the catch never
21
+ * swallows the navigation this action exists to perform.
22
+ */
23
+
24
+ import { subscription } from "@working-theory/api-client";
25
+ import { redirect } from "next/navigation";
26
+
27
+ import { forwardedCookieInit } from "../../../../lib/server-api";
28
+
29
+ /** PII-safe error identity for logs (the `checkout.actions.ts` idiom: `err.name` only). */
30
+ function errorName(err: unknown): string {
31
+ return err instanceof Error ? err.name : "unknown";
32
+ }
33
+
34
+ /**
35
+ * Opens the caller's own Stripe billing-portal session and redirects there.
36
+ * Never returns on success. On a non-2xx or thrown error, logs a PII-safe
37
+ * status/name and redirects back to `/account` unchanged (the banner's
38
+ * action-unavailable posture — there is no error UI slot on this button).
39
+ */
40
+ export async function openBillingPortal(): Promise<void> {
41
+ const init = await forwardedCookieInit();
42
+
43
+ let url: string;
44
+ try {
45
+ const res = await subscription.portalSession(init);
46
+ if (!res.ok) {
47
+ console.error(`[openBillingPortal] status=${res.status}`);
48
+ return;
49
+ }
50
+ url = res.body.url;
51
+ } catch (err) {
52
+ console.error(`[openBillingPortal] errName=${errorName(err)}`);
53
+ return;
54
+ }
55
+
56
+ // Outside the try/catch on purpose — see the file header.
57
+ redirect(url);
58
+ }
@@ -1,16 +1,18 @@
1
1
  /**
2
- * /[locale]/account — the signed-in respondent's own profile, results, and
3
- * purchases (#4984).
2
+ * /[locale]/account — the signed-in respondent's own profile, results,
3
+ * purchases, and subscription status (#4984, #6038).
4
4
  *
5
5
  * RSC. The `(authed)` layout is the gate — it already resolved a session or
6
6
  * rendered `SignInGate`; this page reads the session again ONLY for the
7
7
  * profile fields (name / email / verified). Every data read goes through
8
8
  * `@working-theory/api-client` with the caller's cookie forwarded
9
9
  * (`../../../../lib/server-api`, D9): `waitlistSignups.me(key)` once per key
10
- * in `lib/account-config.ts` (Results) and `flowCheckouts.me()` (Purchases).
11
- * A 404 (no signup / no user row for that key) or a 401/403 (session missing,
12
- * email unverified — the profile row already says so) is skipped, never
13
- * thrown; every other error rethrows to the route error boundary.
10
+ * in `lib/account-config.ts` (Results), `flowCheckouts.me()` (Purchases),
11
+ * and `subscription.me()` (the update-your-card banner, #6038). A 404 (no
12
+ * signup / no user row for that key / commerce module off) or a 401/403
13
+ * (session missing, email unverified — the profile row already says so) is
14
+ * skipped, never thrown; every other error rethrows to the route error
15
+ * boundary.
14
16
  *
15
17
  * Chrome: this page composes `AppShell` + `SidebarNav` directly, with this
16
18
  * page's own three in-page anchors. The page renders inside the locale
@@ -20,16 +22,19 @@
20
22
 
21
23
  import {
22
24
  flowCheckouts,
25
+ subscription,
23
26
  waitlistSignups,
24
27
  type FlowCheckoutMeItem,
28
+ type SubscriptionMeResponse,
25
29
  type WaitlistSignupMeResponse,
26
30
  } from "@working-theory/api-client";
27
31
  import { AppShell } from "@working-theory/templates/shells";
28
32
  import { localeHref } from "@working-theory/ui";
29
- import { DescriptionList } from "@working-theory/ui/blocks";
33
+ import { DescriptionList, SubscriptionStatusBanner } from "@working-theory/ui/blocks";
30
34
  import { SidebarNav, type SidebarNavItem } from "@working-theory/ui/chrome";
31
35
  import {
32
36
  Badge,
37
+ Button,
33
38
  EmptyState,
34
39
  Heading,
35
40
  MoneyAmount,
@@ -53,6 +58,7 @@ import { forwardedCookieInit, unwrapApiResult } from "../../../../lib/server-api
53
58
  import { ForbiddenState } from "../waitlist/components/ForbiddenState";
54
59
  import { formatCount, formatDateTime } from "../waitlist/format";
55
60
 
61
+ import { openBillingPortal } from "./actions";
56
62
  import { SignOutButton } from "./sign-out-button";
57
63
 
58
64
  interface Props {
@@ -93,6 +99,19 @@ async function readCheckouts(init: CookieInit): Promise<FlowCheckoutMeItem[]> {
93
99
  return unwrapApiResult(Promise.resolve(res));
94
100
  }
95
101
 
102
+ /**
103
+ * The update-your-card banner's read (#6038). `null` covers both "nothing
104
+ * to show" (401/403/404 — the commerce-disabled-build 404 included, D8) and
105
+ * "no row" (a healthy subscriber, `actionRequired: false`) — the page's
106
+ * render condition below is `sub?.actionRequired`, so both collapse to no
107
+ * banner without a separate branch.
108
+ */
109
+ async function readSubscription(init: CookieInit): Promise<SubscriptionMeResponse | null> {
110
+ const res = await subscription.me(init);
111
+ if (!res.ok && SKIPPED_STATUSES.has(res.status)) return null; // AC-4, the existing constant
112
+ return unwrapApiResult(Promise.resolve(res));
113
+ }
114
+
96
115
  export default async function AccountPage({ params }: Props): Promise<ReactElement> {
97
116
  const { locale } = await params;
98
117
  const t = await getTranslations({ locale, namespace: "account" });
@@ -112,7 +131,11 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
112
131
  }
113
132
 
114
133
  const init = await forwardedCookieInit();
115
- const [signups, checkouts] = await Promise.all([readSignups(init), readCheckouts(init)]);
134
+ const [signups, checkouts, sub] = await Promise.all([
135
+ readSignups(init),
136
+ readCheckouts(init),
137
+ readSubscription(init),
138
+ ]);
116
139
 
117
140
  const { name, email, emailVerified } = session.user;
118
141
  const displayName = name || email;
@@ -145,6 +168,38 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
145
168
  </Text>
146
169
  </div>
147
170
 
171
+ {sub?.actionRequired ? (
172
+ <SubscriptionStatusBanner
173
+ headline={t("subscription.headline")}
174
+ description={
175
+ sub.status === "canceled" && sub.accessRevokesAt
176
+ ? t("subscription.descriptionCanceled", {
177
+ date: formatDateTime(sub.accessRevokesAt, locale),
178
+ })
179
+ : t("subscription.descriptionPastDue")
180
+ }
181
+ tone={sub.status === "canceled" ? "error" : "warning"}
182
+ action={
183
+ sub.portalAvailable ? (
184
+ <form action={openBillingPortal}>
185
+ {/* variant="unstyled" + text-current/border-current, not variant="secondary" —
186
+ a per-mode-fixed color fails contrast against the banner's warning/error
187
+ fill in dark mode (design-review F4); size="big" for the 44px touch-target
188
+ floor this is a mobile CTA under (design-review F5, Rule 14). */}
189
+ <Button
190
+ type="submit"
191
+ variant="unstyled"
192
+ size="big"
193
+ className="rounded-control border-thin border-current bg-transparent px-control-x text-current hover:opacity-90 active:opacity-80"
194
+ >
195
+ {t("subscription.action")}
196
+ </Button>
197
+ </form>
198
+ ) : undefined
199
+ }
200
+ />
201
+ ) : null}
202
+
148
203
  <section id="profile" className="flex flex-col gap-4" data-testid="account-profile">
149
204
  <Heading level={2}>{t("profile.heading")}</Heading>
150
205
  <DescriptionList
@@ -50,5 +50,22 @@ export default async function AuthedLayout({
50
50
  );
51
51
  }
52
52
 
53
- return <>{children}</>;
53
+ // data-profile="app" puts the Pro-host editor surfaces in this group on
54
+ // @working-theory/theme's app profile (#5953); tokens.css's own
55
+ // [data-profile="app"] header documents what it overrides. The div carries
56
+ // no className and no layout of its own — it exists only because this
57
+ // layout returns a fragment and the attribute needs an element.
58
+ //
59
+ // Two things about the placement are deliberate. Only the SIGNED-IN branch
60
+ // carries it: SignInGate above is the marketing site's sign-in front door,
61
+ // not an app surface. And the attribute goes on this nested div rather than
62
+ // the app's <html> because apps/web's marketing routes share that root and
63
+ // must stay on the marketing profile — which also makes this half immune to
64
+ // the `:root` shadowing the Admin host hit (different element, no
65
+ // specificity tie, so the app values apply and inherit down).
66
+ return (
67
+ <div data-profile="app" data-testid="authed-profile-root">
68
+ {children}
69
+ </div>
70
+ );
54
71
  }
@@ -0,0 +1,44 @@
1
+ // CancellationPolicyPage route (#5947). Reads the CancellationPolicyPage MDX frontmatter, renders sections via
2
+ // SectionRenderer, and emits a JSON-LD <script>.
3
+ import type {
4
+ getMdxSource as defaultGetMdxSource,
5
+ getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
6
+ import {
7
+ renderPage,
8
+ renderPageMetadata
9
+ } from "@working-theory/page-renderer";
10
+ import type { Metadata } from "next";
11
+ import type { ReactElement } from "react";
12
+
13
+ interface Props {
14
+ params: Promise<{ locale: string }>;
15
+ getMdxSource?: typeof defaultGetMdxSource;
16
+ getPageSections?: typeof defaultGetPageSections;
17
+ }
18
+
19
+ export async function generateMetadata({
20
+ params,
21
+ }: Pick<Props, "params">): Promise<Metadata> {
22
+ const { locale } = await params;
23
+ return renderPageMetadata({ type: "CancellationPolicyPage", slug: "cancellation", locale });
24
+ }
25
+
26
+ export default async function Page({
27
+ params,
28
+ getMdxSource,
29
+ getPageSections,
30
+ }: Props): Promise<ReactElement> {
31
+ const { locale } = await params;
32
+ return renderPage({
33
+ type: "CancellationPolicyPage",
34
+ slug: "cancellation",
35
+ locale,
36
+ // Without a namespace `renderPage` 404s on a missing seed — and a scaffold
37
+ // IS a missing-seed environment, since `build-templates.mjs` snapshots
38
+ // `apps/` but deliberately not `content/`. The namespace makes this route
39
+ // render the #4154 authoring empty state at 200, exactly as /terms does.
40
+ translationsNamespace: "cancellation",
41
+ ...(getMdxSource !== undefined && { getMdxSource }),
42
+ ...(getPageSections !== undefined && { getPageSections }),
43
+ });
44
+ }
@@ -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
@@ -48,6 +49,7 @@ import type { ReactElement, ReactNode } from "react";
48
49
  import { auth } from "../../auth";
49
50
  import { GoogleAnalytics } from "../../components/google-analytics";
50
51
  import { SignOutTrigger } from "../../components/sign-out-trigger";
52
+ import { isAuthPath } from "../../lib/auth-routes";
51
53
  import { navItems } from "../../lib/nav-config";
52
54
  import { getOgImageUrl } from "../../lib/og";
53
55
  import { SITE_NAME, siteLogo } from "../../lib/site-brand";
@@ -320,6 +322,21 @@ export default async function LocaleLayout({
320
322
  const pathname = (await headers()).get("x-pathname") ?? homePath;
321
323
  const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
322
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
+
323
340
  // Placeholder social handles + copyright (#5469 D5): `{{COMPANY_HANDLE}}` /
324
341
  // `{{COMPANY_NAME}}` are create-ailk markers substituted at scaffold time
325
342
  // (packages/create-ailk/src/programmatic.ts), so a customer's site never
@@ -420,94 +437,123 @@ export default async function LocaleLayout({
420
437
  * emitted unconditionally in OSS forks; paying customers install
421
438
  * @working-theory/branding and gate it in their own layout.
422
439
  */}
423
- <MarketingShell
424
- contentOwnsMain
425
- gutters="plain"
426
- bannerPlacement="above-nav"
427
- {...(siteChrome.announcementBanner
428
- ? {
429
- banner: (
430
- <Banner
431
- variant="contrast"
432
- placement="above-nav"
433
- message="Medium length banner heading with link goes here"
434
- action={{ label: "Link", href: "/pricing", kind: "link" }}
435
- routes={{ include: [homePath] }}
436
- dismissible={false}
437
- locale={locale}
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}
438
537
  />
439
- ),
440
- }
441
- : {})}
442
- {...(isDeckHome
443
- ? { snap: "mandatory" as const, deckHero: true }
444
- : {})}
445
- nav={
446
- <Nav
447
- locale={locale}
448
- labels={navLabels}
449
- navItems={siteChrome.nav === "mark-only" ? [] : navItems}
450
- siteName={SITE_NAME}
451
- showThemeToggle={false}
452
- logo={brandLogo}
453
- wordmark={brandWordmark}
454
- overflowLabel={navOverflowLabel}
455
- actions={navActions}
456
- {...(hasFlowAction
457
- ? {
458
- renderFlowTrigger: (action, trigger) =>
459
- action.labelKey === "signOut" ? (
460
- <SignOutTrigger>{trigger}</SignOutTrigger>
461
- ) : (
462
- <span
463
- className="text-body-sm text-(--color-text-muted)"
464
- data-testid="nav-account-name"
465
- >
466
- {navLabels.items.account}
467
- </span>
468
- ),
469
- }
470
- : {})}
471
- />
472
- }
473
- footer={
474
- <Footer
475
- locale={locale}
476
- itemLabels={navLabels.items}
477
- navItems={navItems}
478
- siteName={SITE_NAME}
479
- logo={brandLogo}
480
- social={[...socialLinks]}
481
- {...(siteChrome.footer === "centered"
482
- ? { layout: "centered" as const }
483
- : {})}
484
- showThemeToggle
485
- copyrightText={copyrightText}
486
- legalLinks={legalLinks}
487
- languageSwitcher={
488
- <LocaleSwitcherNav
489
- current={locale}
490
- locales={enabledLocales}
491
- pathname={pathname}
492
- />
493
- }
494
- // #4157 — deliberately left as a literal "Built by Working
495
- // Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
496
- // the OSS TEMPLATE this site is built with (a "powered by"
497
- // link, unconditional in OSS forks per the comment above),
498
- // never the customer's own identity, so it isn't the vendor-
499
- // brand-as-default defect #4157 fixes. See
500
- // docs/specs/4157-scaffold-brand-defaults-spec.md § Design
501
- // Decisions for the fuller reasoning.
502
- attribution={{
503
- label: "Built by Working Theory Labs",
504
- href: "https://ailaunchkit.ai",
505
- }}
506
- />
507
- }
508
- >
509
- {children}
510
- </MarketingShell>
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
+ )}
511
557
  {/*
512
558
  * Cookie consent (#5557). Bottom-pinned `info` Banner in consent
513
559
  * mode: `secondaryAction` makes Banner compose BannerConsentIsland,
@@ -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
+ });