create-ailk 0.1.0 → 0.2.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 (620) hide show
  1. package/README.md +124 -3
  2. package/component-catalog.md +456 -0
  3. package/dist/cli.js +92 -16
  4. package/dist/component-catalog.d.ts +11 -0
  5. package/dist/component-catalog.js +39 -0
  6. package/dist/copy.d.ts +7 -0
  7. package/dist/copy.js +24 -0
  8. package/dist/derive-architecture.d.ts +13 -0
  9. package/dist/derive-architecture.js +238 -0
  10. package/dist/index.d.ts +7 -2
  11. package/dist/index.js +19 -2
  12. package/dist/lib/extract-module-bundle.d.ts +16 -0
  13. package/dist/lib/extract-module-bundle.js +184 -0
  14. package/dist/lib/fetch-module.d.ts +55 -0
  15. package/dist/lib/fetch-module.js +125 -0
  16. package/dist/lib/module-license-gate.d.ts +48 -0
  17. package/dist/lib/module-license-gate.js +67 -0
  18. package/dist/lib/module-tier.d.ts +35 -0
  19. package/dist/lib/module-tier.js +37 -0
  20. package/dist/module-architecture.d.ts +175 -0
  21. package/dist/module-architecture.js +663 -0
  22. package/dist/parse-args.d.ts +47 -1
  23. package/dist/parse-args.js +143 -2
  24. package/dist/programmatic.d.ts +160 -3
  25. package/dist/programmatic.js +196 -7
  26. package/dist/surfaces.d.ts +211 -0
  27. package/dist/surfaces.js +532 -0
  28. package/dist/sync-routes.d.ts +92 -0
  29. package/dist/sync-routes.js +350 -0
  30. package/package.json +12 -5
  31. package/templates/.claude/rules/architecture.md +6 -2
  32. package/templates/.claude/skills/README.md +9 -9
  33. package/templates/.claude/skills/add-schema/SKILL.md +14 -7
  34. package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
  35. package/templates/.claude/skills/provision-config/SKILL.md +1 -1
  36. package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
  37. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
  38. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
  39. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
  40. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
  41. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
  42. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
  43. package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
  44. package/templates/.env.example +126 -2
  45. package/templates/CLAUDE.md +11 -5
  46. package/templates/README.md +1 -1
  47. package/templates/apps/api/.env.example +38 -0
  48. package/templates/apps/api/CLAUDE.md +78 -1
  49. package/templates/apps/api/LICENSE +201 -0
  50. package/templates/apps/api/api/index.ts +6 -141
  51. package/templates/apps/api/jest.config.cjs +16 -0
  52. package/templates/apps/api/package.json +22 -6
  53. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
  54. package/templates/apps/api/src/__tests__/server.test.ts +46 -0
  55. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
  56. package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
  57. package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
  58. package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
  59. package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
  60. package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
  61. package/templates/apps/api/src/bin/seed-presets.ts +58 -0
  62. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
  63. package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
  64. package/templates/apps/api/src/cli.ts +5 -11
  65. package/templates/apps/api/src/config/index.ts +35 -0
  66. package/templates/apps/api/src/config/modules.ts +43 -0
  67. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
  68. package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
  69. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
  70. package/templates/apps/api/src/lib/site.ts +16 -0
  71. package/templates/apps/api/src/lib/stripe.ts +22 -0
  72. package/templates/apps/api/src/lib/tenant-db.ts +225 -0
  73. package/templates/apps/api/src/lib/ws-token.ts +91 -0
  74. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
  75. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
  77. package/templates/apps/api/src/middleware/auth.ts +43 -8
  78. package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
  79. package/templates/apps/api/src/middleware/tenant.ts +99 -0
  80. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
  81. package/templates/apps/api/src/openapi/spec.ts +312 -3
  82. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
  83. package/templates/apps/api/src/routes/aeo/index.ts +107 -0
  84. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
  85. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
  86. package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
  87. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
  88. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
  89. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
  90. package/templates/apps/api/src/routes/billing/index.ts +36 -0
  91. package/templates/apps/api/src/routes/billing/portal.ts +182 -0
  92. package/templates/apps/api/src/routes/billing/read.ts +75 -0
  93. package/templates/apps/api/src/routes/billing/usage.ts +153 -0
  94. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
  95. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
  96. package/templates/apps/api/src/routes/content/create.ts +1 -0
  97. package/templates/apps/api/src/routes/content/delete.ts +1 -0
  98. package/templates/apps/api/src/routes/content/index.ts +42 -6
  99. package/templates/apps/api/src/routes/content/update.ts +1 -0
  100. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
  101. package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
  102. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
  103. package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
  104. package/templates/apps/api/src/routes/flows/README.md +147 -0
  105. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
  106. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
  107. package/templates/apps/api/src/routes/flows/index.ts +202 -0
  108. package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
  109. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
  110. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
  111. package/templates/apps/api/src/routes/leads/index.ts +43 -9
  112. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
  113. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
  114. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
  115. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
  116. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
  120. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
  121. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
  122. package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
  123. package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
  124. package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
  125. package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
  126. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
  127. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
  128. package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
  129. package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
  130. package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
  131. package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
  132. package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
  133. package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
  134. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
  135. package/templates/apps/api/src/routes/schedule/index.ts +249 -0
  136. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
  137. package/templates/apps/api/src/routes/slack/actions.ts +177 -0
  138. package/templates/apps/api/src/routes/slack/index.ts +38 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
  140. package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
  141. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
  142. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
  143. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
  144. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
  145. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
  146. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
  147. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
  148. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
  149. package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
  150. package/templates/apps/api/src/routes/webhooks/README.md +35 -12
  151. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
  152. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
  153. package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
  154. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
  155. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
  156. package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
  157. package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
  158. package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
  159. package/templates/apps/api/src/server.ts +307 -7
  160. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
  161. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
  162. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
  163. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  164. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  165. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
  166. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
  167. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
  168. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
  169. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
  170. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
  171. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
  172. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
  173. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
  174. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
  175. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
  176. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
  177. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
  178. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
  179. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
  180. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
  181. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
  182. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
  183. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
  184. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
  185. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
  186. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
  187. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
  188. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
  189. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
  190. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
  191. package/templates/apps/api/src/services/abandon-email.ts +307 -0
  192. package/templates/apps/api/src/services/aeo-score.ts +288 -0
  193. package/templates/apps/api/src/services/answer-distributions.ts +138 -0
  194. package/templates/apps/api/src/services/consent.ts +236 -0
  195. package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
  196. package/templates/apps/api/src/services/entitlements.ts +102 -0
  197. package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
  198. package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
  199. package/templates/apps/api/src/services/flow-engine.ts +1296 -0
  200. package/templates/apps/api/src/services/lead-promotion.ts +176 -0
  201. package/templates/apps/api/src/services/lead-routing.ts +3 -3
  202. package/templates/apps/api/src/services/listing-config.ts +102 -0
  203. package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
  204. package/templates/apps/api/src/services/listing-promotion.ts +342 -0
  205. package/templates/apps/api/src/services/playbook-compile.ts +398 -0
  206. package/templates/apps/api/src/services/playbook-render.ts +263 -0
  207. package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
  208. package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
  209. package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
  210. package/templates/apps/api/src/services/project-listings.ts +1472 -0
  211. package/templates/apps/api/src/services/recommender-capture.ts +835 -0
  212. package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
  213. package/templates/apps/api/src/services/scheduling/index.ts +63 -0
  214. package/templates/apps/api/src/services/scheduling/types.ts +88 -0
  215. package/templates/apps/api/src/services/slack-notify.ts +240 -0
  216. package/templates/apps/api/src/services/tenant-context.ts +451 -0
  217. package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
  218. package/templates/apps/api/src/services/usage-credits.ts +340 -0
  219. package/templates/apps/api/src/services/usage-metering.ts +699 -0
  220. package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
  221. package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
  222. package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
  223. package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
  224. package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
  225. package/templates/apps/api/src/vercel-handler.ts +165 -0
  226. package/templates/apps/api/tsconfig.json +0 -12
  227. package/templates/apps/mcp/CLAUDE.md +19 -9
  228. package/templates/apps/mcp/LICENSE +201 -0
  229. package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
  230. package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
  231. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
  232. package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
  233. package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
  234. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
  235. package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
  236. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
  237. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
  238. package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
  239. package/templates/apps/mcp/package.json +6 -4
  240. package/templates/apps/mcp/src/config/modules.ts +37 -0
  241. package/templates/apps/mcp/src/server.ts +109 -18
  242. package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
  243. package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
  244. package/templates/apps/mcp/src/tools/create_page.ts +1 -0
  245. package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
  246. package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
  247. package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
  248. package/templates/apps/mcp/src/tools/index.ts +25 -0
  249. package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
  250. package/templates/apps/mcp/src/tools/purchase.ts +3 -0
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
  252. package/templates/apps/mcp/src/tools/update_page.ts +1 -0
  253. package/templates/apps/mcp/tsconfig.json +1 -9
  254. package/templates/apps/web/.env.example +44 -0
  255. package/templates/apps/web/CLAUDE.md +28 -0
  256. package/templates/apps/web/LICENSE +201 -0
  257. package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
  258. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
  259. package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
  260. package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
  261. package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
  262. package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
  263. package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
  264. package/templates/apps/web/__tests__/setup.ts +22 -32
  265. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
  266. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
  267. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
  268. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
  269. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
  270. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
  271. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
  272. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
  273. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
  274. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
  275. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
  276. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
  277. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
  278. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
  279. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
  280. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
  281. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
  282. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
  283. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
  284. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
  285. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
  286. package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
  287. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
  288. package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
  289. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
  290. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
  291. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  292. package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
  293. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
  294. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  295. package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
  296. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
  297. package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
  298. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
  299. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
  300. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
  301. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
  302. package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
  303. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
  304. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
  305. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
  306. package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
  307. package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
  308. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
  309. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  310. package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
  311. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
  312. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
  313. package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
  314. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
  315. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
  316. package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
  317. package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
  318. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
  319. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
  320. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
  321. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
  322. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
  323. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
  324. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
  325. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
  326. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
  327. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
  328. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
  329. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
  330. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
  331. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
  332. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
  333. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
  334. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  335. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
  336. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  337. package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
  338. package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
  339. package/templates/apps/web/app/[locale]/layout.tsx +127 -16
  340. package/templates/apps/web/app/[locale]/page.tsx +10 -1
  341. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
  342. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  343. package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
  344. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
  345. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  346. package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
  347. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
  348. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
  349. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
  350. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
  351. package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
  352. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
  353. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
  354. package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
  355. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
  356. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
  357. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
  358. package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
  359. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
  360. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  361. package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
  362. package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
  363. package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
  364. package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
  365. package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
  366. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
  367. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
  368. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  369. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
  370. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  371. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
  372. package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
  373. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
  374. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
  375. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
  376. package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
  377. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
  378. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  379. package/templates/apps/web/app/globals.css +35 -0
  380. package/templates/apps/web/app/llms.txt/route.ts +2 -1
  381. package/templates/apps/web/app/og/route.tsx +10 -2
  382. package/templates/apps/web/auth.ts +8 -0
  383. package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
  384. package/templates/apps/web/content/en/blog/README.txt +25 -0
  385. package/templates/apps/web/content/en/docs/index.mdx +28 -0
  386. package/templates/apps/web/jest.config.cjs +90 -27
  387. package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
  388. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  389. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
  390. package/templates/apps/web/lib/account-config.ts +13 -0
  391. package/templates/apps/web/lib/server-api.ts +85 -0
  392. package/templates/apps/web/lib/site-brand.tsx +121 -0
  393. package/templates/apps/web/lib/site-theme.ts +74 -0
  394. package/templates/apps/web/messages/en.json +275 -7
  395. package/templates/apps/web/middleware.ts +11 -3
  396. package/templates/apps/web/next.config.mjs +8 -2
  397. package/templates/apps/web/package.json +17 -7
  398. package/templates/apps/web/postcss.config.mjs +23 -0
  399. package/templates/apps/web/public/.well-known/security.txt +3 -3
  400. package/templates/apps/web/src/lib/routes.ts +15 -2
  401. package/templates/apps/web/tsconfig.json +1 -16
  402. package/templates/content/_site.mdx +36 -25
  403. package/templates/content/en/AboutPage/about.mdx +24 -14
  404. package/templates/content/en/ContactPage/contact.mdx +23 -14
  405. package/templates/content/en/FAQPage/faq.mdx +10 -392
  406. package/templates/content/en/HomePage/home.mdx +14 -285
  407. package/templates/database/CHANGELOG.md +443 -0
  408. package/templates/database/LICENSE +201 -0
  409. package/templates/database/README.md +3 -1
  410. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
  411. package/templates/database/__tests__/flows-presets.test.ts +173 -0
  412. package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
  413. package/templates/database/__tests__/seed.test.ts +88 -0
  414. package/templates/database/__tests__/user-cascade.test.ts +19 -4
  415. package/templates/database/auth/schema.prisma +521 -0
  416. package/templates/database/config/schema.prisma +234 -0
  417. package/templates/database/content/schema.prisma +263 -0
  418. package/templates/database/flows/listing-diagnostic.ts +182 -0
  419. package/templates/database/flows/presets.ts +315 -0
  420. package/templates/database/inbox/__tests__/schema.test.ts +4 -0
  421. package/templates/database/inbox/schema.prisma +574 -3
  422. package/templates/database/jest.config.cjs +8 -0
  423. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
  424. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
  425. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
  426. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
  427. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
  428. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
  429. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
  430. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
  431. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
  432. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
  433. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
  434. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
  435. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
  436. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
  437. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
  438. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
  439. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
  440. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
  441. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
  442. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
  443. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
  444. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
  445. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
  446. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
  447. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
  448. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
  449. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
  450. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
  451. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
  452. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
  453. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
  454. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
  455. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  456. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  457. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  458. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  459. package/templates/database/ops/schema.prisma +27 -0
  460. package/templates/database/package.json +42 -4
  461. package/templates/database/prisma.config.ts +1 -1
  462. package/templates/database/schema.prisma +19 -435
  463. package/templates/database/seed-test.ts +14 -4
  464. package/templates/database/seed.ts +148 -0
  465. package/templates/database/tenancy.ts +45 -0
  466. package/templates/database/tsconfig.flows.json +15 -0
  467. package/templates/database/tsconfig.json +7 -1
  468. package/templates/database/tsconfig.tenancy.json +14 -0
  469. package/templates/package.json +30 -55
  470. package/templates/pnpm-workspace.yaml +0 -3
  471. package/templates/project.yaml +1 -1
  472. package/templates/tsconfig.json +1 -37
  473. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
  474. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
  475. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
  476. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
  477. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
  478. package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
  479. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
  480. package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
  481. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
  482. package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
  483. package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
  484. package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
  485. package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
  486. package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
  487. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
  488. package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
  489. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
  490. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
  491. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
  492. package/templates/apps/web/__tests__/page.test.tsx +0 -230
  493. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
  494. package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
  495. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
  496. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
  497. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
  498. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
  499. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
  500. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
  501. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
  502. package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
  503. package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
  504. package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
  505. package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
  506. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
  507. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
  508. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
  509. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
  510. package/templates/apps/web/content/en/blog/index.mdx +0 -15
  511. package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
  512. package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
  513. package/templates/content/.gitkeep +0 -0
  514. package/templates/content/CLAUDE.md +0 -85
  515. package/templates/content/ar/HomePage/home.mdx +0 -123
  516. package/templates/content/ar/_site.mdx +0 -35
  517. package/templates/content/de/_site.mdx +0 -29
  518. package/templates/content/en/.gitkeep +0 -0
  519. package/templates/content/en/AboutPage/about.schema.json +0 -82
  520. package/templates/content/en/BookCall/book-call.mdx +0 -19
  521. package/templates/content/en/BookCall/book-call.schema.json +0 -121
  522. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
  523. package/templates/content/en/CareersList/careers.mdx +0 -24
  524. package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
  525. package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
  526. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
  527. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
  528. package/templates/content/en/ClientsList/clients.mdx +0 -38
  529. package/templates/content/en/ContactPage/contact.schema.json +0 -100
  530. package/templates/content/en/ContactPoint/primary.mdx +0 -14
  531. package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
  532. package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
  533. package/templates/content/en/CoursesList/courses.mdx +0 -22
  534. package/templates/content/en/DownloadsList/downloads.mdx +0 -22
  535. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
  536. package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
  537. package/templates/content/en/EventsList/events.mdx +0 -26
  538. package/templates/content/en/FAQPage/faq.schema.json +0 -231
  539. package/templates/content/en/GlossaryList/glossary.mdx +0 -19
  540. package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
  541. package/templates/content/en/HomePage/home.schema.json +0 -88
  542. package/templates/content/en/HomePage/launch-service.mdx +0 -475
  543. package/templates/content/en/HomePage/launch-service.schema.json +0 -138
  544. package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
  545. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
  546. package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
  547. package/templates/content/en/Integration/github-actions.mdx +0 -31
  548. package/templates/content/en/Integration/openai-api.mdx +0 -26
  549. package/templates/content/en/Integration/vercel.mdx +0 -32
  550. package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
  551. package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
  552. package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
  553. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
  554. package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
  555. package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
  556. package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
  557. package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
  558. package/templates/content/en/LandingPage/developers.mdx +0 -19
  559. package/templates/content/en/LandingPage/developers.schema.json +0 -109
  560. package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
  561. package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
  562. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
  563. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
  564. package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
  565. package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
  566. package/templates/content/en/LandingPage/platform.mdx +0 -369
  567. package/templates/content/en/LandingPage/platform.schema.json +0 -166
  568. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
  569. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
  570. package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
  571. package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
  572. package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
  573. package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
  574. package/templates/content/en/Organization/site-owner.mdx +0 -16
  575. package/templates/content/en/Partner/prisma.mdx +0 -31
  576. package/templates/content/en/Partner/supabase.mdx +0 -30
  577. package/templates/content/en/Partner/vercel.mdx +0 -28
  578. package/templates/content/en/PartnersList/partners.mdx +0 -22
  579. package/templates/content/en/Person/founder.mdx +0 -18
  580. package/templates/content/en/Pricing/pricing.mdx +0 -538
  581. package/templates/content/en/Pricing/pricing.schema.json +0 -209
  582. package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
  583. package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
  584. package/templates/content/en/Product/ailk.mdx +0 -19
  585. package/templates/content/en/Product/ailk.schema.json +0 -94
  586. package/templates/content/en/Product/api-launch-kit.mdx +0 -26
  587. package/templates/content/en/Product/launch-kit.mdx +0 -28
  588. package/templates/content/en/ProductsList/products.mdx +0 -26
  589. package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
  590. package/templates/content/en/ProjectsList/projects.mdx +0 -25
  591. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
  592. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
  593. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
  594. package/templates/content/en/ResourcesHub/resources.mdx +0 -22
  595. package/templates/content/en/Service/consulting.mdx +0 -22
  596. package/templates/content/en/Service/implementation.mdx +0 -30
  597. package/templates/content/en/Service/launch-service.mdx +0 -232
  598. package/templates/content/en/Service/launch.mdx +0 -228
  599. package/templates/content/en/ServicesList/services.mdx +0 -84
  600. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
  601. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
  602. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
  603. package/templates/content/en/TeamList/team.mdx +0 -63
  604. package/templates/content/en/TeamMember/founder.mdx +0 -72
  605. package/templates/content/en/TermsPage/terms.mdx +0 -19
  606. package/templates/content/en/TermsPage/terms.schema.json +0 -99
  607. package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
  608. package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
  609. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
  610. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
  611. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
  612. package/templates/content/en-XA/AboutPage/about.mdx +0 -8
  613. package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
  614. package/templates/content/en-XA/HomePage/home.mdx +0 -8
  615. package/templates/content/es/_site.mdx +0 -30
  616. package/templates/content/fr/_site.mdx +0 -30
  617. package/templates/content/ja/_site.mdx +0 -30
  618. package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
  619. package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
  620. package/templates/content/pt-BR/HomePage/home.mdx +0 -23
