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
@@ -1,215 +0,0 @@
1
- /**
2
- * @file delete.test.ts
3
- * DELETE /api/content/:type/:slug — D14a Stage 1 Unit B.
4
- *
5
- * Covers: Pro-tier gating (402), missing/stale If-Match (409), happy path
6
- * (204 no body), not-found (404), unknown :type (400).
7
- *
8
- * Issue #398 follow-up: bespoke version-probe + getPageBySlug pre-check
9
- * removed; the adapter throws `AdapterWriteError("not_found" | "conflict")`
10
- * and the hoisted `mapAdapterError` translates it.
11
- *
12
- * Uses the real content adapter backed by a per-test temp dir (Issue #507).
13
- * @working-theory/license stays mocked — it is an external package boundary.
14
- */
15
-
16
- import type { FastifyInstance } from "fastify";
17
-
18
- const mockCheckLicense = jest.fn();
19
-
20
- // @working-theory/license is an external package boundary — stays mocked.
21
- jest.mock("@working-theory/license", () => ({
22
- checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
23
- }));
24
-
25
- import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
26
- import { contentRoutes } from "../index";
27
-
28
- const fixture = setupContentFixture();
29
-
30
- function proLicense() {
31
- return {
32
- tier: "pro",
33
- plan: "pro",
34
- features: [],
35
- expiresAt: null,
36
- revoked: false,
37
- };
38
- }
39
-
40
- function freeLicense() {
41
- return {
42
- tier: "free",
43
- plan: null,
44
- features: [],
45
- expiresAt: null,
46
- revoked: false,
47
- };
48
- }
49
-
50
- describe("DELETE /api/content/:type/:slug", () => {
51
- let app: FastifyInstance;
52
- // The write handlers only register when content-write is enabled
53
- // (routes/content/index.ts's isModuleEnabled('content-write') gate); an
54
- // unset AILK_ENABLED_MODULES no longer defaults to every module on
55
- // (D-44, #5368), so this suite enables it explicitly.
56
- const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
57
-
58
- beforeEach(async () => {
59
- process.env.AILK_ENABLED_MODULES = "content-write";
60
- app = require("fastify")();
61
- await app.register(contentRoutes);
62
- jest.clearAllMocks();
63
- mockCheckLicense.mockReset();
64
- });
65
-
66
- afterAll(() => {
67
- if (priorEnabledModules === undefined) {
68
- delete process.env.AILK_ENABLED_MODULES;
69
- } else {
70
- process.env.AILK_ENABLED_MODULES = priorEnabledModules;
71
- }
72
- });
73
-
74
- afterEach(async () => {
75
- await app.close();
76
- });
77
-
78
- it("returns 204 with no body on success", async () => {
79
- mockCheckLicense.mockResolvedValue(proLicense());
80
-
81
- // First create the page, get its version, then delete it.
82
- const createRes = await app.inject({
83
- method: "POST",
84
- url: "/v1/content/HomePage",
85
- payload: {
86
- type: "HomePage",
87
- slug: "page-to-delete",
88
- title: "Page To Delete",
89
- description: "This page will be deleted",
90
- body: "Goodbye",
91
- },
92
- });
93
- expect(createRes.statusCode).toBe(201);
94
- const { version } = JSON.parse(createRes.body);
95
-
96
- const res = await app.inject({
97
- method: "DELETE",
98
- url: "/v1/content/HomePage/page-to-delete",
99
- headers: { "if-match": version },
100
- });
101
-
102
- expect(res.statusCode).toBe(204);
103
- expect(res.body).toBe("");
104
- });
105
-
106
- it("returns 402 payment_required on free tier", async () => {
107
- mockCheckLicense.mockResolvedValue(freeLicense());
108
-
109
- const res = await app.inject({
110
- method: "DELETE",
111
- url: "/v1/content/HomePage/home",
112
- headers: { "if-match": "v1" },
113
- });
114
-
115
- expect(res.statusCode).toBe(402);
116
- });
117
-
118
- it("returns 402 payment_required when license is revoked even at pro tier", async () => {
119
- mockCheckLicense.mockResolvedValue({
120
- tier: "pro",
121
- plan: "pro",
122
- features: [],
123
- expiresAt: null,
124
- revoked: true,
125
- });
126
-
127
- const res = await app.inject({
128
- method: "DELETE",
129
- url: "/v1/content/HomePage/home",
130
- headers: { "if-match": "v1" },
131
- });
132
-
133
- expect(res.statusCode).toBe(402);
134
- expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
135
- });
136
-
137
- it("returns 409 conflict when If-Match header is missing", async () => {
138
- mockCheckLicense.mockResolvedValue(proLicense());
139
-
140
- const res = await app.inject({
141
- method: "DELETE",
142
- url: "/v1/content/HomePage/home",
143
- });
144
-
145
- expect(res.statusCode).toBe(409);
146
- expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
147
- });
148
-
149
- it("returns 409 conflict when If-Match is empty string", async () => {
150
- mockCheckLicense.mockResolvedValue(proLicense());
151
-
152
- const res = await app.inject({
153
- method: "DELETE",
154
- url: "/v1/content/HomePage/home",
155
- headers: { "if-match": "" },
156
- });
157
-
158
- expect(res.statusCode).toBe(409);
159
- });
160
-
161
- it("returns 409 conflict with currentVersion echoed when adapter signals stale If-Match", async () => {
162
- mockCheckLicense.mockResolvedValue(proLicense());
163
-
164
- // Pass a stale version — the real adapter will reject it with conflict.
165
- const res = await app.inject({
166
- method: "DELETE",
167
- url: "/v1/content/HomePage/home",
168
- headers: { "if-match": "stale-version-that-does-not-match" },
169
- });
170
-
171
- expect(res.statusCode).toBe(409);
172
- const body = JSON.parse(res.body);
173
- expect(body).toMatchObject({ error: "conflict" });
174
- if (body.details) {
175
- expect(body.details).toHaveProperty("currentVersion");
176
- }
177
- });
178
-
179
- it("returns 404 not_found when adapter signals page missing", async () => {
180
- mockCheckLicense.mockResolvedValue(proLicense());
181
-
182
- const res = await app.inject({
183
- method: "DELETE",
184
- url: "/v1/content/HomePage/slug-that-does-not-exist",
185
- headers: { "if-match": "any-version" },
186
- });
187
-
188
- expect(res.statusCode).toBe(404);
189
- expect(JSON.parse(res.body)).toMatchObject({ error: "not_found" });
190
- });
191
-
192
- it("returns 400 validation_failed on unknown :type", async () => {
193
- mockCheckLicense.mockResolvedValue(proLicense());
194
-
195
- const res = await app.inject({
196
- method: "DELETE",
197
- url: "/v1/content/Bogus/home",
198
- headers: { "if-match": "v1" },
199
- });
200
-
201
- expect(res.statusCode).toBe(400);
202
- });
203
-
204
- it("returns 400 validation_failed on disallowed slug shape", async () => {
205
- mockCheckLicense.mockResolvedValue(proLicense());
206
-
207
- const res = await app.inject({
208
- method: "DELETE",
209
- url: "/v1/content/HomePage/..%2Fetc%2Fpasswd",
210
- headers: { "if-match": "v1" },
211
- });
212
-
213
- expect(res.statusCode).toBe(400);
214
- });
215
- });
@@ -1,266 +0,0 @@
1
- /**
2
- * @file update.test.ts
3
- * PATCH /api/content/:type/:slug — D14a Stage 1 Unit B.
4
- *
5
- * Covers: Pro-tier gating (402), missing If-Match (409), stale If-Match (409
6
- * with current version echoed), validation (400), happy path (200 with
7
- * {page, version}), not-found (404), unknown :type (400).
8
- *
9
- * Issue #398 follow-up: bespoke version-probe + getPageBySlug pre-check
10
- * removed; the adapter throws `AdapterWriteError("not_found" | "conflict")`
11
- * and the hoisted `mapAdapterError` translates it.
12
- *
13
- * Uses the real content adapter backed by a per-test temp dir (Issue #507).
14
- * @working-theory/license stays mocked — it is an external package boundary.
15
- */
16
-
17
- import type { FastifyInstance } from "fastify";
18
-
19
- const mockCheckLicense = jest.fn();
20
-
21
- // @working-theory/license is an external package boundary — stays mocked.
22
- jest.mock("@working-theory/license", () => ({
23
- checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
24
- }));
25
-
26
- import { AdapterWriteError } from "@working-theory/content-adapters";
27
-
28
- import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
29
- import { contentRoutes } from "../index";
30
-
31
- const fixture = setupContentFixture();
32
-
33
- function proLicense() {
34
- return {
35
- tier: "pro",
36
- plan: "pro",
37
- features: [],
38
- expiresAt: null,
39
- revoked: false,
40
- };
41
- }
42
-
43
- function freeLicense() {
44
- return {
45
- tier: "free",
46
- plan: null,
47
- features: [],
48
- expiresAt: null,
49
- revoked: false,
50
- };
51
- }
52
-
53
- // Update body must match the slug from the URL.
54
- const validBody = {
55
- type: "HomePage",
56
- slug: "home",
57
- title: "Welcome (updated)",
58
- description: "Updated description",
59
- body: "Updated body content",
60
- };
61
-
62
- describe("PATCH /api/content/:type/:slug", () => {
63
- let app: FastifyInstance;
64
- // The write handlers only register when content-write is enabled
65
- // (routes/content/index.ts's isModuleEnabled('content-write') gate); an
66
- // unset AILK_ENABLED_MODULES no longer defaults to every module on
67
- // (D-44, #5368), so this suite enables it explicitly.
68
- const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
69
-
70
- beforeEach(async () => {
71
- process.env.AILK_ENABLED_MODULES = "content-write";
72
- app = require("fastify")();
73
- await app.register(contentRoutes);
74
- jest.clearAllMocks();
75
- mockCheckLicense.mockReset();
76
- });
77
-
78
- afterAll(() => {
79
- if (priorEnabledModules === undefined) {
80
- delete process.env.AILK_ENABLED_MODULES;
81
- } else {
82
- process.env.AILK_ENABLED_MODULES = priorEnabledModules;
83
- }
84
- });
85
-
86
- afterEach(async () => {
87
- await app.close();
88
- });
89
-
90
- it("returns 200 with {page, version} on success", async () => {
91
- mockCheckLicense.mockResolvedValue(proLicense());
92
-
93
- // The fixture writes "home" in beforeEach; PATCH it with If-Match.
94
- // First POST to get the version header, then PATCH.
95
- // For simplicity: the real MDX adapter returns a version on create/update.
96
- // We pass a version token that matches — the adapter uses content hash.
97
- // Get current version first by creating it via POST:
98
- const createRes = await app.inject({
99
- method: "POST",
100
- url: "/v1/content/HomePage",
101
- payload: {
102
- type: "HomePage",
103
- slug: "update-target",
104
- title: "Original",
105
- description: "Original description",
106
- body: "Original body",
107
- },
108
- });
109
- expect(createRes.statusCode).toBe(201);
110
- const { version } = JSON.parse(createRes.body);
111
-
112
- const res = await app.inject({
113
- method: "PATCH",
114
- url: "/v1/content/HomePage/update-target",
115
- headers: { "if-match": version },
116
- payload: {
117
- type: "HomePage",
118
- slug: "update-target",
119
- title: "Welcome (updated)",
120
- description: "Updated description",
121
- body: "Updated body",
122
- },
123
- });
124
-
125
- expect(res.statusCode).toBe(200);
126
- const body = JSON.parse(res.body);
127
- expect(body).toHaveProperty("page");
128
- expect(body).toHaveProperty("version");
129
- expect(body.page).toMatchObject({ type: "HomePage", slug: "update-target" });
130
- });
131
-
132
- it("returns 402 payment_required on free tier", async () => {
133
- mockCheckLicense.mockResolvedValue(freeLicense());
134
-
135
- const res = await app.inject({
136
- method: "PATCH",
137
- url: "/v1/content/HomePage/home",
138
- headers: { "if-match": "v1" },
139
- payload: validBody,
140
- });
141
-
142
- expect(res.statusCode).toBe(402);
143
- });
144
-
145
- it("returns 402 payment_required when license is revoked even at pro tier", async () => {
146
- mockCheckLicense.mockResolvedValue({
147
- tier: "pro",
148
- plan: "pro",
149
- features: [],
150
- expiresAt: null,
151
- revoked: true,
152
- });
153
-
154
- const res = await app.inject({
155
- method: "PATCH",
156
- url: "/v1/content/HomePage/home",
157
- headers: { "if-match": "v1" },
158
- payload: validBody,
159
- });
160
-
161
- expect(res.statusCode).toBe(402);
162
- expect(JSON.parse(res.body)).toMatchObject({ error: "payment_required" });
163
- });
164
-
165
- it("returns 409 conflict when If-Match header is missing", async () => {
166
- mockCheckLicense.mockResolvedValue(proLicense());
167
-
168
- const res = await app.inject({
169
- method: "PATCH",
170
- url: "/v1/content/HomePage/home",
171
- payload: validBody,
172
- });
173
-
174
- expect(res.statusCode).toBe(409);
175
- expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
176
- });
177
-
178
- it("returns 409 conflict when If-Match is empty string", async () => {
179
- mockCheckLicense.mockResolvedValue(proLicense());
180
-
181
- const res = await app.inject({
182
- method: "PATCH",
183
- url: "/v1/content/HomePage/home",
184
- headers: { "if-match": "" },
185
- payload: validBody,
186
- });
187
-
188
- expect(res.statusCode).toBe(409);
189
- expect(JSON.parse(res.body)).toMatchObject({ error: "conflict" });
190
- });
191
-
192
- it("returns 409 conflict with currentVersion echoed when adapter signals stale If-Match", async () => {
193
- mockCheckLicense.mockResolvedValue(proLicense());
194
-
195
- // Pass a stale version — the real adapter will reject it with a conflict.
196
- const res = await app.inject({
197
- method: "PATCH",
198
- url: "/v1/content/HomePage/home",
199
- headers: { "if-match": "stale-version-that-does-not-match" },
200
- payload: validBody,
201
- });
202
-
203
- // The real adapter rejects a stale If-Match with 409 + currentVersion.
204
- expect(res.statusCode).toBe(409);
205
- const body = JSON.parse(res.body);
206
- expect(body).toMatchObject({ error: "conflict" });
207
- // currentVersion is echoed in details when the adapter provides it.
208
- if (body.details) {
209
- expect(body.details).toHaveProperty("currentVersion");
210
- }
211
- });
212
-
213
- it("returns 404 not_found when adapter signals page missing", async () => {
214
- mockCheckLicense.mockResolvedValue(proLicense());
215
-
216
- const res = await app.inject({
217
- method: "PATCH",
218
- url: "/v1/content/HomePage/slug-that-does-not-exist",
219
- headers: { "if-match": "any-version" },
220
- payload: { ...validBody, slug: "slug-that-does-not-exist" },
221
- });
222
-
223
- expect(res.statusCode).toBe(404);
224
- expect(JSON.parse(res.body)).toMatchObject({ error: "not_found" });
225
- });
226
-
227
- it("returns 400 validation_failed on bad body", async () => {
228
- mockCheckLicense.mockResolvedValue(proLicense());
229
-
230
- const res = await app.inject({
231
- method: "PATCH",
232
- url: "/v1/content/HomePage/home",
233
- headers: { "if-match": "v1" },
234
- payload: { type: "HomePage", slug: "home" }, // missing required fields
235
- });
236
-
237
- expect(res.statusCode).toBe(400);
238
- expect(JSON.parse(res.body)).toMatchObject({ error: "validation_failed" });
239
- });
240
-
241
- it("returns 400 validation_failed on unknown :type", async () => {
242
- mockCheckLicense.mockResolvedValue(proLicense());
243
-
244
- const res = await app.inject({
245
- method: "PATCH",
246
- url: "/v1/content/Bogus/home",
247
- headers: { "if-match": "v1" },
248
- payload: validBody,
249
- });
250
-
251
- expect(res.statusCode).toBe(400);
252
- });
253
-
254
- it("returns 400 when path :type/:slug does not match body type/slug", async () => {
255
- mockCheckLicense.mockResolvedValue(proLicense());
256
-
257
- const res = await app.inject({
258
- method: "PATCH",
259
- url: "/v1/content/HomePage/home",
260
- headers: { "if-match": "v1" },
261
- payload: { ...validBody, slug: "different-slug" },
262
- });
263
-
264
- expect(res.statusCode).toBe(400);
265
- });
266
- });
@@ -1,115 +0,0 @@
1
- // oss: false
2
- /**
3
- * POST /v1/content/:type
4
- *
5
- * Creates a new page of `:type` from a JSON body matching `FrontmatterSchema`
6
- * (from `@working-theory/content-adapters`, the SSOT for content shape) plus a non-empty
7
- * `body` string. Paid-licensed deploys only — free tier returns 402 via the
8
- * `requirePaidTier` preHandler from Stage 0.
9
- *
10
- * Conflict semantics: if a page with this slug already exists, returns 409
11
- * with the existing page's `version` echoed in `details` so the caller can
12
- * pivot to PATCH without a re-read. The conflict signal is delivered by the
13
- * adapter via `AdapterWriteError("conflict", ..., { version })` and translated
14
- * by the hoisted `mapAdapterError` (issue #398) — the route no longer
15
- * pre-checks slug existence (closes the V2-H2 TOCTOU window between the
16
- * read and the write).
17
- *
18
- * Errors all use the canonical `{error, message, details?}` shape from
19
- * `apps/api/src/lib/errors.ts`.
20
- */
21
- import {
22
- FrontmatterSchema,
23
- SCHEMA_ORG_TYPES,
24
- type SchemaOrgType,
25
- } from "@working-theory/content-adapters";
26
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
27
- import { z } from "zod";
28
-
29
- import { getContentAdapter } from "../../lib/content-adapter.js";
30
- import {
31
- adapterErrorResponse,
32
- validationError,
33
- type ErrorBody,
34
- } from "../../lib/errors.js";
35
- import { requirePaidTier } from "../../lib/require-paid-tier.js";
36
-
37
- import { SLUG_PATTERN } from "./slug.js";
38
-
39
- const paramsSchema = z.object({
40
- type: z.enum(SCHEMA_ORG_TYPES),
41
- });
42
-
43
- const writeBodySchema = FrontmatterSchema.extend({ body: z.string().min(1) });
44
-
45
- function send(reply: FastifyReply, status: number, body: ErrorBody) {
46
- return reply.status(status).send(body);
47
- }
48
-
49
- export const createPageRoute: FastifyPluginAsync = async (fastify) => {
50
- fastify.post<{
51
- Params: { type: string };
52
- Body: unknown;
53
- }>(
54
- "/v1/content/:type",
55
-
56
- { preHandler: requirePaidTier },
57
- async (request, reply) => {
58
- const params = paramsSchema.safeParse(request.params);
59
- if (!params.success) {
60
- const err = validationError("Invalid type", {
61
- issues: params.error.issues,
62
- });
63
- return send(reply, err.status, err.body);
64
- }
65
-
66
- const parsed = writeBodySchema.safeParse(request.body);
67
- if (!parsed.success) {
68
- const err = validationError("Invalid request body", {
69
- issues: parsed.error.issues,
70
- });
71
- return send(reply, err.status, err.body);
72
- }
73
-
74
- const { body: pageBody, ...page } = parsed.data;
75
-
76
- if (!SLUG_PATTERN.test(page.slug)) {
77
- const err = validationError("Invalid request body", {
78
- issues: [
79
- {
80
- path: ["slug"],
81
- message:
82
- "slug must match /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (lowercase, digits, hyphens; no leading/trailing hyphen)",
83
- },
84
- ],
85
- });
86
- return send(reply, err.status, err.body);
87
- }
88
-
89
- if (page.type !== params.data.type) {
90
- const err = validationError(
91
- "Body `type` must match URL :type parameter",
92
- { urlType: params.data.type, bodyType: page.type },
93
- );
94
- return send(reply, err.status, err.body);
95
- }
96
-
97
- const { type } = params.data;
98
-
99
- try {
100
- const adapter = getContentAdapter();
101
- const result = await adapter.createPage(
102
- type as SchemaOrgType,
103
- page.slug,
104
- { ...page, body: pageBody },
105
- );
106
- return reply.status(201).send(result);
107
- } catch (error) {
108
- const err = adapterErrorResponse(error, { type, slug: page.slug });
109
- if (err) return send(reply, err.status, err.body);
110
- request.log.error({ err: error }, "[Content] createPage failed");
111
- return reply.status(500).send({ error: "Internal server error" });
112
- }
113
- },
114
- );
115
- };
@@ -1,82 +0,0 @@
1
- // oss: false
2
- /**
3
- * DELETE /v1/content/:type/:slug
4
- *
5
- * Removes the page of `:type` at `:slug`. Paid-licensed deploys only.
6
- *
7
- * Optimistic locking is mandatory: the request MUST carry an `If-Match`
8
- * header containing the `version` token from a prior read. Missing/empty →
9
- * 409. Stale → 409 with `details.currentVersion` echoed.
10
- *
11
- * 404 when the page does not exist. Success responds 204 No Content.
12
- *
13
- * Adapter errors (`AdapterWriteError("not_found" | "conflict" | …)`) are
14
- * translated by the hoisted `mapAdapterError` from `@working-theory/content-adapters`
15
- * (issue #398) — single source of truth across the API + MCP surfaces.
16
- */
17
- import {
18
- SCHEMA_ORG_TYPES,
19
- type SchemaOrgType,
20
- } from "@working-theory/content-adapters";
21
- import type { FastifyPluginAsync, FastifyReply } from "fastify";
22
- import { z } from "zod";
23
-
24
- import { getContentAdapter } from "../../lib/content-adapter.js";
25
- import {
26
- adapterErrorResponse,
27
- conflictError,
28
- validationError,
29
- type ErrorBody,
30
- } from "../../lib/errors.js";
31
- import { requirePaidTier } from "../../lib/require-paid-tier.js";
32
-
33
- import { SLUG_PATTERN } from "./slug.js";
34
-
35
- const paramsSchema = z.object({
36
- type: z.enum(SCHEMA_ORG_TYPES),
37
- slug: z.string().regex(SLUG_PATTERN),
38
- });
39
-
40
- function send(reply: FastifyReply, status: number, body: ErrorBody) {
41
- return reply.status(status).send(body);
42
- }
43
-
44
- export const deletePageRoute: FastifyPluginAsync = async (fastify) => {
45
- fastify.delete<{
46
- Params: { type: string; slug: string };
47
- }>(
48
- "/v1/content/:type/:slug",
49
-
50
- { preHandler: requirePaidTier },
51
- async (request, reply) => {
52
- const params = paramsSchema.safeParse(request.params);
53
- if (!params.success) {
54
- const err = validationError("Invalid type or slug", {
55
- issues: params.error.issues,
56
- });
57
- return send(reply, err.status, err.body);
58
- }
59
-
60
- const ifMatch = request.headers["if-match"];
61
- if (typeof ifMatch !== "string" || ifMatch.length === 0) {
62
- const err = conflictError(
63
- "Missing required If-Match header (version token)",
64
- );
65
- return send(reply, err.status, err.body);
66
- }
67
-
68
- const { type, slug } = params.data;
69
-
70
- try {
71
- const adapter = getContentAdapter();
72
- await adapter.deletePage(type as SchemaOrgType, slug, { ifMatch });
73
- return reply.status(204).send();
74
- } catch (error) {
75
- const err = adapterErrorResponse(error, { type, slug });
76
- if (err) return send(reply, err.status, err.body);
77
- request.log.error({ err: error }, "[Content] deletePage failed");
78
- return reply.status(500).send({ error: "Internal server error" });
79
- }
80
- },
81
- );
82
- };