create-ailk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (533) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +80 -0
  5. package/dist/copy.d.ts +8 -0
  6. package/dist/copy.js +109 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.js +10 -0
  9. package/dist/lib/pro-callout.d.ts +12 -0
  10. package/dist/lib/pro-callout.js +20 -0
  11. package/dist/parse-args.d.ts +13 -0
  12. package/dist/parse-args.js +81 -0
  13. package/dist/programmatic.d.ts +49 -0
  14. package/dist/programmatic.js +89 -0
  15. package/dist/substitute.d.ts +31 -0
  16. package/dist/substitute.js +45 -0
  17. package/dist/validate.d.ts +8 -0
  18. package/dist/validate.js +70 -0
  19. package/package.json +47 -0
  20. package/templates/.claude/.gitkeep +0 -0
  21. package/templates/.claude/agents/web.md +80 -0
  22. package/templates/.claude/rules/architecture.md +62 -0
  23. package/templates/.claude/rules/preserve-existing-style.md +51 -0
  24. package/templates/.claude/skills/README.md +43 -0
  25. package/templates/.claude/skills/add-schema/SKILL.md +270 -0
  26. package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
  27. package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
  28. package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
  29. package/templates/.claude/skills/audit-site/SKILL.md +213 -0
  30. package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
  31. package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
  32. package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
  33. package/templates/.claude/skills/provision-config/SKILL.md +243 -0
  34. package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
  35. package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
  36. package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
  37. package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
  38. package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
  39. package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
  40. package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
  41. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
  42. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
  43. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
  44. package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
  45. package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
  46. package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
  47. package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
  48. package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
  49. package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
  50. package/templates/.env.example +205 -0
  51. package/templates/.gitkeep +2 -0
  52. package/templates/CLAUDE.md +142 -0
  53. package/templates/CONVENTIONS.md +222 -0
  54. package/templates/README.md +110 -0
  55. package/templates/apps/api/.env.example +40 -0
  56. package/templates/apps/api/CLAUDE.md +63 -0
  57. package/templates/apps/api/api/index.ts +144 -0
  58. package/templates/apps/api/jest.config.cjs +74 -0
  59. package/templates/apps/api/package.json +54 -0
  60. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
  61. package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
  62. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
  63. package/templates/apps/api/src/__tests__/index.test.ts +118 -0
  64. package/templates/apps/api/src/__tests__/server.test.ts +207 -0
  65. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
  66. package/templates/apps/api/src/__tests__/setup.ts +24 -0
  67. package/templates/apps/api/src/cli.ts +30 -0
  68. package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
  69. package/templates/apps/api/src/config/index.ts +56 -0
  70. package/templates/apps/api/src/index.ts +32 -0
  71. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
  72. package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
  73. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
  74. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
  75. package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
  76. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
  77. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
  78. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
  79. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
  81. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
  82. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
  83. package/templates/apps/api/src/lib/content-adapter.ts +86 -0
  84. package/templates/apps/api/src/lib/error-serializer.ts +77 -0
  85. package/templates/apps/api/src/lib/errors.ts +95 -0
  86. package/templates/apps/api/src/lib/file-fallback.ts +87 -0
  87. package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
  88. package/templates/apps/api/src/lib/locales.ts +201 -0
  89. package/templates/apps/api/src/lib/outbound-http.ts +122 -0
  90. package/templates/apps/api/src/lib/prisma.ts +21 -0
  91. package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
  92. package/templates/apps/api/src/lib/stripe.ts +30 -0
  93. package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
  94. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
  95. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
  96. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
  97. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
  98. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
  99. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
  100. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
  101. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
  102. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
  103. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
  104. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
  105. package/templates/apps/api/src/middleware/auth.ts +246 -0
  106. package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
  107. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
  108. package/templates/apps/api/src/openapi/index.ts +12 -0
  109. package/templates/apps/api/src/openapi/spec.ts +408 -0
  110. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
  111. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
  112. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
  113. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
  114. package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
  115. package/templates/apps/api/src/routes/auth/user.ts +244 -0
  116. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
  117. package/templates/apps/api/src/routes/checkout/index.ts +13 -0
  118. package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
  119. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
  120. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
  121. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
  122. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
  123. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
  124. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
  125. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
  126. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
  127. package/templates/apps/api/src/routes/content/create.ts +114 -0
  128. package/templates/apps/api/src/routes/content/delete.ts +81 -0
  129. package/templates/apps/api/src/routes/content/get-page.ts +98 -0
  130. package/templates/apps/api/src/routes/content/index.ts +32 -0
  131. package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
  132. package/templates/apps/api/src/routes/content/list-types.ts +19 -0
  133. package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
  134. package/templates/apps/api/src/routes/content/slug.ts +9 -0
  135. package/templates/apps/api/src/routes/content/update.ts +111 -0
  136. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
  137. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
  138. package/templates/apps/api/src/routes/leads/index.ts +130 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
  140. package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
  141. package/templates/apps/api/src/routes/webhooks/README.md +57 -0
  142. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
  143. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
  144. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
  145. package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
  146. package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
  147. package/templates/apps/api/src/server.ts +157 -0
  148. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
  149. package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
  150. package/templates/apps/api/src/services/lead-routing.ts +338 -0
  151. package/templates/apps/api/tsconfig.json +34 -0
  152. package/templates/apps/api/vercel.json +17 -0
  153. package/templates/apps/mcp/.env.example +52 -0
  154. package/templates/apps/mcp/CLAUDE.md +84 -0
  155. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
  156. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
  157. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
  158. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
  159. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
  160. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
  161. package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
  162. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
  163. package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
  164. package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
  165. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
  166. package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
  167. package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
  168. package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
  169. package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
  170. package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
  171. package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
  172. package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
  173. package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
  174. package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
  175. package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
  176. package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
  177. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
  178. package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
  179. package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
  180. package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
  181. package/templates/apps/mcp/__tests__/server.test.ts +115 -0
  182. package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
  183. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
  184. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
  185. package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
  186. package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
  187. package/templates/apps/mcp/jest.config.cjs +82 -0
  188. package/templates/apps/mcp/package.json +37 -0
  189. package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
  190. package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
  191. package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
  192. package/templates/apps/mcp/src/abuse/index.ts +111 -0
  193. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
  194. package/templates/apps/mcp/src/http-transport.ts +303 -0
  195. package/templates/apps/mcp/src/index.ts +67 -0
  196. package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
  197. package/templates/apps/mcp/src/lib/errors.ts +61 -0
  198. package/templates/apps/mcp/src/lib/locales.ts +171 -0
  199. package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
  200. package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
  201. package/templates/apps/mcp/src/server.ts +139 -0
  202. package/templates/apps/mcp/src/tools/audit.ts +103 -0
  203. package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
  204. package/templates/apps/mcp/src/tools/create_page.ts +95 -0
  205. package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
  206. package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
  207. package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
  208. package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
  209. package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
  210. package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
  211. package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
  212. package/templates/apps/mcp/src/tools/index.ts +125 -0
  213. package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
  214. package/templates/apps/mcp/src/tools/purchase.ts +89 -0
  215. package/templates/apps/mcp/src/tools/read_content.ts +61 -0
  216. package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
  217. package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
  218. package/templates/apps/mcp/src/tools/search_content.ts +59 -0
  219. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
  220. package/templates/apps/mcp/src/tools/update_page.ts +91 -0
  221. package/templates/apps/mcp/tsconfig.json +19 -0
  222. package/templates/apps/web/.env.example +47 -0
  223. package/templates/apps/web/CLAUDE.md +81 -0
  224. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
  225. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
  226. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
  227. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
  228. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
  229. package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
  230. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
  231. package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
  232. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
  233. package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
  234. package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
  235. package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
  236. package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
  237. package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
  238. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
  239. package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
  240. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
  241. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
  242. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
  243. package/templates/apps/web/__tests__/page.test.tsx +230 -0
  244. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
  245. package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
  246. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
  247. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
  248. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
  249. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
  250. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
  251. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
  252. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
  253. package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
  254. package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
  255. package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
  256. package/templates/apps/web/__tests__/setup.ts +82 -0
  257. package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
  258. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
  259. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
  260. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
  261. package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
  262. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
  263. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
  264. package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
  266. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
  267. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
  268. package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
  269. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
  270. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
  271. package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
  272. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
  273. package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
  274. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
  275. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
  276. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
  277. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
  278. package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
  279. package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
  280. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
  281. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
  282. package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
  283. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
  284. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
  285. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
  286. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
  287. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
  288. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
  289. package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
  290. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
  291. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
  292. package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
  293. package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
  294. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
  295. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
  296. package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
  297. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
  298. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
  299. package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
  300. package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
  301. package/templates/apps/web/app/[locale]/layout.tsx +241 -0
  302. package/templates/apps/web/app/[locale]/page.tsx +41 -0
  303. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
  304. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
  305. package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
  306. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
  307. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
  308. package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
  309. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
  310. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
  311. package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
  312. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
  313. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
  314. package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
  315. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
  316. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
  317. package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
  318. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
  319. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
  320. package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
  321. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
  322. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
  323. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
  324. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
  325. package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
  326. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
  327. package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
  328. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
  329. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
  330. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
  331. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
  332. package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
  333. package/templates/apps/web/app/error-fallback.tsx +75 -0
  334. package/templates/apps/web/app/error.tsx +23 -0
  335. package/templates/apps/web/app/layout.tsx +18 -0
  336. package/templates/apps/web/app/llms.txt/route.ts +101 -0
  337. package/templates/apps/web/app/not-found.tsx +48 -0
  338. package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
  339. package/templates/apps/web/app/og/route.tsx +135 -0
  340. package/templates/apps/web/app/robots.txt/route.ts +36 -0
  341. package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
  342. package/templates/apps/web/auth.ts +141 -0
  343. package/templates/apps/web/components/ThemeToggle.tsx +60 -0
  344. package/templates/apps/web/components/google-analytics.tsx +34 -0
  345. package/templates/apps/web/content/en/blog/index.mdx +15 -0
  346. package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
  347. package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
  348. package/templates/apps/web/i18n/request.ts +36 -0
  349. package/templates/apps/web/jest.config.cjs +100 -0
  350. package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
  351. package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
  352. package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
  353. package/templates/apps/web/lib/auth-env.ts +66 -0
  354. package/templates/apps/web/lib/blog-list-data.ts +95 -0
  355. package/templates/apps/web/lib/blog-post-data.ts +79 -0
  356. package/templates/apps/web/lib/nav-config.ts +13 -0
  357. package/templates/apps/web/lib/observability/better-stack.ts +27 -0
  358. package/templates/apps/web/lib/og.ts +23 -0
  359. package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
  360. package/templates/apps/web/lib/source.ts +34 -0
  361. package/templates/apps/web/messages/en.json +131 -0
  362. package/templates/apps/web/middleware.ts +80 -0
  363. package/templates/apps/web/next.config.mjs +167 -0
  364. package/templates/apps/web/package.json +62 -0
  365. package/templates/apps/web/public/.well-known/security.txt +17 -0
  366. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  367. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  368. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  369. package/templates/apps/web/public/favicon-16x16.png +0 -0
  370. package/templates/apps/web/public/favicon-32x32.png +0 -0
  371. package/templates/apps/web/public/favicon.ico +0 -0
  372. package/templates/apps/web/public/favicon.svg +4 -0
  373. package/templates/apps/web/public/logo-dark.svg +21 -0
  374. package/templates/apps/web/public/logo-light.svg +21 -0
  375. package/templates/apps/web/public/logomark-dark.svg +4 -0
  376. package/templates/apps/web/public/logomark-light.svg +4 -0
  377. package/templates/apps/web/public/og-default.png +0 -0
  378. package/templates/apps/web/public/site.webmanifest +19 -0
  379. package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
  380. package/templates/apps/web/security-headers.config.ts +132 -0
  381. package/templates/apps/web/sentry.client.config.ts +27 -0
  382. package/templates/apps/web/sentry.edge.config.ts +15 -0
  383. package/templates/apps/web/sentry.server.config.ts +20 -0
  384. package/templates/apps/web/source.config.ts +78 -0
  385. package/templates/apps/web/src/components/SiteNav.tsx +21 -0
  386. package/templates/apps/web/src/lib/routes.ts +37 -0
  387. package/templates/apps/web/tsconfig.json +35 -0
  388. package/templates/apps/web/types/messages.d.ts +19 -0
  389. package/templates/content/.gitkeep +0 -0
  390. package/templates/content/CLAUDE.md +85 -0
  391. package/templates/content/_site.mdx +38 -0
  392. package/templates/content/ar/HomePage/home.mdx +123 -0
  393. package/templates/content/ar/_site.mdx +35 -0
  394. package/templates/content/de/_site.mdx +29 -0
  395. package/templates/content/en/.gitkeep +0 -0
  396. package/templates/content/en/AboutPage/about.mdx +19 -0
  397. package/templates/content/en/AboutPage/about.schema.json +82 -0
  398. package/templates/content/en/BookCall/book-call.mdx +19 -0
  399. package/templates/content/en/BookCall/book-call.schema.json +121 -0
  400. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
  401. package/templates/content/en/CareersList/careers.mdx +24 -0
  402. package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
  403. package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
  404. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
  405. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
  406. package/templates/content/en/ClientsList/clients.mdx +38 -0
  407. package/templates/content/en/ContactPage/contact.mdx +19 -0
  408. package/templates/content/en/ContactPage/contact.schema.json +100 -0
  409. package/templates/content/en/ContactPoint/primary.mdx +14 -0
  410. package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
  411. package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
  412. package/templates/content/en/CoursesList/courses.mdx +22 -0
  413. package/templates/content/en/DownloadsList/downloads.mdx +22 -0
  414. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
  415. package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
  416. package/templates/content/en/EventsList/events.mdx +26 -0
  417. package/templates/content/en/FAQPage/faq.mdx +420 -0
  418. package/templates/content/en/FAQPage/faq.schema.json +231 -0
  419. package/templates/content/en/GlossaryList/glossary.mdx +19 -0
  420. package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
  421. package/templates/content/en/HomePage/home.mdx +312 -0
  422. package/templates/content/en/HomePage/home.schema.json +88 -0
  423. package/templates/content/en/HomePage/launch-service.mdx +475 -0
  424. package/templates/content/en/HomePage/launch-service.schema.json +138 -0
  425. package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
  426. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
  427. package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
  428. package/templates/content/en/Integration/github-actions.mdx +31 -0
  429. package/templates/content/en/Integration/openai-api.mdx +26 -0
  430. package/templates/content/en/Integration/vercel.mdx +32 -0
  431. package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
  432. package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
  433. package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
  434. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
  435. package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
  436. package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
  437. package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
  438. package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
  439. package/templates/content/en/LandingPage/developers.mdx +19 -0
  440. package/templates/content/en/LandingPage/developers.schema.json +109 -0
  441. package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
  442. package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
  443. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
  444. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
  445. package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
  446. package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
  447. package/templates/content/en/LandingPage/platform.mdx +369 -0
  448. package/templates/content/en/LandingPage/platform.schema.json +166 -0
  449. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
  450. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
  451. package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
  452. package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
  453. package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
  454. package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
  455. package/templates/content/en/Organization/site-owner.mdx +16 -0
  456. package/templates/content/en/Partner/prisma.mdx +31 -0
  457. package/templates/content/en/Partner/supabase.mdx +30 -0
  458. package/templates/content/en/Partner/vercel.mdx +28 -0
  459. package/templates/content/en/PartnersList/partners.mdx +22 -0
  460. package/templates/content/en/Person/founder.mdx +18 -0
  461. package/templates/content/en/Pricing/pricing.mdx +538 -0
  462. package/templates/content/en/Pricing/pricing.schema.json +209 -0
  463. package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
  464. package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
  465. package/templates/content/en/Product/ailk.mdx +19 -0
  466. package/templates/content/en/Product/ailk.schema.json +94 -0
  467. package/templates/content/en/Product/api-launch-kit.mdx +26 -0
  468. package/templates/content/en/Product/launch-kit.mdx +28 -0
  469. package/templates/content/en/ProductsList/products.mdx +26 -0
  470. package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
  471. package/templates/content/en/ProjectsList/projects.mdx +25 -0
  472. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
  473. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
  474. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
  475. package/templates/content/en/ResourcesHub/resources.mdx +22 -0
  476. package/templates/content/en/Service/consulting.mdx +22 -0
  477. package/templates/content/en/Service/implementation.mdx +30 -0
  478. package/templates/content/en/Service/launch-service.mdx +232 -0
  479. package/templates/content/en/Service/launch.mdx +228 -0
  480. package/templates/content/en/ServicesList/services.mdx +84 -0
  481. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
  482. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
  483. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
  484. package/templates/content/en/TeamList/team.mdx +63 -0
  485. package/templates/content/en/TeamMember/founder.mdx +72 -0
  486. package/templates/content/en/TermsPage/terms.mdx +19 -0
  487. package/templates/content/en/TermsPage/terms.schema.json +99 -0
  488. package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
  489. package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
  490. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
  491. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
  492. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
  493. package/templates/content/en-XA/AboutPage/about.mdx +8 -0
  494. package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
  495. package/templates/content/en-XA/HomePage/home.mdx +8 -0
  496. package/templates/content/es/_site.mdx +30 -0
  497. package/templates/content/fr/_site.mdx +30 -0
  498. package/templates/content/ja/_site.mdx +30 -0
  499. package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
  500. package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
  501. package/templates/content/pt-BR/HomePage/home.mdx +23 -0
  502. package/templates/database/CHANGELOG.md +5 -0
  503. package/templates/database/README.md +234 -0
  504. package/templates/database/__tests__/user-cascade.test.ts +125 -0
  505. package/templates/database/auth/.gitkeep +0 -0
  506. package/templates/database/config/.gitkeep +0 -0
  507. package/templates/database/content/.gitkeep +0 -0
  508. package/templates/database/inbox/.gitkeep +0 -0
  509. package/templates/database/inbox/README.md +3 -0
  510. package/templates/database/inbox/__tests__/schema.test.ts +40 -0
  511. package/templates/database/inbox/schema.prisma +52 -0
  512. package/templates/database/jest.config.cjs +29 -0
  513. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
  514. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
  515. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
  516. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
  517. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
  518. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
  519. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
  520. package/templates/database/migrations/migration_lock.toml +3 -0
  521. package/templates/database/ops/.gitkeep +0 -0
  522. package/templates/database/package.json +45 -0
  523. package/templates/database/prisma.config.ts +20 -0
  524. package/templates/database/schema.prisma +487 -0
  525. package/templates/database/scripts/db-generate-locked.sh +80 -0
  526. package/templates/database/seed-test.ts +85 -0
  527. package/templates/database/tsconfig.build.json +16 -0
  528. package/templates/database/tsconfig.json +9 -0
  529. package/templates/package.json +125 -0
  530. package/templates/pnpm-workspace.yaml +6 -0
  531. package/templates/project.yaml +59 -0
  532. package/templates/tsconfig.base.json +18 -0
  533. package/templates/tsconfig.json +45 -0
