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,214 @@
1
+ /**
2
+ * @file auth-email.test.ts — Better Auth's three mail callbacks (#5962 § D3).
3
+ *
4
+ * Written before the implementation. The transport is INJECTED, so nothing
5
+ * here mocks an internal module and nothing here sends mail.
6
+ *
7
+ * What the suite pins, in the order the design depends on it:
8
+ * - ONE serialization: the bytes that are signed are the bytes that are
9
+ * sent. A key-order or whitespace drift between signer and sender is the
10
+ * whole failure mode the raw-string seam exists to make impossible.
11
+ * - the signature is HMAC-SHA256 over `<timestamp>.<body>`, so the
12
+ * timestamp cannot be swapped independently of the body it authenticates.
13
+ * - the credential-account check (§ D14): a user with no `credential`
14
+ * account gets NO reset mail, and the suppression is silent — same return,
15
+ * no throw, log-only. An OAuth-only user who completes a reset would
16
+ * otherwise silently gain a password path around their IdP.
17
+ * - the § D12 PII boundary on every log line: a userId, a kind, a reason and
18
+ * an error KIND. Never the address, never the token, never the link.
19
+ * - nothing throws. A failed notification must never fail the password
20
+ * change it reports.
21
+ */
22
+
23
+ import type { AuthEmailLog } from "../auth-email";
24
+ import {
25
+ buildAuthEmailRequest,
26
+ makeSendResetPassword,
27
+ makeSendVerificationEmail,
28
+ makeSendPasswordChangedNotice,
29
+ } from "../auth-email";
30
+
31
+ const ORIGINAL_ENV = process.env;
32
+
33
+ type LogLine = { level: string; fields: Record<string, unknown> };
34
+
35
+ function makeLog(): { lines: LogLine[]; log: AuthEmailLog } {
36
+ const lines: LogLine[] = [];
37
+ const record =
38
+ (level: string) =>
39
+ (fields: Record<string, unknown>): void => {
40
+ lines.push({ level, fields });
41
+ };
42
+ return { lines, log: { warn: record("warn"), info: record("info") } };
43
+ }
44
+
45
+ /** A prisma double exposing only what these callbacks touch. */
46
+ function makePrisma(hasCredential: boolean) {
47
+ return {
48
+ account: {
49
+ findFirst: jest.fn().mockResolvedValue(
50
+ hasCredential ? { id: "acct_1" } : null,
51
+ ),
52
+ },
53
+ verification: { deleteMany: jest.fn().mockResolvedValue({ count: 2 }) },
54
+ };
55
+ }
56
+
57
+ beforeEach(() => {
58
+ process.env = { ...ORIGINAL_ENV };
59
+ process.env.AUTH_EMAIL_WEBHOOK_SECRET = "webhook-secret";
60
+ process.env.AUTH_EMAIL_FROM = "noreply@site.example";
61
+ process.env.BETTER_AUTH_URL = "https://site.example";
62
+ });
63
+
64
+ afterAll(() => {
65
+ process.env = ORIGINAL_ENV;
66
+ });
67
+
68
+ describe("buildAuthEmailRequest()", () => {
69
+ it("signs the EXACT string it returns as the body", () => {
70
+ const req = buildAuthEmailRequest(
71
+ { kind: "password-reset", userId: "user_1", to: "a@b.test", url: "https://site.example/r" },
72
+ "webhook-secret",
73
+ );
74
+
75
+ const { createHmac } = require("node:crypto") as typeof import("node:crypto");
76
+ const timestamp = req.headers["x-ailk-auth-email-timestamp"];
77
+ const expected = `sha256=${createHmac("sha256", "webhook-secret")
78
+ .update(`${timestamp}.${req.rawBody}`, "utf8")
79
+ .digest("hex")}`;
80
+
81
+ expect(req.headers["x-ailk-auth-email-signature"]).toBe(expected);
82
+ // And the body really is a single string, not an object re-serialized later.
83
+ expect(typeof req.rawBody).toBe("string");
84
+ expect(JSON.parse(req.rawBody)).toMatchObject({
85
+ kind: "password-reset",
86
+ userId: "user_1",
87
+ to: "a@b.test",
88
+ });
89
+ });
90
+
91
+ it("puts a current unix-seconds timestamp inside the signed material", () => {
92
+ const req = buildAuthEmailRequest(
93
+ { kind: "password-changed", userId: "user_1", to: "a@b.test" },
94
+ "webhook-secret",
95
+ );
96
+ const ts = Number(req.headers["x-ailk-auth-email-timestamp"]);
97
+ expect(Math.abs(Math.floor(Date.now() / 1000) - ts)).toBeLessThan(5);
98
+ });
99
+ });
100
+
101
+ describe("sendResetPassword (§ D14 — the credential-account check)", () => {
102
+ it("sends for a user who HAS a credential account", async () => {
103
+ const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
104
+ const { lines, log } = makeLog();
105
+ const prisma = makePrisma(true);
106
+
107
+ const cb = makeSendResetPassword({ prisma, send, log });
108
+ await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/api/auth/reset-password/tok" });
109
+
110
+ expect(send).toHaveBeenCalledTimes(1);
111
+ expect(lines.some((l) => l.fields.reason === "sent")).toBe(true);
112
+ });
113
+
114
+ it("sends NOTHING for a user with no credential account, and does not throw", async () => {
115
+ const send = jest.fn();
116
+ const { lines, log } = makeLog();
117
+ const prisma = makePrisma(false);
118
+
119
+ const cb = makeSendResetPassword({ prisma, send, log });
120
+ await expect(
121
+ cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" }),
122
+ ).resolves.toBeUndefined();
123
+
124
+ expect(send).not.toHaveBeenCalled();
125
+ expect(lines.some((l) => l.fields.reason === "no_credential_account")).toBe(
126
+ true,
127
+ );
128
+ });
129
+
130
+ it("never throws when the transport fails", async () => {
131
+ const send = jest.fn().mockRejectedValue(new Error("network"));
132
+ const { lines, log } = makeLog();
133
+ const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
134
+
135
+ await expect(
136
+ cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" }),
137
+ ).resolves.toBeUndefined();
138
+ expect(lines.some((l) => l.fields.reason === "send_failed")).toBe(true);
139
+ });
140
+
141
+ it("logs nothing when the relay secret is absent — it does not send blind", async () => {
142
+ delete process.env.AUTH_EMAIL_WEBHOOK_SECRET;
143
+ const send = jest.fn();
144
+ const { lines, log } = makeLog();
145
+ const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
146
+
147
+ await cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" });
148
+ expect(send).not.toHaveBeenCalled();
149
+ expect(lines.some((l) => l.fields.reason === "not_configured")).toBe(true);
150
+ });
151
+ });
152
+
153
+ describe("the § D12 PII boundary", () => {
154
+ it("never logs the address, the token or the assembled link", async () => {
155
+ const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
156
+ const { lines, log } = makeLog();
157
+ const url =
158
+ "https://site.example/api/auth/reset-password/SECRETTOKEN?callbackURL=%2Freset-password";
159
+
160
+ const reset = makeSendResetPassword({ prisma: makePrisma(true), send, log });
161
+ await reset({ user: { id: "user_1", email: "person@example.com" }, url });
162
+
163
+ const verify = makeSendVerificationEmail({ send, log });
164
+ await verify({ user: { id: "user_1", email: "person@example.com" }, url });
165
+
166
+ const notice = makeSendPasswordChangedNotice({ send, log });
167
+ await notice({ id: "user_1", email: "person@example.com" });
168
+
169
+ const serialized = JSON.stringify(lines);
170
+ expect(serialized).not.toContain("person@example.com");
171
+ expect(serialized).not.toContain("SECRETTOKEN");
172
+ expect(serialized).not.toContain("site.example");
173
+ // What it DOES carry.
174
+ expect(serialized).toContain("user_1");
175
+ expect(serialized).toContain("password-reset");
176
+ });
177
+
178
+ it("carries only the tagged error KIND, never a message that can quote an address", async () => {
179
+ const send = jest.fn().mockResolvedValue({
180
+ ok: false,
181
+ status: 400,
182
+ body: { error: "validation_failed for person@example.com" },
183
+ });
184
+ const { lines, log } = makeLog();
185
+ const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
186
+
187
+ await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" });
188
+ expect(JSON.stringify(lines)).not.toContain("person@example.com");
189
+ });
190
+ });
191
+
192
+ describe("makeSendPasswordChangedNotice (§ D17)", () => {
193
+ it("sends a password-changed body carrying no url", async () => {
194
+ const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
195
+ const { log } = makeLog();
196
+ const notice = makeSendPasswordChangedNotice({ send, log });
197
+
198
+ await notice({ id: "user_1", email: "person@example.com" });
199
+
200
+ const [rawBody] = send.mock.calls[0];
201
+ const parsed = JSON.parse(rawBody);
202
+ expect(parsed.kind).toBe("password-changed");
203
+ expect(parsed).not.toHaveProperty("url");
204
+ });
205
+
206
+ it("never throws — a failed notice must not fail the password change", async () => {
207
+ const send = jest.fn().mockRejectedValue(new Error("down"));
208
+ const { log } = makeLog();
209
+ const notice = makeSendPasswordChangedNotice({ send, log });
210
+ await expect(
211
+ notice({ id: "user_1", email: "person@example.com" }),
212
+ ).resolves.toBeUndefined();
213
+ });
214
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Tests for the auth-route predicate (#5963).
3
+ *
4
+ * `isAuthPath()` is the ONE list the locale layout branches on and the five
5
+ * `(auth)` page segments are named from, so the layout and the routes cannot
6
+ * drift. Covers bare paths, locale-prefixed paths, near misses, nested paths,
7
+ * and the absent-header fail-safe.
8
+ */
9
+
10
+ import { AUTH_SEGMENTS, isAuthPath } from "../auth-routes";
11
+
12
+ const LOCALES = ["en", "fr", "de"];
13
+
14
+ describe("AUTH_SEGMENTS", () => {
15
+ it("names exactly the five auth routes", () => {
16
+ expect([...AUTH_SEGMENTS]).toEqual([
17
+ "sign-in",
18
+ "sign-up",
19
+ "forgot-password",
20
+ "reset-password",
21
+ "verify-email",
22
+ ]);
23
+ });
24
+ });
25
+
26
+ describe("isAuthPath", () => {
27
+ it.each(AUTH_SEGMENTS)("matches the bare /%s path", (segment) => {
28
+ expect(isAuthPath(`/${segment}`, LOCALES)).toBe(true);
29
+ });
30
+
31
+ it.each(AUTH_SEGMENTS)("matches the locale-prefixed /fr/%s path", (segment) => {
32
+ expect(isAuthPath(`/fr/${segment}`, LOCALES)).toBe(true);
33
+ });
34
+
35
+ it("matches a trailing-slash auth path", () => {
36
+ expect(isAuthPath("/sign-in/", LOCALES)).toBe(true);
37
+ });
38
+
39
+ it("matches an auth path carrying a query string", () => {
40
+ expect(isAuthPath("/sign-in?next=/account", LOCALES)).toBe(true);
41
+ });
42
+
43
+ it("matches a nested path under an auth segment", () => {
44
+ expect(isAuthPath("/reset-password/confirm", LOCALES)).toBe(true);
45
+ });
46
+
47
+ it("does not match a near miss", () => {
48
+ expect(isAuthPath("/sign-in-x", LOCALES)).toBe(false);
49
+ expect(isAuthPath("/sign", LOCALES)).toBe(false);
50
+ });
51
+
52
+ it("does not match a marketing path", () => {
53
+ expect(isAuthPath("/pricing", LOCALES)).toBe(false);
54
+ expect(isAuthPath("/fr/pricing", LOCALES)).toBe(false);
55
+ });
56
+
57
+ it("does not match the locale root or the site root", () => {
58
+ expect(isAuthPath("/", LOCALES)).toBe(false);
59
+ expect(isAuthPath("/fr", LOCALES)).toBe(false);
60
+ });
61
+
62
+ it("does not strip a prefix that is not an enabled locale", () => {
63
+ // `/es` is not enabled here, so `es` is the first segment, not a prefix.
64
+ expect(isAuthPath("/es/sign-in", LOCALES)).toBe(false);
65
+ });
66
+
67
+ it("fails to NOT-an-auth-path for an empty or absent pathname", () => {
68
+ // The fail-safe direction (§ D1): an absent `x-pathname` must never strip
69
+ // a marketing page's chrome. The worst case is an auth page WITH chrome,
70
+ // which the (auth) group's own tests catch.
71
+ expect(isAuthPath("", LOCALES)).toBe(false);
72
+ expect(isAuthPath(undefined, LOCALES)).toBe(false);
73
+ });
74
+ });
@@ -0,0 +1,72 @@
1
+ // oss: false
2
+ /**
3
+ * Tests for enabledMeetingSlugs (#6183 S9b D5) — the D4-resolving read both
4
+ * /meet pages share.
5
+ */
6
+ const mockMeetingTypes = jest.fn();
7
+ jest.mock("@working-theory/api-client", () => ({
8
+ __esModule: true,
9
+ schedule: { meetingTypes: (...args: unknown[]) => mockMeetingTypes(...args) },
10
+ }));
11
+
12
+ import { enabledMeetingSlugs } from "../meeting-slugs";
13
+
14
+ describe("enabledMeetingSlugs", () => {
15
+ const originalEnv = process.env["SCHEDULING_CAL_LINK"];
16
+
17
+ beforeEach(() => {
18
+ mockMeetingTypes.mockReset();
19
+ });
20
+
21
+ afterEach(() => {
22
+ if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
23
+ else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
24
+ });
25
+
26
+ it("passes a real, populated array through unchanged", async () => {
27
+ mockMeetingTypes.mockResolvedValue({
28
+ ok: true,
29
+ body: { slugs: ["intro", "demo"] },
30
+ });
31
+
32
+ await expect(enabledMeetingSlugs()).resolves.toEqual(["intro", "demo"]);
33
+ });
34
+
35
+ it("passes a real, intentionally empty array through unchanged (D4 row 2 — never the fallback)", async () => {
36
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
37
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: [] } });
38
+
39
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
40
+ });
41
+
42
+ it("null + SCHEDULING_CAL_LINK set → the derived singleton (D4 row 1)", async () => {
43
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
44
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
45
+
46
+ await expect(enabledMeetingSlugs()).resolves.toEqual(["intro-call"]);
47
+ });
48
+
49
+ it("null + SCHEDULING_CAL_LINK unset → [] (no shipped page to protect)", async () => {
50
+ delete process.env["SCHEDULING_CAL_LINK"];
51
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
52
+
53
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
54
+ });
55
+
56
+ it("a non-ok API result → [], fail closed — an outage must not open the gate", async () => {
57
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
58
+ mockMeetingTypes.mockResolvedValue({ ok: false, status: 500, body: null });
59
+
60
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
61
+ });
62
+
63
+ it("null + a MALFORMED SCHEDULING_CAL_LINK slug → [], never a corrupt fallback (D14 parity, security review 2026-09-19)", async () => {
64
+ // A comma in the derived slug would corrupt the batched event-meta
65
+ // `calLinks` join downstream (it splits on comma), misattributing the
66
+ // rest of the list to a foreign owner — MeetingSlugSchema rejects it.
67
+ process.env["SCHEDULING_CAL_LINK"] = "acme/intro,evil";
68
+ mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
69
+
70
+ await expect(enabledMeetingSlugs()).resolves.toEqual([]);
71
+ });
72
+ });
@@ -4,11 +4,28 @@
4
4
  * This is the end-to-end claim the wiring makes — a site declares a theme and
