create-ailk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +12248 -0
  3. package/component-catalog.md +193 -68
  4. package/dist/cli.js +29 -1
  5. package/dist/derive-architecture.js +5 -2
  6. package/dist/lib/api-boot-assertion.d.ts +46 -0
  7. package/dist/lib/api-boot-assertion.js +70 -0
  8. package/dist/lib/extract-module-bundle.d.ts +50 -2
  9. package/dist/lib/extract-module-bundle.js +223 -15
  10. package/dist/lib/fetch-guard.d.ts +28 -0
  11. package/dist/lib/fetch-guard.js +101 -0
  12. package/dist/lib/fetch-module.js +4 -0
  13. package/dist/lib/fetch-template.js +4 -0
  14. package/dist/lib/import-graph.d.ts +29 -0
  15. package/dist/lib/import-graph.js +140 -0
  16. package/dist/module-architecture.d.ts +60 -8
  17. package/dist/module-architecture.js +587 -242
  18. package/dist/parse-args.js +6 -2
  19. package/dist/programmatic.d.ts +10 -0
  20. package/dist/programmatic.js +134 -19
  21. package/dist/site-chrome.d.ts +80 -0
  22. package/dist/site-chrome.js +135 -0
  23. package/dist/surfaces.d.ts +21 -6
  24. package/dist/surfaces.js +209 -36
  25. package/dist/template-manifest.d.ts +88 -3
  26. package/dist/template-manifest.js +281 -29
  27. package/dist/template-names.d.ts +1 -1
  28. package/dist/template-names.js +2 -0
  29. package/package.json +10 -2
  30. package/site-templates/_base/ailk-template.json +6 -0
  31. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
  34. package/site-templates/marketing-site/ailk-template.json +11 -0
  35. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  36. package/site-templates/marketing-site/provenance.json +4 -0
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
  39. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
  41. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
  42. package/site-templates/minimalism-site/ailk-template.json +22 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  44. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  49. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  50. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  51. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  53. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  54. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  55. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  56. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  57. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
  58. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  60. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  65. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  68. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  69. package/site-templates/minimalism-site/provenance.json +4 -0
  70. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  71. package/site-templates/saas-product/ailk-template.json +16 -0
  72. package/site-templates/saas-product/provenance.json +10 -0
  73. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  74. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  75. package/site-templates/simple-service/ailk-template.json +10 -0
  76. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  77. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  78. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  79. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  80. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  81. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  82. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  83. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  84. package/site-templates/simple-service/provenance.json +4 -0
  85. package/templates/.claude/skills/README.md +16 -25
  86. package/templates/.env.example +102 -12
  87. package/templates/.github/workflows/ci.yml +48 -0
  88. package/templates/README.md +3 -1
  89. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  90. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  91. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  92. package/templates/apps/api/.env.example +13 -0
  93. package/templates/apps/api/CLAUDE.md +29 -2
  94. package/templates/apps/api/eslint.config.mjs +69 -0
  95. package/templates/apps/api/jest.config.cjs +3 -0
  96. package/templates/apps/api/package.json +7 -2
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  98. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
  99. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  100. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  101. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  102. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
  103. package/templates/apps/api/src/config/index.ts +62 -0
  104. package/templates/apps/api/src/config/modules.ts +7 -3
  105. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
  106. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  107. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  108. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  109. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  110. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  111. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  112. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  113. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  114. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  115. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  116. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  117. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  118. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  119. package/templates/apps/api/src/lib/site.ts +208 -9
  120. package/templates/apps/api/src/lib/stripe.ts +66 -0
  121. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
  122. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
  123. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  124. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  125. package/templates/apps/api/src/openapi/spec.ts +543 -2
  126. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  127. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  128. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  129. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  130. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  131. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  132. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  133. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  134. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  135. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  136. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  137. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  138. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  139. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  140. package/templates/apps/api/src/routes/content/index.ts +13 -73
  141. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  142. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  143. package/templates/apps/api/src/routes/content/search.ts +129 -0
  144. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  145. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  146. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  147. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  148. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  149. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  150. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  151. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  152. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  153. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  154. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  155. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  156. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  157. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  158. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  159. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  160. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  161. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  162. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  163. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  164. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  165. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  166. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  167. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  168. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  169. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  170. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  171. package/templates/apps/api/src/server.ts +521 -55
  172. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  173. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  174. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  175. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  176. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  177. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  178. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  179. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  180. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  181. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  182. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  183. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  184. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  185. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  186. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  187. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  188. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  189. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  190. package/templates/apps/api/src/services/audit-log.ts +668 -0
  191. package/templates/apps/api/src/services/consent.ts +18 -0
  192. package/templates/apps/api/src/services/default-tenant.ts +300 -0
  193. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  194. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  195. package/templates/apps/api/src/services/entitlements.ts +80 -0
  196. package/templates/apps/api/src/services/lead-promotion.ts +160 -0
  197. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  198. package/templates/apps/api/src/services/leads-inbox.ts +284 -0
  199. package/templates/apps/api/src/services/listing-config.ts +27 -9
  200. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  201. package/templates/apps/api/src/services/project-listings.ts +201 -6
  202. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  203. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  204. package/templates/apps/api/src/services/revalidation.ts +173 -0
  205. package/templates/apps/api/src/services/verified-email.ts +71 -0
  206. package/templates/apps/api/src/vercel-handler.ts +75 -35
  207. package/templates/apps/mcp/.env.example +17 -0
  208. package/templates/apps/mcp/CLAUDE.md +58 -25
  209. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  210. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  211. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  212. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  213. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  214. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  215. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  216. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  217. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  218. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  219. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  220. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  221. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  222. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  223. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  224. package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
  225. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  226. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  227. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  228. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  229. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  230. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  231. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  232. package/templates/apps/mcp/eslint.config.mjs +69 -0
  233. package/templates/apps/mcp/jest.config.cjs +13 -0
  234. package/templates/apps/mcp/package.json +8 -2
  235. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  236. package/templates/apps/mcp/src/config/modules.ts +10 -5
  237. package/templates/apps/mcp/src/http-transport.ts +102 -29
  238. package/templates/apps/mcp/src/index.ts +73 -11
  239. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  240. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  241. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  242. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  243. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  244. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  245. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  246. package/templates/apps/mcp/src/server.ts +61 -37
  247. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  248. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  249. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  250. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  251. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  252. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  253. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  254. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  255. package/templates/apps/mcp/src/tools/index.ts +9 -7
  256. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  257. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  258. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  259. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  260. package/templates/apps/web/.env.example +74 -8
  261. package/templates/apps/web/CLAUDE.md +46 -0
  262. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  263. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  264. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  265. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  266. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  267. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  268. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  269. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  270. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  271. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  272. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  273. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  274. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  275. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  276. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  277. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  278. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  279. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  280. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  281. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
  282. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
  283. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  284. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  285. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  286. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  287. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  288. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  289. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  290. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  291. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  292. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  293. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  294. package/templates/apps/web/app/[locale]/layout.tsx +308 -65
  295. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  296. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  297. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  298. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  299. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  300. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  301. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  302. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  303. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  304. package/templates/apps/web/auth.ts +407 -8
  305. package/templates/apps/web/components/google-analytics.tsx +18 -1
  306. package/templates/apps/web/eslint.config.mjs +69 -0
  307. package/templates/apps/web/jest.config.cjs +15 -19
  308. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  309. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  310. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  311. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
  312. package/templates/apps/web/lib/auth-email.ts +318 -0
  313. package/templates/apps/web/lib/auth-env.ts +85 -0
  314. package/templates/apps/web/lib/auth-routes.ts +55 -0
  315. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  316. package/templates/apps/web/lib/nav-config.ts +27 -7
  317. package/templates/apps/web/lib/server-api.ts +1 -1
  318. package/templates/apps/web/lib/site-chrome.ts +29 -0
  319. package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
  320. package/templates/apps/web/messages/en.json +216 -2
  321. package/templates/apps/web/next.config.mjs +20 -0
  322. package/templates/apps/web/package.json +8 -3
  323. package/templates/apps/web/sentry.client.config.ts +131 -0
  324. package/templates/apps/web/sentry.edge.config.ts +72 -0
  325. package/templates/apps/web/sentry.server.config.ts +72 -0
  326. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  327. package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
  328. package/templates/apps/web/src/lib/routes.ts +13 -18
  329. package/templates/brand_assets/README.md +24 -0
  330. package/templates/content/en/AboutPage/about.mdx +160 -15
  331. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  332. package/templates/content/en/ContactPage/contact.mdx +90 -13
  333. package/templates/content/en/FAQPage/faq.mdx +62 -22
  334. package/templates/content/en/HomePage/home.mdx +244 -20
  335. package/templates/content/en/LandingPage/blog.mdx +83 -0
  336. package/templates/content/en/LandingPage/features.mdx +103 -0
  337. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  338. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  339. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  340. package/templates/database/CHANGELOG.md +149 -0
  341. package/templates/database/README.md +10 -3
  342. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  343. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  344. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  345. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  346. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  347. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  348. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  349. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  350. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  351. package/templates/database/__tests__/seed.test.ts +1 -1
  352. package/templates/database/__tests__/tenancy.test.ts +141 -0
  353. package/templates/database/config/schema.prisma +79 -2
  354. package/templates/database/content/schema.prisma +87 -11
  355. package/templates/database/eslint.config.mjs +69 -0
  356. package/templates/database/inbox/schema.prisma +452 -9
  357. package/templates/database/jest.config.cjs +5 -3
  358. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  359. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  360. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  361. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  362. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  363. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  364. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  365. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  366. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  367. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  368. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  369. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  370. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  371. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  372. package/templates/database/package.json +7 -3
  373. package/templates/database/seed.ts +1 -1
  374. package/templates/database/tenancy.ts +86 -1
  375. package/templates/database/tsconfig.json +1 -0
  376. package/templates/package.json +1 -14
  377. package/versions.json +52 -0
  378. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  379. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  380. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  381. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  382. package/templates/apps/api/src/routes/content/create.ts +0 -115
  383. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  384. package/templates/apps/api/src/routes/content/update.ts +0 -112
  385. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  386. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  387. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  388. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  389. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  390. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  391. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  392. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  393. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  394. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  395. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
  396. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * @file dunning-policy.test.ts
