create-ailk 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/cli.js +54 -5
  5. package/dist/copy.d.ts +14 -0
  6. package/dist/copy.js +38 -1
  7. package/dist/lib/api-boot-assertion.d.ts +46 -0
  8. package/dist/lib/api-boot-assertion.js +70 -0
  9. package/dist/lib/extract-module-bundle.d.ts +20 -1
  10. package/dist/lib/extract-module-bundle.js +64 -5
  11. package/dist/lib/import-graph.d.ts +29 -0
  12. package/dist/lib/import-graph.js +140 -0
  13. package/dist/lib/module-license-gate.d.ts +6 -1
  14. package/dist/lib/module-license-gate.js +6 -1
  15. package/dist/lib/pro-callout.js +1 -1
  16. package/dist/module-architecture.d.ts +1 -0
  17. package/dist/module-architecture.js +253 -127
  18. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  19. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  20. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  21. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  22. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  23. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  24. package/dist/programmatic.js +14 -10
  25. package/dist/surfaces.js +105 -7
  26. package/dist/template-manifest.d.ts +33 -0
  27. package/dist/template-manifest.js +99 -16
  28. package/dist/template-names.d.ts +1 -1
  29. package/dist/template-names.js +1 -0
  30. package/package.json +3 -2
  31. package/site-templates/marketing-site/DESIGN.md +148 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  34. package/site-templates/marketing-site/ailk-template.json +1 -1
  35. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  38. package/site-templates/minimalism-site/DESIGN.md +148 -0
  39. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  41. package/site-templates/minimalism-site/ailk-template.json +2 -1
  42. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  43. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  44. package/site-templates/saas-product/DESIGN.md +148 -0
  45. package/site-templates/simple-service/DESIGN.md +148 -0
  46. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  47. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  48. package/site-templates/simple-service/ailk-template.json +10 -0
  49. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  50. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  51. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  52. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  53. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  54. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  55. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  56. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  57. package/site-templates/simple-service/provenance.json +4 -0
  58. package/templates/.env.example +72 -7
  59. package/templates/CLAUDE.md +2 -3
  60. package/templates/DESIGN.md +155 -0
  61. package/templates/README.md +42 -8
  62. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  63. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  64. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  65. package/templates/apps/api/.env.example +13 -0
  66. package/templates/apps/api/CLAUDE.md +26 -2
  67. package/templates/apps/api/jest.config.cjs +3 -0
  68. package/templates/apps/api/package.json +1 -0
  69. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  70. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  71. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  72. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  73. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  74. package/templates/apps/api/src/config/index.ts +62 -0
  75. package/templates/apps/api/src/config/modules.ts +7 -3
  76. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  77. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  78. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  79. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  81. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  82. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  83. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  84. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  85. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  86. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  87. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  88. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  89. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  90. package/templates/apps/api/src/lib/site.ts +208 -9
  91. package/templates/apps/api/src/lib/stripe.ts +66 -0
  92. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  93. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  94. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  95. package/templates/apps/api/src/openapi/spec.ts +543 -2
  96. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  97. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  98. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  99. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  100. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  101. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  102. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  103. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  104. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  105. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  106. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  107. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  108. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  109. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  110. package/templates/apps/api/src/routes/content/index.ts +13 -73
  111. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  112. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  113. package/templates/apps/api/src/routes/content/search.ts +129 -0
  114. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  115. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  116. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  120. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  121. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  122. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  123. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  124. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  125. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  126. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  127. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  128. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  129. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  130. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  131. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  132. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  133. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  134. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  135. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  136. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  137. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  138. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  139. package/templates/apps/api/src/server.ts +439 -49
  140. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  141. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  142. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  143. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  144. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  145. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  146. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  147. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  148. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  149. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  150. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  151. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  152. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  153. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  154. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  155. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  156. package/templates/apps/api/src/services/audit-log.ts +668 -0
  157. package/templates/apps/api/src/services/consent.ts +18 -0
  158. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  159. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  160. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  161. package/templates/apps/api/src/services/entitlements.ts +80 -0
  162. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  163. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  164. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  165. package/templates/apps/api/src/services/listing-config.ts +27 -9
  166. package/templates/apps/api/src/services/project-listings.ts +201 -6
  167. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  168. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  169. package/templates/apps/api/src/services/revalidation.ts +173 -0
  170. package/templates/apps/api/src/services/verified-email.ts +71 -0
  171. package/templates/apps/api/src/vercel-handler.ts +75 -35
  172. package/templates/apps/mcp/.env.example +17 -0
  173. package/templates/apps/mcp/CLAUDE.md +57 -24
  174. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  175. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  176. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  177. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  178. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  179. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  180. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  181. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  182. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  183. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  184. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  185. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  186. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  187. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  188. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  189. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  190. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  191. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  192. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  193. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  194. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  195. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  196. package/templates/apps/mcp/jest.config.cjs +13 -0
  197. package/templates/apps/mcp/package.json +2 -0
  198. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  199. package/templates/apps/mcp/src/config/modules.ts +10 -5
  200. package/templates/apps/mcp/src/http-transport.ts +102 -29
  201. package/templates/apps/mcp/src/index.ts +73 -11
  202. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  203. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  204. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  205. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  206. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  207. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  208. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  209. package/templates/apps/mcp/src/server.ts +59 -35
  210. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  211. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  212. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  213. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  214. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  215. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  216. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  217. package/templates/apps/mcp/src/tools/index.ts +6 -4
  218. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  219. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  220. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  221. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  222. package/templates/apps/web/.env.example +71 -5
  223. package/templates/apps/web/CLAUDE.md +44 -0
  224. package/templates/apps/web/PRODUCT.md +86 -0
  225. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  226. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  227. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  228. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  229. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  230. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  231. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  232. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  233. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  234. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  235. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  236. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  237. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  238. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  239. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  240. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  241. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  242. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  243. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  244. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  245. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  246. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  247. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  248. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  249. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  250. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  251. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  252. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  253. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  254. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  255. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  256. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  257. package/templates/apps/web/auth.ts +407 -8
  258. package/templates/apps/web/jest.config.cjs +8 -4
  259. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  260. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  261. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  262. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  263. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  264. package/templates/apps/web/lib/auth-email.ts +318 -0
  265. package/templates/apps/web/lib/auth-env.ts +85 -0
  266. package/templates/apps/web/lib/auth-routes.ts +55 -0
  267. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  268. package/templates/apps/web/lib/site-brand.tsx +4 -8
  269. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  270. package/templates/apps/web/messages/en.json +187 -0
  271. package/templates/apps/web/middleware.ts +1 -1
  272. package/templates/apps/web/package.json +2 -1
  273. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  274. package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
  275. package/templates/content/_site.mdx +18 -3
  276. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  277. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  278. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  279. package/templates/database/CHANGELOG.md +149 -0
  280. package/templates/database/README.md +10 -3
  281. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  282. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  283. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  284. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  285. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  286. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  287. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  288. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  289. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  290. package/templates/database/__tests__/tenancy.test.ts +141 -0
  291. package/templates/database/config/schema.prisma +79 -2
  292. package/templates/database/content/schema.prisma +87 -11
  293. package/templates/database/inbox/schema.prisma +452 -9
  294. package/templates/database/jest.config.cjs +5 -3
  295. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  296. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  297. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  298. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  299. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  300. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  301. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  302. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  303. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  304. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  305. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  306. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  307. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  308. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  309. package/templates/database/package.json +3 -2
  310. package/templates/database/tenancy.ts +86 -1
  311. package/templates/database/tsconfig.json +1 -0
  312. package/versions.json +34 -34
  313. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  314. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  315. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  316. package/templates/apps/api/src/routes/content/create.ts +0 -115
  317. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  318. package/templates/apps/api/src/routes/content/update.ts +0 -112
  319. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  320. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  321. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  322. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  323. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  324. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  325. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  326. package/templates/apps/web/public/logo-dark.svg +0 -21
  327. package/templates/apps/web/public/logo-light.svg +0 -21
  328. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  329. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -19,6 +19,7 @@ import { join } from "node:path";