5
5
  * the built page renders in those colours — asserted on the actual repo file
6
6
  * rather than a fixture, so deleting the `theme:` block would fail here.
7
+ *
8
+ * Every expectation is DERIVED from whichever `_site.mdx` the test reads, never
9
+ * hard-coded (#6460). This file is snapshotted into `create-ailk`'s
10
+ * `templates/`, so it runs against TWO different `_site.mdx` files: this
11
+ * monorepo's, and the scaffold's own. Hard-coded values made it a snapshot of
12
+ * one tree's palette masquerading as a round-trip test — green here, red in a
13
+ * scaffold whose theme differs, which is exactly what happened when the base
14
+ * scaffold moved to the black-on-white foundation. It also meant a CUSTOMER who
15
+ * took the file's own invitation to "declare only what you change" got a red
16
+ * suite for editing their own colours.
17
+ *
18
+ * Deriving keeps every claim the hard-coded form made — the mapping is applied,
19
+ * the values reach the CSS, and nothing else moves — while making all three
20
+ * true of any declared palette. Deleting the `theme:` block still fails, on the
21
+ * `toBeDefined()` below.
7
22
  */
8
- import { mkdtempSync, writeFileSync } from "node:fs";
23
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
9
24
  import { tmpdir } from "node:os";
10
25
  import { join } from "node:path";
11
26
 
27
+ import { parse as parseYaml } from "yaml";
28
+
12
29
  import { PartialColorPaletteSchema } from "@working-theory/site-config/schema";
13
30
  import { siteColorTokenMap, themeToCssVarsBothModes } from "@working-theory/theme";
14
31
 
@@ -26,6 +43,46 @@ function darkBlock(css: string): string {
26
43
  return css.slice(css.indexOf(':root[data-theme="dark"]'));
27
44
  }
28
45
 
46
+ /** The two modes, as the palette and the emitted CSS both name them. */
47
+ const MODES = ["light", "dark"] as const;
48
+
49
+ /**
50
+ * The `theme.colors` block exactly as the `_site.mdx` under test declares it —
51
+ * raw SLOT names (`text`, `primary`), not token keys. Read with the same
52
+ * frontmatter parse `site-theme.ts` uses, so the test cannot pass against a
53
+ * file the implementation reads differently.
54
+ */
55
+ function declaredColors(): Partial<Record<(typeof MODES)[number], Record<string, string>>> {
56
+ const raw = readFileSync(join(REPO_CONTENT_ROOT, "_site.mdx"), "utf8");
57
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
58
+ const parsed = parseYaml(frontmatter?.[1] ?? "") as
59
+ | { theme?: { colors?: Record<string, Record<string, string>> } }
60
+ | null;
61
+ return parsed?.theme?.colors ?? {};
62
+ }
63
+
64
+ /**
65
+ * The CSS custom property a token key is emitted as — `fgOnBrand` →
66
+ * `--color-fg-on-brand`. The colon in every call site anchors the match, so
67
+ * `--color-surface` never collides with `--color-surface-secondary`.
68
+ */
69
+ function cssVar(tokenKey: string): string {
70
+ return `--color-${tokenKey.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
71
+ }
72
+
73
+ /** `{ [tokenKey]: value }` for one mode's declared slots, mapping applied. */
74
+ function declaredTokens(mode: (typeof MODES)[number]): Record<string, string> {
75
+ const out: Record<string, string> = {};
76
+ for (const [slot, value] of Object.entries(declaredColors()[mode] ?? {})) {
77
+ const tokenKey = siteColorTokenMap[slot as keyof typeof siteColorTokenMap];
78
+ // A slot with no token counterpart cannot reach the CSS at all —
79
+ // `siteThemeToBrandTokens` throws on it, which the schema's own tests
80
+ // cover. Nothing to assert about it here.
81
+ if (typeof tokenKey === "string") out[tokenKey] = value;
82
+ }
83
+ return out;
84
+ }
85
+
29
86
  describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
30
87
  const originalRoot = process.env.AILK_CONTENT_ROOT;
31
88
 
@@ -38,35 +95,93 @@ describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
38
95
  else process.env.AILK_CONTENT_ROOT = originalRoot;
39
96
  });
40
97
 
41
- it("translates the declared theme into the token overlay", () => {
42
- expect(loadSiteBrandTokens()).toEqual({
43
- colors: {
44
- light: { bg: "#fdfcfa", fg: "#1c1917" },
45
- dark: { bg: "#0c0a09" },
46
- },
47
- });
98
+ it("translates every colour the file declares into its mapped token", () => {
99
+ const overlay = loadSiteBrandTokens();
100
+ // The `theme:` block exists and produced an overlay — the claim the
101
+ // hard-coded form made by naming values, kept without naming them.
102
+ expect(overlay).toBeDefined();
103
+
104
+ // `BrandTokenOverrides` is deliberately `Readonly<Record<string, unknown>>`
105
+ // — `@working-theory/theme` cannot name the overlay's shape without
106
+ // pinning it to one token set — so the test states the shape it asserts
107
+ // about. The hard-coded form sidestepped this by comparing whole objects.
108
+ const colors = (overlay?.["colors"] ?? {}) as Partial<
109
+ Record<(typeof MODES)[number], Record<string, string>>
110
+ >;
111
+
112
+ let asserted = 0;
113
+ for (const mode of MODES) {
114
+ const expectedTokens = declaredTokens(mode);
115
+ // Exactly the declared slots, nothing invented and nothing dropped.
116
+ expect(colors[mode] ?? {}).toEqual(expectedTokens);
117
+ asserted += Object.keys(expectedTokens).length;
118
+ }
119
+ // Non-vacuity: a file that declared nothing would satisfy the loop above.
120
+ expect(asserted).toBeGreaterThan(0);
48
121
  });
49
122
 
50
123
  it("reaches the emitted CSS custom properties", () => {
51
124
  const brand = loadSiteBrandTokens();
52
125
  const css = themeToCssVarsBothModes(brand ? { brand } : {});
53
-
54
- expect(lightBlock(css)).toContain("--color-bg: #fdfcfa;");
55
- expect(lightBlock(css)).toContain("--color-fg: #1c1917;");
56
- expect(darkBlock(css)).toContain("--color-bg: #0c0a09;");
126
+ const blockFor = { light: lightBlock(css), dark: darkBlock(css) };
127
+
128
+ let asserted = 0;
129
+ for (const mode of MODES) {
130
+ for (const [tokenKey, value] of Object.entries(declaredTokens(mode))) {
131
+ expect(blockFor[mode]).toContain(`${cssVar(tokenKey)}: ${value};`);
132
+ asserted += 1;
133
+ }
134
+ }
135
+ expect(asserted).toBeGreaterThan(0);
57
136
  });
58
137
 
59
138
  it("changes exactly the declared variables and nothing else", () => {
60
139
  const brand = loadSiteBrandTokens();
61
140
  const themed = themeToCssVarsBothModes(brand ? { brand } : {});
62
- const expected = themeToCssVarsBothModes()
63
- // light block — the two declared light colours
64
- .replace("--color-bg: #ffffff;", "--color-bg: #fdfcfa;")
65
- .replace("--color-fg: #030712;", "--color-fg: #1c1917;")
66
- // dark block — the one declared dark colour
67
- .replace("--color-bg: #0a0a0a;", "--color-bg: #0c0a09;");
68
-
69
- expect(themed).toBe(expected);
141
+
142
+ // Rebuild the expectation from the DEFAULT CSS by overwriting, per mode
143
+ // block, only the variables this site declares. Anything the overlay
144
+ // touched beyond them makes the final equality fail — which is the whole
145
+ // point of this case.
146
+ const base = themeToCssVarsBothModes();
147
+ const rewritten = { light: lightBlock(base), dark: darkBlock(base) };
148
+ for (const mode of MODES) {
149
+ for (const [tokenKey, value] of Object.entries(declaredTokens(mode))) {
150
+ const pattern = new RegExp(`(${cssVar(tokenKey)}: )[^;]+;`);
151
+ // The variable must exist in the default block for the overwrite to
152
+ // mean anything — a replace with no target would weaken the equality
153
+ // below into a tautology. Presence is the right check rather than
154
+ // "the text changed": a site may legitimately declare a value that
155
+ // already MATCHES the default (the black-on-white base declares
156
+ // `bg: #ffffff`, which light mode already is), and that no-op is
157
+ // correct, not vacuous.
158
+ expect(rewritten[mode]).toMatch(pattern);
159
+ rewritten[mode] = rewritten[mode].replace(pattern, `$1${value};`);
160
+ }
161
+
162
+ // `--color-surface-raised` is DERIVED, not independent: the emitter
163
+ // resolves it from `surfaceSecondary` in light mode and from `surface`
164
+ // in dark (`packages/theme/src/theme-to-css.ts`, and the rule is
165
+ // documented at `site-theme.ts`'s header). So a site that declares
166
+ // either of those moves it too, and the equality below has to account
167
+ // for it — re-derived here from whatever the block now carries rather
168
+ // than restated as a value, so the two cannot drift.
169
+ //
170
+ // Kept inside the total equality instead of excluded from it: this
171
+ // variable is SUPPOSED to move, and excluding it would stop the test
172
+ // noticing if it ever stopped moving.
173
+ const derivedFrom = mode === "light" ? "surfaceSecondary" : "surface";
174
+ const sourceValue = new RegExp(`${cssVar(derivedFrom)}: ([^;]+);`).exec(
175
+ rewritten[mode],
176
+ )?.[1];
177
+ expect(sourceValue).toBeDefined();
178
+ rewritten[mode] = rewritten[mode].replace(
179
+ /(--color-surface-raised: )[^;]+;/,
180
+ `$1${sourceValue};`,
181
+ );
182
+ }
183
+
184
+ expect(themed).toBe(rewritten.light + rewritten.dark);
70
185
  });
71
186
 
72
187
  it("treats a bare `theme:` key as no theme, rather than throwing", () => {
@@ -311,7 +311,24 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
311
311
  subtree: true,
312
312
  },
313
313
  { route: "meet", reason: "booking surface, API-backed", subtree: true },
314
- { route: "sign-in", reason: "auth surface, not a content page" },
314
+ // #6084 — both purchase return pages read a Checkout Session (or read
315
+ // nothing, for cancel) and render via SectionRenderer directly; neither
316
+ // is a content-adapter page, and the paid render's own schema.org node
317
+ // (`PaymentComplete`, D6) is asserted directly, not through this walk.
318
+ {
319
+ route: "purchase",
320
+ reason: "checkout return pages, API-backed / read-nothing",
321
+ subtree: true,
322
+ },
323
+ // The whole (auth) route group (#5963). The walk keys on the DIRECTORY
324
+ // path, and a route group's parens are part of it, so this one subtree
325
+ // entry covers all five auth pages — including /sign-in, whose own entry it
326
+ // replaces now that the page sits inside the group.
327
+ {
328
+ route: "(auth)",
329
+ reason: "auth surfaces, not content pages",
330
+ subtree: true,
331
+ },
315
332
  ];
316
333
 
317
334
  interface RouteEntry {