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,273 @@
1
+ /**
2
+ * POST /v1/auth/transactional-email — the signed auth-email relay (#5962 § D3).
3
+ *
4
+ * WHY THIS ROUTE EXISTS. `architecture.yaml`'s `apps/web` node carries an
5
+ * explicit `may_not_import_from` entry for `packages/email` ("email send is
6
+ * server-side only via apps/api"), and `tools/lint-architecture.ts` evaluates
7
+ * deny BEFORE allow — so adding an allow entry would not clear it. Better
8
+ * Auth's password-reset and email-verification callbacks live in `apps/web`
9
+ * and need to send mail. This is where that crosses.
10
+ *
11
+ * API ↔ MCP PARITY — EXEMPT, DECLARED RATHER THAN SKIPPED (§ D19).
12
+ * `CLAUDE.md`'s parity contract binds "every HTTP endpoint an agent could
13
+ * reasonably call" to a 1:1 MCP tool. This is not one: it is an internal,
14
+ * HMAC-signed, machine-to-machine hop whose only caller is `apps/web`'s auth
15
+ * callbacks, and an MCP twin would be an anonymous agent tool that sends
16
+ * password-reset mail to an arbitrary address — a credential-reset oracle on
17
+ * the free surface. The precedent for an `apps/api` route with no twin is
18
+ * `routes/webhooks/stripe.ts`, a signed receiver in the same shape.
19
+ *
20
+ * THE HONEST ACCOUNTING. This endpoint takes a caller-chosen recipient and a
21
+ * caller-chosen URL: it is mail-relay-shaped. What makes it tolerable is the
22
+ * whole set, not the HMAC alone — the secret is deployment-only and never
23
+ * `NEXT_PUBLIC_`; the timestamp is inside the signed material and bounds
24
+ * replay to 300 s; the route sits behind `rateLimitMiddleware` at
25
+ * DEFAULT_PER_IP_PER_MINUTE_STRICT; the per-recipient budget bounds volume per
26
+ * victim; `kind` selects from FIXED templates so a caller chooses neither
27
+ * subject nor copy; and `packages/email`'s CR/LF guard rejects header
28
+ * injection on every string field. Anyone who can sign a body can already read
29
+ * AUTH_EMAIL_WEBHOOK_SECRET off the web deployment — at which point they can
30
+ * read BETTER_AUTH_SECRET too and forge sessions directly.
31
+ *
32
+ * ORDER: 503 → 401 → 400, signature verified over the RAW body BEFORE it is
33
+ * parsed. The reason is `apps/web/app/api/revalidate/route.ts`'s, verbatim: "a
34
+ * 400 before 401 would hand an unauthenticated caller a JSON-parser oracle".
35
+ *
36
+ * PII BOUNDARY (§ D12): a log line carries `userId`, `kind`, `reason` and an
37
+ * error KIND. Never the recipient, never the token, never the assembled link —
38
+ * the link IS the credential, so a log line quoting it hands anyone with log
39
+ * access a live reset for an arbitrary account.
40
+ */
41
+
42
+ import { createHmac, timingSafeEqual } from "node:crypto";
43
+
44
+ import {
45
+ sendEmailVerificationEmail,
46
+ sendPasswordChangedEmail,
47
+ sendPasswordResetEmail,
48
+ } from "@working-theory/email";
49
+ import type { FastifyPluginAsync, FastifyRequest } from "fastify";
50
+
51
+ import {
52
+ consumeRecipientBudget,
53
+ siteVolumeAlert,
54
+ type AuthEmailKind,
55
+ } from "../../lib/auth-email-budget.js";
56
+
57
+ const SIGNATURE_HEADER = "x-ailk-auth-email-signature";
58
+ const TIMESTAMP_HEADER = "x-ailk-auth-email-timestamp";
59
+
60
+ /**
61
+ * A FORMAT guard, not a secret-dependent one — it compares the header against
62
+ * a constant pattern, never against the digest.
63
+ */
64
+ const SIGNATURE_SHAPE = /^sha256=[0-9a-f]{64}$/;
65
+
66
+ /**
67
+ * How stale a signed body may be. #5851's revalidate pair has NO replay window
68
+ * and #5877 § D7 accepts that, because a replayed `revalidateTag` is a no-op.
69
+ * A replayed EMAIL is not a no-op, so this pair closes the gap at the one seam
70
+ * where it matters.
71
+ */
72
+ const MAX_SKEW_SECONDS = 300;
73
+
74
+ const KINDS = new Set<AuthEmailKind>([
75
+ "password-reset",
76
+ "email-verification",
77
+ "password-changed",
78
+ ]);
79
+
80
+ type RelayBody = {
81
+ kind: AuthEmailKind;
82
+ userId: string;
83
+ to: string;
84
+ url?: string;
85
+ expiresInMinutes?: number;
86
+ };
87
+
88
+ /**
89
+ * Line-for-line the repo's shipped verifier shape
90
+ * (`apps/web/app/api/revalidate/route.ts`), with the base string changed to
91
+ * `<timestamp>.<rawBody>` so the timestamp cannot be swapped independently of
92
+ * the body it authenticates. The length guard is required, not decorative:
93
+ * `timingSafeEqual` throws on unequal lengths. The `sha256=` prefix stays
94
+ * inside the compared material so a same-length `sha512=` header is rejected
95
+ * outright rather than compared against a sha256 digest.
96
+ */
97
+ function isValidSignature(
98
+ rawBody: string,
99
+ timestamp: string,
100
+ provided: string,
101
+ secret: string,
102
+ ): boolean {
103
+ const expected = `sha256=${createHmac("sha256", secret)
104
+ .update(`${timestamp}.${rawBody}`, "utf8")
105
+ .digest("hex")}`;
106
+ const expectedBuf = Buffer.from(expected, "utf8");
107
+ const providedBuf = Buffer.from(provided, "utf8");
108
+ if (expectedBuf.length !== providedBuf.length) return false;
109
+ return timingSafeEqual(expectedBuf, providedBuf);
110
+ }
111
+
112
+ /** The parsed body, or null if the payload is not one this relay will send. */
113
+ function parseBody(rawBody: string): RelayBody | null {
114
+ let parsed: unknown;
115
+ try {
116
+ parsed = JSON.parse(rawBody);
117
+ } catch {
118
+ return null;
119
+ }
120
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
121
+ return null;
122
+ }
123
+ const { kind, userId, to, url, expiresInMinutes } = parsed as Record<
124
+ string,
125
+ unknown
126
+ >;
127
+ if (
128
+ typeof kind !== "string" ||
129
+ !KINDS.has(kind as AuthEmailKind) ||
130
+ typeof userId !== "string" ||
131
+ userId.length === 0 ||
132
+ typeof to !== "string" ||
133
+ to.length === 0
134
+ ) {
135
+ return null;
136
+ }
137
+ // The two link-carrying kinds need a url; the notice must NOT have one
138
+ // acted on even if a caller supplies it (§ D17 — the notice carries no
139
+ // credential, and that is enforced by the sender's input type too).
140
+ const needsUrl = kind === "password-reset" || kind === "email-verification";
141
+ if (needsUrl && (typeof url !== "string" || url.length === 0)) return null;
142
+ if (expiresInMinutes !== undefined && typeof expiresInMinutes !== "number") {
143
+ return null;
144
+ }
145
+
146
+ return {
147
+ kind: kind as AuthEmailKind,
148
+ userId,
149
+ to,
150
+ ...(typeof url === "string" ? { url } : {}),
151
+ ...(typeof expiresInMinutes === "number" ? { expiresInMinutes } : {}),
152
+ };
153
+ }
154
+
155
+ export const transactionalEmailRoute: FastifyPluginAsync = async (fastify) => {
156
+ fastify.post(
157
+ "/v1/auth/transactional-email",
158
+ async (request: FastifyRequest, reply) => {
159
+ // Read per request, never at module load: a module-scope const latches
160
+ // the value at cold start, so a secret added to a running deployment
161
+ // would keep returning 503 until the lambda recycles.
162
+ const secret = process.env.AUTH_EMAIL_WEBHOOK_SECRET;
163
+ const fromAddress = process.env.AUTH_EMAIL_FROM;
164
+
165
+ if (!secret || !fromAddress) {
166
+ request.log.warn(
167
+ { reason: "not_configured" },
168
+ "auth-email.not_configured",
169
+ );
170
+ return reply.status(503).send({ error: "not_configured" });
171
+ }
172
+
173
+ // Kept as the signed material — never re-serialized. The route's scope
174
+ // installs a buffer parser for exactly this reason.
175
+ const raw = request.body;
176
+ const rawBody = Buffer.isBuffer(raw)
177
+ ? raw.toString("utf8")
178
+ : typeof raw === "string"
179
+ ? raw
180
+ : "";
181
+
182
+ const provided = request.headers[SIGNATURE_HEADER];
183
+ const timestamp = request.headers[TIMESTAMP_HEADER];
184
+
185
+ if (
186
+ typeof provided !== "string" ||
187
+ typeof timestamp !== "string" ||
188
+ !SIGNATURE_SHAPE.test(provided) ||
189
+ !isValidSignature(rawBody, timestamp, provided, secret)
190
+ ) {
191
+ request.log.warn({ reason: "bad_signature" }, "auth-email.unauthorized");
192
+ return reply.status(401).send({ error: "unauthorized" });
193
+ }
194
+
195
+ // Checked AFTER the signature so an unauthenticated caller learns
196
+ // nothing from the skew answer, and the timestamp it bounds is one the
197
+ // signature already authenticated.
198
+ const tsSeconds = Number(timestamp);
199
+ const skew = Math.abs(Math.floor(Date.now() / 1000) - tsSeconds);
200
+ if (!Number.isFinite(tsSeconds) || skew > MAX_SKEW_SECONDS) {
201
+ request.log.warn({ reason: "stale_timestamp" }, "auth-email.unauthorized");
202
+ return reply.status(401).send({ error: "unauthorized" });
203
+ }
204
+
205
+ const body = parseBody(rawBody);
206
+ if (body === null) {
207
+ request.log.warn(
208
+ { reason: "validation_failed" },
209
+ "auth-email.validation_failed",
210
+ );
211
+ return reply.status(400).send({ error: "validation_failed" });
212
+ }
213
+
214
+ if (siteVolumeAlert()) {
215
+ request.log.warn(
216
+ { reason: "site_volume_high" },
217
+ "auth-email.site-volume-high",
218
+ );
219
+ }
220
+
221
+ // Over budget → skip the send, log one line, and answer exactly what a
222
+ // sent message answers. Suppression is NEVER observable (§ D8c).
223
+ if (!consumeRecipientBudget(body.to, body.kind)) {
224
+ request.log.warn(
225
+ { userId: body.userId, kind: body.kind, reason: "budget_recipient" },
226
+ "auth-email.suppressed",
227
+ );
228
+ return reply.status(202).send({ ok: true });
229
+ }
230
+
231
+ // `kind` selects from FIXED templates. A caller supplies no subject and
232
+ // no copy — only a recipient and, for the two link kinds, a URL.
233
+ const linkInput = {
234
+ to: body.to,
235
+ url: body.url ?? "",
236
+ fromAddress,
237
+ ...(body.expiresInMinutes !== undefined
238
+ ? { expiresInMinutes: body.expiresInMinutes }
239
+ : {}),
240
+ };
241
+ const result =
242
+ body.kind === "password-changed"
243
+ ? await sendPasswordChangedEmail({ to: body.to, fromAddress })
244
+ : body.kind === "email-verification"
245
+ ? await sendEmailVerificationEmail(linkInput)
246
+ : await sendPasswordResetEmail(linkInput);
247
+
248
+ if (!result.ok) {
249
+ // `error.kind` only — `error.message` can quote an address (§ D12).
250
+ request.log.warn(
251
+ {
252
+ userId: body.userId,
253
+ kind: body.kind,
254
+ reason: "send_failed",
255
+ errorKind: result.error.kind,
256
+ },
257
+ "auth-email.send_failed",
258
+ );
259
+ } else {
260
+ request.log.info(
261
+ { userId: body.userId, kind: body.kind, reason: "sent" },
262
+ "auth-email.sent",
263
+ );
264
+ }
265
+
266
+ // 202 on every accepted outcome, a failed send included: the visitor's
267
+ // answer must not distinguish "this address exists" from "it does not",
268
+ // and a send failure is the operator's signal (the log line above), not
269
+ // the caller's.
270
+ return reply.status(202).send({ ok: true });
271
+ },
272
+ );
273
+ };
@@ -20,6 +20,29 @@ const userDataSchema = z.object({
20
20
  value: z.unknown(),
21
21
  });
