create-ailk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) 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/lib/api-boot-assertion.d.ts +46 -0
  5. package/dist/lib/api-boot-assertion.js +70 -0
  6. package/dist/lib/import-graph.d.ts +29 -0
  7. package/dist/lib/import-graph.js +140 -0
  8. package/dist/module-architecture.d.ts +1 -0
  9. package/dist/module-architecture.js +253 -127
  10. package/dist/surfaces.js +105 -7
  11. package/dist/template-manifest.d.ts +33 -0
  12. package/dist/template-manifest.js +99 -16
  13. package/dist/template-names.d.ts +1 -1
  14. package/dist/template-names.js +1 -0
  15. package/package.json +3 -2
  16. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  17. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  18. package/site-templates/marketing-site/ailk-template.json +1 -1
  19. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  20. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  21. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  22. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  23. package/site-templates/minimalism-site/ailk-template.json +2 -1
  24. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  25. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  26. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  27. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  28. package/site-templates/simple-service/ailk-template.json +10 -0
  29. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  30. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  31. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  32. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  33. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  34. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  35. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  36. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  37. package/site-templates/simple-service/provenance.json +4 -0
  38. package/templates/.env.example +72 -7
  39. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  40. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  41. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  42. package/templates/apps/api/.env.example +13 -0
  43. package/templates/apps/api/CLAUDE.md +26 -2
  44. package/templates/apps/api/jest.config.cjs +3 -0
  45. package/templates/apps/api/package.json +1 -0
  46. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  47. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  48. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  49. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  50. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  51. package/templates/apps/api/src/config/index.ts +62 -0
  52. package/templates/apps/api/src/config/modules.ts +7 -3
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  54. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  55. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  56. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  57. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  58. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  59. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  60. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  61. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  62. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  63. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  64. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  65. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  66. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  67. package/templates/apps/api/src/lib/site.ts +208 -9
  68. package/templates/apps/api/src/lib/stripe.ts +66 -0
  69. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  70. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  71. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  72. package/templates/apps/api/src/openapi/spec.ts +543 -2
  73. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  74. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  75. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  76. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  77. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  78. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  79. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  80. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  81. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  82. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  83. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  84. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  85. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  86. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  87. package/templates/apps/api/src/routes/content/index.ts +13 -73
  88. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  89. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  90. package/templates/apps/api/src/routes/content/search.ts +129 -0
  91. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  92. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  93. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  94. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  95. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  96. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  97. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  98. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  99. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  100. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  101. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  102. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  103. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  104. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  105. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  106. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  111. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  112. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  113. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  114. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  115. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  116. package/templates/apps/api/src/server.ts +439 -49
  117. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  118. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  119. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  120. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  121. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  122. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  123. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  124. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  125. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  126. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  127. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  128. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  129. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  130. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  131. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  132. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  133. package/templates/apps/api/src/services/audit-log.ts +668 -0
  134. package/templates/apps/api/src/services/consent.ts +18 -0
  135. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  136. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  137. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  138. package/templates/apps/api/src/services/entitlements.ts +80 -0
  139. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  140. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  141. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  142. package/templates/apps/api/src/services/listing-config.ts +27 -9
  143. package/templates/apps/api/src/services/project-listings.ts +201 -6
  144. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  145. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  146. package/templates/apps/api/src/services/revalidation.ts +173 -0
  147. package/templates/apps/api/src/services/verified-email.ts +71 -0
  148. package/templates/apps/api/src/vercel-handler.ts +75 -35
  149. package/templates/apps/mcp/.env.example +17 -0
  150. package/templates/apps/mcp/CLAUDE.md +57 -24
  151. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  152. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  153. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  154. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  155. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  156. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  157. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  158. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  159. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  160. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  161. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  162. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  163. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  164. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  165. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  166. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  167. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  168. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  169. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  170. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  171. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  172. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  173. package/templates/apps/mcp/jest.config.cjs +13 -0
  174. package/templates/apps/mcp/package.json +2 -0
  175. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  176. package/templates/apps/mcp/src/config/modules.ts +10 -5
  177. package/templates/apps/mcp/src/http-transport.ts +102 -29
  178. package/templates/apps/mcp/src/index.ts +73 -11
  179. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  180. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  181. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  182. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  183. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  184. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  185. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  186. package/templates/apps/mcp/src/server.ts +59 -35
  187. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  188. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  189. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  190. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  191. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  192. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  193. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  194. package/templates/apps/mcp/src/tools/index.ts +6 -4
  195. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  196. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  197. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  198. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  199. package/templates/apps/web/.env.example +71 -5
  200. package/templates/apps/web/CLAUDE.md +44 -0
  201. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  202. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  203. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  204. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  205. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  206. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  207. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  208. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  209. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  210. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  211. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  212. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  213. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  214. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  215. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  216. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  217. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  218. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  219. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  220. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  221. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  222. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  223. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  224. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  225. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  226. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  227. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  228. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  229. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  230. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  232. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  233. package/templates/apps/web/auth.ts +407 -8
  234. package/templates/apps/web/jest.config.cjs +8 -4
  235. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  236. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  237. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  238. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  239. package/templates/apps/web/lib/auth-email.ts +318 -0
  240. package/templates/apps/web/lib/auth-env.ts +85 -0
  241. package/templates/apps/web/lib/auth-routes.ts +55 -0
  242. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  243. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  244. package/templates/apps/web/messages/en.json +187 -0
  245. package/templates/apps/web/package.json +2 -1
  246. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  247. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  248. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  249. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  250. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  251. package/templates/database/CHANGELOG.md +142 -0
  252. package/templates/database/README.md +10 -3
  253. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  254. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  255. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  256. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  257. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  258. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  259. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  260. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  261. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  262. package/templates/database/__tests__/tenancy.test.ts +141 -0
  263. package/templates/database/config/schema.prisma +79 -2
  264. package/templates/database/content/schema.prisma +87 -11
  265. package/templates/database/inbox/schema.prisma +452 -9
  266. package/templates/database/jest.config.cjs +5 -3
  267. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  268. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  269. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  270. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  271. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  272. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  273. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  274. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  275. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  276. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  277. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  278. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  279. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  280. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  281. package/templates/database/package.json +1 -1
  282. package/templates/database/tenancy.ts +86 -1
  283. package/templates/database/tsconfig.json +1 -0
  284. package/versions.json +34 -34
  285. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  286. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  287. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  288. package/templates/apps/api/src/routes/content/create.ts +0 -115
  289. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  290. package/templates/apps/api/src/routes/content/update.ts +0 -112
  291. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  292. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  293. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  294. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  295. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  296. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  297. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  298. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  299. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,125 @@
