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,144 @@
1
+ /**
2
+ * @file file-fallback.test.ts
3
+ * @description Unit tests for file-fallback.ts — writeLeadFallback
4
+ *
5
+ * Test path deviation from spec §3: spec states apps/api/__tests__/lib/,
6
+ * but jest.config.cjs roots at <rootDir>/src, so tests live at
7
+ * apps/api/src/lib/__tests__/ to match the existing repo convention.
8
+ */
9
+
10
+ import * as fs from "fs/promises";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+
14
+ import { writeLeadFallback } from "../file-fallback.js";
15
+ import type { FallbackLead } from "../file-fallback.js";
16
+
17
+ function makeLead(overrides: Partial<FallbackLead> = {}): FallbackLead {
18
+ return {
19
+ id: "abc123de",
20
+ siteId: "site-001",
21
+ source: "oss-form",
22
+ email: "test@example.com",
23
+ payload: { message: "hello" },
24
+ createdAt: "2026-05-08T12:00:00.000Z",
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ let tmpDir: string;
30
+
31
+ beforeEach(async () => {
32
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "file-fallback-test-"));
33
+ process.env.AILK_INBOX_FALLBACK_DIR = tmpDir;
34
+ });
35
+
36
+ afterEach(async () => {
37
+ delete process.env.AILK_INBOX_FALLBACK_DIR;
38
+ await fs.rm(tmpDir, { recursive: true, force: true });
39
+ });
40
+
41
+ describe("writeLeadFallback", () => {
42
+ it("happy path: writes <timestamp>-<id>.json; file content parses back to lead", async () => {
43
+ const lead = makeLead();
44
+ const result = await writeLeadFallback(lead);
45
+
46
+ expect(result).toMatchObject({ ok: true });
47
+ if (!result.ok) throw new Error("expected ok");
48
+
49
+ const raw = await fs.readFile(result.path, "utf8");
50
+ const parsed = JSON.parse(raw);
51
+ expect(parsed).toMatchObject({ id: lead.id, email: lead.email });
52
+ expect(result.path).toContain(`-${lead.id}.json`);
53
+ });
54
+
55
+ it("directory creation: creates target dir if it does not exist", async () => {
56
+ const nested = path.join(tmpDir, "new-subdir", "inbox");
57
+ process.env.AILK_INBOX_FALLBACK_DIR = nested;
58
+
59
+ const lead = makeLead();
60
+ const result = await writeLeadFallback(lead);
61
+
62
+ expect(result).toMatchObject({ ok: true });
63
+ const stat = await fs.stat(nested);
64
+ expect(stat.isDirectory()).toBe(true);
65
+ });
66
+
67
+ it("invalid createdAt falls back to current ISO timestamp", async () => {
68
+ const lead = makeLead({ createdAt: "not-a-date" });
69
+ const result = await writeLeadFallback(lead);
70
+
71
+ expect(result).toMatchObject({ ok: true });
72
+ if (!result.ok) throw new Error("expected ok");
73
+
74
+ // Filename must match the ISO-derived pattern: YYYY-MM-DDTHH-MM-SS.mmmZ-<id>.json
75
+ const filename = path.basename(result.path);
76
+ expect(filename).toMatch(
77
+ /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z-abc123de\.json$/,
78
+ );
79
+ });
80
+
81
+ it("path traversal via lead.id: rejected with invalid_id (no fs touch)", async () => {
82
+ const lead = makeLead({ id: "../../etc/passwd" });
83
+ const result = await writeLeadFallback(lead);
84
+
85
+ expect(result).toMatchObject({ ok: false, error: { kind: "invalid_id" } });
86
+ const files = await fs.readdir(tmpDir);
87
+ expect(files).toHaveLength(0);
88
+ });
89
+
90
+ it("path traversal via env var: resolved path stays inside targetDir", async () => {
91
+ // Env var with traversal — operator-controlled but function validates
92
+ const resolved = path.resolve(
93
+ process.cwd(),
94
+ `${tmpDir}/legit/../../${path.basename(tmpDir)}`,
95
+ );
96
+ process.env.AILK_INBOX_FALLBACK_DIR = `${tmpDir}/legit/../../${path.basename(tmpDir)}`;
97
+
98
+ const lead = makeLead();
99
+ const result = await writeLeadFallback(lead);
100
+
101
+ // The resolved dir is valid — write succeeds; path stays inside resolved targetDir
102
+ expect(result).toMatchObject({ ok: true });
103
+ if (!result.ok) throw new Error("expected ok");
104
+ expect(result.path.startsWith(resolved)).toBe(true);
105
+ });
106
+
107
+ it("filesystem error: returns fs_error result; does not throw", async () => {
108
+ // Make targetDir a file (not a directory) to force mkdir failure
109
+ await fs.rm(tmpDir, { recursive: true });
110
+ await fs.writeFile(tmpDir, "not-a-dir");
111
+
112
+ const lead = makeLead();
113
+ const result = await writeLeadFallback(lead);
114
+
115
+ expect(result).toMatchObject({ ok: false, error: { kind: "fs_error" } });
116
+ });
117
+
118
+ it("Windows-compat: colons are replaced with hyphens in the timestamp portion of the filename", async () => {
119
+ const lead = makeLead({ createdAt: "2026-05-08T12:00:00.000Z" });
120
+ const result = await writeLeadFallback(lead);
121
+
122
+ expect(result).toMatchObject({ ok: true });
123
+ if (!result.ok) throw new Error("expected ok");
124
+
125
+ const filename = path.basename(result.path);
126
+ expect(filename).not.toContain(":");
127
+ });
128
+
129
+ it("crafted createdAt traversal: ISO validator rejects non-ISO, falls back to current, writes inside targetDir", async () => {
130
+ const lead = makeLead({ createdAt: "../2026" });
131
+ const result = await writeLeadFallback(lead);
132
+
133
+ // ISO validator rejects '../2026' → falls back to new Date().toISOString()
134
+ // Write succeeds inside targetDir; no escape file created
135
+ expect(result).toMatchObject({ ok: true });
136
+ if (!result.ok) throw new Error("expected ok");
137
+ expect(result.path.startsWith(tmpDir)).toBe(true);
138
+
139
+ // Confirm no file was written outside tmpDir (escape path would be one level up)
140
+ const escapedDir = path.resolve(tmpDir, "..");
141
+ const escapedFile = path.join(escapedDir, `2026-${lead.id}.json`);
142
+ await expect(fs.stat(escapedFile)).rejects.toThrow();
143
+ });
144
+ });
@@ -0,0 +1,8 @@
1
+ ---
2
+ type: HomePage
3
+ slug: home
4
+ title: AI Launch Kit
5
+ description: AX-first OSS template for shipping software with API and MCP.
6
+ ---
7
+
8
+ Home content.
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @file lead-capture-store.test.ts
3
+ * Unit tests for the lead-capture-store write path (Issue #27 Unit 1).
4
+ *
5
+ * All fs operations are mocked; no real disk I/O in these tests.
6
+ * Acceptance criteria covered (spec §5.1):
7
+ * AC1 — file write on every successful submission (always-on by default)
8
+ * AC3 — IPs default OFF; opt-in via LEAD_CAPTURE_INCLUDE_IP=true
9
+ * AC4 — LEAD_CAPTURE_FILE_DISABLED=true short-circuits write
10
+ * AC5 — LEAD_CAPTURE_FILE env drives output path
11
+ * AC7 — 50 concurrent writes land without corruption (POSIX O_APPEND atomicity)
12
+ * AC8 — startup-time directory check (tested in server.test.ts)
13
+ * AC9 — LeadCaptureRecord shape validated by Zod schema
14
+ * AC14 — appendLeadRecord resolves ok:false on fs error (never throws)
15
+ */
16
+
17
+ // ─── fs mock ─────────────────────────────────────────────────────────────────
18
+ const mockAppendFile = jest.fn();
19
+ const mockAccess = jest.fn();
20
+ const mockMkdir = jest.fn();
21
+
22
+ jest.mock("fs/promises", () => ({
23
+ appendFile: (...args: unknown[]) => mockAppendFile(...args),
24
+ access: (...args: unknown[]) => mockAccess(...args),
25
+ mkdir: (...args: unknown[]) => mockMkdir(...args),
26
+ }));
27
+
28
+ import {
29
+ appendLeadRecord,
30
+ validateLeadCaptureDir,
31
+ } from "../lead-capture-store.js";
32
+ import type { LeadCaptureRecord } from "../lead-capture-store.js";
33
+
34
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
35
+
36
+ const record: LeadCaptureRecord = {
37
+ id: "cuid-test-1",
38
+ createdAt: "2024-01-01T00:00:00.000Z",
39
+ source: "contact",
40
+ data: { email: "test@example.com", message: "hello" },
41
+ metadata: {},
42
+ };
43
+
44
+ // ─── Tests ───────────────────────────────────────────────────────────────────
45
+
46
+ describe("appendLeadRecord", () => {
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ mockAppendFile.mockResolvedValue(undefined);
50
+ mockMkdir.mockResolvedValue(undefined);
51
+ // Clear lead-capture env vars
52
+ delete process.env.LEAD_CAPTURE_FILE;
53
+ delete process.env.LEAD_CAPTURE_FILE_DISABLED;
54
+ delete process.env.LEAD_CAPTURE_INCLUDE_IP;
55
+ });
56
+
57
+ afterEach(() => {
58
+ delete process.env.LEAD_CAPTURE_FILE;
59
+ delete process.env.LEAD_CAPTURE_FILE_DISABLED;
60
+ delete process.env.LEAD_CAPTURE_INCLUDE_IP;
61
+ });
62
+
63
+ // AC1 — always-on by default (no env set)
64
+ it("writes a JSONL line by default (always-on)", async () => {
65
+ const result = await appendLeadRecord(record);
66
+
67
+ expect(result.ok).toBe(true);
68
+ expect(mockAppendFile).toHaveBeenCalledTimes(1);
69
+ // Verify the written content is valid JSON with a newline
70
+ const [, content, opts] = mockAppendFile.mock.calls[0];
71
+ expect(opts).toMatchObject({ flag: "a" });
72
+ const parsed = JSON.parse(content.trimEnd());
73
+ expect(parsed.id).toBe(record.id);
74
+ expect(parsed.source).toBe(record.source);
75
+ });
76
+
77
+ // AC5 — LEAD_CAPTURE_FILE drives output path
78
+ it("uses LEAD_CAPTURE_FILE env for output path", async () => {
79
+ process.env.LEAD_CAPTURE_FILE = "/tmp/test-leads.jsonl";
80
+ const result = await appendLeadRecord(record);
81
+
82
+ expect(result.ok).toBe(true);
83
+ expect(mockAppendFile).toHaveBeenCalledWith(
84
+ "/tmp/test-leads.jsonl",
85
+ expect.any(String),
86
+ expect.objectContaining({ flag: "a" }),
87
+ );
88
+ });
89
+
90
+ // AC5 — defaults to apps/api/leads.jsonl relative to cwd when unset
91
+ it("defaults to apps/api/leads.jsonl when LEAD_CAPTURE_FILE not set", async () => {
92
+ const result = await appendLeadRecord(record);
93
+
94
+ expect(result.ok).toBe(true);
95
+ const [filePath] = mockAppendFile.mock.calls[0];
96
+ expect(filePath).toMatch(/leads\.jsonl$/);
97
+ });
98
+
99
+ // AC4 — LEAD_CAPTURE_FILE_DISABLED=true skips the write
100
+ it("short-circuits write when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
101
+ process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
102
+ const result = await appendLeadRecord(record);
103
+
104
+ expect(result.ok).toBe(true);
105
+ expect((result as { ok: true; skipped: true }).skipped).toBe(true);
106
+ expect(mockAppendFile).not.toHaveBeenCalled();
107
+ });
108
+
109
+ // AC3 — IPs default OFF
110
+ it("does not include IP by default", async () => {
111
+ const recordWithIp: LeadCaptureRecord = {
112
+ ...record,
113
+ metadata: { ip: "1.2.3.4" },
114
+ };
115
+ const result = await appendLeadRecord(recordWithIp);
116
+
117
+ expect(result.ok).toBe(true);
118
+ const [, content] = mockAppendFile.mock.calls[0];
119
+ const parsed = JSON.parse(content.trimEnd());
120
+ expect(parsed.metadata?.ip).toBeUndefined();
121
+ });
122
+
123
+ // AC3 — LEAD_CAPTURE_INCLUDE_IP=true includes the IP
124
+ it("includes IP when LEAD_CAPTURE_INCLUDE_IP=true", async () => {
125
+ process.env.LEAD_CAPTURE_INCLUDE_IP = "true";
126
+ const recordWithIp: LeadCaptureRecord = {
127
+ ...record,
128
+ metadata: { ip: "1.2.3.4" },
129
+ };
130
+ const result = await appendLeadRecord(recordWithIp);
131
+
132
+ expect(result.ok).toBe(true);
133
+ const [, content] = mockAppendFile.mock.calls[0];
134
+ const parsed = JSON.parse(content.trimEnd());
135
+ expect(parsed.metadata?.ip).toBe("1.2.3.4");
136
+ });
137
+
138
+ // AC14 — never throws; returns ok:false on fs error
139
+ it("returns ok:false on fs error, never throws", async () => {
140
+ mockAppendFile.mockRejectedValue(new Error("ENOSPC: no space"));
141
+
142
+ const result = await appendLeadRecord(record);
143
+
144
+ expect(result.ok).toBe(false);
145
+ expect((result as { ok: false; error: string }).error).toMatch(/ENOSPC/);
146
+ await expect(appendLeadRecord(record)).resolves.toBeDefined();
147
+ });
148
+
149
+ // Written content must be JSONL (one JSON object per line + newline)
150
+ it("writes valid JSONL format (one JSON object + trailing newline)", async () => {
151
+ await appendLeadRecord(record);
152
+ const [, content] = mockAppendFile.mock.calls[0];
153
+ expect(content).toMatch(/\n$/);
154
+ // Must parse as JSON
155
+ expect(() => JSON.parse(content.trimEnd())).not.toThrow();
156
+ });
157
+
158
+ // AC7 — 50-concurrent write integrity (atomicity via O_APPEND flag)
159
+ // We verify the flag is set and all 50 calls complete successfully.
160
+ // Real O_APPEND atomicity is kernel-guaranteed for local filesystems.
161
+ it("50 concurrent writes all resolve ok:true with flag:a", async () => {
162
+ const promises = Array.from({ length: 50 }, (_, i) =>
163
+ appendLeadRecord({ ...record, id: `cuid-${i}` }),
164
+ );
165
+ const results = await Promise.all(promises);
166
+ expect(results).toHaveLength(50);
167
+ results.forEach((r) => expect(r.ok).toBe(true));
168
+ // All calls use flag:'a'
169
+ mockAppendFile.mock.calls.forEach(([, , opts]) => {
170
+ expect(opts).toMatchObject({ flag: "a" });
171
+ });
172
+ expect(mockAppendFile).toHaveBeenCalledTimes(50);
173
+ });
174
+
175
+ // Record shape must match LeadCaptureRecord (id, createdAt, source, data, metadata)
176
+ it("written JSONL contains all required top-level fields", async () => {
177
+ await appendLeadRecord(record);
178
+ const [, content] = mockAppendFile.mock.calls[0];
179
+ const parsed = JSON.parse(content.trimEnd());
180
+ expect(parsed).toHaveProperty("id");
181
+ expect(parsed).toHaveProperty("createdAt");
182
+ expect(parsed).toHaveProperty("source");
183
+ expect(parsed).toHaveProperty("data");
184
+ expect(parsed).toHaveProperty("metadata");
185
+ });
186
+ });
187
+
188
+ describe("validateLeadCaptureDir", () => {
189
+ beforeEach(() => {
190
+ jest.clearAllMocks();
191
+ delete process.env.LEAD_CAPTURE_FILE;
192
+ delete process.env.LEAD_CAPTURE_FILE_DISABLED;
193
+ });
194
+
195
+ afterEach(() => {
196
+ delete process.env.LEAD_CAPTURE_FILE;
197
+ delete process.env.LEAD_CAPTURE_FILE_DISABLED;
198
+ });
199
+
200
+ // AC8 — no LEAD_CAPTURE_FILE set → no-op (validation passes)
201
+ it("resolves without error when LEAD_CAPTURE_FILE not set", async () => {
202
+ await expect(validateLeadCaptureDir()).resolves.toBeUndefined();
203
+ });
204
+
205
+ // AC8 — LEAD_CAPTURE_FILE_DISABLED → no-op
206
+ it("resolves without error when LEAD_CAPTURE_FILE_DISABLED=true", async () => {
207
+ process.env.LEAD_CAPTURE_FILE_DISABLED = "true";
208
+ process.env.LEAD_CAPTURE_FILE = "/nonexistent/dir/leads.jsonl";
209
+ await expect(validateLeadCaptureDir()).resolves.toBeUndefined();
210
+ });
211
+
212
+ // AC8 — directory exists → resolves
213
+ it("resolves when LEAD_CAPTURE_FILE directory exists", async () => {
214
+ process.env.LEAD_CAPTURE_FILE = "/tmp/exists/leads.jsonl";
215
+ mockAccess.mockResolvedValue(undefined);
216
+ await expect(validateLeadCaptureDir()).resolves.toBeUndefined();
217
+ });
218
+
219
+ // AC8 — directory missing → throws
220
+ it("throws when LEAD_CAPTURE_FILE directory does not exist", async () => {
221
+ process.env.LEAD_CAPTURE_FILE = "/nonexistent/dir/leads.jsonl";
222
+ mockAccess.mockRejectedValue(
223
+ Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
224
+ );
225
+ await expect(validateLeadCaptureDir()).rejects.toThrow(/LEAD_CAPTURE_FILE/);
226
+ });
227
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * outbound-http — fetch wrapper with timeout + SSRF allow-list.
3
+ *
4
+ * Every outbound HTTP call originating from apps/api/src/lib goes through this
5
+ * wrapper. Two guarantees:
6
+ *
7
+ * 1. Default 10s timeout via AbortSignal.timeout
8
+ * 2. SSRF allow-list — RFC 1918 / loopback / link-local addresses are rejected
9
+ * unless explicitly allowed (test-mode escape hatch only)
10
+ *
11
+ * The wrapper is forward-looking — apps/api/src/lib currently has no outbound
12
+ * fetch callsites — so the wrapper exists and tests pin its contract before
13
+ * the first consumer arrives.
14
+ */
15
+ import { ssrfBlocked, outboundFetch } from "../outbound-http.js";
16
+
17
+ describe("outbound-http — SSRF allow-list", () => {
18
+ it.each([
19
+ ["http://127.0.0.1/path", "loopback"],
20
+ ["http://127.255.0.1/path", "loopback"],
21
+ ["http://localhost/path", "loopback hostname"],
22
+ ["http://10.0.0.1/", "RFC 1918 10/8"],
23
+ ["http://10.255.255.255/", "RFC 1918 10/8 edge"],
24
+ ["http://172.16.0.1/", "RFC 1918 172.16/12"],
25
+ ["http://172.31.255.255/", "RFC 1918 172.16/12 edge"],
26
+ ["http://192.168.0.1/", "RFC 1918 192.168/16"],
27
+ ["http://192.168.255.255/", "RFC 1918 192.168/16 edge"],
28
+ ["http://169.254.169.254/latest/meta-data/", "AWS metadata link-local"],
29
+ ["http://169.254.0.1/", "link-local"],
30
+ ["http://[::1]/", "IPv6 loopback"],
31
+ ["http://0.0.0.0/", "all-interfaces wildcard"],
32
+ ])("rejects %s (%s)", (url) => {
33
+ expect(ssrfBlocked(url)).toBe(true);
34
+ });
35
+
36
+ it.each([
37
+ ["https://api.stripe.com/v1/x", "public Stripe"],
38
+ ["https://api.example.com/path", "generic public host"],
39
+ ["http://8.8.8.8/", "public IP"],
40
+ ["https://172.15.0.1/", "172.15 is OUT of RFC 1918 172.16/12"],
41
+ ["https://172.32.0.1/", "172.32 is OUT of RFC 1918 172.16/12"],
42
+ ])("allows %s (%s)", (url) => {
43
+ expect(ssrfBlocked(url)).toBe(false);
44
+ });
45
+
46
+ it("rejects non-HTTP schemes", () => {
47
+ expect(ssrfBlocked("file:///etc/passwd")).toBe(true);
48
+ expect(ssrfBlocked("ftp://example.com/")).toBe(true);
49
+ expect(ssrfBlocked("javascript:alert(1)")).toBe(true);
50
+ expect(ssrfBlocked("gopher://example.com/")).toBe(true);
51
+ });
52
+
53
+ it("rejects unparseable URLs", () => {
54
+ expect(ssrfBlocked("not a url")).toBe(true);
55
+ expect(ssrfBlocked("")).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe("outbound-http — outboundFetch", () => {
60
+ let originalFetch: typeof globalThis.fetch;
61
+
62
+ beforeEach(() => {
63
+ originalFetch = globalThis.fetch;
64
+ });
65
+
66
+ afterEach(() => {
67
+ globalThis.fetch = originalFetch;
68
+ });
69
+
70
+ it("rejects SSRF-blocked URLs before issuing a network call", async () => {
71
+ const fetchSpy: jest.Mock<ReturnType<typeof globalThis.fetch>, Parameters<typeof globalThis.fetch>> =
72
+ jest.fn();
73
+ globalThis.fetch = fetchSpy;
74
+
75
+ await expect(outboundFetch("http://127.0.0.1/admin")).rejects.toThrow(
76
+ /SSRF|disallowed|forbidden/i,
77
+ );
78
+ expect(fetchSpy).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it("issues fetch on an allowed URL with a default timeout signal", async () => {
82
+ const fetchSpy: typeof globalThis.fetch = jest.fn(async (_url, init) => {
83
+ // confirm AbortSignal is wired
84
+ expect((init as RequestInit | undefined)?.signal).toBeDefined();
85
+ return new Response("ok", { status: 200 });
86
+ });
87
+ globalThis.fetch = fetchSpy;
88
+
89
+ const res = await outboundFetch("https://api.example.com/x");
90
+ expect(res.status).toBe(200);
91
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it("aborts when timeout elapses before fetch resolves", async () => {
95
+ // Mock fetch to honor the abort signal — simulates a real slow server.
96
+ const slowFetch: typeof globalThis.fetch = async (_url, init) => {
97
+ return new Promise((_resolve, reject) => {
98
+ (init as RequestInit | undefined)?.signal?.addEventListener(
99
+ "abort",
100
+ () => {
101
+ reject(new DOMException("aborted", "AbortError"));
102
+ },
103
+ );
104
+ });
105
+ };
106
+ globalThis.fetch = slowFetch;
107
+
108
+ await expect(
109
+ outboundFetch("https://api.example.com/slow", { timeoutMs: 50 }),
110
+ ).rejects.toThrow(/abort|timeout/i);
111
+ });
112
+
113
+ it("respects the per-call allowPrivate escape hatch (test/dev only)", async () => {
114
+ const fetchSpy: typeof globalThis.fetch = jest.fn(
115
+ async () => new Response("ok", { status: 200 }),
116
+ );
117
+ globalThis.fetch = fetchSpy;
118
+
119
+ const res = await outboundFetch("http://127.0.0.1/local-svc", {
120
+ allowPrivate: true,
121
+ });
122
+ expect(res.status).toBe(200);
123
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
124
+ });
125
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Unit tests for the prisma.ts singleton — verify construction, log routing,
3
+ * and globalThis caching behaviour without touching the real adapter or DB.
4
+ */
5
+
6
+ const mockPrismaInstance = {
7
+ $connect: jest.fn(),
8
+ $disconnect: jest.fn(),
9
+ user: {},
10
+ apiToken: {},
11
+ userData: {},
12
+ };
13
+
14
+ const MockPrismaClient = jest.fn().mockImplementation((options) => {
15
+ expect(options).toBeDefined();
16
+ expect(options.adapter).toBeDefined();
17
+ return mockPrismaInstance;
18
+ });
19
+
20
+ // Mock @prisma/adapter-pg so the import in prisma.ts doesn't construct a
21
+ // real pg.Pool against DATABASE_URL.
22
+ const MockPrismaPg = jest.fn().mockImplementation((config) => {
23
+ expect(config).toBeDefined();
24
+ expect(config.connectionString).toBeDefined();
25
+ return { __mockAdapter: true };
26
+ });
27
+
28
+ jest.mock('@prisma/adapter-pg', () => ({
29
+ PrismaPg: MockPrismaPg,
30
+ }));
31
+
32
+ jest.mock('@working-theory/database', () => ({
33
+ PrismaClient: MockPrismaClient,
34
+ }));
35
+
36
+ describe('Prisma Client', () => {
37
+ const originalEnv = process.env;
38
+
39
+ beforeEach(() => {
40
+ jest.resetModules();
41
+ process.env = { ...originalEnv };
42
+ // Clear global prisma
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
44
+ (globalThis as any).prisma = undefined;
45
+ jest.clearAllMocks();
46
+ });
47
+
48
+ afterEach(() => {
49
+ process.env = originalEnv;
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
51
+ (globalThis as any).prisma = undefined;
52
+ });
53
+
54
+ describe('PrismaClient initialization', () => {
55
+ it('should create new PrismaClient instance when globalThis.prisma is undefined', () => {
56
+ // ARRANGE
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
58
+ (globalThis as any).prisma = undefined;
59
+ process.env.NODE_ENV = 'test';
60
+
61
+ // ACT
62
+ const { prisma } = require('../prisma');
63
+
64
+ // ASSERT
65
+ expect(prisma).toBeDefined();
66
+ const { PrismaClient } = require('@working-theory/database');
67
+ expect(PrismaClient).toHaveBeenCalled();
68
+ });
69
+
70
+ it('should use existing globalThis.prisma instance if available', () => {
71
+ // ARRANGE
72
+ const mockPrisma = { $connect: jest.fn(), user: {} };
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
74
+ (globalThis as any).prisma = mockPrisma;
75
+
76
+ // ACT
77
+ const { prisma } = require('../prisma');
78
+
79
+ // ASSERT
80
+ expect(prisma).toBe(mockPrisma);
81
+ const { PrismaClient } = require('@working-theory/database');
82
+ expect(PrismaClient).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('should configure logging for development environment', () => {
86
+ // ARRANGE
87
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
88
+ (globalThis as any).prisma = undefined;
89
+ process.env.NODE_ENV = 'development';
90
+
91
+ // ACT
92
+ require('../prisma');
93
+
94
+ // ASSERT
95
+ const { PrismaClient } = require('@working-theory/database');
96
+ expect(PrismaClient).toHaveBeenCalledWith({
97
+ adapter: expect.objectContaining({ __mockAdapter: true }),
98
+ log: ['query', 'error', 'warn'],
99
+ });
100
+ });
101
+
102
+ it('should configure minimal logging for production environment', () => {
103
+ // ARRANGE
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
105
+ (globalThis as any).prisma = undefined;
106
+ process.env.NODE_ENV = 'production';
107
+
108
+ // ACT
109
+ require('../prisma');
110
+
111
+ // ASSERT
112
+ const { PrismaClient } = require('@working-theory/database');
113
+ expect(PrismaClient).toHaveBeenCalledWith({
114
+ adapter: expect.objectContaining({ __mockAdapter: true }),
115
+ log: ['error'],
116
+ });
117
+ });
118
+
119
+ it('should configure minimal logging for test environment', () => {
120
+ // ARRANGE
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
122
+ (globalThis as any).prisma = undefined;
123
+ process.env.NODE_ENV = 'test';
124
+
125
+ // ACT
126
+ require('../prisma');
127
+
128
+ // ASSERT
129
+ const { PrismaClient } = require('@working-theory/database');
130
+ expect(PrismaClient).toHaveBeenCalledWith({
131
+ adapter: expect.objectContaining({ __mockAdapter: true }),
132
+ log: ['error'],
133
+ });
134
+ });
135
+
136
+ it('should store prisma instance in globalThis for non-production environments', () => {
137
+ // ARRANGE
138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
139
+ (globalThis as any).prisma = undefined;
140
+ process.env.NODE_ENV = 'development';
141
+
142
+ // ACT
143
+ const { prisma } = require('../prisma');
144
+
145
+ // ASSERT
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
147
+ expect((globalThis as any).prisma).toBe(prisma);
148
+ });
149
+
150
+ it('should not store prisma instance in globalThis for production', () => {
151
+ // ARRANGE
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #506/#507: pre-existing test-hygiene violation
153
+ (globalThis as any).prisma = undefined;
154
+ process.env.NODE_ENV = 'production';
155
+
156
+ // ACT
157
+ const { prisma } = require('../prisma');
158
+
159
+ // ASSERT
160
+ // In production, it should not be stored in globalThis
161
+ // But the instance should still be created
162
+ expect(prisma).toBeDefined();
163
+ });
164
+ });
165
+ });