22
22
 
23
+ /**
24
+ * Key namespaces owned by the server. A caller may never read or write these
25
+ * through /v1/user/data/:key — the rows are written by trusted server paths
26
+ * (e.g. the Stripe webhook's dunning record, #6034 § D12) and are the
27
+ * SUBJECT of decisions about the caller, not the caller's own storage.
28
+ * Without this guard the subject of an access decision is also its author:
29
+ * a subscriber whose renewal has failed could read their own dunning record,
30
+ * PUT it back as a self-granted "active" state, and poison the record's
31
+ * ordering watermark so every future genuine Stripe event for that
32
+ * subscription is rejected as stale forever.
33
+ */
34
+ const RESERVED_USER_DATA_KEY_PREFIXES = ['commerce.', 'system.'] as const;
35
+
36
+ /** #6034 § D12 — a serialized `value` above this bound is refused on PUT. */
37
+ const USER_DATA_MAX_VALUE_BYTES = 64 * 1024;
38
+
39
+ /** Case-insensitive prefix match on the raw key (after Fastify's own param
40
+ * decoding) — `Commerce.` and `COMMERCE.` are refused too. */
41
+ function isReservedUserDataKey(key: string): boolean {
42
+ const lower = key.toLowerCase();
43
+ return RESERVED_USER_DATA_KEY_PREFIXES.some((prefix) => lower.startsWith(prefix));
44
+ }
45
+
23
46
  export const userRoutes: FastifyPluginAsync = async (fastify) => {
24
47
  /**
25
48
  * GET /v1/user/profile
@@ -136,6 +159,13 @@ export const userRoutes: FastifyPluginAsync = async (fastify) => {
136
159
 
137
160
  const { key } = request.params;
138
161
 
162
+ if (isReservedUserDataKey(key)) {
163
+ return reply.status(403).send({
164
+ success: false,
165
+ error: 'reserved-key',
166
+ });
167
+ }
168
+
139
169
  const userData = await prisma.userData.findUnique({
140
170
  where: {
141
171
  userId_key: { userId, key },
@@ -177,9 +207,33 @@ export const userRoutes: FastifyPluginAsync = async (fastify) => {
177
207
  }
178
208
 
179
209
  const { key } = request.params;
210
+
211
+ if (isReservedUserDataKey(key)) {
212
+ return reply.status(403).send({
213
+ success: false,
214
+ error: 'reserved-key',
215
+ });
216
+ }
217
+
180
218
  const { value } = userDataSchema.parse({ key, value: request.body });
181
219
 
182
220
  const jsonValue = value as Prisma.InputJsonValue;
221
+
222
+ // `JSON.stringify` returns `undefined` (not the string "undefined")
223
+ // for an `undefined` input — never pass that to `Buffer.byteLength`,
224
+ // which throws on a non-string. An undefined value serializes to
225
+ // nothing on the wire, so it never exceeds the bound.
226
+ const serializedValue = JSON.stringify(jsonValue);
227
+ if (
228
+ serializedValue !== undefined &&
229
+ Buffer.byteLength(serializedValue) > USER_DATA_MAX_VALUE_BYTES
230
+ ) {
231
+ return reply.status(400).send({
232
+ success: false,
233
+ error: 'value-too-large',
234
+ });
235
+ }
236
+
183
237
  const userData = await prisma.userData.upsert({
184
238
  where: {
185
239
  userId_key: { userId, key },
@@ -225,6 +279,13 @@ export const userRoutes: FastifyPluginAsync = async (fastify) => {
225
279
 
226
280
  const { key } = request.params;
227
281
 
282
+ if (isReservedUserDataKey(key)) {
283
+ return reply.status(403).send({
284
+ success: false,
285
+ error: 'reserved-key',
286
+ });
287
+ }
288
+
228
289
  await prisma.userData.deleteMany({
229
290
  where: { userId, key },
230
291
  });