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,54 @@
1
+ /**
2
+ * Unit tests for abuse/user-agent-deny.ts
3
+ *
4
+ * Verifies built-in bot UA deny list plus env-configured additions.
5
+ */
6
+
7
+ import {
8
+ isUserAgentBlocked,
9
+ buildDenyList,
10
+ } from "../../src/abuse/user-agent-deny.js";
11
+
12
+ describe("buildDenyList", () => {
13
+ it("includes built-in bot user agents", () => {
14
+ const list = buildDenyList();
15
+ expect(list.some((r) => r.test("python-requests/2.28"))).toBe(true);
16
+ expect(list.some((r) => r.test("curl/7.88"))).toBe(true);
17
+ expect(list.some((r) => r.test("Wget/1.21"))).toBe(true);
18
+ expect(list.some((r) => r.test("Scrapy/2.8"))).toBe(true);
19
+ });
20
+
21
+ it("merges env-configured additions", () => {
22
+ const list = buildDenyList("BadBot/1.0,EvilCrawler");
23
+ expect(list.some((r) => r.test("BadBot/1.0"))).toBe(true);
24
+ expect(list.some((r) => r.test("EvilCrawler/2"))).toBe(true);
25
+ });
26
+ });
27
+
28
+ describe("isUserAgentBlocked", () => {
29
+ it("blocks a known bot UA", () => {
30
+ const denyList = buildDenyList();
31
+ expect(isUserAgentBlocked("python-requests/2.28.1", denyList)).toBe(true);
32
+ });
33
+
34
+ it("allows a real browser UA", () => {
35
+ const denyList = buildDenyList();
36
+ expect(
37
+ isUserAgentBlocked(
38
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
39
+ denyList,
40
+ ),
41
+ ).toBe(false);
42
+ });
43
+
44
+ it("blocks missing UA (empty string)", () => {
45
+ const denyList = buildDenyList();
46
+ expect(isUserAgentBlocked("", denyList)).toBe(true);
47
+ });
48
+
49
+ it("blocks undefined/null UA", () => {
50
+ const denyList = buildDenyList();
51
+ expect(isUserAgentBlocked(undefined, denyList)).toBe(true);
52
+ expect(isUserAgentBlocked(null, denyList)).toBe(true);
53
+ });
54
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * AEO CLI ↔ MCP tool parity test.
3
+ *
4
+ * Verifies that `ailkAudit` produces per-page output that matches what
5
+ * you'd get by calling `runAudit` directly with the same single-page input
6
+ * that `buildSinglePageAuditInput` produces.
7
+ *
8
+ * Both code paths use only the public `@working-theory/aeo` API — no imports from
9
+ * CLI-internal files (packages/aeo/src/cli/*). The parity claim is:
10
+ * buildSinglePageAuditInput → runAudit(input, RULES)
11
+ * ≡ ailkAudit internally (same adapter, same RULES)
12
+ *
13
+ * Acceptance criteria verified:
14
+ * - Output of audit for a fixture page equals the result of calling
15
+ * runAudit with the same AuditInput (Layer 2 AC: "identical per-page output")
16
+ * - runAudit invocation uses canonical RULES from packages/aeo/src/rules/index.ts
17
+ */
18
+
19
+ import { resolve, join } from "node:path";
20
+
21
+ import {
22
+ runAudit,
23
+ alwaysFails,
24
+ alwaysPasses,
25
+ entityMarkup,
26
+ faqHowtoIncentive,
27
+ jsonldValidity,
28
+ schemaCoverage,
29
+ type Rule,
30
+ } from "@working-theory/aeo";
31
+
32
+ import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
33
+ import { ailkAudit } from "../src/tools/audit.js";
34
+
35
+ const REPO_ROOT = resolve(__dirname, "../../..");
36
+ const CONTENT_ROOT = join(REPO_ROOT, "content");
37
+
38
+ // Canonical rule set — mirrors packages/aeo/src/cli/index.ts RULES array.
39
+ // Must stay in sync with that definition; both are sourced from @working-theory/aeo.
40
+ const RULES: Rule[] = [
41
+ alwaysPasses,
42
+ alwaysFails,
43
+ schemaCoverage,
44
+ jsonldValidity,
45
+ faqHowtoIncentive,
46
+ entityMarkup,
47
+ ];
48
+
49
+ const FIXTURE_SLUG = "home";
50
+ const FIXTURE_TYPE = "HomePage" as const;
51
+
52
+ describe("audit ↔ runAudit direct parity", () => {
53
+ it("per-page score matches runAudit(buildSinglePageAuditInput(slug), RULES)", async () => {
54
+ // --- Direct path (mirrors what the tool does internally) ---
55
+ const auditInput = await buildSinglePageAuditInput(
56
+ { slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
57
+ CONTENT_ROOT,
58
+ );
59
+ const directReport = runAudit(auditInput, RULES);
60
+ const directPage = directReport.perPage.find(
61
+ (p) => p.slug === FIXTURE_SLUG && p.type === FIXTURE_TYPE,
62
+ );
63
+ expect(directPage).toBeDefined();
64
+
65
+ // --- MCP tool path ---
66
+ const mcpResult = await ailkAudit(
67
+ { slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
68
+ CONTENT_ROOT,
69
+ );
70
+ const mcpPage = mcpResult.perPage.find(
71
+ (p) => p.slug === FIXTURE_SLUG && p.type === FIXTURE_TYPE,
72
+ );
73
+ expect(mcpPage).toBeDefined();
74
+
75
+ // Per-page fields must match
76
+ expect(mcpPage!.score).toBe(directPage!.score);
77
+ expect(mcpPage!.slug).toBe(directPage!.slug);
78
+ expect(mcpPage!.type).toBe(directPage!.type);
79
+ });
80
+
81
+ it("overall score matches runAudit called with the same AuditInput", async () => {
82
+ const auditInput = await buildSinglePageAuditInput(
83
+ { slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
84
+ CONTENT_ROOT,
85
+ );
86
+ const directReport = runAudit(auditInput, RULES);
87
+
88
+ const mcpResult = await ailkAudit(
89
+ { slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
90
+ CONTENT_ROOT,
91
+ );
92
+
93
+ expect(mcpResult.score).toBe(directReport.score);
94
+ });
95
+
96
+ it("perRule count matches — tool uses the same RULES array", async () => {
97
+ const auditInput = await buildSinglePageAuditInput(
98
+ { slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
99
+ CONTENT_ROOT,
100
+ );
101
+ const directReport = runAudit(auditInput, RULES);
102
+
103
+ const mcpResult = await ailkAudit(
104
+ { slug: FIXTURE_SLUG, type: FIXTURE_TYPE },
105
+ CONTENT_ROOT,
106
+ );
107
+
108
+ expect(mcpResult.perRule).toHaveLength(directReport.perRule.length);
109
+ });
110
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Tests for apps/mcp/src/lib/audit-single-page.ts
3
+ *
4
+ * TDD: URL validation cases (security boundary) first, then happy paths.
5
+ *
6
+ * Acceptance criteria verified here:
7
+ * - Rejects URLs not on the AILK installation's own domain (SSRF guard)
8
+ * - Rejects malformed URLs
9
+ * - Rejects when both slug and url are provided
10
+ * - Rejects when neither slug nor url is provided
11
+ * - Accepts slug-only input and builds AuditInput
12
+ * - Accepts same-origin url input and builds AuditInput
13
+ * - Strips query strings + fragments before slug extraction
14
+ * - Cross-domain error message references ailk audit CLI
15
+ * - No fetch() call (structural test)
16
+ */
17
+
18
+ import { readFileSync } from "node:fs";
19
+ import { resolve, join } from "node:path";
20
+
21
+ import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
22
+
23
+ const REPO_ROOT = resolve(__dirname, "../../..");
24
+ const CONTENT_ROOT = join(REPO_ROOT, "content");
25
+
26
+ // Set env to a known base URL for tests that exercise same-origin logic.
27
+ const TEST_BASE_URL = "https://example.ailk.test";
28
+
29
+ function withBaseUrl(
30
+ url: string,
31
+ fn: () => Promise<unknown>,
32
+ ): Promise<unknown> {
33
+ const prev = process.env.AILK_BASE_URL;
34
+ process.env.AILK_BASE_URL = url;
35
+ return fn().finally(() => {
36
+ if (prev === undefined) delete process.env.AILK_BASE_URL;
37
+ else process.env.AILK_BASE_URL = prev;
38
+ });
39
+ }
40
+
41
+ // ── URL-validation tests (security boundary — RED first) ──────────────────────
42
+
43
+ describe("buildSinglePageAuditInput — URL validation (security boundary)", () => {
44
+ it("rejects a cross-domain URL", async () => {
45
+ await expect(
46
+ withBaseUrl(TEST_BASE_URL, () =>
47
+ buildSinglePageAuditInput(
48
+ { url: "https://evil.example.com/HomePage/some-page" },
49
+ CONTENT_ROOT,
50
+ ),
51
+ ),
52
+ ).rejects.toThrow(/out of scope|use `ailk audit`/i);
53
+ });
54
+
55
+ it("cross-domain error message references ailk audit CLI", async () => {
56
+ const err = await withBaseUrl(TEST_BASE_URL, () =>
57
+ buildSinglePageAuditInput(
58
+ { url: "https://evil.example.com/HomePage/some-page" },
59
+ CONTENT_ROOT,
60
+ ),
61
+ ).catch((e: Error) => e);
62
+ expect(err).toBeInstanceOf(Error);
63
+ expect((err as Error).message).toMatch(/ailk audit/);
64
+ });
65
+
66
+ it("rejects a URL with user-info (credential-stuffed URL bypass attempt)", async () => {
67
+ // The bypass form: https://trusted-domain@attacker.com/page
68
+ // WHATWG URL: userinfo="trusted-domain", hostname="attacker.com"
69
+ // hostname !== base.hostname → rejected.
70
+ const bypassUrl = `https://${TEST_BASE_URL.replace("https://", "")}@attacker.com/page`;
71
+ await expect(
72
+ withBaseUrl(TEST_BASE_URL, () =>
73
+ buildSinglePageAuditInput({ url: bypassUrl }, CONTENT_ROOT),
74
+ ),
75
+ ).rejects.toThrow(/out of scope|use `ailk audit`/i);
76
+ });
77
+
78
+ it("rejects a malformed URL", async () => {
79
+ await expect(
80
+ withBaseUrl(TEST_BASE_URL, () =>
81
+ buildSinglePageAuditInput({ url: "not-a-url" }, CONTENT_ROOT),
82
+ ),
83
+ ).rejects.toThrow();
84
+ });
85
+
86
+ it("rejects input with both slug and url provided", async () => {
87
+ await expect(
88
+ withBaseUrl(TEST_BASE_URL, () =>
89
+ buildSinglePageAuditInput(
90
+ // eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
91
+ {
92
+ slug: "home",
93
+ url: `${TEST_BASE_URL}/HomePage/home`,
94
+ } as unknown as Parameters<typeof buildSinglePageAuditInput>[0],
95
+ CONTENT_ROOT,
96
+ ),
97
+ ),
98
+ ).rejects.toThrow(/both|exclusive/i);
99
+ });
100
+
101
+ it("rejects input with neither slug nor url", async () => {
102
+ await expect(
103
+ withBaseUrl(TEST_BASE_URL, () =>
104
+ buildSinglePageAuditInput(
105
+ // eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
106
+ {} as unknown as Parameters<typeof buildSinglePageAuditInput>[0],
107
+ CONTENT_ROOT,
108
+ ),
109
+ ),
110
+ ).rejects.toThrow(/slug.*url|url.*slug|required/i);
111
+ });
112
+
113
+ it("accepts same-origin URL with matching hostname", async () => {
114
+ await expect(
115
+ withBaseUrl(TEST_BASE_URL, () =>
116
+ buildSinglePageAuditInput(
117
+ { url: `${TEST_BASE_URL}/HomePage/home` },
118
+ CONTENT_ROOT,
119
+ ),
120
+ ),
121
+ ).resolves.toBeDefined();
122
+ });
123
+
124
+ it("strips query string from same-origin URL before slug extraction", async () => {
125
+ const result = await withBaseUrl(TEST_BASE_URL, () =>
126
+ buildSinglePageAuditInput(
127
+ { url: `${TEST_BASE_URL}/HomePage/home?utm_source=test` },
128
+ CONTENT_ROOT,
129
+ ),
130
+ );
131
+ expect(result).toBeDefined();
132
+ });
133
+
134
+ it("strips fragment from same-origin URL before slug extraction", async () => {
135
+ const result = await withBaseUrl(TEST_BASE_URL, () =>
136
+ buildSinglePageAuditInput(
137
+ { url: `${TEST_BASE_URL}/HomePage/home#section` },
138
+ CONTENT_ROOT,
139
+ ),
140
+ );
141
+ expect(result).toBeDefined();
142
+ });
143
+ });
144
+
145
+ // ── Slug happy-path tests ─────────────────────────────────────────────────────
146
+
147
+ describe("buildSinglePageAuditInput — slug input", () => {
148
+ it("returns AuditInput with one page for a known HomePage slug", async () => {
149
+ const result = await buildSinglePageAuditInput(
150
+ { slug: "home", type: "HomePage" },
151
+ CONTENT_ROOT,
152
+ );
153
+ expect(result.pages).toHaveLength(1);
154
+ expect(result.pages[0].slug).toBe("home");
155
+ expect(result.pages[0].type).toBe("HomePage");
156
+ });
157
+
158
+ it("result has locale, identityMode, registry, jsonLdByPage fields", async () => {
159
+ const result = await buildSinglePageAuditInput(
160
+ { slug: "home", type: "HomePage" },
161
+ CONTENT_ROOT,
162
+ );
163
+ expect(typeof result.locale).toBe("string");
164
+ expect(
165
+ result.identityMode === "person" ||
166
+ result.identityMode === "organization",
167
+ ).toBe(true);
168
+ expect(Array.isArray(result.registry)).toBe(true);
169
+ expect(typeof result.jsonLdByPage).toBe("object");
170
+ });
171
+
172
+ it("throws for a slug that does not exist in the content directory", async () => {
173
+ await expect(
174
+ buildSinglePageAuditInput(
175
+ { slug: "does-not-exist-at-all", type: "HomePage" },
176
+ CONTENT_ROOT,
177
+ ),
178
+ ).rejects.toThrow(/not found|does not exist/i);
179
+ });
180
+ });
181
+
182
+ // ── URL happy-path tests ──────────────────────────────────────────────────────
183
+
184
+ describe("buildSinglePageAuditInput — url input (same-origin)", () => {
185
+ it("returns AuditInput for a same-origin URL pointing at a known page", async () => {
186
+ const result = (await withBaseUrl(TEST_BASE_URL, () =>
187
+ buildSinglePageAuditInput(
188
+ { url: `${TEST_BASE_URL}/HomePage/home` },
189
+ CONTENT_ROOT,
190
+ ),
191
+ )) as Awaited<ReturnType<typeof buildSinglePageAuditInput>>;
192
+ expect(result.pages).toHaveLength(1);
193
+ expect(result.pages[0].slug).toBe("home");
194
+ expect(result.pages[0].type).toBe("HomePage");
195
+ });
196
+ });
197
+
198
+ // ── Structural guard ──────────────────────────────────────────────────────────
199
+
200
+ describe("structural guard — no fetch()", () => {
201
+ it("apps/mcp/src/lib/audit-single-page.ts does not call fetch()", () => {
202
+ const src = readFileSync(
203
+ resolve(__dirname, "../src/lib/audit-single-page.ts"),
204
+ "utf-8",
205
+ );
206
+ // Strip block comments (/** ... */ and /* ... */) and line comments (// ...)
207
+ // before checking — comments may mention fetch() as a negative assertion.
208
+ const codeOnly = src
209
+ .replace(/\/\*[\s\S]*?\*\//g, "")
210
+ .replace(/\/\/[^\n]*/g, "");
211
+ expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
212
+ });
213
+ });
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Tests for the audit MCP tool handler.
3
+ *
4
+ * Acceptance criteria verified here:
5
+ * - auditInputSchema validates slug input (with regex enforcement)
6
+ * - auditInputSchema validates url input
7
+ * - auditInputSchema rejects invalid slugs (uppercase, slashes, dots)
8
+ * - ailkAudit returns score + perRule + perPage + meta for a slug input
9
+ * - ailkAudit returns an error result for a cross-domain URL
10
+ * - Output validates against auditOutputSchema
11
+ * - No console.* calls in handler (structural test)
12
+ * - No fetch() call in tool file (structural test)
13
+ *
14
+ * Phase 3 U0 (#294): adds a sanity-check that purchaseInputSchema relocated
15
+ * to @working-theory/validation (per ADR 0013) is reachable and parses the same input
16
+ * shape the inline schema parsed pre-relocation.
17
+ */
18
+
19
+ import { readFileSync } from "node:fs";
20
+ import { resolve, join } from "node:path";
21
+
22
+ import {
23
+ auditInputSchema,
24
+ auditOutputSchema,
25
+ purchaseInputSchema,
26
+ } from "@working-theory/validation";
27
+
28
+ import { ailkAudit } from "../src/tools/audit.js";
29
+
30
+ const REPO_ROOT = resolve(__dirname, "../../..");
31
+ const CONTENT_ROOT = join(REPO_ROOT, "content");
32
+ const TEST_BASE_URL = "https://example.ailk.test";
33
+
34
+ function withBaseUrl(
35
+ url: string,
36
+ fn: () => Promise<unknown>,
37
+ ): Promise<unknown> {
38
+ const prev = process.env.AILK_BASE_URL;
39
+ process.env.AILK_BASE_URL = url;
40
+ return fn().finally(() => {
41
+ if (prev === undefined) delete process.env.AILK_BASE_URL;
42
+ else process.env.AILK_BASE_URL = prev;
43
+ });
44
+ }
45
+
46
+ // ── Input schema tests ────────────────────────────────────────────────────────
47
+
48
+ describe("auditInputSchema", () => {
49
+ it("accepts a valid slug", () => {
50
+ const result = auditInputSchema.safeParse({
51
+ slug: "home",
52
+ type: "HomePage",
53
+ });
54
+ expect(result.success).toBe(true);
55
+ });
56
+
57
+ it("accepts slug without explicit type", () => {
58
+ const result = auditInputSchema.safeParse({ slug: "home" });
59
+ expect(result.success).toBe(true);
60
+ });
61
+
62
+ it("accepts a url string", () => {
63
+ const result = auditInputSchema.safeParse({
64
+ url: "https://example.com/page",
65
+ });
66
+ expect(result.success).toBe(true);
67
+ });
68
+
69
+ it("rejects slug with uppercase letters", () => {
70
+ const result = auditInputSchema.safeParse({ slug: "LaunchingAilk" });
71
+ expect(result.success).toBe(false);
72
+ });
73
+
74
+ it("rejects slug with forward slash (path traversal guard)", () => {
75
+ const result = auditInputSchema.safeParse({ slug: "some/path" });
76
+ expect(result.success).toBe(false);
77
+ });
78
+
79
+ it("rejects slug with dots (path traversal guard)", () => {
80
+ const result = auditInputSchema.safeParse({ slug: "../etc" });
81
+ expect(result.success).toBe(false);
82
+ });
83
+
84
+ it("rejects empty slug", () => {
85
+ const result = auditInputSchema.safeParse({ slug: "" });
86
+ expect(result.success).toBe(false);
87
+ });
88
+
89
+ it("rejects non-url string in url field", () => {
90
+ const result = auditInputSchema.safeParse({ url: "not-a-url" });
91
+ expect(result.success).toBe(false);
92
+ });
93
+
94
+ it("rejects empty object (neither slug nor url)", () => {
95
+ const result = auditInputSchema.safeParse({});
96
+ expect(result.success).toBe(false);
97
+ });
98
+ });
99
+
100
+ // ── Handler happy-path tests ──────────────────────────────────────────────────
101
+
102
+ describe("ailkAudit handler — slug input", () => {
103
+ it("returns score, perRule, perPage, meta for a known HomePage slug", async () => {
104
+ const result = await ailkAudit(
105
+ { slug: "home", type: "HomePage" },
106
+ CONTENT_ROOT,
107
+ );
108
+ expect(typeof result.score).toBe("number");
109
+ expect(result.score).toBeGreaterThanOrEqual(0);
110
+ expect(result.score).toBeLessThanOrEqual(100);
111
+ expect(Array.isArray(result.perRule)).toBe(true);
112
+ expect(Array.isArray(result.perPage)).toBe(true);
113
+ expect(typeof result.meta).toBe("object");
114
+ expect(typeof result.meta.auditedAt).toBe("string");
115
+ expect(typeof result.meta.ruleSet).toBe("string");
116
+ expect(typeof result.meta.locale).toBe("string");
117
+ });
118
+
119
+ it("perRule entries have ruleId, name, weight, status fields", async () => {
120
+ const result = await ailkAudit(
121
+ { slug: "home", type: "HomePage" },
122
+ CONTENT_ROOT,
123
+ );
124
+ for (const rule of result.perRule) {
125
+ expect(typeof rule.ruleId).toBe("string");
126
+ expect(typeof rule.name).toBe("string");
127
+ expect(typeof rule.weight).toBe("number");
128
+ expect(rule.status === "pass" || rule.status === "fail").toBe(true);
129
+ }
130
+ });
131
+
132
+ it("perPage entries have slug and type fields", async () => {
133
+ const result = await ailkAudit(
134
+ { slug: "home", type: "HomePage" },
135
+ CONTENT_ROOT,
136
+ );
137
+ expect(result.perPage).toHaveLength(1);
138
+ expect(result.perPage[0].slug).toBe("home");
139
+ expect(result.perPage[0].type).toBe("HomePage");
140
+ });
141
+
142
+ it("output validates against auditOutputSchema", async () => {
143
+ const result = await ailkAudit(
144
+ { slug: "home", type: "HomePage" },
145
+ CONTENT_ROOT,
146
+ );
147
+ const parsed = auditOutputSchema.safeParse(result);
148
+ expect(parsed.success).toBe(true);
149
+ });
150
+ });
151
+
152
+ describe("ailkAudit handler — url input (same-origin)", () => {
153
+ it("returns audit result for a same-origin URL", async () => {
154
+ const result = (await withBaseUrl(TEST_BASE_URL, () =>
155
+ ailkAudit(
156
+ { url: `${TEST_BASE_URL}/HomePage/home` },
157
+ CONTENT_ROOT,
158
+ ),
159
+ )) as Awaited<ReturnType<typeof ailkAudit>>;
160
+ expect(typeof result.score).toBe("number");
161
+ expect(result.perPage[0].slug).toBe("home");
162
+ });
163
+ });
164
+
165
+ describe("ailkAudit handler — cross-domain URL", () => {
166
+ it("throws for a cross-domain URL (no fetch attempted)", async () => {
167
+ await expect(
168
+ withBaseUrl(TEST_BASE_URL, () =>
169
+ ailkAudit(
170
+ { url: "https://evil.example.com/HomePage/page" },
171
+ CONTENT_ROOT,
172
+ ),
173
+ ),
174
+ ).rejects.toThrow(/out of scope|use `ailk audit`/i);
175
+ });
176
+ });
177
+
178
+ // ── Structural guards ─────────────────────────────────────────────────────────
179
+
180
+ describe("structural guards", () => {
181
+ it("apps/mcp/src/tools/audit.ts does not call console.*", () => {
182
+ const src = readFileSync(
183
+ resolve(__dirname, "../src/tools/audit.ts"),
184
+ "utf-8",
185
+ );
186
+ expect(src).not.toMatch(/\bconsole\.(log|warn|error|info|debug)\b/);
187
+ });
188
+
189
+ it("apps/mcp/src/tools/audit.ts does not call fetch()", () => {
190
+ const src = readFileSync(
191
+ resolve(__dirname, "../src/tools/audit.ts"),
192
+ "utf-8",
193
+ );
194
+ // Strip comments before checking — JSDoc may mention fetch() as a negative assertion.
195
+ const codeOnly = src
196
+ .replace(/\/\*[\s\S]*?\*\//g, "")
197
+ .replace(/\/\/[^\n]*/g, "");
198
+ expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
199
+ });
200
+ });
201
+
202
+ // ── Schema relocation parity (Phase 3 U0 / ADR 0013) ──────────────────────────
203
+
204
+ describe("purchaseInputSchema relocated to @working-theory/validation", () => {
205
+ it("parses the same shape the inline schema parsed pre-relocation", () => {
206
+ const result = purchaseInputSchema.safeParse({
207
+ priceId: "price_test_123",
208
+ quantity: 2,
209
+ });
210
+ expect(result.success).toBe(true);
211
+ });
212
+
213
+ it("rejects an empty priceId", () => {
214
+ const result = purchaseInputSchema.safeParse({ priceId: "" });
215
+ expect(result.success).toBe(false);
216
+ });
217
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Drift-detection test (issue #209): the MCP server's registered tool list
3
+ * MUST equal the canonical catalog from @working-theory/metadata.
4
+ *
5
+ * Without this, two surfaces (apps/web manifest + apps/mcp server) could drift
6
+ * on tool name or description and only an integration test on a real MCP
7
+ * client would catch it. This test forces them in lock-step.
8
+ *
9
+ * What this asserts:
10
+ * - Tool names registered on the McpServer == catalog tool names
11
+ * - Tool descriptions registered on the McpServer == catalog descriptions
12
+ *
13
+ * What this does NOT assert:
14
+ * - inputSchema equality — the server registers Zod schemas; the catalog
15
+ * publishes JSON-Schema descriptors. Same source upstream
16
+ * (packages/validation), but distinct shapes downstream by design.
17
+ */
18
+
19
+ import { getMcpToolCatalog } from "@working-theory/metadata";
20
+
21
+ import { createMcpServer } from "../src/server.js";
22
+
23
+ interface RegisteredTool {
24
+ description?: string;
25
+ }
26
+
27
+ describe("MCP catalog ↔ server drift", () => {
28
+ it("registered tool names equal catalog tool names", () => {
29
+ const server = createMcpServer();
30
+ const registered = (
31
+ // eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
32
+ server as unknown as { _registeredTools: Record<string, RegisteredTool> }
33
+ )._registeredTools;
34
+ const registeredNames = Object.keys(registered).sort();
35
+ const catalogNames = getMcpToolCatalog()
36
+ .map((t) => t.name)
37
+ .sort();
38
+ expect(registeredNames).toEqual(catalogNames);
39
+ });
40
+
41
+ it("registered tool descriptions equal catalog descriptions", () => {
42
+ const server = createMcpServer();
43
+ const registered = (
44
+ // eslint-disable-next-line no-restricted-syntax -- #506/#507: pre-existing test-hygiene violation
45
+ server as unknown as { _registeredTools: Record<string, RegisteredTool> }
46
+ )._registeredTools;
47
+ for (const entry of getMcpToolCatalog()) {
48
+ expect(registered[entry.name]).toBeDefined();
49
+ expect(registered[entry.name]!.description).toBe(entry.description);
50
+ }
51
+ });
52
+ });