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,109 @@
1
+ /**
2
+ * `/[locale]/forgot-password` — request a password-reset link (#5963, wired by #5962).
3
+ *
4
+ * RSC. #5963 shipped this as a static `forgot` intent with no `onSubmit`;
5
+ * #5962 hands the available branch to a client island that calls Better Auth.
6
+ *
7
+ * Unavailable (the default) renders the `invitation` skeleton relabelled
8
+ * through `labels`: copy only, with the footer's "Back to sign in". Never a
9
+ * 404 — the segment exists as a file, so Next resolves it whatever the
10
+ * predicate says.
11
+ *
12
+ * The predicate is `resetSendConfigured()`, NOT the flag-only
13
+ * `isResetAvailable()`. The flag alone is the operator's INTENT; the
14
+ * conjunction is whether the flow can actually complete. Keying the page on
15
+ * intent would render a live form on a deployment whose server refuses to
16
+ * send — a form whose only possible outcome is the error state.
17
+ *
18
+ * WHAT SHARES THAT PREDICATE, AND WHAT DOES NOT (#6068). One predicate decides
19
+ * this page, `/reset-password`, and whether `sendResetPassword` is wired at
20
+ * all, so no visitor is offered a step the server would refuse. `auth.ts`'s
21
+ * `onPasswordReset` is DELIBERATELY OUTSIDE it: Better Auth registers the
22
+ * `/reset-password` POST on `emailAndPassword.enabled` rather than on the send
23
+ * conjunction, so a token minted before an operator removed a conjunct stays
24
+ * consumable for its whole hour. Gating that callback too would make exactly
25
+ * those resets the ones that skip sibling-token invalidation and the § D17
26
+ * notice — the degraded deployment getting the weaker guarantee. Offering a
27
+ * step is predicated; cleaning up after one that already happened is not.
28
+ *
29
+ * The `redirectTo` the island needs is built HERE, server-side, from
30
+ * `BETTER_AUTH_URL`. Resolving it in the browser from `window.location` would
31
+ * hand an attacker-influenced origin to the one call that mints a credential.
32
+ */
33
+
34
+ import { localeHref } from "@working-theory/ui";
35
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
36
+ import type { Metadata } from "next";
37
+ import { getTranslations } from "next-intl/server";
38
+ import type { ReactElement } from "react";
39
+
40
+ import { resetSendConfigured } from "../../../../lib/auth-env";
41
+
42
+ import { ForgotPasswordCard } from "./forgot-password-card";
43
+
44
+ interface Props {
45
+ params: Promise<{ locale: string }>;
46
+ }
47
+
48
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
49
+ const { locale } = await params;
50
+ const t = await getTranslations({ locale, namespace: "auth" });
51
+ return { title: t("forgotTitle"), description: t("forgotDescription") };
52
+ }
53
+
54
+ export default async function ForgotPasswordPage({
55
+ params,
56
+ }: Props): Promise<ReactElement> {
57
+ const { locale } = await params;
58
+ const t = await getTranslations({ locale, namespace: "auth" });
59
+ const signInHref = localeHref(locale, "/sign-in");
60
+
61
+ const available = resetSendConfigured();
62
+
63
+ if (!available) {
64
+ const labels: AuthCardLabels = {
65
+ invitationHeading: t("forgotUnavailableHeading"),
66
+ invitationSubhead: t("forgotUnavailableSubhead"),
67
+ invitationBody: t("forgotUnavailableBody"),
68
+ backToSignIn: t("backToSignIn"),
69
+ };
70
+ return (
71
+ <div className="flex flex-col gap-4" data-testid="forgot-password-page">
72
+ <AuthCard
73
+ intent="invitation"
74
+ signInHref={signInHref}
75
+ labels={labels}
76
+ />
77
+ </div>
78
+ );
79
+ }
80
+
81
+ const labels: AuthCardLabels = {
82
+ forgotHeading: t("forgotHeading"),
83
+ forgotSubhead: t("forgotSubhead"),
84
+ forgotSubmit: t("forgotSubmit"),
85
+ email: t("email"),
86
+ backToSignIn: t("backToSignIn"),
87
+ sentHeading: t("sentHeading"),
88
+ sentSubhead: t("sentSubhead"),
89
+ sentBody: t("sentBody"),
90
+ sentToPrefix: t("sentToPrefix"),
91
+ };
92
+
93
+ // Absolute and same-origin: `originCheck` compares an ORIGIN, and the
94
+ // trusted allow-list has exactly one member. An empty or relative value
95
+ // would make Better Auth reject the emitted link's own callback.
96
+ const resetUrl = `${process.env.BETTER_AUTH_URL ?? ""}${localeHref(
97
+ locale,
98
+ "/reset-password",
99
+ )}`;
100
+
101
+ return (
102
+ <ForgotPasswordCard
103
+ resetUrl={resetUrl}
104
+ signInHref={signInHref}
105
+ labels={labels}
106
+ errorLabels={{ requestFailed: t("resetRequestFailed") }}
107
+ />
108
+ );
109
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `/[locale]/(auth)` — the auth route group's layout (#5963).
3
+ *
4
+ * Mounts `AuthShell` for the five auth pages (`/sign-in`, `/sign-up`,
5
+ * `/forgot-password`, `/reset-password`, `/verify-email`). A route group
6
+ * contributes no URL segment, so every existing link to `/sign-in` keeps
7
+ * working; what changes is the frame.
8
+ *
9
+ * The marketing Nav + Footer are dropped ONE level up, in
10
+ * `app/[locale]/layout.tsx`: a route group's layout nests INSIDE its parent
11
+ * segment's layout, so this file cannot escape an ancestor's chrome on its
12
+ * own. It branches on `lib/auth-routes.ts`'s `isAuthPath()` — the same module
13
+ * that names the five segments below.
14
+ *
15
+ * Slots only: the shell owns the geometry, this layout supplies the site's own
16
+ * brand mark, footer hrefs and language switcher.
17
+ */
18
+
19
+ import { AuthShell } from "@working-theory/templates/shells";
20
+ import { localeHref } from "@working-theory/ui";
21
+ import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
22
+ import { readLocales } from "@working-theory/web-i18n";
23
+ import { headers } from "next/headers";
24
+ import { getTranslations } from "next-intl/server";
25
+ import type { ReactElement, ReactNode } from "react";
26
+
27
+ import { siteLogo } from "../../../lib/site-brand";
28
+
29
+ interface Props {
30
+ children: ReactNode;
31
+ params: Promise<{ locale: string }>;
32
+ }
33
+
34
+ export default async function AuthLayout({
35
+ children,
36
+ params,
37
+ }: Props): Promise<ReactElement> {
38
+ const { locale } = await params;
39
+ const t = await getTranslations({ locale, namespace: "auth" });
40
+
41
+ // `/faq` is the third always-scaffolded core route beside `/privacy` and
42
+ // `/terms` (the locale layout's own `legalLinks` comment): there is no
43
+ // `/help` segment in the tree, and inventing one is out of scope.
44
+ const footerLinks = [
45
+ { label: t("footerPrivacy"), href: localeHref(locale, "/privacy") },
46
+ { label: t("footerTerms"), href: localeHref(locale, "/terms") },
47
+ { label: t("footerHelp"), href: localeHref(locale, "/faq") },
48
+ ];
49
+
50
+ const pathname = (await headers()).get("x-pathname") ?? "/";
51
+
52
+ return (
53
+ <AuthShell
54
+ brand={
55
+ /*
56
+ * The mark is clamped to 28px the way `BrandLogo` clamps it inside
57
+ * Nav: `siteLogo()` emits an <img> with a height ATTRIBUTE and no
58
+ * width, and an SVG with no intrinsic size then stretches to the
59
+ * container — it rendered 48 x 350 here before the clamp. The shell
60
+ * owns the row's geometry; the site owns its mark's size.
61
+ */
62
+ <span className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain">
63
+ {siteLogo()}
64
+ </span>
65
+ }
66
+ brandHref={localeHref(locale, "/")}
67
+ footerLinks={footerLinks}
68
+ languageSwitcher={
69
+ <LocaleSwitcherNav
70
+ current={locale}
71
+ locales={readLocales()}
72
+ pathname={pathname}
73
+ // AuthShell's footer link rows are already drawn at a 44px tap
74
+ // target (`max-md:py-3.5`) below `md` — this matches the
75
+ // switcher's DRAWN size to them so it doesn't read as a stray
76
+ // smaller control (#6018 A2). Not a new accessibility fix: the
77
+ // switcher's touch TARGET was already 44px via its own hit-area
78
+ // overlay (#5989) regardless of this prop.
79
+ size="touch"
80
+ />
81
+ }
82
+ >
83
+ {/*
84
+ * ADR-0027 — exactly one <main> per page. AuthShell's content slot is a
85
+ * non-landmark <div> (the shell's own contract), and every page in this
86
+ * group is one card, so the group's layout owns the landmark rather than
87
+ * repeating it in five thin pages.
88
+ */}
89
+ <main>{children}</main>
90
+ </AuthShell>
91
+ );
92
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `/[locale]/reset-password` — set a new password (#5963, wired by #5962).
3
+ *
4
+ * Three states, decided server-side:
5
+ * - reset not available → the relabelled `invitation` skeleton. A
6
+ * reset link cannot be used on a site that
7
+ * does not offer reset.
8
+ * - available, `?error=…` → the expired/invalid-token state. Better
9
+ * Auth redirects here with `?error=INVALID_TOKEN`
10
+ * when the token is missing, unknown or past
11
+ * its hour.
12
+ * - available, otherwise → the `reset` island, carrying `?token=`.
13
+ *
14
+ * Never a 404 in any of the three — the segment is a file.
15
+ *
16
+ * The availability predicate is `resetSendConfigured()`, NOT the flag-only
17
+ * `isResetAvailable()` (code review, AC-2). This is the CONSUME half of the
18
+ * flow, and Better Auth registers its `/reset-password` POST on
19
+ * `emailAndPassword.enabled` — which stays true when a send conjunct goes
20
+ * away. So for the token's whole hour after an operator removes, say,
21
+ * `BETTER_AUTH_URL`, an outstanding link would otherwise still set a password
22
+ * on a site that has stopped offering reset. Keying this page on the same
23
+ * predicate that decides `sendResetPassword` closes the visible half; the
24
+ * server-side half is `onPasswordReset` in `auth.ts`, which is now
25
+ * unconditional so a consumed token always invalidates its siblings and
26
+ * always attempts the § D17 notice, configured or not.
27
+ *
28
+ * The token is read from `searchParams` and handed to the island as a prop. It
29
+ * is the credential, so nothing here logs it and nothing re-appends it to a
30
+ * URL.
31
+ */
32
+
33
+ import { localeHref } from "@working-theory/ui";
34
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
35
+ import type { Metadata } from "next";
36
+ import { getTranslations } from "next-intl/server";
37
+ import type { ReactElement } from "react";
38
+
39
+ import { resetSendConfigured } from "../../../../lib/auth-env";
40
+
41
+ import { ResetPasswordCard } from "./reset-password-card";
42
+
43
+ interface Props {
44
+ params: Promise<{ locale: string }>;
45
+ searchParams?: Promise<{ token?: string | string[]; error?: string | string[] }>;
46
+ }
47
+
48
+ /** First value only — a repeated query param must not become an array here. */
49
+ function firstValue(raw: string | string[] | undefined): string {
50
+ if (Array.isArray(raw)) return raw[0] ?? "";
51
+ return raw ?? "";
52
+ }
53
+
54
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
55
+ const { locale } = await params;
56
+ const t = await getTranslations({ locale, namespace: "auth" });
57
+ return { title: t("resetTitle"), description: t("resetDescription") };
58
+ }
59
+
60
+ export default async function ResetPasswordPage({
61
+ params,
62
+ searchParams,
63
+ }: Props): Promise<ReactElement> {
64
+ const { locale } = await params;
65
+ const t = await getTranslations({ locale, namespace: "auth" });
66
+ const signInHref = localeHref(locale, "/sign-in");
67
+
68
+ const query = searchParams ? await searchParams : {};
69
+ const token = firstValue(query.token);
70
+ const error = firstValue(query.error);
71
+
72
+ const available = resetSendConfigured();
73
+
74
+ if (!available) {
75
+ const labels: AuthCardLabels = {
76
+ invitationHeading: t("resetUnavailableHeading"),
77
+ invitationSubhead: t("resetUnavailableSubhead"),
78
+ invitationBody: t("resetUnavailableBody"),
79
+ backToSignIn: t("backToSignIn"),
80
+ };
81
+ return (
82
+ <div className="flex flex-col gap-4" data-testid="reset-password-page">
83
+ <AuthCard intent="invitation" signInHref={signInHref} labels={labels} />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ if (error) {
89
+ // The `invitation` skeleton relabelled again — copy only, with the route
90
+ // back to sign-in. No new AuthCard intent and no new prop: `resolveLabel()`
91
+ // is the seam that already exists, so `packages/ui` stays out of this diff.
92
+ const labels: AuthCardLabels = {
93
+ invitationHeading: t("resetExpiredHeading"),
94
+ invitationSubhead: t("resetExpiredSubhead"),
95
+ invitationBody: t("resetExpiredBody"),
96
+ backToSignIn: t("backToSignIn"),
97
+ };
98
+ return (
99
+ <div
100
+ className="flex flex-col gap-4"
101
+ data-testid="reset-password-page"
102
+ >
103
+ <div data-testid="reset-token-error">
104
+ <AuthCard
105
+ intent="invitation"
106
+ signInHref={signInHref}
107
+ labels={labels}
108
+ />
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ const labels: AuthCardLabels = {
115
+ resetHeading: t("resetHeading"),
116
+ resetSubhead: t("resetSubhead"),
117
+ resetSubmit: t("resetSubmit"),
118
+ newPassword: t("newPassword"),
119
+ confirmPassword: t("confirmPassword"),
120
+ backToSignIn: t("backToSignIn"),
121
+ };
122
+
123
+ return (
124
+ <ResetPasswordCard
125
+ token={token}
126
+ signInHref={signInHref}
127
+ labels={labels}
128
+ errorLabels={{
129
+ resetFailed: t("resetFailed"),
130
+ invalidToken: t("resetExpiredSubhead"),
131
+ mismatch: t("passwordMismatch"),
132
+ }}
133
+ />
134
+ );
135
+ }
@@ -0,0 +1,109 @@
1
+ "use client"; // Better Auth's client call (resetPassword) + the error state
2
+
3
+ /**
4
+ * ResetPasswordCard — `/reset-password`'s client island (#5962).
5
+ *
6
+ * The token identifies the account, so the card renders no email field. It is
7
+ * passed to `authClient.resetPassword` in the request BODY and is never
8
+ * re-appended to a URL by this component — the one it arrived on is already in
9
+ * the browser's history and in every access log between here and the edge, and
10
+ * a second copy buys nothing but more places to leak from.
11
+ *
12
+ * On success it hard-navigates rather than routing client-side — see the
13
+ * comment at the call.
14
+ *
15
+ * An absent token is treated exactly like an expired one. That case is
16
+ * reachable — someone opening `/reset-password` directly — and a form that
17
+ * submits nothing and fails obscurely is worse than the honest message.
18
+ */
19
+
20
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
21
+ import { createAuthClient } from "better-auth/react";
22
+ import type { ReactElement } from "react";
23
+ import { useState } from "react";
24
+
25
+ const authClient = createAuthClient();
26
+
27
+ export interface ResetPasswordCardProps {
28
+ /** The `?token=` Better Auth redirected back with. May be empty. */
29
+ token: string;
30
+ /** Where a completed reset lands. */
31
+ signInHref: string;
32
+ /** Translated AuthCard labels (resolved server-side by the page). */
33
+ labels: AuthCardLabels;
34
+ /** Translated error copy. */
35
+ errorLabels: { resetFailed: string; invalidToken: string; mismatch: string };
36
+ /**
37
+ * How a completed reset leaves the page. Defaults to a FULL navigation,
38
+ * deliberately: the reset just revoked every session this browser held, so
39
+ * every piece of client state behind it is stale and a hard load is the
40
+ * honest way to drop it.
41
+ *
42
+ * Injectable for the same reason `createAuth` takes an optional `prisma` —
43
+ * jsdom's `window.location` is read-only, so a test cannot observe the real
44
+ * one. No production caller passes this.
45
+ */
46
+ navigate?: (href: string) => void;
47
+ }
48
+
49
+ const defaultNavigate = (href: string): void => {
50
+ window.location.assign(href);
51
+ };
52
+
53
+ export function ResetPasswordCard({
54
+ token,
55
+ signInHref,
56
+ labels,
57
+ errorLabels,
58
+ navigate = defaultNavigate,
59
+ }: ResetPasswordCardProps): ReactElement {
60
+ const [errors, setErrors] = useState<{
61
+ password?: string;
62
+ confirmPassword?: string;
63
+ }>({});
64
+
65
+ const submit = async (
66
+ password: string,
67
+ confirmPassword: string,
68
+ ): Promise<void> => {
69
+ setErrors({});
70
+
71
+ if (password !== confirmPassword) {
72
+ setErrors({ confirmPassword: errorLabels.mismatch });
73
+ return;
74
+ }
75
+
76
+ if (!token) {
77
+ setErrors({ password: errorLabels.invalidToken });
78
+ return;
79
+ }
80
+
81
+ const failed = await authClient
82
+ .resetPassword({ newPassword: password, token })
83
+ .then(
84
+ (res) => Boolean(res?.error),
85
+ () => true,
86
+ );
87
+
88
+ if (failed) {
89
+ setErrors({ password: errorLabels.resetFailed });
90
+ return;
91
+ }
92
+
93
+ navigate(signInHref);
94
+ };
95
+
96
+ return (
97
+ <div className="flex flex-col gap-4" data-testid="reset-password-page">
98
+ <AuthCard
99
+ intent="reset"
100
+ signInHref={signInHref}
101
+ labels={labels}
102
+ onSubmit={(data) =>
103
+ void submit(data.password ?? "", data.confirmPassword ?? "")
104
+ }
105
+ errors={errors}
106
+ />
107
+ </div>
108
+ );
109
+ }
@@ -13,6 +13,11 @@
13
13
  * (e) a foreign / protocol-relative `?next=` falls back to /account.
14
14
  * (f) password submit → `signIn.email` with the typed credentials; an
15
15
  * error result renders the signInFailed message.
16
+ * (g) the "Forgot password?" link renders only when the reset predicate is
17
+ * true — never a dead end pointing at reset's not-available state
18
+ * (#6018 AC-1). (#6068 repointed that predicate from `isResetAvailable()`
19
+ * to `resetSendConfigured()`; the mock below binds both to one fn, so
20
+ * these cases read identically either way.)
16
21
  *
17
22
  * `next-intl/server` is mocked globally (apps/web/__tests__/setup.ts) against
18
23
  * the real en.json, so `signIn` copy is asserted by its real strings.
@@ -21,10 +26,21 @@ import { render, screen } from "@testing-library/react";
21
26
  import userEvent from "@testing-library/user-event";
22
27
 
23
28
  const mockGetActiveProviderIds = jest.fn();
29
+ const mockIsResetAvailable = jest.fn();
24
30
  // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth-env.ts is a pure env-introspection module with no external boundary to mock instead.
25
- jest.mock("../../../../lib/auth-env", () => ({
31
+ jest.mock("../../../../../lib/auth-env", () => ({
26
32
  __esModule: true,
27
33
  getActiveProviderIds: () => mockGetActiveProviderIds(),
34
+ isResetAvailable: () => mockIsResetAvailable(),
35
+ // #6068 repointed the forgot link from the flag-only `isResetAvailable()`
36
+ // at the send conjunction. This is a PARTIAL module mock, so an absent
37
+ // member is `undefined` rather than the real function — the page would
38
+ // throw on EVERY render here, not only the two (g) cases. Bound to the SAME
39
+ // jest.fn() the flag already drives, so every case below keeps the exact
40
+ // truth value and the exact expectation it was written with: no assertion
41
+ // in this file changes. Same remedy, same reason, as the sibling
42
+ // `auth-pages.test.tsx` mock.
43
+ resetSendConfigured: () => mockIsResetAvailable(),
28
44
  }));
29
45
 
30
46
  const mockSocial = jest.fn();
@@ -39,7 +55,7 @@ jest.mock("better-auth/react", () => ({
39
55
  }),
40
56
  }));
41
57
 
42
- import en from "../../../../messages/en.json";
58
+ import en from "../../../../../messages/en.json";
43
59
  import Page, { generateMetadata } from "../page";
44
60
 
45
61
  async function renderPage(next?: string): Promise<void> {
@@ -52,6 +68,7 @@ async function renderPage(next?: string): Promise<void> {
52
68
 
53
69
  beforeEach(() => {
54
70
  mockGetActiveProviderIds.mockReset().mockReturnValue(["google"]);
71
+ mockIsResetAvailable.mockReset().mockReturnValue(false);
55
72
  mockSocial.mockReset().mockResolvedValue({ data: {}, error: null });
56
73
  mockEmail.mockReset().mockResolvedValue({ data: {}, error: null });
57
74
  });
@@ -149,4 +166,33 @@ describe("/sign-in page", () => {
149
166
  });
150
167
  expect(await screen.findByText(en.signIn.signInFailed)).toBeInTheDocument();
151
168
  });
169
+
170
+ it("(f) a rejected signIn.email call surfaces signInFailed too (code review, #6018)", async () => {
171
+ const user = userEvent.setup();
172
+ mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
173
+ mockEmail.mockRejectedValue(new Error("network"));
174
+ await renderPage("/account");
175
+ await user.type(screen.getByTestId("auth-email"), "ada@example.com");
176
+ await user.type(screen.getByTestId("auth-password"), "s3cr3t");
177
+ await user.click(screen.getByTestId("auth-submit"));
178
+ expect(await screen.findByText(en.signIn.signInFailed)).toBeInTheDocument();
179
+ });
180
+
181
+ it("(g) renders the forgot link when password reset is available", async () => {
182
+ mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
183
+ mockIsResetAvailable.mockReturnValue(true);
184
+ await renderPage("/account");
185
+ expect(screen.getByTestId("auth-forgot")).toHaveAttribute(
186
+ "href",
187
+ "/forgot-password",
188
+ );
189
+ });
190
+
191
+ it("(g) omits the forgot link when password reset is unavailable", async () => {
192
+ mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
193
+ mockIsResetAvailable.mockReturnValue(false);
194
+ await renderPage("/account");
195
+ expect(screen.getByTestId("auth-password")).toBeInTheDocument();
196
+ expect(screen.queryByTestId("auth-forgot")).toBeNull();
197
+ });
152
198
  });
@@ -4,9 +4,13 @@
4
4
  * RSC: resolves the active providers from auth-env, the sign-in method
5
5
  * (password when the email provider is active, else sso), the sanitised
6
6
  * `?next=` return path, and the translated `signIn` labels — then renders the
7
- * `SignInCard` client island. The locale layout above owns the Nav/Footer +
8
- * MarketingShell frame; this page contributes only the centered card region
9
- * (no nested shell).
7
+ * `SignInCard` client island.
8
+ *
9
+ * Since #5963 the page lives in the `(auth)` route group — the URL is
10
+ * unchanged (a group contributes no path segment) but the frame is
11
+ * `AuthShell`, not the marketing Nav + Footer. The shell owns the brand mark
12
+ * and the 400px column, so this page passes NO `logo` (it would render twice)
13
+ * and no framing of its own.
10
14
  *
11
15
  * Signed-in visitors are not redirected here — the Nav's session-aware action
12
16
  * cluster (Account / Sign out) is the affordance for them.
@@ -18,8 +22,10 @@ import type { Metadata } from "next";
18
22
  import { getTranslations } from "next-intl/server";
19
23
  import type { ReactElement } from "react";
20
24
 
21
- import { getActiveProviderIds } from "../../../lib/auth-env";
22
- import { siteLogo } from "../../../lib/site-brand";
25
+ import {
26
+ getActiveProviderIds,
27
+ resetSendConfigured,
28
+ } from "../../../../lib/auth-env";
23
29
 
24
30
  import { resolveNextPath } from "./next-path";
25
31
  import { SignInCard, type SignInProvider } from "./sign-in-card";
@@ -71,7 +77,15 @@ export default async function SignInPage({
71
77
  providers={providers}
72
78
  method={method}
73
79
  next={next}
74
- logo={siteLogo()}
80
+ // The "Forgot password?" link now has somewhere real to go (#5963);
81
+ // AuthCard's default is a dead "#".
82
+ forgotHref={localeHref(locale, "/forgot-password")}
83
+ // Hide it entirely when reset isn't wired server-side, rather than
84
+ // pointing at a page that renders its own not-available state (#6018).
85
+ // `resetSendConfigured()`, not the flag: /forgot-password keys on the
86
+ // send conjunction, so the flag alone would offer a link to a page
87
+ // rendering its own not-available card (#6068 AC-2).
88
+ forgot={resetSendConfigured() ? "link" : "none"}
75
89
  labels={labels}
76
90
  notConfiguredSubhead={t("notConfiguredSubhead")}
77
91
  errorLabels={{