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,129 @@
1
+ /**
2
+ * Unit tests for the require-paid-tier Fastify preHandler.
3
+ *
4
+ * Mocks @working-theory/license `checkLicense` and asserts:
5
+ * - free tier → reply.status(402).send(payment_required canonical body)
6
+ * - pro tier → preHandler returns void (handler proceeds)
7
+ * - checkLicense throws → safe-by-default 402 (no false-positive proceed)
8
+ */
9
+
10
+ const mockCheckLicense = jest.fn();
11
+
12
+ jest.mock("@working-theory/license", () => ({
13
+ checkLicense: (...args: unknown[]) => mockCheckLicense(...args),
14
+ }));
15
+
16
+ import type { FastifyReply, FastifyRequest } from "fastify";
17
+
18
+ import { requirePaidTier } from "../require-paid-tier.js";
19
+
20
+ // ── Typed reply builder ───────────────────────────────────────────────────────
21
+ //
22
+ // A minimal Fastify reply stub that satisfies the FastifyReply interface surface
23
+ // used by requirePaidTier (status + send chain). Using a properly typed factory
24
+ // avoids the `as unknown as FastifyReply` double assertion pattern.
25
+
26
+ interface StubReply {
27
+ statusCalled: number | null;
28
+ sendCalled: unknown;
29
+ status: jest.MockedFunction<(code: number) => StubReply>;
30
+ send: jest.MockedFunction<(body: unknown) => StubReply>;
31
+ }
32
+
33
+ function makeReply(): StubReply {
34
+ const reply: StubReply = {
35
+ statusCalled: null,
36
+ sendCalled: undefined,
37
+ status: jest.fn(),
38
+ send: jest.fn(),
39
+ };
40
+ reply.status.mockImplementation((code: number) => {
41
+ reply.statusCalled = code;
42
+ return reply;
43
+ });
44
+ reply.send.mockImplementation((body: unknown) => {
45
+ reply.sendCalled = body;
46
+ return reply;
47
+ });
48
+ return reply;
49
+ }
50
+
51
+ // Cast the stub to FastifyReply once, at a typed boundary, so all call-sites
52
+ // use the actual StubReply type rather than repeating the double assertion.
53
+ function asReply(stub: StubReply): FastifyReply {
54
+ // @ts-expect-error: StubReply satisfies the status+send surface used by requirePaidTier;
55
+ // a full Fastify reply cannot be constructed without a live server instance in unit tests.
56
+ return stub;
57
+ }
58
+
59
+ const fakeRequest = {} as FastifyRequest;
60
+
61
+ describe("requirePaidTier", () => {
62
+ beforeEach(() => {
63
+ mockCheckLicense.mockReset();
64
+ });
65
+
66
+ it("free tier — replies 402 with canonical payment_required body", async () => {
67
+ mockCheckLicense.mockResolvedValue({
68
+ tier: "free",
69
+ plan: null,
70
+ features: [],
71
+ expiresAt: null,
72
+ revoked: false,
73
+ });
74
+ const reply = makeReply();
75
+
76
+ await requirePaidTier(fakeRequest, asReply(reply));
77
+
78
+ expect(reply.statusCalled).toBe(402);
79
+ expect(reply.sendCalled).toEqual({
80
+ error: "payment_required",
81
+ message: expect.stringContaining("paid license"),
82
+ });
83
+ });
84
+
85
+ it("pro tier — proceeds without calling reply.status/send", async () => {
86
+ mockCheckLicense.mockResolvedValue({
87
+ tier: "pro",
88
+ plan: "pro",
89
+ features: [],
90
+ expiresAt: null,
91
+ revoked: false,
92
+ });
93
+ const reply = makeReply();
94
+
95
+ await requirePaidTier(fakeRequest, asReply(reply));
96
+
97
+ expect(reply.status).not.toHaveBeenCalled();
98
+ expect(reply.send).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it("checkLicense throws — degrades to 402 (safe default)", async () => {
102
+ mockCheckLicense.mockRejectedValue(new Error("boom"));
103
+ const reply = makeReply();
104
+
105
+ await requirePaidTier(fakeRequest, asReply(reply));
106
+
107
+ expect(reply.statusCalled).toBe(402);
108
+ expect(reply.sendCalled).toMatchObject({ error: "payment_required" });
109
+ });
110
+
111
+ it("revoked paid license — replies 402 even though tier is pro (issue #398)", async () => {
112
+ // After a 403/revoked refresh, checkLicense() returns tier:'pro' until
113
+ // the existing JWT exp (up to 24h). The revoked flag is the early
114
+ // signal — we must close the gate now, not wait for exp.
115
+ mockCheckLicense.mockResolvedValue({
116
+ tier: "pro",
117
+ plan: "pro",
118
+ features: [],
119
+ expiresAt: null,
120
+ revoked: true,
121
+ });
122
+ const reply = makeReply();
123
+
124
+ await requirePaidTier(fakeRequest, asReply(reply));
125
+
126
+ expect(reply.statusCalled).toBe(402);
127
+ expect(reply.sendCalled).toMatchObject({ error: "payment_required" });
128
+ });
129
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * apps/api stripe shim — surface-drift smoke tests.
3
+ *
4
+ * Ensures apps/api/src/lib/stripe.ts re-exports exactly what
5
+ * @working-theory/stripe-client exports, with no surface drift.
6
+ */
7
+
8
+ jest.mock("@working-theory/stripe-client", () => ({
9
+ getStripeClient: jest.fn(),
10
+ assertSandboxKey: jest.fn(),
11
+ verifyWebhookSignature: jest.fn(),
12
+ createCheckoutSession: jest.fn(),
13
+ STRIPE_API_VERSION: "2023-10-16",
14
+ WEBHOOK_TOLERANCE_SECONDS: 300,
15
+ StripeConfigError: class StripeConfigError extends Error {},
16
+ StripeWebhookSignatureError: class StripeWebhookSignatureError extends Error {},
17
+ UnknownProductCodeError: class UnknownProductCodeError extends Error {},
18
+ }));
19
+
20
+ // Import the shim (not the wrapper directly)
21
+ import * as stripeShim from "../stripe.js";
22
+ import * as stripeClient from "@working-theory/stripe-client";
23
+
24
+ describe("apps/api/src/lib/stripe — surface re-exports", () => {
25
+ it("exports getStripeClient as a function", () => {
26
+ expect(typeof stripeShim.getStripeClient).toBe("function");
27
+ });
28
+
29
+ it("exports assertSandboxKey as a function", () => {
30
+ expect(typeof stripeShim.assertSandboxKey).toBe("function");
31
+ });
32
+
33
+ it("exports verifyWebhookSignature as a function", () => {
34
+ expect(typeof stripeShim.verifyWebhookSignature).toBe("function");
35
+ });
36
+
37
+ it("exports createCheckoutSession as a function", () => {
38
+ expect(typeof stripeShim.createCheckoutSession).toBe("function");
39
+ });
40
+
41
+ it("exports STRIPE_API_VERSION", () => {
42
+ expect(stripeShim.STRIPE_API_VERSION).toBe("2023-10-16");
43
+ });
44
+
45
+ it("exports WEBHOOK_TOLERANCE_SECONDS", () => {
46
+ expect(stripeShim.WEBHOOK_TOLERANCE_SECONDS).toBe(300);
47
+ });
48
+
49
+ it("exports StripeConfigError as a constructor producing Error instances", () => {
50
+ expect(typeof stripeShim.StripeConfigError).toBe("function");
51
+ expect(new stripeShim.StripeConfigError()).toBeInstanceOf(Error);
52
+ });
53
+
54
+ it("exports StripeWebhookSignatureError as a constructor producing Error instances", () => {
55
+ expect(typeof stripeShim.StripeWebhookSignatureError).toBe("function");
56
+ expect(new stripeShim.StripeWebhookSignatureError()).toBeInstanceOf(Error);
57
+ });
58
+
59
+ it("exports UnknownProductCodeError as a constructor producing Error instances", () => {
60
+ expect(typeof stripeShim.UnknownProductCodeError).toBe("function");
61
+ expect(new stripeShim.UnknownProductCodeError()).toBeInstanceOf(Error);
62
+ });
63
+
64
+ it("shim functions are the same references as @working-theory/stripe-client exports", () => {
65
+ expect(stripeShim.getStripeClient).toBe(stripeClient.getStripeClient);
66
+ expect(stripeShim.assertSandboxKey).toBe(stripeClient.assertSandboxKey);
67
+ expect(stripeShim.verifyWebhookSignature).toBe(
68
+ stripeClient.verifyWebhookSignature,
69
+ );
70
+ expect(stripeShim.createCheckoutSession).toBe(
71
+ stripeClient.createCheckoutSession,
72
+ );
73
+ });
74
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shared content adapter singleton for apps/api.
3
+ *
4
+ * The MDX adapter is the v1 default. Set `CONTENT_ADAPTER=projection` to opt
5
+ * into the build-time projection adapter backed by a compiled manifest.
6
+ *
7
+ * Adapter selection:
8
+ * - (unset or any value other than "projection") → MDX adapter
9
+ * - "projection" → projection adapter (requires compiled manifest)
10
+ *
11
+ * Content-root resolution order:
12
+ * 1. `CONTENT_ROOT` env var (explicit override — recommended for production)
13
+ * 2. `<cwd>/content` (works when the server is started from the repo root)
14
+ * 3. `<cwd>/../../content` (works when started from `apps/api/` via pnpm)
15
+ *
16
+ * Manifest-path resolution (projection only):
17
+ * 1. `MANIFEST_PATH` env var (explicit override)
18
+ * 2. `<content-root>/.compiled/manifest.json` (default)
19
+ */
20
+ import { existsSync } from "node:fs";
21
+ import { resolve } from "node:path";
22
+
23
+ import {
24
+ createMdxAdapter,
25
+ type ContentAdapter,
26
+ type MdxContentAdapter,
27
+ } from "@working-theory/content-adapters";
28
+ import { createProjectionAdapter } from "@working-theory/content-projection";
29
+ import { loadManifestFromDisk } from "@working-theory/content-projection/node";
30
+
31
+ import { readDefaultLocale } from "./locales.js";
32
+
33
+ let cachedAdapter: ContentAdapter | undefined;
34
+
35
+ /**
36
+ * Resolve the content root. Honours `CONTENT_ROOT` env first, then probes the
37
+ * cwd-relative candidates above.
38
+ */
39
+ export function getContentRoot(): string {
40
+ if (process.env.CONTENT_ROOT) return process.env.CONTENT_ROOT;
41
+
42
+ const cwd = process.cwd();
43
+ const cwdContent = resolve(cwd, "content");
44
+ const repoContent = resolve(cwd, "../../content");
45
+ if (existsSync(cwdContent)) return cwdContent;
46
+ if (existsSync(repoContent)) return repoContent;
47
+ // Last resort — cwd-relative, even if missing. The adapter will return
48
+ // null/empty for missing pages; routes will surface 404s.
49
+ return cwdContent;
50
+ }
51
+
52
+ /**
53
+ * Returns the process-wide content adapter, lazily constructed on first use.
54
+ * Safe to call repeatedly. Adapter type is determined by `CONTENT_ADAPTER` env.
55
+ */
56
+ export function getContentAdapter(): ContentAdapter {
57
+ if (!cachedAdapter) {
58
+ cachedAdapter = buildAdapter();
59
+ }
60
+ return cachedAdapter;
61
+ }
62
+
63
+ function buildAdapter(): ContentAdapter {
64
+ const contentRoot = getContentRoot();
65
+
66
+ if (process.env.CONTENT_ADAPTER === "projection") {
67
+ const manifestPath =
68
+ process.env.MANIFEST_PATH ??
69
+ resolve(contentRoot, ".compiled", "manifest.json");
70
+
71
+ const manifest = loadManifestFromDisk(manifestPath);
72
+ return createProjectionAdapter({ manifest });
73
+ }
74
+
75
+ return createMdxAdapter({ contentRoot, defaultLocale: readDefaultLocale() });
76
+ }
77
+
78
+ /**
79
+ * Test/utility helper — drop the cached adapter so the next call rebuilds
80
+ * with current env. Not used in production.
81
+ */
82
+ export function resetContentAdapter(): void {
83
+ cachedAdapter = undefined;
84
+ }
85
+
86
+ export type { MdxContentAdapter };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Fastify error-serializer — prevents internal error detail from leaking to clients.
3
+ *
4
+ * Fastify's default error handler echoes `err.message` (and in dev,
5
+ * `err.stack`) in the response body. For internal errors (DB failures, SDK
6
+ * exceptions, unhandled throws) that's sensitive: connection strings, host
7
+ * names, schema hints, internal paths. This handler:
8
+ *
9
+ * - Maps any 5xx-class internal Error to `{ error: "internal_error" }`
10
+ * with no detail
11
+ * - Logs the full Error (message, stack, request context) to pino at
12
+ * error level
13
+ * - Lets Fastify's built-in 4xx validation errors flow through
14
+ * (their `validation` payload is safe to surface — it describes the
15
+ * client's input, not server internals)
16
+ * - Leaves replies that handlers already committed (explicit status + body)
17
+ * untouched
18
+ */
19
+ import type { FastifyError, FastifyInstance } from "fastify";
20
+
21
+ const GENERIC_INTERNAL_BODY = { error: "internal_error" } as const;
22
+
23
+ export function registerErrorSerializer(server: FastifyInstance): void {
24
+ server.setErrorHandler(async (err: FastifyError, request, reply) => {
25
+ // Validation errors (Fastify's schema validator) — keep structured detail.
26
+ // These describe the client request, not server internals.
27
+ if (err.validation) {
28
+ const status = err.statusCode ?? 400;
29
+ request.log.warn(
30
+ {
31
+ method: request.method,
32
+ path: request.url,
33
+ validation: err.validation,
34
+ },
35
+ "request validation failed",
36
+ );
37
+ return reply.code(status).send({
38
+ error: "validation-error",
39
+ details: err.validation,
40
+ });
41
+ }
42
+
43
+ // Explicit 4xx errors thrown by handlers (e.g. fastify.httpErrors.notFound)
44
+ // also flow through — they're a deliberate boundary signal, not an
45
+ // internal leak.
46
+ const status = err.statusCode ?? 500;
47
+ if (status >= 400 && status < 500) {
48
+ request.log.info(
49
+ {
50
+ method: request.method,
51
+ path: request.url,
52
+ statusCode: status,
53
+ errName: err.name,
54
+ },
55
+ "request returned 4xx",
56
+ );
57
+ // Don't echo err.message — handlers that want a structured body should
58
+ // call reply.code().send() directly. This branch is the safe fallback.
59
+ return reply.code(status).send({ error: err.code ?? "client_error" });
60
+ }
61
+
62
+ // 5xx — log everything, return nothing identifying.
63
+ request.log.error(
64
+ {
65
+ method: request.method,
66
+ path: request.url,
67
+ errName: err.name,
68
+ errMessage: err.message,
69
+ errStack: err.stack,
70
+ errCode: err.code,
71
+ },
72
+ "internal server error",
73
+ );
74
+
75
+ return reply.code(500).send(GENERIC_INTERNAL_BODY);
76
+ });
77
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Canonical error shape for apps/api content-write routes (D14a Stage 0).
3
+ *
4
+ * Every error response from create/update/delete content routes returns this
5
+ * shape as the JSON body, paired with the appropriate HTTP status. The MCP
6
+ * twin (`apps/mcp/src/lib/errors.ts`) returns the same shape sans `status` —
7
+ * MCP carries the error in the tool result body itself.
8
+ *
9
+ * Codes are a closed set; consumers can switch on them to drive UI / retry
10
+ * logic without parsing free-form `message` text. New codes get added here
11
+ * before any route uses them.
12
+ *
13
+ * Usage:
14
+ * const err = validationError("Invalid type or slug", { field: "type" });
15
+ * return reply.status(err.status).send(err.body);
16
+ */
17
+ import { mapAdapterError } from "@working-theory/content-adapters";
18
+
19
+ export type ErrorCode =
20
+ | "validation_failed"
21
+ | "not_found"
22
+ | "conflict"
23
+ | "payment_required"
24
+ | "unauthorized";
25
+
26
+ export interface ErrorBody {
27
+ error: ErrorCode;
28
+ message: string;
29
+ details?: Record<string, unknown>;
30
+ }
31
+
32
+ export interface ErrorResponse {
33
+ status: number;
34
+ body: ErrorBody;
35
+ }
36
+
37
+ function makeBody(
38
+ code: ErrorCode,
39
+ message: string,
40
+ details?: Record<string, unknown>,
41
+ ): ErrorBody {
42
+ if (details === undefined) {
43
+ return { error: code, message };
44
+ }
45
+ return { error: code, message, details };
46
+ }
47
+
48
+ export function validationError(
49
+ message: string,
50
+ details?: Record<string, unknown>,
51
+ ): ErrorResponse {
52
+ return { status: 400, body: makeBody("validation_failed", message, details) };
53
+ }
54
+
55
+ export function unauthorizedError(message: string): ErrorResponse {
56
+ return { status: 401, body: makeBody("unauthorized", message) };
57
+ }
58
+
59
+ export function paymentRequiredError(message: string): ErrorResponse {
60
+ return { status: 402, body: makeBody("payment_required", message) };
61
+ }
62
+
63
+ export function notFoundError(message: string): ErrorResponse {
64
+ return { status: 404, body: makeBody("not_found", message) };
65
+ }
66
+
67
+ export function conflictError(
68
+ message: string,
69
+ details?: Record<string, unknown>,
70
+ ): ErrorResponse {
71
+ return { status: 409, body: makeBody("conflict", message, details) };
72
+ }
73
+
74
+ /**
75
+ * Route-layer adapter for `mapAdapterError` from `@working-theory/content-adapters`.
76
+ * The package emits a canonical `{error, message, details?}` body but
77
+ * cannot know the HTTP status (it has no app dependency). This helper
78
+ * pairs the body with the right status. Returns `null` if the error
79
+ * wasn't an `AdapterWriteError` (caller should treat as 500).
80
+ */
81
+ export function adapterErrorResponse(
82
+ err: unknown,
83
+ ctx: { type: string; slug: string },
84
+ ): ErrorResponse | null {
85
+ const body = mapAdapterError(err, ctx);
86
+ if (body === null) return null;
87
+ switch (body.error) {
88
+ case "conflict":
89
+ return { status: 409, body };
90
+ case "not_found":
91
+ return { status: 404, body };
92
+ case "validation_failed":
93
+ return { status: 400, body };
94
+ }
95
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * File-fallback writer for the AILK inbox.
3
+ *
4
+ * Writes a Lead to ${AILK_INBOX_FALLBACK_DIR ?? '.ailk/inbox'}/<ISO-ts>-<lead.id>.json
5
+ * when the database is unavailable. Engaged by B6's lead-routing service on DB failure.
6
+ *
7
+ * Path-traversal-safe by construction:
8
+ * 1. lead.id is validated against /^[a-z0-9_-]{8,64}$/i before any fs operation.
9
+ * 2. The resolved write path is checked to be strictly inside targetDir.
10
+ *
11
+ * No quota / count cap. Operators running with this fallback engaged for extended periods
12
+ * should monitor disk usage and replay leads back to DB once it returns.
13
+ *
14
+ * Module is import-side-effect-free. No env reads at module-load time.
15
+ */
16
+
17
+ import * as fs from "fs/promises";
18
+ import * as path from "path";
19
+
20
+ import type { Lead } from "@working-theory/validation";
21
+
22
+ export type FallbackLead = Lead;
23
+
24
+ export type FallbackResult =
25
+ | { ok: true; path: string }
26
+ | {
27
+ ok: false;
28
+ error: {
29
+ kind: "invalid_id" | "path_escape" | "fs_error";
30
+ message: string;
31
+ };
32
+ };
33
+
34
+ const LEAD_ID_RE = /^[a-z0-9_-]{8,64}$/i;
35
+
36
+ export async function writeLeadFallback(
37
+ lead: FallbackLead,
38
+ ): Promise<FallbackResult> {
39
+ if (!LEAD_ID_RE.test(lead.id)) {
40
+ return {
41
+ ok: false,
42
+ error: { kind: "invalid_id", message: `Invalid lead id: ${lead.id}` },
43
+ };
44
+ }
45
+
46
+ // Resolve target directory — read env inside the function (no module-level side effects)
47
+ const envDir = process.env.AILK_INBOX_FALLBACK_DIR ?? ".ailk/inbox";
48
+ const targetDir = path.resolve(process.cwd(), envDir);
49
+
50
+ const rawTs = isValidIso(lead.createdAt)
51
+ ? lead.createdAt
52
+ : new Date().toISOString();
53
+ // Replace colons with hyphens for Windows filesystem compatibility
54
+ const ts = rawTs.replace(/:/g, "-");
55
+
56
+ const filename = `${ts}-${lead.id}.json`;
57
+ const resolved = path.resolve(targetDir, filename);
58
+
59
+ // Path-escape check: relative path must not start with '..' or be absolute
60
+ const rel = path.relative(targetDir, resolved);
61
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
62
+ return {
63
+ ok: false,
64
+ error: {
65
+ kind: "path_escape",
66
+ message: `Path escape detected: ${resolved}`,
67
+ },
68
+ };
69
+ }
70
+
71
+ try {
72
+ await fs.mkdir(targetDir, { recursive: true });
73
+ await fs.writeFile(resolved, JSON.stringify(lead, null, 2));
74
+ return { ok: true, path: resolved };
75
+ } catch (err) {
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ return { ok: false, error: { kind: "fs_error", message } };
78
+ }
79
+ }
80
+
81
+ // Require YYYY-MM-DD prefix before accepting as ISO — rejects slash-dates, traversal strings, etc.
82
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]*)?$/;
83
+
84
+ function isValidIso(value: string): boolean {
85
+ if (!value || !ISO_RE.test(value)) return false;
86
+ return !isNaN(new Date(value).getTime());
87
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Lead capture store — always-on JSONL writer (Issue #27 Unit 1).
3
+ *
4
+ * Appends one JSON line per successful lead submission to apps/api/leads.jsonl
5
+ * (or LEAD_CAPTURE_FILE if set). Uses fs.appendFile with flag:'a' which maps to
6
+ * POSIX O_APPEND — the kernel guarantees atomicity for writes ≤ PIPE_BUF on
7
+ * local filesystems, making this safe for concurrent processes/requests.
8
+ *
9
+ * Semantic difference from B6 file-fallback:
10
+ * B6 file-fallback (.ailk/inbox/) — engaged only when the DB throws (salvage path)
11
+ * This module — always-on canonical AX surface; disabled explicitly via env, not by DB presence
12
+ *
13
+ * Env vars (all read inside function bodies — module is import-side-effect-free):
14
+ * LEAD_CAPTURE_FILE — output path (default: apps/api/leads.jsonl relative to cwd)
15
+ * LEAD_CAPTURE_FILE_DISABLED — set to "true" to skip writes (opt-out)
16
+ * LEAD_CAPTURE_INCLUDE_IP — set to "true" to include metadata.ip in the record
17
+ *
18
+ * PII discipline:
19
+ * IP is excluded from the written record by default.
20
+ * No other PII is filtered — `data` carries whatever the route gave us.
21
+ * Callers are responsible for what they put in `data`.
22
+ */
23
+
24
+ import * as fs from "fs/promises";
25
+ import * as path from "path";
26
+
27
+ import type { LeadCaptureRecord } from "@working-theory/validation";
28
+
29
+ // Re-export so consumers can import from one place.
30
+ export type { LeadCaptureRecord };
31
+
32
+ export type AppendResult =
33
+ | { ok: true; skipped?: true }
34
+ | { ok: false; error: string };
35
+
36
+ /**
37
+ * Append one lead record as a JSONL line.
38
+ *
39
+ * Always resolves — never throws. Returns ok:false on fs errors so the caller
40
+ * can log and continue without crashing the request.
41
+ */
42
+ export async function appendLeadRecord(
43
+ record: LeadCaptureRecord,
44
+ ): Promise<AppendResult> {
45
+ // Read env inside the function — no module-level side effects.
46
+ const disabled = process.env.LEAD_CAPTURE_FILE_DISABLED === "true";
47
+ if (disabled) {
48
+ return { ok: true, skipped: true };
49
+ }
50
+
51
+ const includeIp = process.env.LEAD_CAPTURE_INCLUDE_IP === "true";
52
+ const outputPath = resolveOutputPath();
53
+
54
+ // Strip IP from metadata unless explicitly opted in.
55
+ const cleanMeta = includeIp
56
+ ? record.metadata
57
+ : Object.fromEntries(
58
+ Object.entries(record.metadata).filter(([k]) => k !== "ip"),
59
+ );
60
+
61
+ const line = JSON.stringify({ ...record, metadata: cleanMeta }) + "\n";
62
+
63
+ try {
64
+ await fs.appendFile(outputPath, line, { flag: "a", encoding: "utf8" });
65
+ return { ok: true };
66
+ } catch (err) {
67
+ const message = err instanceof Error ? err.message : String(err);
68
+ return { ok: false, error: message };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Startup-time validation: if LEAD_CAPTURE_FILE points at a path whose parent
74
+ * directory does not exist, throw immediately so the server fails fast rather
75
+ * than erroring on first write.
76
+ *
77
+ * Call from server.ts build() before registering routes.
78
+ * No-op when LEAD_CAPTURE_FILE is unset or LEAD_CAPTURE_FILE_DISABLED=true.
79
+ */
80
+ export async function validateLeadCaptureDir(): Promise<void> {
81
+ const disabled = process.env.LEAD_CAPTURE_FILE_DISABLED === "true";
82
+ const filePath = process.env.LEAD_CAPTURE_FILE;
83
+
84
+ if (!filePath || disabled) {
85
+ return;
86
+ }
87
+
88
+ const dir = path.dirname(path.resolve(filePath));
89
+
90
+ try {
91
+ await fs.access(dir);
92
+ } catch {
93
+ throw new Error(
94
+ `LEAD_CAPTURE_FILE directory does not exist: "${dir}". ` +
95
+ `Create the directory or unset LEAD_CAPTURE_FILE.`,
96
+ );
97
+ }
98
+ }
99
+
100
+ // ─── Internal ─────────────────────────────────────────────────────────────────
101
+
102
+ function resolveOutputPath(): string {
103
+ const envPath = process.env.LEAD_CAPTURE_FILE;
104
+ if (envPath) {
105
+ return path.resolve(envPath);
106
+ }
107
+ // Default: apps/api/leads.jsonl relative to the process cwd.
108
+ // In production (Vercel function or Node standalone), cwd is the project root.
109
+ return path.resolve(process.cwd(), "apps/api/leads.jsonl");
110
+ }