@@ -0,0 +1,338 @@
1
+ /**
2
+ * @file lead-routing.ts
3
+ * @description Cross-namespace lead orchestration service (ADR 0009 D3).
4
+ *
5
+ * Pipeline: validate → enrich → persist (DB with file fallback) → email.
6
+ * This is the first implementation of ADR 0009 D3's service-layer pattern.
7
+ * Future cross-namespace orchestrators in apps/api/src/services/ should mirror
8
+ * this shape: type-result returns, mocked-dep tests, non-transactional secondary
9
+ * effects, PII-safe logging.
10
+ *
11
+ * PII boundary: only { siteId, kind, ip, persisted, emailKind } appear in logs.
12
+ * Never log email, phone, name, or payload values.
13
+ */
14
+
15
+ import { randomUUID } from "node:crypto";
16
+
17
+ import type { Prisma } from "@working-theory/database";
18
+ import { sendLeadNotification } from "@working-theory/email";
19
+ import { leadInputSchema } from "@working-theory/validation";
20
+
21
+ import { writeLeadFallback } from "../lib/file-fallback.js";
22
+ import { appendLeadRecord } from "../lib/lead-capture-store.js";
23
+ import { prisma } from "../lib/prisma.js";
24
+
25
+ import { enrichLead } from "./enrichment-shim.js";
26
+
27
+ // ─── Types ────────────────────────────────────────────────────────────────────
28
+
29
+ export type LeadKind = "lead" | "subscribe" | "callback";
30
+
31
+ export type RouteLeadInput =
32
+ | {
33
+ kind: "lead";
34
+ siteId: string;
35
+ source: string;
36
+ email: string;
37
+ payload: Record<string, unknown>;
38
+ }
39
+ | { kind: "subscribe"; siteId: string; email: string; source: string }
40
+ | {
41
+ kind: "callback";
42
+ siteId: string;
43
+ name: string;
44
+ phone: string;
45
+ preferredTime?: string;
46
+ payload: Record<string, unknown>;
47
+ };
48
+
49
+ export type SourceMeta = {
50
+ ip: string;
51
+ userAgent: string | undefined;
52
+ receivedAt: string; // ISO
53
+ };
54
+
55
+ export type RouteLeadResult =
56
+ | {
57
+ ok: true;
58
+ id: string;
59
+ persisted: "db" | "file";
60
+ email:
61
+ | { ok: true; messageId: string }
62
+ | { ok: false; error: string }
63
+ | { skipped: true; reason: string };
64
+ }
65
+ | {
66
+ ok: false;
67
+ error: {
68
+ kind: "invalid_input" | "persist_failed" | "config_missing";
69
+ message: string;
70
+ };
71
+ };
72
+
73
+ // ─── Validation ───────────────────────────────────────────────────────────────
74
+
75
+ function validateInput(
76
+ input: RouteLeadInput,
77
+ ): { ok: true } | { ok: false; message: string } {
78
+ const result = leadInputSchema.safeParse(input);
79
+ if (!result.success) {
80
+ return {
81
+ ok: false,
82
+ message: result.error.issues[0]?.message ?? "invalid input",
83
+ };
84
+ }
85
+ return { ok: true };
86
+ }
87
+
88
+ // ─── DB persist ───────────────────────────────────────────────────────────────
89
+
90
+ type DbRow = { id: string };
91
+ type PersistResult =
92
+ | {
93
+ ok: true;
94
+ id: string;
95
+ source: "db";
96
+ skipEmail: boolean;
97
+ skipReason?: string;
98
+ }
99
+ | { ok: false; error: string };
100
+
101
+ async function persistToDb(input: RouteLeadInput): Promise<PersistResult> {
102
+ if (input.kind === "lead") {
103
+ const row = (await prisma.lead.create({
104
+ data: {
105
+ siteId: input.siteId,
106
+ source: input.source,
107
+ email: input.email,
108
+ payload: input.payload as Prisma.InputJsonValue,
109
+ },
110
+ select: { id: true },
111
+ })) as DbRow;
112
+ return { ok: true, id: row.id, source: "db", skipEmail: false };
113
+ }
114
+
115
+ if (input.kind === "subscribe") {
116
+ try {
117
+ const row = (await prisma.subscriber.create({
118
+ data: {
119
+ siteId: input.siteId,
120
+ email: input.email,
121
+ source: input.source,
122
+ },
123
+ select: { id: true },
124
+ })) as DbRow;
125
+ return { ok: true, id: row.id, source: "db", skipEmail: false };
126
+ } catch (err) {
127
+ // Handle unique constraint violation (duplicate subscriber)
128
+ if (
129
+ err != null &&
130
+ typeof err === "object" &&
131
+ "code" in err &&
132
+ (err as { code: string }).code === "P2002"
133
+ ) {
134
+ const existing = (await prisma.subscriber.findUnique({
135
+ where: { siteId_email: { siteId: input.siteId, email: input.email } },
136
+ select: { id: true },
137
+ })) as DbRow | null;
138
+ const existingId = existing?.id ?? "duplicate-no-id";
139
+ return {
140
+ ok: true,
141
+ id: existingId,
142
+ source: "db",
143
+ skipEmail: true,
144
+ skipReason: "duplicate",
145
+ };
146
+ }
147
+ throw err; // re-throw non-P2002 errors to trigger fallback
148
+ }
149
+ }
150
+
151
+ // kind === "callback"
152
+ const row = (await prisma.callback.create({
153
+ data: {
154
+ siteId: input.siteId,
155
+ name: input.name,
156
+ phone: input.phone,
157
+ ...(input.preferredTime !== undefined && {
158
+ preferredTime: input.preferredTime,
159
+ }),
160
+ payload: input.payload as Prisma.InputJsonValue,
161
+ },
162
+ select: { id: true },
163
+ })) as DbRow;
164
+ return { ok: true, id: row.id, source: "db", skipEmail: false };
165
+ }
166
+
167
+ // ─── Email ────────────────────────────────────────────────────────────────────
168
+
169
+ type EmailResult =
170
+ | { ok: true; messageId: string }
171
+ | { ok: false; error: string }
172
+ | { skipped: true; reason: string };
173
+
174
+ async function sendEmail(
175
+ input: RouteLeadInput,
176
+ id: string,
177
+ ): Promise<EmailResult> {
178
+ const to = process.env.AILK_LEAD_NOTIFICATION_TO;
179
+ const from = process.env.AILK_LEAD_NOTIFICATION_FROM;
180
+
181
+ if (!to || !from) {
182
+ return { skipped: true, reason: "env_unset" };
183
+ }
184
+
185
+ const leadForEmail = {
186
+ id,
187
+ siteId: input.siteId,
188
+ source: input.kind === "callback" ? "callback" : input.source,
189
+ email: input.kind === "callback" ? "" : input.email,
190
+ payload: input.kind === "subscribe" ? {} : input.payload,
191
+ createdAt: new Date().toISOString(),
192
+ };
193
+
194
+ const emailResult = await sendLeadNotification({
195
+ lead: leadForEmail,
196
+ recipientEmail: to,
197
+ fromAddress: from,
198
+ });
199
+
200
+ if (emailResult.ok) {
201
+ return { ok: true, messageId: emailResult.messageId };
202
+ }
203
+
204
+ const { kind, message } = emailResult.error;
205
+ if (kind === "config_missing") {
206
+ return { skipped: true, reason: "resend_key_unset" };
207
+ }
208
+ return { ok: false, error: `${kind}: ${message}` };
209
+ }
210
+
211
+ // ─── Main export ──────────────────────────────────────────────────────────────
212
+
213
+ export async function routeLead(
214
+ input: RouteLeadInput,
215
+ sourceMeta: SourceMeta,
216
+ ): Promise<RouteLeadResult> {
217
+ // 1. Validate
218
+ const validation = validateInput(input);
219
+ if (!validation.ok) {
220
+ return {
221
+ ok: false,
222
+ error: { kind: "invalid_input", message: validation.message },
223
+ };
224
+ }
225
+
226
+ // 2. Guard: if file capture is disabled AND no DB is configured, bail early.
227
+ // Both paths are unavailable — returning a clear error is better than a
228
+ // misleading 201 or a stack trace from Prisma on a missing DATABASE_URL.
229
+ if (
230
+ process.env.LEAD_CAPTURE_FILE_DISABLED === "true" &&
231
+ !process.env.DATABASE_URL
232
+ ) {
233
+ return {
234
+ ok: false,
235
+ error: {
236
+ kind: "config_missing",
237
+ message: "Lead capture is not configured.",
238
+ },
239
+ };
240
+ }
241
+
242
+ // 3. Enrich (reserved for Phase 3 — today returns lead unchanged).
243
+ // Cast via unknown: enrichLead accepts CreateLead (B3 shim type) but the
244
+ // pass-through contract means RouteLeadInput is safe here until Phase 3 wires
245
+ // the real enrichment call with proper type alignment.
246
+ await enrichLead(input as unknown as Parameters<typeof enrichLead>[0]);
247
+
248
+ // 4. Persist primary (DB), fallback to file on error
249
+ let id: string;
250
+ let persisted: "db" | "file";
251
+ let skipEmail = false;
252
+ let skipReason: string | undefined;
253
+
254
+ try {
255
+ const dbResult = await persistToDb(input);
256
+ if (!dbResult.ok) {
257
+ // persistToDb returned ok:false — defensive guard; current impl doesn't reach this
258
+ return {
259
+ ok: false,
260
+ error: { kind: "persist_failed", message: dbResult.error },
261
+ };
262
+ }
263
+ id = dbResult.id;
264
+ persisted = "db";
265
+ skipEmail = dbResult.skipEmail;
266
+ skipReason = dbResult.skipReason;
267
+ } catch (dbErr) {
268
+ // 5. File fallback
269
+ // Include a UUID suffix so concurrent fallbacks produce unique IDs even
270
+ // within the same millisecond. Format still matches /^[a-z0-9_-]{8,64}$/i.
271
+ const fallbackLead = {
272
+ id: `fallback-${Date.now()}-${randomUUID().slice(0, 8)}`,
273
+ siteId: input.siteId,
274
+ source: input.kind === "callback" ? "callback" : input.source,
275
+ email: input.kind === "callback" ? "" : input.email,
276
+ payload: input.kind === "subscribe" ? {} : input.payload,
277
+ createdAt: sourceMeta.receivedAt,
278
+ };
279
+
280
+ let fallbackResult;
281
+ try {
282
+ fallbackResult = await writeLeadFallback(fallbackLead);
283
+ } catch {
284
+ const dbMsg = dbErr instanceof Error ? dbErr.message : String(dbErr);
285
+ return {
286
+ ok: false,
287
+ error: {
288
+ kind: "persist_failed",
289
+ message: `db: ${dbMsg}; fallback threw`,
290
+ },
291
+ };
292
+ }
293
+
294
+ if (!fallbackResult.ok) {
295
+ const dbMsg = dbErr instanceof Error ? dbErr.message : String(dbErr);
296
+ return {
297
+ ok: false,
298
+ error: {
299
+ kind: "persist_failed",
300
+ message: `db: ${dbMsg}; fallback: ${fallbackResult.error.message}`,
301
+ },
302
+ };
303
+ }
304
+
305
+ id = fallbackLead.id;
306
+ persisted = "file";
307
+ }
308
+
309
+ // 6. Write to lead-capture JSONL (always-on canonical AX surface).
310
+ // Best-effort — a write failure does NOT roll back persistence or block email.
311
+ // The file is the agent-readable surface; DB is the transactional record.
312
+ const captureRecord = {
313
+ id: id!,
314
+ createdAt: sourceMeta.receivedAt,
315
+ source: input.kind === "callback" ? "callback" : input.source,
316
+ data:
317
+ input.kind === "callback"
318
+ ? { name: input.name, phone: input.phone, ...(input.payload ?? {}) }
319
+ : input.kind === "subscribe"
320
+ ? { email: input.email }
321
+ : { email: input.email, ...(input.payload ?? {}) },
322
+ metadata: {
323
+ ip: sourceMeta.ip,
324
+ userAgent: sourceMeta.userAgent,
325
+ },
326
+ };
327
+ await appendLeadRecord(captureRecord);
328
+
329
+ // 7. Email notification (best-effort — never rolls back persistence)
330
+ let emailResult: EmailResult;
331
+ if (skipEmail) {
332
+ emailResult = { skipped: true, reason: skipReason ?? "skipped" };
333
+ } else {
334
+ emailResult = await sendEmail(input, id!);
335
+ }
336
+
337
+ return { ok: true, id: id!, persisted, email: emailResult };
338
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "composite": true,
12
+ "types": ["node", "jest"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": [
16
+ "node_modules",
17
+ "dist",
18
+ "**/__tests__/**",
19
+ "**/*.test.ts",
20
+ "**/*.spec.ts"
21
+ ],
22
+ "references": [
23
+ { "path": "../../packages/analytics" },
24
+ { "path": "../../packages/common" },
25
+ { "path": "../../packages/content-adapters" },
26
+ { "path": "../../packages/content-projection" },
27
+ { "path": "../../packages/email" },
28
+ { "path": "../../packages/license" },
29
+ { "path": "../../packages/observability" },
30
+ { "path": "../../packages/schema" },
31
+ { "path": "../../packages/stripe-client" },
32
+ { "path": "../../packages/validation" }
33
+ ]
34
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": 2,
3
+ "ignoreCommand": "git diff --quiet HEAD^ HEAD -- apps/api packages database",
4
+ "buildCommand": "cd ../.. && pnpm run --filter @ai-launch-kit/backend build:vercel",
5
+ "installCommand": "cd ../.. && pnpm install --frozen-lockfile",
6
+ "outputDirectory": "public",
7
+ "framework": null,
8
+ "routes": [
9
+ {
10
+ "src": "/(.*)",
11
+ "dest": "/api/index.ts"
12
+ }
13
+ ],
14
+ "env": {
15
+ "NODE_ENV": "production"
16
+ }
17
+ }
@@ -0,0 +1,52 @@
1
+ # @working-theory/mcp — Environment variables
2
+ #
3
+ # Copy to .env and fill in values for your deployment.
4
+ # All variables are optional; defaults are shown.
5
+
6
+ # ── Transport selection ────────────────────────────────────────────────────────
7
+
8
+ # Which transport to boot. "stdio" is the default for local subprocess use
9
+ # (Claude Desktop, Cursor, etc.). "http" enables the network-reachable transport.
10
+ MCP_TRANSPORT=stdio
11
+
12
+ # ── HTTP transport ─────────────────────────────────────────────────────────────
13
+ # Only read when MCP_TRANSPORT=http.
14
+
15
+ # Port to bind the HTTP server on.
16
+ MCP_HTTP_PORT=8080
17
+
18
+ # URL path for the MCP endpoint.
19
+ MCP_HTTP_PATH=/mcp
20
+
21
+ # Public URL of the HTTP transport (used by /.well-known/mcp.json discovery).
22
+ # Set this to the externally-reachable URL of the MCP server.
23
+ # Example: https://mcp.yourdomain.com
24
+ MCP_PUBLIC_HTTP_URL=
25
+
26
+ # ── Rate limiting ──────────────────────────────────────────────────────────────
27
+
28
+ # Max requests per minute per IP for read tools.
29
+ MCP_RATE_LIMIT_READ_PER_MIN=60
30
+
31
+ # Max requests per minute per IP for write tools (capture_lead, subscribe_newsletter, request_callback).
32
+ MCP_RATE_LIMIT_WRITE_PER_MIN=10
33
+
34
+ # ── CORS (write tools) ─────────────────────────────────────────────────────────
35
+ # Read tools allow any origin. Write tools require explicit allowlist.
36
+
37
+ # Comma-separated list of origins allowed to call write tools.
38
+ # Empty = no cross-origin writes (most secure default).
39
+ # Use "*" only in development.
40
+ # Example: https://app.yourdomain.com,https://other.yourdomain.com
41
+ MCP_CORS_WRITE_ALLOWED_ORIGINS=
42
+
43
+ # ── Abuse protection ───────────────────────────────────────────────────────────
44
+
45
+ # Maximum number of capture_lead submissions per IP per day.
46
+ MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
47
+
48
+ # Comma-separated list of User-Agent substrings to block (case-insensitive).
49
+ # Built-in list already blocks curl, wget, python-requests, scrapy, etc.
50
+ # Add extra patterns here as needed.
51
+ # Example: BadBot,EvilCrawler
52
+ MCP_ABUSE_BLOCKED_USER_AGENTS=
@@ -0,0 +1,84 @@
1
+ # apps/mcp — MCP Server
2
+
3
+ Inherits root `CLAUDE.md`. This file adds MCP-specific rules.
4
+
5
+ ## Stack
6
+
7
+ - TypeScript, MCP SDK
8
+ - Transport: stdio at launch
9
+ - Discovery: `/.well-known/mcp.json` published from `apps/web`
10
+
11
+ ## Identity-blind by design (hard rule)
12
+
13
+ - OSS tools accept NO identity argument. No user, no session, no role.
14
+ - Anonymous-agent surface is the wedge. Adding identity to a tool breaks the OSS thesis.
15
+ - Identity-aware behavior (deferred Pro tools) lives behind a paid-tier check in `@working-theory/*` — never in the tool signature itself.
16
+
17
+ ## API ↔ MCP parity (hard rule)
18
+
19
+ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change the endpoint in the same PR. CI enforces.
20
+
21
+ <!-- SOFT_LOCK_OK: doc clarification for issue #2238 — explaining 14 vs 15 vs 18 tool counts in CLAUDE.md (no code or signature changes) -->
22
+
23
+ ## The 14 OSS tools (enabled at launch)
24
+
25
+ **Read (9):** `list_pages`, `read_content`, `search_content`, `get_site_info`, `get_business_hours`, `get_locations`, `get_offerings`, `get_pricing`, `get_faq`
26
+ **Conversion-write (3):** `capture_lead`, `subscribe_newsletter`, `request_callback`
27
+ **Maintenance (2):** `audit`, `scaffold`
28
+
29
+ ### Why the catalog reports 18 and the server registers 17
30
+
31
+ - **18 in the catalog / parity test**: the `TOOLS` array in
32
+ `packages/metadata/src/mcp-tool-catalog.ts` defines 18 tools; the parity
33
+ test's `tools.size === 18` assertion counts that full catalog.
34
+ - **17 registered at runtime**: `createMcpServer` (`apps/mcp/src/server.ts`)
35
+ registers `getMcpToolRegistrations()` minus `OSS_DISABLED_TOOLS`, whose only
36
+ entry is `start_purchase` — so 17 tools are registered on the server.
37
+ - **14 free anonymous tools**: of the 17 registered, three are Pro tools
38
+ (`create_page`, `update_page`, `delete_page`) gated at call time by
39
+ `requirePaidTier()`, leaving the 14 free anonymous tools above.
40
+ `index.ts` is the transport entry point and exports no tools — registration
41
+ is entirely via `createMcpServer`.
42
+
43
+ ## Pro + disabled tools (not part of the free surface)
44
+
45
+ - `create_page`, `update_page`, `delete_page` — high-stakes CMS writes,
46
+ `tier: "pro"`. Registered by `createMcpServer` but gated at call time by
47
+ `requirePaidTier()` (`apps/mcp/src/lib/require-paid-tier.ts`) — the paid-tier
48
+ check lives in the handler, never in the tool signature, per the
49
+ identity-blind rule above.
50
+ - `start_purchase` — transactional, Pro-only; the sole entry in
51
+ `OSS_DISABLED_TOOLS`, so it is filtered out of catalog registration and the
52
+ server entirely. Re-enable criteria live at
53
+ `docs/internal/roadmaps/phase-3-readiness.md`.
54
+
55
+ Don't add identity logic to any tool signature. Don't remove `start_purchase`
56
+ from `OSS_DISABLED_TOOLS` without satisfying the Phase 3 re-enable criteria.
57
+
58
+ ## Tool implementation pattern
59
+
60
+ 1. Input/output schemas in `@working-theory/validation`.
61
+ 2. Tool handler calls the matching `apps/api` endpoint (or shares a service when collocated).
62
+ 3. Errors map to MCP error responses using the same code the HTTP endpoint returned.
63
+
64
+ Never duplicate business logic. The tool is a thin adapter; the endpoint owns the implementation.
65
+
66
+ ## Pro post-processing pattern
67
+
68
+ Pro features add post-processing behind the **same anonymous tool surface**. Tool signature never changes — what changes is what happens after the call.
69
+
70
+ Example (lead routing):
71
+
72
+ - `apps/mcp` tool signature: unchanged
73
+ - `apps/api` endpoint: checks JWT entitlement via `@working-theory/license-client`
74
+ - Pipeline: entirely in `@working-theory/lead-routing`
75
+
76
+ ## What NOT to do
77
+
78
+ - Don't add identity arguments to OSS tools.
79
+ - Don't expose a Pro tool without its `requirePaidTier()` gate.
80
+ - Don't import `@working-theory/*` from `apps/mcp`.
81
+ - Don't add a tool without the matching `apps/api` endpoint.
82
+ - Don't put business logic in tool handlers — they're adapters.
83
+
84
+ Last updated: 2026-07-09 — corrected the tool-count explanation (18 catalog → 17 registered → 14 free anonymous) and the Pro-tool gating (registered + `requirePaidTier()`-gated, not `OSS_DISABLED_TOOLS`-disabled).
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Unit tests for abuse/daily-cap.ts
3
+ *
4
+ * Verifies per-IP daily cap counter with configurable limit and daily reset.
5
+ */
6
+
7
+ import { DailyCapTracker } from "../../src/abuse/daily-cap.js";
8
+
9
+ describe("DailyCapTracker", () => {
10
+ it("allows requests below the cap", () => {
11
+ const tracker = new DailyCapTracker(3);
12
+ expect(tracker.isAllowed("1.2.3.4")).toBe(true);
13
+ expect(tracker.isAllowed("1.2.3.4")).toBe(true);
14
+ expect(tracker.isAllowed("1.2.3.4")).toBe(true);
15
+ });
16
+
17
+ it("blocks requests at and above the cap", () => {
18
+ const tracker = new DailyCapTracker(2);
19
+ tracker.isAllowed("1.2.3.4");
20
+ tracker.isAllowed("1.2.3.4");
21
+ // Third call should be blocked
22
+ expect(tracker.isAllowed("1.2.3.4")).toBe(false);
23
+ });
24
+
25
+ it("tracks IPs independently", () => {
26
+ const tracker = new DailyCapTracker(1);
27
+ tracker.isAllowed("1.2.3.4");
28
+ // Exhausted for .4 but .5 is fresh
29
+ expect(tracker.isAllowed("1.2.3.4")).toBe(false);
30
+ expect(tracker.isAllowed("1.2.3.5")).toBe(true);
31
+ });
32
+
33
+ it("resets counters after the daily window expires", () => {
34
+ jest.useFakeTimers();
35
+ const tracker = new DailyCapTracker(1);
36
+ tracker.isAllowed("1.2.3.4");
37
+ expect(tracker.isAllowed("1.2.3.4")).toBe(false);
38
+
39
+ // Advance 24 hours + 1ms
40
+ jest.advanceTimersByTime(24 * 60 * 60 * 1000 + 1);
41
+ expect(tracker.isAllowed("1.2.3.4")).toBe(true);
42
+ jest.useRealTimers();
43
+ });
44
+
45
+ it("uses cap=50 when none supplied", () => {
46
+ const tracker = new DailyCapTracker();
47
+ for (let i = 0; i < 50; i++) {
48
+ expect(tracker.isAllowed("9.9.9.9")).toBe(true);
49
+ }
50
+ expect(tracker.isAllowed("9.9.9.9")).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Unit tests for abuse/email-validator.ts
3
+ *
4
+ * Verifies RFC 5322-style email format validation used by
5
+ * capture_lead and subscribe_newsletter abuse rules.
6
+ */
7
+
8
+ import { isValidEmail } from "../../src/abuse/email-validator.js";
9
+
10
+ describe("isValidEmail", () => {
11
+ it("accepts a well-formed email address", () => {
12
+ expect(isValidEmail("jane@example.com")).toBe(true);
13
+ });
14
+
15
+ it("accepts email with subdomain", () => {
16
+ expect(isValidEmail("user@mail.example.com")).toBe(true);
17
+ });
18
+
19
+ it("accepts email with plus addressing", () => {
20
+ expect(isValidEmail("user+tag@example.com")).toBe(true);
21
+ });
22
+
23
+ it("rejects empty string", () => {
24
+ expect(isValidEmail("")).toBe(false);
25
+ });
26
+
27
+ it("rejects missing @", () => {
28
+ expect(isValidEmail("notanemail")).toBe(false);
29
+ });
30
+
31
+ it("rejects missing domain", () => {
32
+ expect(isValidEmail("user@")).toBe(false);
33
+ });
34
+
35
+ it("rejects missing local part", () => {
36
+ expect(isValidEmail("@example.com")).toBe(false);
37
+ });
38
+
39
+ it("rejects missing TLD dot", () => {
40
+ expect(isValidEmail("user@localhost")).toBe(false);
41
+ });
42
+
43
+ it("rejects plainly malformed input", () => {
44
+ expect(isValidEmail("not@valid@example.com")).toBe(false);
45
+ });
46
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Unit tests for abuse/honeypot.ts
3
+ *
4
+ * Verifies that a non-empty `honeypot` field in tool input triggers rejection.
5
+ */
6
+
7
+ import { isHoneypotTripped } from "../../src/abuse/honeypot.js";
8
+
9
+ describe("isHoneypotTripped", () => {
10
+ it("returns false when input has no honeypot field", () => {
11
+ expect(isHoneypotTripped({ name: "Jane", email: "jane@example.com" })).toBe(
12
+ false,
13
+ );
14
+ });
15
+
16
+ it("returns false when honeypot field is empty string", () => {
17
+ expect(isHoneypotTripped({ honeypot: "" })).toBe(false);
18
+ });
19
+
20
+ it("returns true when honeypot field is non-empty", () => {
21
+ expect(isHoneypotTripped({ honeypot: "bot-filled" })).toBe(true);
22
+ });
23
+
24
+ it("returns true when honeypot field is a space", () => {
25
+ expect(isHoneypotTripped({ honeypot: " " })).toBe(true);
26
+ });
27
+
28
+ it("returns false for null honeypot", () => {
29
+ expect(isHoneypotTripped({ honeypot: null })).toBe(false);
30
+ });
31
+
32
+ it("accepts non-string inputs without throwing", () => {
33
+ expect(() => isHoneypotTripped(null)).not.toThrow();
34
+ expect(isHoneypotTripped(null)).toBe(false);
35
+ });
36
+ });