@@ -21,9 +21,10 @@ export function buildOpenApiSpec() {
21
21
  title: "AI Launch Kit — Public Content API",
22
22
  version: API_VERSION,
23
23
  description:
24
- "Read-only HTTP API exposing AILK content as schema.org JSON-LD. " +
25
- "Designed for agent consumption (AX) every response carries " +
26
- "`@context` and `@type` at the document root.",
24
+ "HTTP API exposing AILK content as schema.org JSON-LD, plus a small set " +
25
+ "of anonymous write routes (lead capture, checkout). Designed for agent " +
26
+ "consumption (AX) — every content response carries `@context` and " +
27
+ "`@type` at the document root.",
27
28
  license: { name: "MIT" },
28
29
  },
29
30
  servers: [
@@ -104,6 +105,175 @@ export function buildOpenApiSpec() {
104
105
  },
105
106
  },
106
107
  },
108
+ "/v1/leads/submit": {
109
+ post: {
110
+ operationId: "submitLead",
111
+ summary: "Capture a contact-form lead",
112
+ description:
113
+ "Stores a contact-form-style lead — an email plus an arbitrary payload of " +
114
+ "form fields — for the given siteId. `source` is a free-form tag the caller " +
115
+ "sets to identify the channel (for example `agent`); it is stored as-is and " +
116
+ "not checked against a fixed vocabulary. A 201 response means the lead " +
117
+ "landed, in the database or (if the database is unavailable) a file " +
118
+ "fallback; the response's `email` field reports, best-effort, whether the " +
119
+ "site operator was notified.",
120
+ requestBody: {
121
+ required: true,
122
+ content: {
123
+ "application/json": {
124
+ schema: { $ref: "#/components/schemas/LeadSubmitRequest" },
125
+ },
126
+ },
127
+ },
128
+ responses: {
129
+ "201": {
130
+ description: "Lead captured",
131
+ content: {
132
+ "application/json": {
133
+ schema: { $ref: "#/components/schemas/LeadWriteResponse" },
134
+ },
135
+ },
136
+ },
137
+ "400": {
138
+ description: "Validation error — invalid request body",
139
+ content: {
140
+ "application/json": {
141
+ schema: { $ref: "#/components/schemas/InvalidInputResponse" },
142
+ },
143
+ },
144
+ },
145
+ "503": {
146
+ description:
147
+ "Lead capture unavailable — persistence failed, or neither file " +
148
+ "capture nor a database is configured",
149
+ content: {
150
+ "application/json": {
151
+ schema: {
152
+ oneOf: [
153
+ { $ref: "#/components/schemas/PersistFailedResponse" },
154
+ {
155
+ $ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
156
+ },
157
+ ],
158
+ },
159
+ },
160
+ },
161
+ },
162
+ },
163
+ },
164
+ },
165
+ "/v1/leads/subscribe": {
166
+ post: {
167
+ operationId: "subscribeLead",
168
+ summary: "Add an email to the newsletter list",
169
+ description:
170
+ "Adds an email address to the given siteId's newsletter list. `source` is " +
171
+ "a free-form tag the caller sets to identify the signup channel (for " +
172
+ "example `agent`); it is stored as-is and not checked against a fixed " +
173
+ "vocabulary. A 201 response means the subscription landed — a duplicate " +
174
+ "email for the same site returns the existing subscriber's id rather than " +
175
+ "erroring.",
176
+ requestBody: {
177
+ required: true,
178
+ content: {
179
+ "application/json": {
180
+ schema: { $ref: "#/components/schemas/LeadSubscribeRequest" },
181
+ },
182
+ },
183
+ },
184
+ responses: {
185
+ "201": {
186
+ description: "Subscription captured",
187
+ content: {
188
+ "application/json": {
189
+ schema: { $ref: "#/components/schemas/LeadWriteResponse" },
190
+ },
191
+ },
192
+ },
193
+ "400": {
194
+ description: "Validation error — invalid request body",
195
+ content: {
196
+ "application/json": {
197
+ schema: { $ref: "#/components/schemas/InvalidInputResponse" },
198
+ },
199
+ },
200
+ },
201
+ "503": {
202
+ description:
203
+ "Lead capture unavailable — persistence failed, or neither file " +
204
+ "capture nor a database is configured",
205
+ content: {
206
+ "application/json": {
207
+ schema: {
208
+ oneOf: [
209
+ { $ref: "#/components/schemas/PersistFailedResponse" },
210
+ {
211
+ $ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
212
+ },
213
+ ],
214
+ },
215
+ },
216
+ },
217
+ },
218
+ },
219
+ },
220
+ },
221
+ "/v1/leads/callback": {
222
+ post: {
223
+ operationId: "requestCallback",
224
+ summary: "Request a callback",
225
+ description:
226
+ "Stores a name, phone number, optional preferred time, and an arbitrary " +
227
+ "payload for the given siteId so a site operator can call the lead back. " +
228
+ "Unlike submit/subscribe there is no `source` or `email` field. A 201 " +
229
+ "response means the request landed, in the database or (if the database " +
230
+ "is unavailable) a file fallback; the response's `email` field reports, " +
231
+ "best-effort, whether the site operator was notified.",
232
+ requestBody: {
233
+ required: true,
234
+ content: {
235
+ "application/json": {
236
+ schema: { $ref: "#/components/schemas/LeadCallbackRequest" },
237
+ },
238
+ },
239
+ },
240
+ responses: {
241
+ "201": {
242
+ description: "Callback request captured",
243
+ content: {
244
+ "application/json": {
245
+ schema: { $ref: "#/components/schemas/LeadWriteResponse" },
246
+ },
247
+ },
248
+ },
249
+ "400": {
250
+ description: "Validation error — invalid request body",
251
+ content: {
252
+ "application/json": {
253
+ schema: { $ref: "#/components/schemas/InvalidInputResponse" },
254
+ },
255
+ },
256
+ },
257
+ "503": {
258
+ description:
259
+ "Lead capture unavailable — persistence failed, or neither file " +
260
+ "capture nor a database is configured",
261
+ content: {
262
+ "application/json": {
263
+ schema: {
264
+ oneOf: [
265
+ { $ref: "#/components/schemas/PersistFailedResponse" },
266
+ {
267
+ $ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
268
+ },
269
+ ],
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ },
276
+ },
107
277
  "/v1/content/types": {
108
278
  get: {
109
279
  operationId: "listContentTypes",
@@ -251,6 +421,11 @@ export function buildOpenApiSpec() {
251
421
  default: 1,
252
422
  description: "Line-item quantity; defaults to 1",
253
423
  },
424
+ allowPromotionCodes: {
425
+ type: "boolean",
426
+ description:
427
+ "Show the promotion-code field on the hosted Checkout page. Omit to leave it off",
428
+ },
254
429
  },
255
430
  },
256
431
  CheckoutSessionResponse: {
@@ -300,6 +475,140 @@ export function buildOpenApiSpec() {
300
475
  error: { type: "string", enum: ["checkout-unavailable"] },
301
476
  },
302
477
  },
478
+ LeadSubmitRequest: {
479
+ type: "object",
480
+ additionalProperties: false,
481
+ required: ["siteId", "source", "email", "payload"],
482
+ properties: {
483
+ siteId: {
484
+ type: "string",
485
+ pattern: "^[a-zA-Z0-9._-]{1,128}$",
486
+ description: "Site/tenant identifier",
487
+ },
488
+ source: {
489
+ type: "string",
490
+ pattern: "^[^\\r\\n]{1,64}$",
491
+ description:
492
+ "Free-form caller-set channel tag (e.g. `agent`); no CR/LF",
493
+ },
494
+ email: { type: "string", format: "email", maxLength: 254 },
495
+ payload: {
496
+ type: "object",
497
+ maxProperties: 64,
498
+ additionalProperties: true,
499
+ description: "Arbitrary form fields, up to 64 keys",
500
+ },
501
+ },
502
+ },
503
+ LeadSubscribeRequest: {
504
+ type: "object",
505
+ additionalProperties: false,
506
+ required: ["siteId", "email", "source"],
507
+ properties: {
508
+ siteId: {
509
+ type: "string",
510
+ pattern: "^[a-zA-Z0-9._-]{1,128}$",
511
+ description: "Site/tenant identifier",
512
+ },
513
+ email: { type: "string", format: "email", maxLength: 254 },
514
+ source: {
515
+ type: "string",
516
+ pattern: "^[^\\r\\n]{1,64}$",
517
+ description:
518
+ "Free-form caller-set channel tag (e.g. `agent`); no CR/LF",
519
+ },
520
+ },
521
+ },
522
+ LeadCallbackRequest: {
523
+ type: "object",
524
+ additionalProperties: false,
525
+ required: ["siteId", "name", "phone", "payload"],
526
+ properties: {
527
+ siteId: {
528
+ type: "string",
529
+ pattern: "^[a-zA-Z0-9._-]{1,128}$",
530
+ description: "Site/tenant identifier",
531
+ },
532
+ name: { type: "string", minLength: 1, maxLength: 128 },
533
+ phone: { type: "string", minLength: 3, maxLength: 32 },
534
+ preferredTime: { type: "string", maxLength: 64 },
535
+ payload: {
536
+ type: "object",
537
+ maxProperties: 64,
538
+ additionalProperties: true,
539
+ description: "Arbitrary form fields, up to 64 keys",
540
+ },
541
+ },
542
+ },
543
+ LeadEmailResult: {
544
+ description:
545
+ "Best-effort operator-notification outcome. `skipped` means email " +
546
+ "notification isn't configured; persistence already succeeded either way.",
547
+ oneOf: [
548
+ {
549
+ type: "object",
550
+ required: ["ok", "messageId"],
551
+ properties: {
552
+ ok: { type: "boolean", enum: [true] },
553
+ messageId: { type: "string" },
554
+ },
555
+ },
556
+ {
557
+ type: "object",
558
+ required: ["ok", "error"],
559
+ properties: {
560
+ ok: { type: "boolean", enum: [false] },
561
+ error: { type: "string" },
562
+ },
563
+ },
564
+ {
565
+ type: "object",
566
+ required: ["skipped", "reason"],
567
+ properties: {
568
+ skipped: { type: "boolean", enum: [true] },
569
+ reason: { type: "string" },
570
+ },
571
+ },
572
+ ],
573
+ },
574
+ LeadWriteResponse: {
575
+ type: "object",
576
+ required: ["id", "persisted", "email"],
577
+ properties: {
578
+ id: {
579
+ type: "string",
580
+ description:
581
+ "Server-assigned record id — a DB-prefixed entity id when " +
582
+ 'persisted is "db", or a `fallback-<ts>-<rand>` file id when ' +
583
+ 'persisted is "file"',
584
+ },
585
+ persisted: { type: "string", enum: ["db", "file"] },
586
+ email: { $ref: "#/components/schemas/LeadEmailResult" },
587
+ },
588
+ },
589
+ InvalidInputResponse: {
590
+ type: "object",
591
+ required: ["error", "message"],
592
+ properties: {
593
+ error: { type: "string", enum: ["invalid_input"] },
594
+ message: { type: "string" },
595
+ },
596
+ },
597
+ PersistFailedResponse: {
598
+ type: "object",
599
+ required: ["error"],
600
+ properties: {
601
+ error: { type: "string", enum: ["persist_failed"] },
602
+ },
603
+ },
604
+ LeadCaptureNotConfiguredResponse: {
605
+ type: "object",
606
+ required: ["error", "message"],
607
+ properties: {
608
+ error: { type: "string", enum: ["lead_capture_not_configured"] },
609
+ message: { type: "string" },
610
+ },
611
+ },
303
612
  ContentPage: {
304
613
  type: "object",
305
614
  required: ["type", "slug", "title", "description"],
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file index.test.ts
3
+ * Route-level inject tests for POST /v1/aeo/score and GET /v1/aeo/score/example
4
+ * (#4666 D5).
5
+ *
6
+ * Mirrors apps/api/src/routes/schedule/__tests__/index.test.ts's shape: a bare
7
+ * Fastify instance with only the routes under test registered; the fetch
8
+ * boundary is stubbed rather than the service, so these tests cover the real
9
+ * crawler + scorer alongside the route's envelope and status mapping. Test
10
+ * URLs use literal public/blocked IP addresses so the SSRF gate's DNS step is
11
+ * never exercised (no real network egress needed).
12
+ */
13
+ import type { FastifyInstance } from "fastify";
14
+
15
+ import { scoreAeoRoute, exampleAeoRoute } from "../index";
16
+
17
+ const HOME_HTML =
18
+ '<html><script type="application/ld+json">{"@type":"HomePage","name":"Home"}</script></html>';
19
+
20
+ /** Typed as the platform signature so it assigns to `globalThis.fetch` with no cast. */
21
+ type FetchStub = jest.Mock<Promise<Response>, [RequestInfo | URL, RequestInit?]>;
22
+
23
+ function buildApp(): FastifyInstance {
24
+ const fastify = require("fastify")();
25
+ fastify.register(scoreAeoRoute);
26
+ fastify.register(exampleAeoRoute);
27
+ return fastify as FastifyInstance;
28
+ }
29
+
30
+ describe("POST /v1/aeo/score", () => {
31
+ let app: FastifyInstance;
32
+ const realFetch = globalThis.fetch;
33
+
34
+ beforeEach(() => {
35
+ app = buildApp();
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await app.close();
40
+ globalThis.fetch = realFetch;
41
+ delete process.env["AEO_SCORE_BUDGET_MS"];
42
+ });
43
+
44
+ it("400s on a malformed body", async () => {
45
+ const res = await app.inject({
46
+ method: "POST",
47
+ url: "/v1/aeo/score",
48
+ payload: { url: "not-a-url" },
49
+ });
50
+ expect(res.statusCode).toBe(400);
51
+ const body = JSON.parse(res.body) as { ok: boolean; error: { code: string } };
52
+ expect(body.ok).toBe(false);
53
+ expect(body.error.code).toBe("invalid_input");
54
+ });
55
+
56
+ it("200s with data.status ready for a reachable public-IP host", async () => {
57
+ const stub: FetchStub = jest.fn(async () => new Response(HOME_HTML));
58
+ globalThis.fetch = stub;
59
+
60
+ const res = await app.inject({
61
+ method: "POST",
62
+ url: "/v1/aeo/score",
63
+ payload: { url: "http://93.184.216.34/" },
64
+ });
65
+
66
+ expect(res.statusCode).toBe(200);
67
+ const body = JSON.parse(res.body) as { ok: boolean; data: { status: string; score: number } };
68
+ expect(body.ok).toBe(true);
69
+ expect(body.data.status).toBe("ready");
70
+ expect(typeof body.data.score).toBe("number");
71
+ });
72
+
73
+ it("200s with data.status unavailable/ssrf_blocked for a loopback URL (never 4xx/5xx)", async () => {
74
+ const stub: FetchStub = jest.fn();
75
+ globalThis.fetch = stub;
76
+
77
+ const res = await app.inject({
78
+ method: "POST",
79
+ url: "/v1/aeo/score",
80
+ payload: { url: "http://127.0.0.1/admin" },
81
+ });
82
+
83
+ expect(res.statusCode).toBe(200);
84
+ const body = JSON.parse(res.body) as { ok: boolean; data: { status: string; reason: string } };
85
+ expect(body.ok).toBe(true);
86
+ expect(body.data.status).toBe("unavailable");
87
+ expect(body.data.reason).toBe("ssrf_blocked");
88
+ expect(stub).not.toHaveBeenCalled();
89
+ });
90
+
91
+ // AILK #4707 — the specific SSRF rejection detail (which internal
92
+ // hostname/IP triggered the block, or that DNS resolved to one) is an
93
+ // anonymous-endpoint recon primitive and must never reach the caller; the
94
+ // response carries only the generic reason enum + a fixed message, with
95
+ // the specific detail logged server-side only.
96
+ it("returns a generic message for an SSRF-rejected URL — the specific rejection reason never reaches the response body", async () => {
97
+ const stub: FetchStub = jest.fn();
98
+ globalThis.fetch = stub;
99
+
100
+ const res = await app.inject({
101
+ method: "POST",
102
+ url: "/v1/aeo/score",
103
+ payload: { url: "http://127.0.0.1/admin" },
104
+ });
105
+
106
+ const body = JSON.parse(res.body) as {
107
+ ok: boolean;
108
+ data: { status: string; reason: string; message: string };
109
+ };
110
+ expect(body.data.reason).toBe("ssrf_blocked");
111
+ // Same fixed text flow-engine.ts's aeo_score step already scrubs its
112
+ // own SSRF-blocked result to (#4663 M1) — one client-facing message
113
+ // across both aeo_score entry points.
114
+ expect(body.data.message).toBe("This URL cannot be scored.");
115
+ expect(body.data.message).not.toMatch(/127\.0\.0\.1/);
116
+ expect(body.data.message).not.toMatch(/blocked|internal|loopback|metadata/i);
117
+ });
118
+
119
+ it("logs the specific SSRF rejection reason server-side (never returned to the caller)", async () => {
120
+ const logLines: string[] = [];
121
+ const logCapture = require("fastify")({
122
+ logger: {
123
+ level: "info",
124
+ stream: { write: (msg: string) => logLines.push(msg) },
125
+ },
126
+ }) as FastifyInstance;
127
+ await logCapture.register(scoreAeoRoute);
128
+ await logCapture.ready();
129
+
130
+ const stub: FetchStub = jest.fn();
131
+ globalThis.fetch = stub;
132
+
133
+ try {
134
+ await logCapture.inject({
135
+ method: "POST",
136
+ url: "/v1/aeo/score",
137
+ payload: { url: "http://127.0.0.1/admin" },
138
+ });
139
+
140
+ const logText = logLines.join("\n");
141
+ expect(logText).toMatch(/127\.0\.0\.1/);
142
+ expect(logText).toMatch(/blocked host/i);
143
+ } finally {
144
+ await logCapture.close();
145
+ }
146
+ });
147
+ });
148
+
149
+ describe("GET /v1/aeo/score/example", () => {
150
+ let app: FastifyInstance;
151
+
152
+ beforeEach(() => {
153
+ app = buildApp();
154
+ });
155
+
156
+ afterEach(async () => {
157
+ await app.close();
158
+ });
159
+
160
+ it("200s with the bundled example, meta.example true, and the illustrative disclosure wire-visible (acceptance-review finding)", async () => {
161
+ const res = await app.inject({ method: "GET", url: "/v1/aeo/score/example" });
162
+ expect(res.statusCode).toBe(200);
163
+ const body = JSON.parse(res.body) as {
164
+ ok: boolean;
165
+ data: { status: string; meta: { example: boolean; sourceUrl: string; illustrative: boolean } };
166
+ };
167
+ expect(body.ok).toBe(true);
168
+ expect(body.data.status).toBe("ready");
169
+ expect(body.data.meta.example).toBe(true);
170
+ expect(body.data.meta.sourceUrl).toBe("https://ailaunchkit.ai");
171
+ // Today's fixture is hand-authored, not a genuine crawl — the caller
172
+ // must not be told this is a live measurement without qualification.
173
+ expect(body.data.meta.illustrative).toBe(true);
174
+ });
175
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * AEO score route group (#4666 D5).
3
+ *
4
+ * Routes:
5
+ * POST /v1/aeo/score — score a URL against the four content rules
6
+ * GET /v1/aeo/score/example — the bundled pre-launch annotated example
7
+ *
8
+ * Anonymous, like the sibling `/v1/flows/*` and `/v1/schedule/*` routes — a
9
+ * waitlist respondent has no account. Both routes return `200 { ok: true,
10
+ * data }` for BOTH `ready` and `unavailable` results — an `unavailable`
11
+ * result is a successful call reporting a degraded outcome, not an HTTP
12
+ * error; the caller branches on `data.status`, mirroring how the browser
13
+ * will handle the `computing` state this route never itself emits. Only a
14
+ * malformed request body is a `400 invalid_input`.
15
+ *
16
+ * No MCP twin (a recorded, deliberate exception — brief D7 / apps/api/CLAUDE.md):
17
+ * exposing an arbitrary-URL crawler to anonymous agents is a security-posture
18
+ * decision the AEO Layer 1 MCP tool (`apps/mcp/src/tools/audit.ts`) already
19
+ * declines by staying same-origin-locked.
20
+ *
21
+ * Envelope per apps/api/CLAUDE.md: `{ ok: true, data }` /
22
+ * `{ ok: false, error: { code, message } }`, never 200-with-ok:false.
23
+ *
24
+ * SSRF-reason leak (security review, AILK #4707): `assertUrlSafe`'s reject
25
+ * `reason` text (e.g. "Host x resolves to a blocked address: 10.1.2.3")
26
+ * is a recon primitive for an anonymous caller mapping internal
27
+ * infrastructure via DNS-rebinding probes. It is logged server-side only
28
+ * (`request.log.info`); the response's `data.message` is replaced with the
29
+ * fixed `SSRF_BLOCKED_CLIENT_MESSAGE` for `reason: "ssrf_blocked"`. The
30
+ * `reason` enum itself is unchanged — it is already generic, and is the
31
+ * discriminant the api-client contract keys on. `flow-engine.ts`'s
32
+ * `aeo_score` step landed the identical scrub for its own caller in #4663
33
+ * (M1) and named this route's gap explicitly as out of that fix's scope —
34
+ * this closes it, reusing that fix's exact client-facing message text for
35
+ * consistency across both `aeo_score` entry points.
36
+ */
37
+ import { aeoScoreInputSchema } from "@working-theory/validation";
38
+ import type { FastifyPluginAsync } from "fastify";
39
+
40
+ import {
41
+ resolveScoreBudgetMs,
42
+ loadExampleScore,
43
+ scoreUrl,
44
+ } from "../../services/aeo-score.js";
45
+
46
+ const SSRF_BLOCKED_CLIENT_MESSAGE = "This URL cannot be scored.";
47
+
48
+ export const scoreAeoRoute: FastifyPluginAsync = async (fastify) => {
49
+ fastify.post("/v1/aeo/score", async (request, reply) => {
50
+ const parsedBody = aeoScoreInputSchema.safeParse(request.body);
51
+ if (!parsedBody.success) {
52
+ return reply.status(400).send({
53
+ ok: false,
54
+ error: {
55
+ code: "invalid_input",
56
+ message: parsedBody.error.issues[0]?.message ?? "Invalid request body.",
57
+ },
58
+ });
59
+ }
60
+
61
+ const budgetMs = resolveScoreBudgetMs(process.env["AEO_SCORE_BUDGET_MS"], {
62
+ warn: (msg) => request.log.warn(msg),
63
+ });
64
+ const data = await scoreUrl(parsedBody.data.url, { budgetMs });
65
+
66
+ const host = safeHost(parsedBody.data.url);
67
+ if (data.status === "ready") {
68
+ request.log.info(
69
+ { host, pagesAudited: data.meta.pagesAudited, skipped: data.meta.skipped.length, status: data.status },
70
+ "aeo score computed",
71
+ );
72
+ return reply.send({ ok: true, data });
73
+ }
74
+
75
+ if (data.status === "unavailable") {
76
+ // The specific rejection detail (e.g. which internal hostname/IP an
77
+ // SSRF probe resolved to) is logged here, server-side only — never
78
+ // sent to the caller.
79
+ request.log.info(
80
+ { host, status: data.status, reason: data.reason, message: data.message },
81
+ "aeo score unavailable",
82
+ );
83
+ const clientData: typeof data =
84
+ data.reason === "ssrf_blocked"
85
+ ? { ...data, message: SSRF_BLOCKED_CLIENT_MESSAGE }
86
+ : data;
87
+ return reply.send({ ok: true, data: clientData });
88
+ }
89
+
90
+ return reply.send({ ok: true, data });
91
+ });
92
+ };
93
+
94
+ export const exampleAeoRoute: FastifyPluginAsync = async (fastify) => {
95
+ fastify.get("/v1/aeo/score/example", async (_request, reply) => {
96
+ return reply.send({ ok: true, data: loadExampleScore() });
97
+ });
98
+ };
99
+
100
+ /** Never log the full URL — host only (§ 5.2 Logging: no PII, no query strings). */
101
+ function safeHost(rawUrl: string): string {
102
+ try {
103
+ return new URL(rawUrl).host;
104
+ } catch {
105
+ return "invalid";
106
+ }
107
+ }
@@ -149,6 +149,9 @@ describe("tokens routes — functional via dual-mode preHandler", () => {
149
149
 
150
150
  it("DELETE /api/auth/tokens/:id succeeds via bearer; revokes the row", async () => {
151
151
  const bearer = "tok_live_zzz";
152
+ // Well-formed apitok_ id (ADR-0031): the :id param is validated with
153
+ // zApiTokenId before any query runs.
154
+ const targetId = "apitok_00000000-0000-7000-8000-000000000009";
152
155
  (prisma.apiToken.findUnique as jest.Mock).mockResolvedValue({
153
156
  id: "tok_id",
154
157
  userId: "user_bear",
@@ -160,13 +163,13 @@ describe("tokens routes — functional via dual-mode preHandler", () => {
160
163
  app = await buildApp();
161
164
  const res = await app.inject({
162
165
  method: "DELETE",
163
- url: "/v1/auth/tokens/tok_other",
166
+ url: `/v1/auth/tokens/${targetId}`,
164
167
  headers: { authorization: `Bearer ${bearer}` },
165
168
  });
166
169
 
167
170
  expect(res.statusCode).toBe(200);
168
171
  expect(prisma.apiToken.updateMany).toHaveBeenCalledWith({
169
- where: { id: "tok_other", userId: "user_bear", revokedAt: null },
172
+ where: { id: targetId, userId: "user_bear", revokedAt: null },
170
173
  data: { revokedAt: expect.any(Date) },
171
174
  });
172
175
  });