1
+ "use client"; // Better Auth's client calls (signIn.social / signIn.email) + error state
2
+
3
+ /**
4
+ * SignInCard — the /sign-in page's client island (#4984).
5
+ *
6
+ * Composes the shipped `AuthCard` block (`intent="sign-in"`, `footer="none"`)
7
+ * and wires its provider buttons / password form to the Better Auth client
8
+ * via the shared `useAuthCardFlow` hook (#6018 AC-3) — the same hook
9
+ * `SignUpCard` next door uses, using the SAME `createAuthClient()` idiom the
10
+ * `(authed)` group's `SignInGate` uses. The page (RSC) decides providers +
11
+ * method from auth-env and hands them in; this component never reads env
12
+ * itself.
13
+ *
14
+ * The brand mark is the SHELL's since #5963 — this island renders no `logo`,
15
+ * or it would appear twice on the page.
16
+ *
17
+ * No sign-up affordance anywhere: `footer="none"` drops the "Not a member?
18
+ * Create an account" cross-link (Better Auth `emailAndPassword.disableSignUp`
19
+ * posture).
20
+ *
21
+ * Password path: `apps/web/auth.ts` does not enable `emailAndPassword` today,
22
+ * so `signIn.email` is wired but inert (Better Auth answers with an error)
23
+ * until an operator enables it server-side — the page only selects
24
+ * `method="password"` when auth-env reports the "email" provider.
25
+ *
26
+ * Zero providers (method sso, providers []): the card renders in its inert
27
+ * state — heading + `notConfiguredSubhead`, no buttons, no form. Never a
28
+ * fabricated session.
29
+ *
30
+ * Error surface: AuthCard's `errors` prop is per-field. A failed provider
31
+ * launch lands on `errors.email` — the social row sits inside the password
32
+ * form beside the email field, so it renders there. The SSO body has no
33
+ * fields at all, so for `method="sso"` the same message is rendered as an
34
+ * `Alert` above the card instead.
35
+ *
36
+ * `forgot` (#6018 AC-1): the page passes `forgot="none"` when
37
+ * `isResetAvailable()` is false, so the "Forgot password?" link never renders
38
+ * a dead end — AuthCard's own default (`"link"`) assumes reset is wired.
39
+ */
40
+
41
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
42
+ import { Alert, AlertDescription } from "@working-theory/ui/primitives";
43
+ import type { ReactElement } from "react";
44
+
45
+ import { authClient, useAuthCardFlow } from "../use-auth-card-flow";
46
+
47
+ /** The OAuth providers this page can launch — a subset of AuthCard's. */
48
+ export type SignInProvider = "google" | "github";
49
+
50
+ export interface SignInCardProps {
51
+ /** Active OAuth providers, exactly as auth-env reports them. */
52
+ providers: SignInProvider[];
53
+ /** "password" when the email provider is active; otherwise "sso". */
54
+ method: "sso" | "password";
55
+ /** Sanitised post-sign-in path (see `next-path.ts`). */
56
+ next: string;
57
+ /** Href for the card's "Forgot password?" link (#5963). */
58
+ forgotHref?: string;
59
+ /** "link" (AuthCard's default) unless password reset is unavailable (#6018). */
60
+ forgot?: "link" | "none";
61
+ /** Translated AuthCard labels (resolved server-side by the page). */
62
+ labels: AuthCardLabels;
63
+ /** Subhead shown when no provider is configured. */
64
+ notConfiguredSubhead: string;
65
+ /** Translated error copy. */
66
+ errorLabels: { signInFailed: string; providerFailed: string };
67
+ }
68
+
69
+ export function SignInCard({
70
+ providers,
71
+ method,
72
+ next,
73
+ forgotHref,
74
+ forgot,
75
+ labels,
76
+ notConfiguredSubhead,
77
+ errorLabels,
78
+ }: SignInCardProps): ReactElement {
79
+ const { errors, providerError, launchProvider, submitPassword } =
80
+ useAuthCardFlow(next, {
81
+ providerFailed: errorLabels.providerFailed,
82
+ passwordFailed: errorLabels.signInFailed,
83
+ });
84
+
85
+ const notConfigured = method === "sso" && providers.length === 0;
86
+
87
+ return (
88
+ /*
89
+ * No page framing of its own (#5963): AuthShell owns the brand row, the
90
+ * 400px column and the vertical rhythm. The `min-h-screen items-center
91
+ * justify-center` wrapper this used to carry is exactly the "card centered
92
+ * in empty space, first input at 470px" defect the auth route group fixes.
93
+ */
94
+ <div className="flex flex-col gap-4" data-testid="sign-in-page">
95
+ {method === "sso" && providerError ? (
96
+ <Alert variant="error" data-testid="sign-in-provider-error">
97
+ <AlertDescription>{providerError}</AlertDescription>
98
+ </Alert>
99
+ ) : null}
100
+ <AuthCard
101
+ intent="sign-in"
102
+ method={method}
103
+ footer="none"
104
+ {...(forgotHref !== undefined ? { forgotHref } : {})}
105
+ {...(forgot !== undefined ? { forgot } : {})}
106
+ {...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
107
+ providers={providers.map((provider) => ({
108
+ provider,
109
+ onClick: () => void launchProvider(provider),
110
+ }))}
111
+ onSubmit={(data) =>
112
+ void submitPassword(
113
+ authClient.signIn.email({
114
+ email: data.email,
115
+ password: data.password ?? "",
116
+ callbackURL: next,
117
+ }),
118
+ )
119
+ }
120
+ errors={errors}
121
+ labels={labels}
122
+ />
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `/[locale]/sign-up` — the site's sign-up page (#5963).
3
+ *
4
+ * RSC. Branches on `isSignUpOpen()`:
5
+ * - open → the sign-up form island, wired to Better Auth exactly as
6
+ * `/sign-in`'s island is (providers + method from auth-env).
7
+ * - closed → AuthCard's `invitation` intent: copy only, with the footer's
8
+ * "Back to sign in" link. This is the DEFAULT — `apps/web/auth.ts`
9
+ * does not enable `emailAndPassword`, so a scaffold offers no
10
+ * public sign-up and the page must not advertise one.
11
+ *
12
+ * `AuthShell` (the (auth) group's layout) owns the brand mark and the 400px
13
+ * column, so this page passes no `logo` and no framing of its own.
14
+ */
15
+
16
+ import { localeHref } from "@working-theory/ui";
17
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
18
+ import type { Metadata } from "next";
19
+ import { getTranslations } from "next-intl/server";
20
+ import type { ReactElement } from "react";
21
+
22
+ import { getActiveProviderIds, isSignUpOpen } from "../../../../lib/auth-env";
23
+
24
+ import { SignUpCard, type SignUpProvider } from "./sign-up-card";
25
+
26
+ interface Props {
27
+ params: Promise<{ locale: string }>;
28
+ }
29
+
30
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
31
+ const { locale } = await params;
32
+ const t = await getTranslations({ locale, namespace: "auth" });
33
+ return { title: t("signUpTitle"), description: t("signUpDescription") };
34
+ }
35
+
36
+ export default async function SignUpPage({
37
+ params,
38
+ }: Props): Promise<ReactElement> {
39
+ const { locale } = await params;
40
+ const t = await getTranslations({ locale, namespace: "auth" });
41
+ const signInHref = localeHref(locale, "/sign-in");
42
+
43
+ if (!isSignUpOpen()) {
44
+ const closedLabels: AuthCardLabels = {
45
+ invitationHeading: t("invitationHeading"),
46
+ invitationSubhead: t("invitationSubhead"),
47
+ invitationBody: t("invitationBody"),
48
+ backToSignIn: t("backToSignIn"),
49
+ };
50
+ return (
51
+ <div className="flex flex-col gap-4" data-testid="sign-up-page">
52
+ <AuthCard
53
+ intent="invitation"
54
+ signInHref={signInHref}
55
+ labels={closedLabels}
56
+ />
57
+ </div>
58
+ );
59
+ }
60
+
61
+ const active = getActiveProviderIds();
62
+ // Exactly the OAuth providers auth-env reports — never a default set.
63
+ const providers = active.filter(
64
+ (id): id is SignUpProvider => id === "google" || id === "github",
65
+ );
66
+ const method = active.includes("email") ? "password" : "sso";
67
+
68
+ const labels: AuthCardLabels = {
69
+ signUpHeading: t("signUpHeading"),
70
+ signUpSubhead: t("signUpSubhead"),
71
+ signUpSubmit: t("signUpSubmit"),
72
+ signUpFooterPrompt: t("signUpFooterPrompt"),
73
+ signUpFooterLink: t("signUpFooterLink"),
74
+ ssoPrefix: t("ssoPrefix"),
75
+ google: t("google"),
76
+ github: t("github"),
77
+ email: t("email"),
78
+ password: t("password"),
79
+ orContinue: t("orContinue"),
80
+ };
81
+
82
+ return (
83
+ <SignUpCard
84
+ providers={providers}
85
+ method={method}
86
+ next={localeHref(locale, "/account")}
87
+ signInHref={signInHref}
88
+ labels={labels}
89
+ notConfiguredSubhead={t("signUpNotConfiguredSubhead")}
90
+ errorLabels={{
91
+ signUpFailed: t("signUpFailed"),
92
+ providerFailed: t("providerFailed"),
93
+ }}
94
+ />
95
+ );
96
+ }
@@ -0,0 +1,101 @@
1
+ "use client"; // Better Auth's client calls (signUp.email / signIn.social) + error state
2
+
3
+ /**
4
+ * SignUpCard — the /sign-up page's client island (#5963).
5
+ *
6
+ * The sign-in island's twin: composes `AuthCard` (`intent="sign-up"`) and
7
+ * wires its provider buttons / password form to the Better Auth client via
8
+ * the shared `useAuthCardFlow` hook (#6018 AC-3) — the same hook
9
+ * `SignInCard` next door uses. The page (RSC) decides providers + method from
10
+ * auth-env and hands them in; this component never reads env.
11
+ *
12
+ * Inert until an operator enables `emailAndPassword` server-side — the same
13
+ * documented posture the sign-in island already carries for its password
14
+ * path. The page only renders this island at all when `isSignUpOpen()`.
15
+ *
16
+ * The brand mark is the shell's: no `logo` is passed, or it would render twice.
17
+ *
18
+ * Error surface: `AuthCard`'s `errors` prop is per-field, so a failed provider
19
+ * launch lands on `errors.email` (the social row sits inside the password form
20
+ * beside the email field). The SSO body has no fields, so the same message is
21
+ * rendered as an `Alert` above the card for `method="sso"`.
22
+ */
23
+
24
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
25
+ import { Alert, AlertDescription } from "@working-theory/ui/primitives";
26
+ import type { ReactElement } from "react";
27
+
28
+ import { authClient, useAuthCardFlow } from "../use-auth-card-flow";
29
+
30
+ /** The OAuth providers this page can launch — a subset of AuthCard's. */
31
+ export type SignUpProvider = "google" | "github";
32
+
33
+ export interface SignUpCardProps {
34
+ /** Active OAuth providers, exactly as auth-env reports them. */
35
+ providers: SignUpProvider[];
36
+ /** "password" when the email provider is active; otherwise "sso". */
37
+ method: "sso" | "password";
38
+ /** Post-sign-up path. */
39
+ next: string;
40
+ /** Href for the footer's "Sign in" cross-link. */
41
+ signInHref: string;
42
+ /** Translated AuthCard labels (resolved server-side by the page). */
43
+ labels: AuthCardLabels;
44
+ /** Subhead shown when no provider is configured. */
45
+ notConfiguredSubhead: string;
46
+ /** Translated error copy. */
47
+ errorLabels: { signUpFailed: string; providerFailed: string };
48
+ }
49
+
50
+ export function SignUpCard({
51
+ providers,
52
+ method,
53
+ next,
54
+ signInHref,
55
+ labels,
56
+ notConfiguredSubhead,
57
+ errorLabels,
58
+ }: SignUpCardProps): ReactElement {
59
+ const { errors, providerError, launchProvider, submitPassword } =
60
+ useAuthCardFlow(next, {
61
+ providerFailed: errorLabels.providerFailed,
62
+ passwordFailed: errorLabels.signUpFailed,
63
+ });
64
+
65
+ const notConfigured = method === "sso" && providers.length === 0;
66
+
67
+ return (
68
+ <div className="flex flex-col gap-4" data-testid="sign-up-page">
69
+ {method === "sso" && providerError ? (
70
+ <Alert variant="error" data-testid="sign-up-provider-error">
71
+ <AlertDescription>{providerError}</AlertDescription>
72
+ </Alert>
73
+ ) : null}
74
+ <AuthCard
75
+ intent="sign-up"
76
+ method={method}
77
+ signInHref={signInHref}
78
+ {...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
79
+ providers={providers.map((provider) => ({
80
+ provider,
81
+ onClick: () => void launchProvider(provider),
82
+ }))}
83
+ onSubmit={(data) =>
84
+ // Better Auth requires a `name` on email sign-up; this card collects
85
+ // none, so the address doubles as the display name until the person
86
+ // sets one.
87
+ void submitPassword(
88
+ authClient.signUp.email({
89
+ email: data.email,
90
+ password: data.password ?? "",
91
+ name: data.email,
92
+ callbackURL: next,
93
+ }),
94
+ )
95
+ }
96
+ errors={errors}
97
+ labels={labels}
98
+ />
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,102 @@
1
+ "use client"; // useState + the Better Auth client's browser-side calls
2
+
3
+ /**
4
+ * useAuthCardFlow — the provider-launch + password-submit boilerplate shared
5
+ * by SignInCard and SignUpCard (#6018 AC-3).
6
+ *
7
+ * Both cards wire an `AuthCard`'s provider buttons and password form to the
8
+ * same two moves: launch a Better Auth OAuth provider (a failed launch lands
9
+ * the shared `providerFailed` message on the email field, and — for an
10
+ * SSO-only card with no fields — as a banner above the card) and submit the
11
+ * password form (a failed submit lands a caller-named message on the
12
+ * password field).
13
+ *
14
+ * What differs between sign-in and sign-up is the underlying Better Auth CALL
15
+ * itself — `signIn.email(...)` vs `signUp.email(...)`, the latter needing a
16
+ * `name` — so `submitPassword` takes the call's own promise as an argument
17
+ * rather than owning the choice of which Better Auth method to invoke. The
18
+ * single shared `authClient` this module exports is what each card's
19
+ * `onSubmit` calls that method on.
20
+ */
21
+
22
+ import { createAuthClient } from "better-auth/react";
23
+ import { useState } from "react";
24
+
25
+ export const authClient = createAuthClient();
26
+
27
+ /** The OAuth providers either card can launch. */
28
+ export type AuthProvider = "google" | "github";
29
+
30
+ export interface AuthCardFlowErrorLabels {
31
+ /** Shown on a failed OAuth provider launch (shared by both cards). */
32
+ providerFailed: string;
33
+ /** Shown on a failed password submit — signInFailed or signUpFailed, caller-named. */
34
+ passwordFailed: string;
35
+ }
36
+
37
+ interface FieldErrors {
38
+ email?: string;
39
+ password?: string;
40
+ }
41
+
42
+ /** The shape every Better Auth client call this hook awaits resolves to. */
43
+ type AuthResult = { error?: unknown } | null | undefined;
44
+
45
+ export interface AuthCardFlow {
46
+ /** Per-field form errors, in `AuthCard`'s `errors` prop shape. */
47
+ errors: FieldErrors;
48
+ /** The SSO-body error banner text, or null when none is pending. */
49
+ providerError: string | null;
50
+ /** Launches an OAuth provider via `signIn.social`. */
51
+ launchProvider: (provider: AuthProvider) => Promise<void>;
52
+ /**
53
+ * Awaits the caller's own Better Auth call (`signIn.email(...)` or
54
+ * `signUp.email(...)`) and surfaces a `{ error }` result — or a rejection —
55
+ * on the password field.
56
+ */
57
+ submitPassword: (action: Promise<AuthResult>) => Promise<void>;
58
+ }
59
+
60
+ /**
61
+ * @param next - the post-auth `callbackURL` every provider launch uses.
62
+ * @param errorLabels - translated copy for the two failure cases.
63
+ */
64
+ export function useAuthCardFlow(
65
+ next: string,
66
+ errorLabels: AuthCardFlowErrorLabels,
67
+ ): AuthCardFlow {
68
+ const [errors, setErrors] = useState<FieldErrors>({});
69
+ const [providerError, setProviderError] = useState<string | null>(null);
70
+
71
+ const launchProvider = async (provider: AuthProvider): Promise<void> => {
72
+ setProviderError(null);
73
+ // Better Auth reports a failed launch as `{ error }`; a network/runtime
74
+ // failure rejects. Both land on the same providerFailed message.
75
+ const failed = await authClient.signIn
76
+ .social({ provider, callbackURL: next })
77
+ .then(
78
+ (res: AuthResult) => Boolean(res?.error),
79
+ () => true,
80
+ );
81
+ if (failed) {
82
+ setProviderError(errorLabels.providerFailed);
83
+ setErrors((prev) => ({ ...prev, email: errorLabels.providerFailed }));
84
+ }
85
+ };
86
+
87
+ const submitPassword = async (action: Promise<AuthResult>): Promise<void> => {
88
+ setErrors({});
89
+ // Mirrors launchProvider's own resilience: a `{ error }` result AND a
90
+ // rejected promise both land on the same passwordFailed message, so a
91
+ // network/runtime failure from the caller's signIn.email/signUp.email
92
+ // call surfaces in the form rather than becoming an unhandled rejection
93
+ // (code review, #6018).
94
+ const failed = await action.then(
95
+ (res) => Boolean(res?.error),
96
+ () => true,
97
+ );
98
+ if (failed) setErrors({ password: errorLabels.passwordFailed });
99
+ };
100
+
101
+ return { errors, providerError, launchProvider, submitPassword };
102
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `/[locale]/verify-email` — the check-your-email terminus (#5963, wired by #5962).
3
+ *
4
+ * Two states, both copy-only:
5
+ * - no `?error` → the `sent` intent. What #5963 shipped: this page states
6
+ * what just happened.
7
+ * - `?error=…` → the same expired/invalid-token state `/reset-password`
8
+ * renders. Better Auth's `/verify-email` redirects to its
9
+ * callback with `?error=<code>` when the JWT is expired,
10
+ * malformed or signed with a rotated secret.
11
+ *
12
+ * The verification token's properties are NOT the reset token's, and the copy
13
+ * is written accordingly: it is a signed JWT rather than a row, so it is
14
+ * neither single-use nor revocable — the same link keeps working until it
15
+ * expires. That asymmetry is acceptable precisely because its blast radius is
16
+ * `emailVerified: true` on one row rather than a credential.
17
+ */
18
+
19
+ import { localeHref } from "@working-theory/ui";
20
+ import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
21
+ import type { Metadata } from "next";
22
+ import { getTranslations } from "next-intl/server";
23
+ import type { ReactElement } from "react";
24
+
25
+ interface Props {
26
+ params: Promise<{ locale: string }>;
27
+ searchParams?: Promise<{ error?: string | string[] }>;
28
+ }
29
+
30
+ function firstValue(raw: string | string[] | undefined): string {
31
+ if (Array.isArray(raw)) return raw[0] ?? "";
32
+ return raw ?? "";
33
+ }
34
+
35
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
36
+ const { locale } = await params;
37
+ const t = await getTranslations({ locale, namespace: "auth" });
38
+ return {
39
+ title: t("verifyEmailTitle"),
40
+ description: t("verifyEmailDescription"),
41
+ };
42
+ }
43
+
44
+ export default async function VerifyEmailPage({
45
+ params,
46
+ searchParams,
47
+ }: Props): Promise<ReactElement> {
48
+ const { locale } = await params;
49
+ const t = await getTranslations({ locale, namespace: "auth" });
50
+ const signInHref = localeHref(locale, "/sign-in");
51
+
52
+ const query = searchParams ? await searchParams : {};
53
+ const error = firstValue(query.error);
54
+
55
+ if (error) {
56
+ const labels: AuthCardLabels = {
57
+ invitationHeading: t("verifyExpiredHeading"),
58
+ invitationSubhead: t("verifyExpiredSubhead"),
59
+ invitationBody: t("verifyExpiredBody"),
60
+ backToSignIn: t("backToSignIn"),
61
+ };
62
+ return (
63
+ <div className="flex flex-col gap-4" data-testid="verify-email-page">
64
+ <div data-testid="verify-token-error">
65
+ <AuthCard
66
+ intent="invitation"
67
+ signInHref={signInHref}
68
+ labels={labels}
69
+ />
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ const labels: AuthCardLabels = {
76
+ sentHeading: t("sentHeading"),
77
+ sentSubhead: t("sentSubhead"),
78
+ sentBody: t("sentBody"),
79
+ backToSignIn: t("backToSignIn"),
80
+ };
81
+
82
+ return (
83
+ <div className="flex flex-col gap-4" data-testid="verify-email-page">
84
+ <AuthCard intent="sent" signInHref={signInHref} labels={labels} />
85
+ </div>
86
+ );
87
+ }
@@ -26,7 +26,9 @@ const mockGetSession = jest.fn();
26
26
  // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test of the session-branch can mock without standing up Prisma.
27
27
  jest.mock("../../../../auth", () => ({
28
28
  __esModule: true,
29
- auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
29
+ auth: {
30
+ api: { getSession: (...args: unknown[]) => mockGetSession(...args) },
31
+ },
30
32
  }));
31
33
 
32
34
  const mockGetActiveProviderIds = jest.fn();
@@ -86,6 +88,20 @@ describe("AuthedLayout — the (authed) session guard", () => {
86
88
  expect(screen.queryByTestId("sign-in-gate")).not.toBeInTheDocument();
87
89
  });
88
90
 
91
+ // #5953 AC-1, web half — the app profile applies to the signed-in editor
92
+ // surfaces only; SignInGate stays on the marketing profile.
93
+ it('wraps the signed-in children in a data-profile="app" root', async () => {
94
+ mockGetSession.mockResolvedValue({ user: { id: "user_1" } });
95
+ const element = await AuthedLayout({
96
+ params: Promise.resolve({ locale: "en" }),
97
+ children: <div data-testid="dashboard-content" />,
98
+ });
99
+ render(element);
100
+ const root = screen.getByTestId("authed-profile-root");
101
+ expect(root).toHaveAttribute("data-profile", "app");
102
+ expect(root).toContainElement(screen.getByTestId("dashboard-content"));
103
+ });
104
+
89
105
  it("ignores AILK_AUTH_REQUIRED entirely — never read by the guard", async () => {
90
106
  const original = process.env.AILK_AUTH_REQUIRED;
91
107
  process.env.AILK_AUTH_REQUIRED = "0";
@@ -114,18 +130,14 @@ describe("<SignInGate/> — the dashboard's own front door", () => {
114
130
  });
115
131
 
116
132
  it("renders AuthPanel with the active providers", () => {
117
- render(
118
- <SignInGate labels={GATE_LABELS} signInPath="/sign-in" />,
119
- );
133
+ render(<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />);
120
134
  expect(screen.getByTestId("sign-in-gate")).toBeInTheDocument();
121
135
  expect(screen.getByTestId("auth-panel-card")).toBeInTheDocument();
122
136
  });
123
137
 
124
138
  it("renders an EmptyState when no provider is active", () => {
125
139
  mockGetActiveProviderIds.mockReturnValue([]);
126
- render(
127
- <SignInGate labels={GATE_LABELS} signInPath="/sign-in" />,
128
- );
140
+ render(<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />);
129
141
  expect(screen.getByTestId("sign-in-gate-empty")).toBeInTheDocument();
130
142
  expect(screen.getByText("No providers")).toBeInTheDocument();
131
143
  expect(screen.queryByTestId("auth-panel-card")).not.toBeInTheDocument();
@@ -133,9 +145,7 @@ describe("<SignInGate/> — the dashboard's own front door", () => {
133
145
 
134
146
  it("filters an inactive/unsupported provider id out of the panel", () => {
135
147
  mockGetActiveProviderIds.mockReturnValue(["email"]);
136
- render(
137
- <SignInGate labels={GATE_LABELS} signInPath="/sign-in" />,
138
- );
148
+ render(<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />);
139
149
  // "email" is reported by getActiveProviderIds but is not a supported
140
150
  // AuthPanel provider (google | github) — treated as zero active providers.
141
151
  expect(screen.getByTestId("sign-in-gate-empty")).toBeInTheDocument();
@@ -50,10 +50,16 @@ jest.mock("../../../../../auth", () => ({
50
50
 
51
51
  const mockSignupMe = jest.fn();
52
52
  const mockCheckoutsMe = jest.fn();
53
+ const mockSubscriptionMe = jest.fn();
54
+ const mockSubscriptionPortalSession = jest.fn();
53
55
  jest.mock("@working-theory/api-client", () => ({
54
56
  __esModule: true,
55
57
  waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
56
58
  flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
59
+ subscription: {
60
+ me: (...args: unknown[]) => mockSubscriptionMe(...args),
61
+ portalSession: (...args: unknown[]) => mockSubscriptionPortalSession(...args),
62
+ },
57
63
  }));
58
64
 
59
65
  let mockExperimentKeys: string[] = [];
@@ -139,6 +145,12 @@ beforeEach(() => {
139
145
  key === SIGNUP.experimentKey ? { ok: true, body: SIGNUP } : NOT_FOUND,
140
146
  );
141
147
  mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: CHECKOUTS });
148
+ // Default: a healthy subscriber (no banner) — commerce-disabled builds
149
+ // read identically (a 404), covered explicitly below (D8).
150
+ mockSubscriptionMe.mockReset().mockResolvedValue({
151
+ ok: true,
152
+ body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
153
+ });
142
154
  mockSignOut.mockReset().mockResolvedValue(undefined);
143
155
  mockPush.mockReset();
144
156
  mockRefresh.mockReset();
@@ -258,3 +270,75 @@ describe("AccountPage — sign out", () => {
258
270
  expect(mockSignOut.mock.invocationCallOrder[0]).toBeLessThan(mockPush.mock.invocationCallOrder[0]!);
259
271
  });
260
272
  });
273
+
274
+ describe("AccountPage — subscription status banner (#6038)", () => {
275
+ it("renders the banner for a subscriber with actionRequired: true", async () => {
276
+ mockSubscriptionMe.mockResolvedValue({
277
+ ok: true,
278
+ body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
279
+ });
280
+ await renderPage();
281
+ expect(screen.getByTestId("subscription-status-banner")).toBeInTheDocument();
282
+ });
283
+
284
+ it("renders no banner for a healthy subscriber — AC-1", async () => {
285
+ mockSubscriptionMe.mockResolvedValue({
286
+ ok: true,
287
+ body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: true },
288
+ });
289
+ await renderPage();
290
+ expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
291
+ });
292
+
293
+ it("clears once the recovery lands — AC-3, no dismissal consulted", async () => {
294
+ mockSubscriptionMe.mockResolvedValue({
295
+ ok: true,
296
+ body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: true },
297
+ });
298
+ await renderPage();
299
+ expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
300
+ });
301
+
302
+ it.each([401, 403, 404])(
303
+ "renders no banner and no throw on a %d read (AC-4, the existing SKIPPED_STATUSES constant)",
304
+ async (status) => {
305
+ mockSubscriptionMe.mockResolvedValue({
306
+ ok: false,
307
+ status,
308
+ body: { error: { code: "n/a", message: "n/a" } },
309
+ });
310
+ await renderPage();
311
+ expect(screen.getByTestId("account-page")).toBeInTheDocument();
312
+ expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
313
+ },
314
+ );
315
+
316
+ it("rejects to the route error boundary on a 500 subscription read", async () => {
317
+ mockSubscriptionMe.mockResolvedValue({
318
+ ok: false,
319
+ status: 500,
320
+ body: { error: { code: "internal_error", message: "Boom" } },
321
+ });
322
+ await expect(Page({ params: Promise.resolve({ locale: "en" }) })).rejects.toThrow("Boom");
323
+ });
324
+
325
+ it("renders the action slot when portalAvailable is true", async () => {
326
+ mockSubscriptionMe.mockResolvedValue({
327
+ ok: true,
328
+ body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
329
+ });
330
+ await renderPage();
331
+ const banner = screen.getByTestId("subscription-status-banner");
332
+ expect(within(banner).getByRole("button")).toBeInTheDocument();
333
+ });
334
+
335
+ it("renders no action slot when portalAvailable is false — D3.0a", async () => {
336
+ mockSubscriptionMe.mockResolvedValue({
337
+ ok: true,
338
+ body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: false },
339
+ });
340
+ await renderPage();
341
+ const banner = screen.getByTestId("subscription-status-banner");
342
+ expect(within(banner).queryByRole("button")).not.toBeInTheDocument();
343
+ });
344
+ });