3
+ * @description Unit tests for the per-tenant dunning grace policy (#6034
4
+ * §§ D4, D5) — the § D4 resolution table row by row, the key form, the
5
+ * rejecting query, and both exported predicates across every recorded
6
+ * state.
7
+ *
8
+ * External dependency boundaries mocked:
9
+ * - @working-theory/database (via moduleNameMapper → prisma-client.js mock; prisma singleton via __mocks__/prisma.ts)
10
+ * - @working-theory/observability logger
11
+ *
12
+ * `isModuleEnabled` reads `process.env.AILK_ENABLED_MODULES` directly — the
13
+ * REAL env boundary, set/restored per `lead-promotion.test.ts`'s convention,
14
+ * rather than a `jest.mock` of the internal `../../config/modules.js` module
15
+ * (CONTRIBUTING.md § Test code quality forbids mocking an internal module).
16
+ */
17
+
18
+ // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock (Jest requires explicit call for local files)
19
+ jest.mock("../../lib/prisma.js");
20
+
21
+ const mockLoggerInfo = jest.fn();
22
+ const mockLoggerWarn = jest.fn();
23
+ const mockLoggerError = jest.fn();
24
+ jest.mock("@working-theory/observability", () => ({
25
+ logger: {
26
+ info: (...args: unknown[]) => mockLoggerInfo(...args),
27
+ warn: (...args: unknown[]) => mockLoggerWarn(...args),
28
+ error: (...args: unknown[]) => mockLoggerError(...args),
29
+ debug: jest.fn(),
30
+ },
31
+ }));
32
+
33
+ import {
34
+ mockNoWorkspace,
35
+ mockSentinelWorkspaceExists,
36
+ mockWorkspaceExists,
37
+ SENTINEL_WORKSPACE_ID,
38
+ } from "../../lib/__mocks__/prisma.js";
39
+ import { prisma } from "../../lib/prisma.js";
40
+ import {
41
+ DUNNING_GRACE_DAYS_MAX,
42
+ DUNNING_POLICY_DEFAULTS,
43
+ dunningPolicyFor,
44
+ isSubscriberActionRequired,
45
+ isSubscriberRevoked,
46
+ type DunningEntry,
47
+ } from "../dunning-policy.js";
48
+
49
+ const SITE_ID = "site-abc";
50
+ const EXPECTED_KEY = `commerce.dunning_policy:${SITE_ID}`;
51
+
52
+ const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
53
+
54
+ beforeEach(() => {
55
+ jest.clearAllMocks();
56
+ delete process.env.AILK_ENABLED_MODULES;
57
+ // #6335 — the shared prisma mock no longer resolves a workspace for any id
58
+ // asked of it, so this suite states the one it expects. Every case below
59
+ // reaches `dunningPolicyFor(SITE_ID)`, which resolves the alias before it
60
+ // reads `site_settings`; without this the resolution returns null and the
61
+ // read never happens, so the § D4 table would assert nothing.
62
+ mockWorkspaceExists(prisma, SITE_ID);
63
+ });
64
+
65
+ afterAll(() => {
66
+ if (priorEnabledModules === undefined) {
67
+ delete process.env.AILK_ENABLED_MODULES;
68
+ } else {
69
+ process.env.AILK_ENABLED_MODULES = priorEnabledModules;
70
+ }
71
+ });
72
+
73
+ // ── § D4's resolution table, row by row ────────────────────────────────────
74
+
75
+ describe("dunningPolicyFor — the resolution table (§ D4)", () => {
76
+ it("no row → the platform default, logged info 'no_policy_row'", async () => {
77
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue(null);
78
+
79
+ const policy = await dunningPolicyFor(SITE_ID);
80
+
81
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
82
+ expect(mockLoggerInfo).toHaveBeenCalledWith(
83
+ expect.objectContaining({ outcome: "no_policy_row", key: EXPECTED_KEY }),
84
+ expect.any(String),
85
+ );
86
+ expect(mockLoggerError).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it.each([
90
+ ["a string", "not-json"],
91
+ ["an array", [1, 2, 3]],
92
+ ["a number", 5],
93
+ ["null", null],
94
+ ])(
95
+ "stored value is %s (not a JSON object) → default, error 'malformed'",
96
+ async (_label, value) => {
97
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({ value });
98
+
99
+ const policy = await dunningPolicyFor(SITE_ID);
100
+
101
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
102
+ expect(mockLoggerError).toHaveBeenCalledWith(
103
+ expect.objectContaining({ outcome: "malformed" }),
104
+ expect.any(String),
105
+ );
106
+ },
107
+ );
108
+
109
+ it("graceDays absent → default, error 'malformed'", async () => {
110
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
111
+ value: {},
112
+ });
113
+
114
+ const policy = await dunningPolicyFor(SITE_ID);
115
+
116
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
117
+ expect(mockLoggerError).toHaveBeenCalledWith(
118
+ expect.objectContaining({ outcome: "malformed" }),
119
+ expect.any(String),
120
+ );
121
+ });
122
+
123
+ it.each([
124
+ ["a string", "14"],
125
+ ["null", null],
126
+ ["a bool", true],
127
+ ["an array", [14]],
128
+ ["an object", { days: 14 }],
129
+ ])(
130
+ "graceDays is %s → default, error 'malformed'",
131
+ async (_label, graceDays) => {
132
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
133
+ value: { graceDays },
134
+ });
135
+
136
+ const policy = await dunningPolicyFor(SITE_ID);
137
+
138
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
139
+ expect(mockLoggerError).toHaveBeenCalledWith(
140
+ expect.objectContaining({ outcome: "malformed" }),
141
+ expect.any(String),
142
+ );
143
+ },
144
+ );
145
+
146
+ it("graceDays is a magnitude JSON can hold but JS cannot (1e999 → Infinity) → default, error 'non_finite'", async () => {
147
+ // `1e999` is the real-world value (a magnitude `JSON.parse` overflows to
148
+ // `Infinity`); `Number.POSITIVE_INFINITY` is used directly here so the
149
+ // literal itself doesn't trip `no-loss-of-precision` — both reach the
150
+ // exact same `Number.isFinite()` guard.
151
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
152
+ value: { graceDays: Number.POSITIVE_INFINITY },
153
+ });
154
+
155
+ const policy = await dunningPolicyFor(SITE_ID);
156
+
157
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
158
+ expect(mockLoggerError).toHaveBeenCalledWith(
159
+ expect.objectContaining({ outcome: "non_finite" }),
160
+ expect.any(String),
161
+ );
162
+ });
163
+
164
+ it("graceDays negative and finite → clamped to 0, error 'clamped_low'", async () => {
165
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
166
+ value: { graceDays: -5 },
167
+ });
168
+
169
+ const policy = await dunningPolicyFor(SITE_ID);
170
+
171
+ expect(policy).toEqual({ graceDays: 0 });
172
+ expect(mockLoggerError).toHaveBeenCalledWith(
173
+ expect.objectContaining({ outcome: "clamped_low", stored: -5 }),
174
+ expect.any(String),
175
+ );
176
+ });
177
+
178
+ it("graceDays finite and > the ceiling → clamped to the ceiling, error 'clamped_high'", async () => {
179
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
180
+ value: { graceDays: 365 },
181
+ });
182
+
183
+ const policy = await dunningPolicyFor(SITE_ID);
184
+
185
+ expect(policy).toEqual({ graceDays: DUNNING_GRACE_DAYS_MAX });
186
+ expect(mockLoggerError).toHaveBeenCalledWith(
187
+ expect.objectContaining({ outcome: "clamped_high", stored: 365 }),
188
+ expect.any(String),
189
+ );
190
+ });
191
+
192
+ it("graceDays non-integer (e.g. 7.5) → floored, warn 'floored'", async () => {
193
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
194
+ value: { graceDays: 7.5 },
195
+ });
196
+
197
+ const policy = await dunningPolicyFor(SITE_ID);
198
+
199
+ expect(policy).toEqual({ graceDays: 7 });
200
+ expect(mockLoggerWarn).toHaveBeenCalledWith(
201
+ expect.objectContaining({ outcome: "floored", stored: 7.5 }),
202
+ expect.any(String),
203
+ );
204
+ expect(mockLoggerError).not.toHaveBeenCalled();
205
+ });
206
+
207
+ it("the query itself throws → default, error 'read_failed'", async () => {
208
+ (prisma.siteSetting.findUnique as jest.Mock).mockRejectedValue(
209
+ new Error("connection reset"),
210
+ );
211
+
212
+ const policy = await dunningPolicyFor(SITE_ID);
213
+
214
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
215
+ expect(mockLoggerError).toHaveBeenCalledWith(
216
+ expect.objectContaining({ outcome: "read_failed" }),
217
+ expect.any(String),
218
+ );
219
+ });
220
+
221
+ it("a valid integer in [0, DUNNING_GRACE_DAYS_MAX] → that value, no log", async () => {
222
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
223
+ value: { graceDays: 21 },
224
+ });
225
+
226
+ const policy = await dunningPolicyFor(SITE_ID);
227
+
228
+ expect(policy).toEqual({ graceDays: 21 });
229
+ expect(mockLoggerInfo).not.toHaveBeenCalled();
230
+ expect(mockLoggerWarn).not.toHaveBeenCalled();
231
+ expect(mockLoggerError).not.toHaveBeenCalled();
232
+ });
233
+ });
234
+
235
+ // ── The key form ────────────────────────────────────────────────────────────
236
+
237
+ describe("dunningPolicyFor — the key form", () => {
238
+ it("queries exactly commerce.dunning_policy:<siteId>, never an unsuffixed key", async () => {
239
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
240
+ value: { graceDays: 14 },
241
+ });
242
+
243
+ await dunningPolicyFor(SITE_ID);
244
+
245
+ expect(prisma.siteSetting.findUnique).toHaveBeenCalledTimes(1);
246
+ const call = (prisma.siteSetting.findUnique as jest.Mock).mock.calls[0][0];
247
+ // #6374 D3 — `site_settings` is now unique on (workspace_id, key), so
248
+ // the selector is the COMPOUND `workspaceId_key`, not a bare `key`.
249
+ expect(call).toEqual({
250
+ where: { workspaceId_key: { workspaceId: SITE_ID, key: EXPECTED_KEY } },
251
+ select: { value: true },
252
+ });
253
+ expect(call.where.workspaceId_key.key).not.toBe("commerce.dunning_policy");
254
+ });
255
+
256
+ // ── #6374 AC-7 — the key is built from the UNRESOLVED siteId ─────────────
257
+ it("builds `key` from the UNRESOLVED siteId parameter, even when the alias resolves to something else", async () => {
258
+ // The alias `"default"` resolves to the sentinel workspace id. The KEY
259
+ // must still read `commerce.dunning_policy:default` — the alias is
260
+ // embedded in the key STRING because `site_settings` never had a
261
+ // `site_id` column, and the migration's backfill leaves every key
262
+ // verbatim. Rebuilding the key from the RESOLVED id would match no row
263
+ // and hand every seller the built-in default policy, silently. Only the
264
+ // WORKSPACE half is resolved.
265
+ mockSentinelWorkspaceExists(prisma);
266
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
267
+ value: { graceDays: 14 },
268
+ });
269
+
270
+ await dunningPolicyFor("default");
271
+
272
+ const call = (prisma.siteSetting.findUnique as jest.Mock).mock.calls[0][0];
273
+ expect(call.where.workspaceId_key.key).toBe(
274
+ "commerce.dunning_policy:default",
275
+ );
276
+ expect(call.where.workspaceId_key.workspaceId).toBe(SENTINEL_WORKSPACE_ID);
277
+ });
278
+
279
+ it("an alias that resolves to no workspace returns the platform default and reads nothing", async () => {
280
+ mockNoWorkspace(prisma);
281
+
282
+ const policy = await dunningPolicyFor("no-such-site");
283
+
284
+ expect(policy).toEqual(DUNNING_POLICY_DEFAULTS);
285
+ expect(prisma.siteSetting.findUnique).not.toHaveBeenCalled();
286
+ });
287
+
288
+ it("multi-tenant deployments log the deployment-wide-policy warning once per process", async () => {
289
+ process.env.AILK_ENABLED_MODULES = "multi-tenant";
290
+ (prisma.siteSetting.findUnique as jest.Mock).mockResolvedValue({
291
+ value: { graceDays: 14 },
292
+ });
293
+
294
+ await dunningPolicyFor(SITE_ID);
295
+ await dunningPolicyFor(SITE_ID);
296
+
297
+ const deploymentWideCalls = mockLoggerWarn.mock.calls.filter(
298
+ ([fields]) => fields?.outcome === "deployment_wide_policy",
299
+ );
300
+ expect(deploymentWideCalls).toHaveLength(1);
301
+ });
302
+ });
303
+
304
+ // ── The two exported predicates, across every recorded state ───────────────
305
+
306
+ function entry(overrides: Partial<DunningEntry> = {}): DunningEntry {
307
+ return {
308
+ status: "active",
309
+ accessState: "granted",
310
+ dunningArmedAt: null,
311
+ accessRevokesAt: null,
312
+ lastEventAt: 1_700_000_000,
313
+ lastEventId: "evt_base",
314
+ ...overrides,
315
+ };
316
+ }
317
+
318
+ const NOW = Date.parse("2026-09-18T00:00:00.000Z");
319
+
320
+ describe("isSubscriberRevoked / isSubscriberActionRequired — every recorded state", () => {
321
+ it("active — not revoked, no action required", () => {
322
+ const e = entry({ status: "active", accessState: "granted" });
323
+ expect(isSubscriberRevoked(e, NOW)).toBe(false);
324
+ expect(isSubscriberActionRequired(e, NOW)).toBe(false);
325
+ });
326
+
327
+ it("past_due — not revoked, action required", () => {
328
+ const e = entry({
329
+ status: "past_due",
330
+ accessState: "granted",
331
+ dunningArmedAt: "2026-09-10T00:00:00.000Z",
332
+ });
333
+ expect(isSubscriberRevoked(e, NOW)).toBe(false);
334
+ expect(isSubscriberActionRequired(e, NOW)).toBe(true);
335
+ });
336
+
337
+ it("in-grace canceled — not yet revoked, but action IS required (§ 9 SEC-09)", () => {
338
+ const e = entry({
339
+ status: "canceled",
340
+ accessState: "granted",
341
+ dunningArmedAt: "2026-09-10T00:00:00.000Z",
342
+ accessRevokesAt: "2026-09-24T00:00:00.000Z", // in the future relative to NOW
343
+ });
344
+ expect(isSubscriberRevoked(e, NOW)).toBe(false);
345
+ expect(isSubscriberActionRequired(e, NOW)).toBe(true);
346
+ });
347
+
348
+ it("expired canceled — revoked, no further action predicate (already gone)", () => {
349
+ const e = entry({
350
+ status: "canceled",
351
+ accessState: "revoked",
352
+ dunningArmedAt: "2026-08-01T00:00:00.000Z",
353
+ accessRevokesAt: "2026-08-15T00:00:00.000Z", // in the past relative to NOW
354
+ });
355
+ expect(isSubscriberRevoked(e, NOW)).toBe(true);
356
+ expect(isSubscriberActionRequired(e, NOW)).toBe(false);
357
+ });
358
+
359
+ it("accessRevokesAt exactly at `now` is revoked (>=, not >)", () => {
360
+ const revokesAt = "2026-09-18T00:00:00.000Z";
361
+ const e = entry({
362
+ status: "canceled",
363
+ accessState: "granted",
364
+ accessRevokesAt: revokesAt,
365
+ });
366
+ expect(isSubscriberRevoked(e, Date.parse(revokesAt))).toBe(true);
367
+ });
368
+ });