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,70 @@
1
+ /**
2
+ * @file factories/index.ts
3
+ * Typed test data factories for apps/api route tests.
4
+ *
5
+ * Each factory returns a fully-typed object with all required fields populated.
6
+ * Pass overrides to vary only the fields relevant to the test case.
7
+ *
8
+ * Used by: content, leads, checkout route tests (Issue #507 real-dependency refactor).
9
+ */
10
+
11
+ import type { ContentPage } from "@working-theory/content-adapters";
12
+ import type { Lead, LeadCaptureRecord } from "@working-theory/validation";
13
+
14
+ // ─── Content factories ────────────────────────────────────────────────────────
15
+
16
+ export type ContentPageOverrides = Partial<ContentPage>;
17
+
18
+ /**
19
+ * Build a fully-typed ContentPage. Defaults to a HomePage with deterministic values.
20
+ */
21
+ export function buildContent(
22
+ overrides: ContentPageOverrides = {},
23
+ ): ContentPage {
24
+ return {
25
+ type: "HomePage",
26
+ slug: "test-page",
27
+ title: "Test Page Title",
28
+ description: "A test page description for automated tests.",
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ // ─── Lead factories ───────────────────────────────────────────────────────────
34
+
35
+ export type LeadOverrides = Partial<Lead>;
36
+
37
+ /**
38
+ * Build a fully-typed Lead record (as returned by the DB / written to file).
39
+ */
40
+ export function buildLead(overrides: LeadOverrides = {}): Lead {
41
+ return {
42
+ id: "test-lead-id-00000001",
43
+ siteId: "test-site-abc",
44
+ source: "contact",
45
+ email: "test@example.com",
46
+ payload: { message: "Hello from factory" },
47
+ createdAt: new Date().toISOString(),
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ // ─── LeadCaptureRecord factories ──────────────────────────────────────────────
53
+
54
+ export type LeadCaptureRecordOverrides = Partial<LeadCaptureRecord>;
55
+
56
+ /**
57
+ * Build a fully-typed LeadCaptureRecord (as written to the JSONL capture file).
58
+ */
59
+ export function buildLeadCaptureRecord(
60
+ overrides: LeadCaptureRecordOverrides = {},
61
+ ): LeadCaptureRecord {
62
+ return {
63
+ id: "test-capture-id-00000001",
64
+ createdAt: new Date().toISOString(),
65
+ source: "contact",
66
+ data: { email: "test@example.com" },
67
+ metadata: {},
68
+ ...overrides,
69
+ };
70
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @file lead-fixture.ts
3
+ * Filesystem fixture helpers for leads route tests.
4
+ *
5
+ * Sets up per-test temp paths for the real lead-capture-store and
6
+ * file-fallback modules, so tests can assert on actual file writes
7
+ * without mocking those internal modules.
8
+ *
9
+ * Issue #507: replaces jest.mock("../../../lib/lead-capture-store.js") and
10
+ * jest.mock("../../../lib/file-fallback.js") in leads route tests.
11
+ */
12
+
13
+ import * as fs from "fs/promises";
14
+ import * as os from "os";
15
+ import * as path from "path";
16
+
17
+ export interface LeadFileFixture {
18
+ /** Path to the per-test JSONL capture file (LEAD_CAPTURE_FILE). */
19
+ getCaptureFilePath: () => string;
20
+ /** Path to the per-test fallback dir (AILK_INBOX_FALLBACK_DIR). */
21
+ getFallbackDir: () => string;
22
+ /**
23
+ * Read and parse the last line written to the capture file.
24
+ * Throws if the file does not exist or is empty.
25
+ */
26
+ readLastCaptureRecord: () => Promise<Record<string, unknown>>;
27
+ }
28
+
29
+ /**
30
+ * Register beforeEach/afterEach hooks that create a fresh LEAD_CAPTURE_FILE
31
+ * path and AILK_INBOX_FALLBACK_DIR per test, then clean up afterward.
32
+ *
33
+ * Call at describe-scope. Returns a handle for asserting on file contents.
34
+ */
35
+ export function setupLeadFileEnv(label = "leads"): LeadFileFixture {
36
+ let captureFilePath = "";
37
+ let fallbackDir = "";
38
+
39
+ beforeEach(async () => {
40
+ captureFilePath = path.join(
41
+ os.tmpdir(),
42
+ `ailk-test-${label}-${Date.now()}.jsonl`,
43
+ );
44
+ process.env.LEAD_CAPTURE_FILE = captureFilePath;
45
+
46
+ fallbackDir = await fs.mkdtemp(
47
+ path.join(os.tmpdir(), `ailk-test-fallback-${label}-`),
48
+ );
49
+ process.env.AILK_INBOX_FALLBACK_DIR = fallbackDir;
50
+ });
51
+
52
+ afterEach(async () => {
53
+ delete process.env.LEAD_CAPTURE_FILE;
54
+ delete process.env.AILK_INBOX_FALLBACK_DIR;
55
+
56
+ // Best-effort cleanup — never fail the test on cleanup errors.
57
+ try {
58
+ await fs.unlink(captureFilePath);
59
+ } catch {
60
+ // file may not exist if appendLeadRecord was skipped
61
+ }
62
+ try {
63
+ await fs.rm(fallbackDir, { recursive: true, force: true });
64
+ } catch {
65
+ // ignore
66
+ }
67
+
68
+ captureFilePath = "";
69
+ fallbackDir = "";
70
+ });
71
+
72
+ return {
73
+ getCaptureFilePath: () => captureFilePath,
74
+ getFallbackDir: () => fallbackDir,
75
+ readLastCaptureRecord: async () => {
76
+ const content = await fs.readFile(captureFilePath, "utf8");
77
+ const lines = content.trim().split("\n").filter(Boolean);
78
+ if (lines.length === 0) throw new Error("Capture file is empty");
79
+ return JSON.parse(lines[lines.length - 1]) as Record<string, unknown>;
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @file index.test.ts
3
+ * @description Unit tests for index.ts - Entry point
4
+ *
5
+ * Uses jest.spyOn on the real build() function (from ../server) to stub the
6
+ * server object returned, rather than mocking the server module wholesale.
7
+ * The real config module is imported directly — PORT and NODE_ENV come from
8
+ * the test setup env vars.
9
+ */
10
+
11
+ import { start } from '../index';
12
+ import * as serverModule from '../server';
13
+
14
+ // Mock @working-theory/observability so assertions check structured logger calls
15
+ // rather than depending on pino's stdout serialization.
16
+ const mockLoggerInfo = jest.fn();
17
+ const mockLoggerError = jest.fn();
18
+ jest.mock('@working-theory/observability', () => ({
19
+ logger: {
20
+ info: (...args: unknown[]) => mockLoggerInfo(...args),
21
+ error: (...args: unknown[]) => mockLoggerError(...args),
22
+ warn: jest.fn(),
23
+ debug: jest.fn(),
24
+ },
25
+ }));
26
+
27
+ // @working-theory/email — ESM-only transitive dep (@react-email/components) cannot be
28
+ // resolved by jest-resolve in CommonJS mode. Mock at the package boundary.
29
+ jest.mock('@working-theory/email', () => ({
30
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
31
+ }));
32
+
33
+ // External Fastify plugins — mocked because they require live infrastructure.
34
+ jest.mock('@fastify/cors', () => jest.fn().mockResolvedValue(undefined));
35
+ jest.mock('@fastify/rate-limit', () => jest.fn().mockResolvedValue(undefined));
36
+
37
+ describe('Index Entry Point', () => {
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mockServer holds the Jest-stubbed Fastify instance; typed loosely to avoid importing full FastifyInstance here
39
+ let mockServer: any;
40
+ let buildSpy: jest.SpyInstance;
41
+
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+ mockLoggerInfo.mockClear();
45
+ mockLoggerError.mockClear();
46
+
47
+ // Re-establish spies each test so afterEach's restoreAllMocks doesn't leave
48
+ // process.exit unmocked across tests — the real exit would kill the worker.
49
+ jest.spyOn(process, 'exit').mockImplementation((() => {
50
+ throw new Error('Process exit called');
51
+ }) as never);
52
+
53
+ mockServer = {
54
+ listen: jest.fn().mockResolvedValue(undefined),
55
+ };
56
+
57
+ // Spy on build() from the real server module instead of mocking the module.
58
+ // This keeps the module graph intact while controlling the returned instance.
59
+ buildSpy = jest.spyOn(serverModule, 'build').mockResolvedValue(mockServer);
60
+ });
61
+
62
+ afterEach(() => {
63
+ jest.restoreAllMocks();
64
+ });
65
+
66
+ describe('start function', () => {
67
+ it('should build server and start listening on configured port', async () => {
68
+ // ACT
69
+ await start();
70
+
71
+ // ASSERT
72
+ expect(buildSpy).toHaveBeenCalled();
73
+ expect(mockServer.listen).toHaveBeenCalledWith({
74
+ port: 3002,
75
+ host: '0.0.0.0',
76
+ });
77
+ });
78
+
79
+ it('should log server startup information', async () => {
80
+ // ACT
81
+ await start();
82
+
83
+ // ASSERT — single structured info call covers port + env + endpoints.
84
+ expect(mockLoggerInfo).toHaveBeenCalledTimes(1);
85
+ const [payload, msg] = mockLoggerInfo.mock.calls[0];
86
+ expect(msg).toContain('AI Launch Kit API');
87
+ expect(payload).toMatchObject({
88
+ port: 3002,
89
+ env: 'test',
90
+ endpoints: expect.objectContaining({ auth: expect.stringContaining('/v1/auth/tokens') }),
91
+ });
92
+ });
93
+
94
+ it('should handle server build failure', async () => {
95
+ // ARRANGE
96
+ const buildError = new Error('Build failed');
97
+ buildSpy.mockRejectedValue(buildError);
98
+
99
+ // ACT & ASSERT - process.exit throws due to mock
100
+ await expect(start()).rejects.toThrow('Process exit called');
101
+ const [payload, msg] = mockLoggerError.mock.calls[0];
102
+ expect(msg).toBe('Failed to start server');
103
+ expect(payload).toMatchObject({ err: expect.any(Error) });
104
+ });
105
+
106
+ it('should handle server listen failure', async () => {
107
+ // ARRANGE
108
+ const listenError = new Error('Port already in use');
109
+ mockServer.listen.mockRejectedValue(listenError);
110
+
111
+ // ACT & ASSERT - process.exit throws due to mock
112
+ await expect(start()).rejects.toThrow('Process exit called');
113
+ const [payload, msg] = mockLoggerError.mock.calls[0];
114
+ expect(msg).toBe('Failed to start server');
115
+ expect(payload).toMatchObject({ err: expect.any(Error) });
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @file server.test.ts
3
+ * @description Unit tests for server.ts - Server setup
4
+ *
5
+ * Uses the real build() function with real route plugins. All internal modules
6
+ * run real; external boundaries (CORS, rate-limit) are mocked at the plugin
7
+ * boundary since they would require live network/Redis in production.
8
+ */
9
+
10
+ import { build } from '../server';
11
+
12
+ // External service boundaries — mocked because they require live infrastructure
13
+ // or have ESM-only transitive deps that Jest's CJS preset cannot resolve:
14
+ // @fastify/cors, @fastify/rate-limit — Fastify plugins that would require
15
+ // live network infrastructure (CORS policy sync, Redis store) in unit tests.
16
+ // @working-theory/email — transitive dep on @react-email/components which is ESM-only
17
+ // and cannot be resolved by jest-resolve in CommonJS mode.
18
+ jest.mock('@fastify/cors', () => {
19
+ return jest.fn().mockResolvedValue(undefined);
20
+ });
21
+
22
+ jest.mock('@fastify/rate-limit', () => {
23
+ return jest.fn().mockResolvedValue(undefined);
24
+ });
25
+
26
+ jest.mock('@working-theory/email', () => ({
27
+ sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
28
+ }));
29
+
30
+ describe('Server', () => {
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- server is typed via Fastify generics; explicit any avoids pulling in the full FastifyInstance type here
32
+ let app: any;
33
+
34
+ beforeEach(async () => {
35
+ jest.clearAllMocks();
36
+ });
37
+
38
+ afterEach(async () => {
39
+ if (app) {
40
+ await app.close();
41
+ }
42
+ });
43
+
44
+ describe('build function', () => {
45
+ it('should build Fastify server instance', async () => {
46
+ // ACT
47
+ app = await build();
48
+
49
+ // ASSERT
50
+ expect(app).toBeDefined();
51
+ expect(typeof app.inject).toBe('function');
52
+ });
53
+
54
+ it('should register health check route returning static OK without external calls', async () => {
55
+ // ARRANGE
56
+ app = await build();
57
+
58
+ // ACT
59
+ const response = await app.inject({
60
+ method: 'GET',
61
+ url: '/health',
62
+ });
63
+
64
+ // ASSERT
65
+ expect(response.statusCode).toBe(200);
66
+ const body = JSON.parse(response.body);
67
+ expect(body.status).toBe('healthy');
68
+ expect(body.timestamp).toBeDefined();
69
+ // No external dependencies in the health check anymore
70
+ expect(body.platform).toBeUndefined();
71
+ });
72
+
73
+ it('should register root endpoint', async () => {
74
+ // ARRANGE
75
+ app = await build();
76
+
77
+ // ACT
78
+ const response = await app.inject({
79
+ method: 'GET',
80
+ url: '/',
81
+ });
82
+
83
+ // ASSERT
84
+ expect(response.statusCode).toBe(200);
85
+ const body = JSON.parse(response.body);
86
+ expect(body.name).toBe('AI Launch Kit API');
87
+ expect(body.endpoints).toBeDefined();
88
+ });
89
+
90
+ it('should register CORS', async () => {
91
+ // ACT
92
+ app = await build();
93
+
94
+ // ASSERT - CORS should be registered (no error thrown)
95
+ expect(app).toBeDefined();
96
+ });
97
+
98
+ it('should register all route plugins', async () => {
99
+ // ARRANGE
100
+ app = await build();
101
+
102
+ // ACT & ASSERT - Test that routes are registered by checking they don't 404
103
+ const healthResponse = await app.inject({
104
+ method: 'GET',
105
+ url: '/health',
106
+ });
107
+ expect(healthResponse.statusCode).toBe(200);
108
+
109
+ const rootResponse = await app.inject({
110
+ method: 'GET',
111
+ url: '/',
112
+ });
113
+ expect(rootResponse.statusCode).toBe(200);
114
+ });
115
+
116
+ it('should include all endpoints in root response', async () => {
117
+ // ARRANGE
118
+ app = await build();
119
+
120
+ // ACT
121
+ const response = await app.inject({
122
+ method: 'GET',
123
+ url: '/',
124
+ });
125
+
126
+ // ASSERT
127
+ const body = JSON.parse(response.body);
128
+ expect(body.endpoints).toHaveProperty('health');
129
+ expect(body.endpoints).toHaveProperty('tokens');
130
+ expect(body.endpoints).toHaveProperty('profile');
131
+ });
132
+
133
+ it('startup throws when LEAD_CAPTURE_FILE points at a missing directory', async () => {
134
+ // ARRANGE — set LEAD_CAPTURE_FILE to a path whose parent dir doesn't exist
135
+ // so the real validateLeadCaptureDir() throws (AC8). Restore after test.
136
+ const prior = process.env.LEAD_CAPTURE_FILE;
137
+ process.env.LEAD_CAPTURE_FILE = '/nonexistent-ailk-test-dir/leads.jsonl';
138
+
139
+ try {
140
+ // ACT & ASSERT
141
+ await expect(build()).rejects.toThrow(/LEAD_CAPTURE_FILE/);
142
+ } finally {
143
+ if (prior === undefined) {
144
+ delete process.env.LEAD_CAPTURE_FILE;
145
+ } else {
146
+ process.env.LEAD_CAPTURE_FILE = prior;
147
+ }
148
+ }
149
+ });
150
+ });
151
+ });
152
+
153
+ // ── Bootstrap test: raw-body parser scope encapsulation (AC 13) ──────────────
154
+ //
155
+ // Proves that registering the webhooks plugin (with its raw-body parser scoped
156
+ // to application/json) does NOT leak that parser to other routes. The global
157
+ // Fastify JSON parser must remain active for all non-webhook routes.
158
+
159
+ import Fastify from 'fastify';
160
+ import { webhooksRoutes } from '../routes/webhooks/index.js';
161
+
162
+ describe('Parser scope — webhooks raw-body parser does not leak', () => {
163
+ it('other routes still receive parsed JSON objects after webhooks plugin registers', async () => {
164
+ // Build a minimal server that mirrors the encapsulation pattern in server.ts:
165
+ // 1. Register a JSON route that echoes the parsed body.
166
+ // 2. Register the webhooks plugin inside an encapsulated scope with a
167
+ // raw-body (buffer) parser.
168
+ // If the raw-body parser leaks out of the webhooks scope, the echo route
169
+ // would receive a Buffer instead of a parsed object.
170
+
171
+ const server = Fastify({ logger: false });
172
+
173
+ // A simple JSON echo route — simulates any existing JSON-parsed route
174
+ server.post('/api/echo', async (req) => ({ received: req.body }));
175
+
176
+ // Mirror of server.ts registration pattern
177
+ await server.register(async (webhooksScope) => {
178
+ webhooksScope.addContentTypeParser(
179
+ 'application/json',
180
+ { parseAs: 'buffer' },
181
+ (_req, body, done) => {
182
+ done(null, body);
183
+ },
184
+ );
185
+ await webhooksScope.register(webhooksRoutes);
186
+ });
187
+
188
+ await server.ready();
189
+
190
+ try {
191
+ const response = await server.inject({
192
+ method: 'POST',
193
+ url: '/api/echo',
194
+ payload: { hello: 'world' },
195
+ headers: { 'content-type': 'application/json' },
196
+ });
197
+
198
+ expect(response.statusCode).toBe(200);
199
+ const body = JSON.parse(response.body);
200
+ // Must be a plain object, not a stringified Buffer
201
+ expect(body.received).toEqual({ hello: 'world' });
202
+ expect(Buffer.isBuffer(body.received)).toBe(false);
203
+ } finally {
204
+ await server.close();
205
+ }
206
+ });
207
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @file enrichment-shim.test.ts
3
+ * @description Unit tests for enrichment-shim — license-gated lead enrichment stub.
4
+ *
5
+ * Module-state hygiene: proSentinelLogged is module-scoped and persists across calls
6
+ * within the same module instance. Each test uses jest.isolateModules() to load a
7
+ * fresh copy of the shim (resetting proSentinelLogged) while keeping the jest.mock
8
+ * factory active so @working-theory/license is always intercepted.
9
+ */
10
+
11
+ import { createHash } from "crypto";
12
+
13
+ // Stable mock function reference — survives resetModules because it lives in this
14
+ // module's scope, not in the module registry.
15
+ const mockCheckLicense = jest.fn();
16
+ const mockLoggerInfo = jest.fn();
17
+
18
+ jest.mock("@working-theory/license", () => ({
19
+ checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
20
+ }));
21
+
22
+ jest.mock("@working-theory/observability", () => ({
23
+ logger: {
24
+ info: (...args: unknown[]) => mockLoggerInfo(...args),
25
+ warn: jest.fn(),
26
+ error: jest.fn(),
27
+ debug: jest.fn(),
28
+ },
29
+ }));
30
+
31
+ describe("enrichLead", () => {
32
+ let enrichLead: (lead: unknown) => Promise<unknown>;
33
+ const consoleInfoSpy = mockLoggerInfo;
34
+
35
+ beforeEach(() => {
36
+ mockCheckLicense.mockReset();
37
+ mockLoggerInfo.mockClear();
38
+
39
+ // Load a fresh shim module instance per test to reset proSentinelLogged.
40
+ jest.isolateModules(() => {
41
+
42
+ const shim = require("../../services/enrichment-shim.js") as {
43
+ enrichLead: (lead: unknown) => Promise<unknown>;
44
+ };
45
+ enrichLead = shim.enrichLead;
46
+ });
47
+ });
48
+
49
+ afterEach(() => {
50
+ jest.restoreAllMocks();
51
+ delete process.env["AILK_LICENSE_KEY"];
52
+ });
53
+
54
+ it("free tier — returns lead unchanged, no sentinel log", async () => {
55
+ mockCheckLicense.mockResolvedValue({
56
+ tier: "free",
57
+ plan: null,
58
+ features: [],
59
+ expiresAt: null,
60
+ revoked: false,
61
+ });
62
+ const lead = { email: "test@example.com", name: "Test User" };
63
+
64
+ const result = await enrichLead(lead);
65
+
66
+ expect(result).toBe(lead);
67
+ expect(consoleInfoSpy).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it("pro tier (first call) — returns lead unchanged, sentinel logged once with key hash prefix", async () => {
71
+ mockCheckLicense.mockResolvedValue({
72
+ tier: "pro",
73
+ plan: "pro",
74
+ features: [],
75
+ expiresAt: null,
76
+ revoked: false,
77
+ });
78
+ process.env["AILK_LICENSE_KEY"] = "test-key-123";
79
+
80
+ const expectedPrefix = createHash("sha256")
81
+ .update("test-key-123")
82
+ .digest("hex")
83
+ .substring(0, 8);
84
+
85
+ const lead = { email: "test@example.com", name: "Test User" };
86
+
87
+ const result = await enrichLead(lead);
88
+
89
+ expect(result).toBe(lead);
90
+ expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
91
+ // pino convention: logger.info(structuredPayload, message).
92
+ const [payload] = consoleInfoSpy.mock.calls[0];
93
+ expect(payload).toMatchObject({ licensePrefix: expectedPrefix });
94
+ });
95
+
96
+ it("pro tier (second call, same module) — sentinel logged only once total", async () => {
97
+ mockCheckLicense.mockResolvedValue({
98
+ tier: "pro",
99
+ plan: "pro",
100
+ features: [],
101
+ expiresAt: null,
102
+ revoked: false,
103
+ });
104
+ process.env["AILK_LICENSE_KEY"] = "test-key-123";
105
+
106
+ const lead1 = { email: "first@example.com", name: "First" };
107
+ const lead2 = { email: "second@example.com", name: "Second" };
108
+
109
+ const result1 = await enrichLead(lead1);
110
+ const result2 = await enrichLead(lead2);
111
+
112
+ expect(result1).toBe(lead1);
113
+ expect(result2).toBe(lead2);
114
+ expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
115
+ });
116
+
117
+ it("checkLicense throws — returns lead unchanged, no sentinel, error not propagated", async () => {
118
+ mockCheckLicense.mockRejectedValue(new Error("network error"));
119
+ const lead = { email: "test@example.com", name: "Test User" };
120
+
121
+ const result = await enrichLead(lead);
122
+
123
+ expect(result).toBe(lead);
124
+ expect(consoleInfoSpy).not.toHaveBeenCalled();
125
+ });
126
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Jest Global Setup
3
+ * Sets required environment variables for all tests
4
+ */
5
+
6
+ // Database
7
+ process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://test:test@localhost:5432/test';
8
+ process.env.DATABASE_URL_NON_POOLED = process.env.DATABASE_URL_NON_POOLED || process.env.DATABASE_URL;
9
+
10
+ // Auth — Better Auth (post-#587). NEXTAUTH_* removed.
11
+ process.env.BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET || 'test-secret-key';
12
+ process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret-min-32-characters-required!!';
13
+ process.env.GOOGLE_OAUTH_CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID || 'test-google-client-id.apps.googleusercontent.com';
14
+
15
+ // Stripe
16
+ process.env.STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || 'sk_test_mock_key';
17
+ process.env.STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_test_webhook_secret';
18
+ process.env.STRIPE_API_VERSION = process.env.STRIPE_API_VERSION || '2023-10-16';
19
+
20
+ // Frontend
21
+ process.env.FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3001';
22
+
23
+ // Node environment
24
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /* istanbul ignore file -- Node CLI bootstrap; import.meta.url is unreachable from Jest CJS.
3
+ See the JSDoc below and docs at https://jestjs.io/docs/configuration#coverageprovider-string.
4
+ Real entry-point semantics are integration-tested by running the compiled dist directly. */
5
+ /**
6
+ * CLI Entry Point
7
+ *
8
+ * This file is the actual entry point for running the server from CLI.
9
+ * Separated from index.ts to allow Jest testing without parsing import.meta.
10
+ */
11
+ import { pathToFileURL } from 'url';
12
+
13
+ import { start } from './index.js';
14
+
15
+ // Only start if this is the main module
16
+ // Check if executed file matches current file using import.meta.url
17
+ if (process.argv[1]) {
18
+ const isEntryPoint = process.argv[1].endsWith('dist/cli.js') ||
19
+ process.argv[1].endsWith('cli.js');
20
+
21
+ if (isEntryPoint) {
22
+ // ESM-accurate verification using import.meta.url
23
+ const currentFileUrl = import.meta.url;
24
+ const executedFileUrl = pathToFileURL(process.argv[1]).href;
25
+
26
+ if (currentFileUrl === executedFileUrl) {
27
+ void start();
28
+ }
29
+ }
30
+ }