19
19
 
20
20
  import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
21
21
  import { logger } from "@working-theory/observability";
22
+ import { SITE_CATEGORIES } from "@working-theory/schema/categories";
22
23
  import { parse as parseYaml } from "yaml";
23
24
 
24
25
  import { config } from "../config/index.js";
@@ -251,6 +252,29 @@ export function readRegisteredRoutes(source: unknown): RegisteredRoute[] {
251
252
  return Array.isArray(routes) ? (routes as RegisteredRoute[]) : [];
252
253
  }
253
254
 
255
+ /**
256
+ * The `{ ok: false, error: { code, message } }` envelope (apps/api/CLAUDE.md)
257
+ * identity-bound tenant routes answer on 400/401/403 — distinct from the
258
+ * `{ error: string }` shape `ErrorResponse` documents for the anonymous
259
+ * content routes. Shared here so `/v1/schedule/appointments`'s three error
260
+ * responses (#6085) reference one literal rather than three copies.
261
+ */
262
+ const tenantErrorResponseSchema = {
263
+ type: "object",
264
+ required: ["ok", "error"],
265
+ properties: {
266
+ ok: { type: "boolean", enum: [false] },
267
+ error: {
268
+ type: "object",
269
+ required: ["code", "message"],
270
+ properties: {
271
+ code: { type: "string" },
272
+ message: { type: "string" },
273
+ },
274
+ },
275
+ },
276
+ } as const;
277
+
254
278
  /**
255
279
  * Root-index keys this API has already published, by Fastify path (#5314 D6).
256
280
  *
@@ -271,9 +295,13 @@ const CURATED_ROOT_INDEX_KEYS: Record<string, string> = {
271
295
  "/v1/user/profile": "profile",
272
296
  "/v1/user/data/:key": "userData",
273
297
  "/v1/content/types": "contentTypes",
298
+ "/v1/content/search": "contentSearch",
274
299
  "/v1/content/:type": "contentList",
275
300
  "/v1/content/:type/:slug": "contentPage",
276
301
  "/v1/checkout/sessions": "checkoutSessions",
302
+ // #6084 §4.2 — one entry, so the root index publishes a stable
303
+ // `checkoutSession` key rather than a `generatedRootIndexKey` fallback.
304
+ "/v1/checkout/sessions/:id": "checkoutSession",
277
305
  "/v1/billing": "billing",
278
306
  "/v1/billing/portal-session": "billingPortalSession",
279
307
  };
@@ -437,6 +465,9 @@ export function buildOpenApiSpec(
437
465
  registeredRoutes: RegisteredRoute[] = [],
438
466
  ) {
439
467
  const typeEnum = [...SCHEMA_ORG_TYPES];
468
+ // Derived, never hand-listed: widening the registry widens the documented
469
+ // enum with no second edit here.
470
+ const categoryEnum = SITE_CATEGORIES.map((category) => category.slug);
440
471
 
441
472
  return {
442
473
  openapi: "3.1.0",
@@ -544,6 +575,168 @@ export function buildOpenApiSpec(
544
575
  },
545
576
  },
546
577
  },
578
+ // #6084 — the read half, under the SAME commerce gate as the POST
579
+ // above (AC-3). PII-free by construction: the response schema names
580
+ // the D3 six-key allow-list, never a spread.
581
+ "/v1/checkout/sessions/{id}": {
582
+ get: {
583
+ operationId: "getCheckoutSession",
584
+ summary: "Read a Checkout Session's payment status and paid line items",
585
+ description:
586
+ "Reads a Checkout Session's status, payment status, and paid line " +
587
+ "items. Read-only — never creates or mutates a Stripe object. The " +
588
+ "response is PII-free: no customer identity, payment method, or " +
589
+ "metadata field is ever returned.",
590
+ parameters: [
591
+ {
592
+ name: "id",
593
+ in: "path",
594
+ required: true,
595
+ description: "Stripe Checkout Session id (cs_…)",
596
+ schema: { type: "string", pattern: "^cs_[A-Za-z0-9_]{1,250}$" },
597
+ },
598
+ ],
599
+ responses: {
600
+ "200": {
601
+ description: "Checkout Session status, payment status, and paid line items",
602
+ content: {
603
+ "application/json": {
604
+ schema: {
605
+ $ref: "#/components/schemas/CheckoutSessionReadResponse",
606
+ },
607
+ },
608
+ },
609
+ },
610
+ "400": {
611
+ description: "Validation error — the id fails the shape check",
612
+ content: {
613
+ "application/json": {
614
+ schema: {
615
+ $ref: "#/components/schemas/ValidationErrorResponse",
616
+ },
617
+ },
618
+ },
619
+ },
620
+ "404": {
621
+ description: "Unknown session — no Checkout Session matches the given id",
622
+ content: {
623
+ "application/json": {
624
+ schema: {
625
+ $ref: "#/components/schemas/UnknownSessionResponse",
626
+ },
627
+ },
628
+ },
629
+ },
630
+ "500": {
631
+ description:
632
+ "Live-mode forbidden — STRIPE_SECRET_KEY is a live key",
633
+ content: {
634
+ "application/json": {
635
+ schema: {
636
+ $ref: "#/components/schemas/LiveModeForbiddenResponse",
637
+ },
638
+ },
639
+ },
640
+ },
641
+ "502": {
642
+ description:
643
+ "Checkout unavailable — Stripe returned an unexpected error",
644
+ content: {
645
+ "application/json": {
646
+ schema: {
647
+ $ref: "#/components/schemas/CheckoutUnavailableResponse",
648
+ },
649
+ },
650
+ },
651
+ },
652
+ },
653
+ },
654
+ },
655
+ }),
656
+ // Module-conditional (scheduling, #6085 brief H7) — mirrors the
657
+ // `/v1/checkout/sessions` gate immediately above: without it a build
658
+ // with the module off would still advertise the operation while
659
+ // `registeredRoutes` carries no matching entry (surface-drift.test.ts,
660
+ // #5314). The doc comment below deliberately never names the
661
+ // scheduling provider (§ 1.12, § 9 SEC-13) — this file is scanned by
662
+ // `architecture.yaml`'s `cal\.com` forbidden pattern like every other
663
+ // `apps/api/**` file, which is EXCLUDED from it, but the wording
664
+ // itself is written provider-neutral anyway since it documents the
665
+ // AILK-side contract, not the provider's.
666
+ ...(isModuleEnabled("scheduling") && {
667
+ "/v1/schedule/appointments": {
668
+ get: {
669
+ operationId: "listAppointments",
670
+ summary: "List this site's own appointments",
671
+ description:
672
+ "The tenant-guarded read over this site's normalized booking " +
673
+ "rows (#6085). Identity-bound: requires an authenticated " +
674
+ "session carrying an owner|admin role on the active workspace " +
675
+ "— the rows carry attendee contact PII. `window` selects " +
676
+ "upcoming (the default), past, or all; `cursor` is the " +
677
+ "previous page's last row id.",
678
+ parameters: [
679
+ {
680
+ name: "limit",
681
+ in: "query",
682
+ required: false,
683
+ schema: { type: "integer", minimum: 1, maximum: 200, default: 50 },
684
+ },
685
+ {
686
+ name: "cursor",
687
+ in: "query",
688
+ required: false,
689
+ description: "The previous page's last row id.",
690
+ schema: { type: "string" },
691
+ },
692
+ {
693
+ name: "window",
694
+ in: "query",
695
+ required: false,
696
+ schema: {
697
+ type: "string",
698
+ enum: ["upcoming", "past", "all"],
699
+ default: "upcoming",
700
+ },
701
+ },
702
+ ],
703
+ responses: {
704
+ "200": {
705
+ description: "OK",
706
+ content: {
707
+ "application/json": {
708
+ schema: { $ref: "#/components/schemas/AppointmentListResponse" },
709
+ },
710
+ },
711
+ },
712
+ // The `{ ok: false, error: { code, message } }` shape
713
+ // (apps/api/CLAUDE.md) — distinct from the `{ error }` string
714
+ // shape `ErrorResponse` documents for the anonymous content
715
+ // routes, so 400/401/403 each get this inline schema rather
716
+ // than `$ref`ing that one.
717
+ "400": {
718
+ description: "Invalid query parameters, or an unresolvable cursor",
719
+ content: {
720
+ "application/json": { schema: tenantErrorResponseSchema },
721
+ },
722
+ },
723
+ "401": {
724
+ description: "Authentication required",
725
+ content: {
726
+ "application/json": { schema: tenantErrorResponseSchema },
727
+ },
728
+ },
729
+ "403": {
730
+ description: "Forbidden — caller lacks the owner|admin role floor",
731
+ content: {
732
+ "application/json": { schema: tenantErrorResponseSchema },
733
+ },
734
+ },
735
+ "429": { $ref: "#/components/responses/RateLimited" },
736
+ "500": { $ref: "#/components/responses/ServerError" },
737
+ },
738
+ },
739
+ },
547
740
  }),
548
741
  "/v1/leads/submit": {
549
742
  post: {
@@ -658,6 +851,150 @@ export function buildOpenApiSpec(
658
851
  },
659
852
  },
660
853
  },
854
+ "/v1/requests/{id}/confirm": {
855
+ get: {
856
+ operationId: "getRequestConfirmForm",
857
+ summary: "Open a callback request's confirm form",
858
+ description:
859
+ "Renders the site owner's confirm form for one callback request " +
860
+ "(#5970). The `token` query parameter is a one-time, expiring " +
861
+ "credential emailed to the owner when the request landed; it is " +
862
+ "the sole gate on this route, so there is no auth header. This " +
863
+ "operation answers `text/html` rather than JSON — it is a page a " +
864
+ "person opens from a mail client, not an agent surface. A token " +
865
+ "that is tampered with, expired, already used, or valid for a " +
866
+ "different request all answer the same 404. Requires the " +
867
+ "`commerce` licence claim; without it the route answers 404, " +
868
+ "indistinguishable from a build without the module.",
869
+ parameters: [
870
+ {
871
+ name: "id",
872
+ in: "path",
873
+ required: true,
874
+ schema: { type: "string" },
875
+ description: "The callback request's id.",
876
+ },
877
+ {
878
+ name: "token",
879
+ in: "query",
880
+ required: true,
881
+ schema: { type: "string" },
882
+ description: "The one-time confirm credential from the email.",
883
+ },
884
+ ],
885
+ responses: {
886
+ "200": {
887
+ description: "The confirm form",
888
+ content: { "text/html": { schema: { type: "string" } } },
889
+ },
890
+ "404": {
891
+ description:
892
+ "No live request for that id and token — or the module is not " +
893
+ "licensed",
894
+ content: { "text/html": { schema: { type: "string" } } },
895
+ },
896
+ },
897
+ },
898
+ post: {
899
+ operationId: "confirmRequest",
900
+ summary: "Confirm a callback request and send a payment link",
901
+ description:
902
+ "Stamps the confirmed time, creates one Stripe Payment Link on " +
903
+ "the site's own Stripe account, stores its id on the request, and " +
904
+ "emails the customer that hosted URL (#5970). The credential is " +
905
+ "spent in the same statement that stamps the confirmation, so a " +
906
+ "second submission answers 404 and no second Payment Link is ever " +
907
+ "created. The body carries EITHER `amountMinor` + `currency` (a " +
908
+ "price set here and now) OR `lookupKey` (a price already in the " +
909
+ "site's Stripe catalogue) — both, or neither, is a 400 before any " +
910
+ "Stripe call. Sandbox-only: a live Stripe key is refused before " +
911
+ "the credential is claimed.",
912
+ parameters: [
913
+ {
914
+ name: "id",
915
+ in: "path",
916
+ required: true,
917
+ schema: { type: "string" },
918
+ description: "The callback request's id.",
919
+ },
920
+ ],
921
+ requestBody: {
922
+ required: true,
923
+ content: {
924
+ "application/x-www-form-urlencoded": {
925
+ schema: {
926
+ type: "object",
927
+ required: ["token", "confirmedTime", "buyerEmail"],
928
+ properties: {
929
+ token: {
930
+ type: "string",
931
+ description: "The one-time confirm credential.",
932
+ },
933
+ confirmedTime: {
934
+ type: "string",
935
+ description:
936
+ "The agreed time, as the site owner typed it.",
937
+ },
938
+ buyerEmail: {
939
+ type: "string",
940
+ format: "email",
941
+ description: "Where the payment link is sent.",
942
+ },
943
+ amountMinor: {
944
+ type: "integer",
945
+ minimum: 1,
946
+ description:
947
+ "The price in MINOR units. Mutually exclusive with " +
948
+ "lookupKey, and capped by AILK_REQUEST_CONFIRM_MAX_MINOR.",
949
+ },
950
+ currency: {
951
+ type: "string",
952
+ description:
953
+ "Lowercase ISO-4217. Defaults to the site's configured " +
954
+ "currency. Only meaningful beside amountMinor.",
955
+ },
956
+ lookupKey: {
957
+ type: "string",
958
+ description:
959
+ "An existing Stripe price's lookup_key. Mutually " +
960
+ "exclusive with amountMinor.",
961
+ },
962
+ },
963
+ },
964
+ },
965
+ },
966
+ },
967
+ responses: {
968
+ "200": {
969
+ description:
970
+ "Confirmed — the page carries the hosted Stripe payment URL",
971
+ content: { "text/html": { schema: { type: "string" } } },
972
+ },
973
+ "400": {
974
+ description:
975
+ "Invalid form, both or neither price path, an amount above the " +
976
+ "site's ceiling, or a lookup key no active Stripe price matches",
977
+ content: { "text/html": { schema: { type: "string" } } },
978
+ },
979
+ "404": {
980
+ description:
981
+ "No live request for that id and token — or the module is not " +
982
+ "licensed",
983
+ content: { "text/html": { schema: { type: "string" } } },
984
+ },
985
+ "500": {
986
+ description: "Stripe is not configured, or the key is not test-mode",
987
+ content: { "text/html": { schema: { type: "string" } } },
988
+ },
989
+ "502": {
990
+ description:
991
+ "Stripe could not create the Payment Link. The request is " +
992
+ "confirmed; the credential is spent.",
993
+ content: { "text/html": { schema: { type: "string" } } },
994
+ },
995
+ },
996
+ },
997
+ },
661
998
  "/v1/leads/callback": {
662
999
  post: {
663
1000
  operationId: "requestCallback",
@@ -733,6 +1070,70 @@ export function buildOpenApiSpec(
733
1070
  },
734
1071
  },
735
1072
  },
1073
+ "/v1/content/search": {
1074
+ get: {
1075
+ operationId: "searchContent",
1076
+ summary: "Search published pages by keyword",
1077
+ description:
1078
+ "Keyword search across published pages, scored on title, description and body and returned highest-first. The HTTP twin of the `search_content` MCP tool — the same arguments return the same result set. Optional `category` filters to pages whose `categories` frontmatter includes the given LEAF slug from the closed site-category registry; unlike `locale`, a slug outside the registry is a 400 rather than a silent fallback. Optional locale query parameter selects the locale to search; invalid or unknown locales silently fall back to the project's default locale (AC-i18n-07).",
1079
+ parameters: [
1080
+ {
1081
+ name: "q",
1082
+ in: "query",
1083
+ required: true,
1084
+ description:
1085
+ "The search string. 1–200 characters; the ceiling matches the `search_content` MCP tool's own cap.",
1086
+ schema: { type: "string", minLength: 1, maxLength: 200 },
1087
+ },
1088
+ {
1089
+ name: "type",
1090
+ in: "query",
1091
+ required: false,
1092
+ schema: { type: "string", enum: typeEnum },
1093
+ },
1094
+ {
1095
+ name: "category",
1096
+ in: "query",
1097
+ required: false,
1098
+ description:
1099
+ "A leaf slug from the site-category registry. Group slugs are not valid values — a group is always derived from its leaves.",
1100
+ schema: { type: "string", enum: categoryEnum },
1101
+ },
1102
+ {
1103
+ name: "limit",
1104
+ in: "query",
1105
+ required: false,
1106
+ schema: {
1107
+ type: "integer",
1108
+ minimum: 1,
1109
+ maximum: 50,
1110
+ default: 10,
1111
+ },
1112
+ },
1113
+ {
1114
+ name: "locale",
1115
+ in: "query",
1116
+ required: false,
1117
+ description:
1118
+ "BCP-47 locale tag (e.g. `en`, `es`, `zh-CN`). Invalid or unknown locales silently fall back to the project's default locale.",
1119
+ schema: { type: "string", pattern: "^[a-z]{2}(-[A-Z]{2})?$" },
1120
+ },
1121
+ ],
1122
+ responses: {
1123
+ "200": {
1124
+ description: "OK",
1125
+ content: {
1126
+ "application/json": {
1127
+ schema: { $ref: "#/components/schemas/SearchResponse" },
1128
+ },
1129
+ },
1130
+ },
1131
+ "400": { $ref: "#/components/responses/BadRequest" },
1132
+ "429": { $ref: "#/components/responses/RateLimited" },
1133
+ "500": { $ref: "#/components/responses/ServerError" },
1134
+ },
1135
+ },
1136
+ },
736
1137
  "/v1/content/{type}": {
737
1138
  get: {
738
1139
  operationId: "listPagesByType",
@@ -792,7 +1193,7 @@ export function buildOpenApiSpec(
792
1193
  operationId: "getPageBySlug",
793
1194
  summary: "Get a single page by type and slug",
794
1195
  description:
795
- "Returns a single page with resolved schema.org JSON-LD spread at the document root, plus the raw frontmatter under `page`. Optional locale query parameter selects the locale to read from; invalid or unknown locales silently fall back to the project's default locale (AC-i18n-07).",
1196
+ "Returns a single page with resolved schema.org JSON-LD spread at the document root, plus the raw frontmatter under `page`. Optional locale query parameter selects the locale to read from; invalid or unknown locales silently fall back to the project's default locale (AC-i18n-07). Optional `include=body` adds the raw published MDX body under `body`; a deployment whose content adapter cannot serve a raw body answers 400.",
796
1197
  parameters: [
797
1198
  {
798
1199
  name: "type",
@@ -814,6 +1215,14 @@ export function buildOpenApiSpec(
814
1215
  "BCP-47 locale tag (e.g. `en`, `es`, `zh-CN`). Invalid or unknown locales silently fall back to the project's default locale.",
815
1216
  schema: { type: "string", pattern: "^[a-z]{2}(-[A-Z]{2})?$" },
816
1217
  },
1218
+ {
1219
+ name: "include",
1220
+ in: "query",
1221
+ required: false,
1222
+ description:
1223
+ "Opt-in extra field. `body` adds the raw published MDX body under `body`; omitting the parameter serves the base shape unchanged (#5890).",
1224
+ schema: { type: "string", enum: ["body"] },
1225
+ },
817
1226
  ],
818
1227
  responses: {
819
1228
  "200": {
@@ -915,6 +1324,67 @@ export function buildOpenApiSpec(
915
1324
  error: { type: "string", enum: ["checkout-unavailable"] },
916
1325
  },
917
1326
  },
1327
+ // #6084 D3 — the six-key allow-list. Nothing else may appear here;
1328
+ // AC-3's own test asserts set equality against exactly these names.
1329
+ CheckoutSessionReadResponse: {
1330
+ type: "object",
1331
+ required: [
1332
+ "id",
1333
+ "status",
1334
+ "paymentStatus",
1335
+ "currency",
1336
+ "amountTotal",
1337
+ "lineItems",
1338
+ ],
1339
+ properties: {
1340
+ id: {
1341
+ type: "string",
1342
+ description: "The echoed cs_… id, for the buyer to quote to support",
1343
+ },
1344
+ status: {
1345
+ type: "string",
1346
+ enum: ["open", "complete", "expired"],
1347
+ },
1348
+ paymentStatus: {
1349
+ type: "string",
1350
+ enum: ["paid", "unpaid", "no_payment_required"],
1351
+ },
1352
+ currency: {
1353
+ type: "string",
1354
+ description: "ISO-4217 currency code, lowercase",
1355
+ },
1356
+ amountTotal: {
1357
+ type: ["integer", "null"],
1358
+ description: "Session total, in minor currency units",
1359
+ },
1360
+ lineItems: {
1361
+ type: "array",
1362
+ items: {
1363
+ type: "object",
1364
+ required: ["description", "quantity", "amountTotal", "currency"],
1365
+ properties: {
1366
+ description: {
1367
+ type: "string",
1368
+ description: "Stripe's own line-item description (the product name)",
1369
+ },
1370
+ quantity: { type: "integer" },
1371
+ amountTotal: {
1372
+ type: "integer",
1373
+ description: "Line total, in minor currency units",
1374
+ },
1375
+ currency: { type: "string" },
1376
+ },
1377
+ },
1378
+ },
1379
+ },
1380
+ },
1381
+ UnknownSessionResponse: {
1382
+ type: "object",
1383
+ required: ["error"],
1384
+ properties: {
1385
+ error: { type: "string", enum: ["unknown-session"] },
1386
+ },
1387
+ },
918
1388
  LeadSubmitRequest: {
919
1389
  type: "object",
920
1390
  additionalProperties: false,
@@ -1085,7 +1555,32 @@ export function buildOpenApiSpec(
1085
1555
  },
1086
1556
  },
1087
1557
  },
1088
- ListResponse: {
1558
+ SearchResult: {
1559
+ type: "object",
1560
+ required: ["slug", "type", "title", "snippet", "score"],
1561
+ properties: {
1562
+ slug: { type: "string" },
1563
+ type: { type: "string", enum: typeEnum },
1564
+ title: { type: "string" },
1565
+ snippet: { type: "string" },
1566
+ score: { type: "number" },
1567
+ },
1568
+ },
1569
+ SearchResponse: {
1570
+ type: "object",
1571
+ required: ["@context", "@type", "query", "items", "totalItems"],
1572
+ properties: {
1573
+ "@context": { type: "string", enum: ["https://schema.org"] },
1574
+ "@type": { type: "string", enum: ["ItemList"] },
1575
+ query: { type: "string" },
1576
+ items: {
1577
+ type: "array",
1578
+ items: { $ref: "#/components/schemas/SearchResult" },
1579
+ },
1580
+ totalItems: { type: "integer", minimum: 0 },
1581
+ },
1582
+ },
1583
+ ListResponse: {
1089
1584
  type: "object",
1090
1585
  required: ["@context", "@type", "type", "items", "pagination"],
1091
1586
  properties: {
@@ -1106,9 +1601,55 @@ export function buildOpenApiSpec(
1106
1601
  "@context": { type: "string", enum: ["https://schema.org"] },
1107
1602
  "@type": { type: "string" },
1108
1603
  page: { $ref: "#/components/schemas/ContentPage" },
1604
+ body: {
1605
+ type: "string",
1606
+ description:
1607
+ "Raw published MDX body. Present only when the request carried `include=body` (#5890).",
1608
+ },
1109
1609
  },
1110
1610
  additionalProperties: true,
1111
1611
  },
1612
+ Appointment: {
1613
+ type: "object",
1614
+ required: [
1615
+ "id",
1616
+ "startTime",
1617
+ "endTime",
1618
+ "title",
1619
+ "attendeeName",
1620
+ "attendeeEmail",
1621
+ "status",
1622
+ "paid",
1623
+ ],
1624
+ properties: {
1625
+ id: { type: "string" },
1626
+ startTime: { type: "string", format: "date-time" },
1627
+ endTime: { type: "string", format: "date-time" },
1628
+ title: { type: "string", nullable: true },
1629
+ attendeeName: { type: "string" },
1630
+ attendeeEmail: { type: "string", format: "email" },
1631
+ status: { type: "string", enum: ["pending", "confirmed", "cancelled"] },
1632
+ paid: { type: "boolean" },
1633
+ },
1634
+ },
1635
+ AppointmentListResponse: {
1636
+ type: "object",
1637
+ required: ["ok", "data"],
1638
+ properties: {
1639
+ ok: { type: "boolean", enum: [true] },
1640
+ data: {
1641
+ type: "object",
1642
+ required: ["appointments", "nextCursor"],
1643
+ properties: {
1644
+ appointments: {
1645
+ type: "array",
1646
+ items: { $ref: "#/components/schemas/Appointment" },
1647
+ },
1648
+ nextCursor: { type: "string", nullable: true },
1649
+ },
1650
+ },
1651
+ },
1652
+ },
1112
1653
  ErrorResponse: {
1113
1654
  type: "object",
1114
1655
  required: ["error"],
@@ -176,6 +176,54 @@ describe("user routes — functional via dual-mode preHandler", () => {
176
176
  });
177
177
  });
178
178
 
179
+ describe("reserved user-data keys — #6034 § D12", () => {
180
+ it.each([
181
+ ["GET", "commerce.subscription_dunning"],
182
+ ["PUT", "commerce.subscription_dunning"],
183
+ ["DELETE", "commerce.subscription_dunning"],
184
+ ["GET", "Commerce.Subscription_Dunning"],
185
+ ["GET", "system.anything"],
186
+ ])("%s /api/user/data/%s → 403 before any Prisma call", async (method, key) => {
187
+ withBearer("user_bear");
188
+
189
+ app = await buildApp();
190
+ const res = await app.inject({
191
+ method: method as "GET" | "PUT" | "DELETE",
192
+ url: `/v1/user/data/${key}`,
193
+ headers: { authorization: "Bearer tok_live_xx" },
194
+ payload: method === "PUT" ? { status: "active" } : undefined,
195
+ });
196
+
197
+ expect(res.statusCode).toBe(403);
198
+ expect(JSON.parse(res.body)).toEqual({
199
+ success: false,
200
+ error: "reserved-key",
201
+ });
202
+ expect(prisma.userData.findUnique).not.toHaveBeenCalled();
203
+ expect(prisma.userData.upsert).not.toHaveBeenCalled();
204
+ expect(prisma.userData.deleteMany).not.toHaveBeenCalled();
205
+ });
206
+
207
+ it("the unreserved `prefs` path is unchanged", async () => {
208
+ withBearer("user_bear");
209
+ (prisma.userData.findUnique as jest.Mock).mockResolvedValue({
210
+ value: { theme: "dark" },
211
+ });
212
+
213
+ app = await buildApp();
214
+ const res = await app.inject({
215
+ method: "GET",
216
+ url: "/v1/user/data/prefs",
217
+ headers: { authorization: "Bearer tok_live_xx" },
218
+ });
219
+
220
+ expect(res.statusCode).toBe(200);
221
+ expect(prisma.userData.findUnique).toHaveBeenCalledWith({
222
+ where: { userId_key: { userId: "user_bear", key: "prefs" } },
223
+ });
224
+ });
225
+ });
226
+
179
227
  describe("unauthenticated", () => {
180
228
  it.each([
181
229
  ["GET", "/v1/user/profile", undefined],