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
@@ -12,21 +12,57 @@
12
12
  * DELETE /v1/content/:type/:slug — delete; mandatory `If-Match`
13
13
  *
14
14
  * Rate-limit, CORS, and the OpenAPI spec are wired at server level.
15
+ *
16
+ * The three write handlers are content-write SLICE files, which the OSS strip
17
+ * removes (#5182). They are reached here the same way `server.ts` reaches
18
+ * them — behind `isModuleEnabled('content-write')` — but through a specifier
19
+ * this module does not spell as a literal, because `tsc` resolves a literal
20
+ * specifier whether or not the branch can run and would report TS2307 on the
21
+ * stripped tree. Registration is unchanged when the module is present, which
22
+ * is the default (`AILK_ENABLED_MODULES` unset means every module on).
15
23
  */
16
24
  import type { FastifyPluginAsync } from "fastify";
17
25
 
18
- import { createPageRoute } from "./create.js";
19
- import { deletePageRoute } from "./delete.js";
26
+ import { isModuleEnabled } from "../../config/modules.js";
27
+
20
28
  import { getPageRoute } from "./get-page.js";
21
29
  import { listPagesRoute } from "./list-pages.js";
22
30
  import { listTypesRoute } from "./list-types.js";
23
- import { updatePageRoute } from "./update.js";
31
+
32
+ /**
33
+ * Content-write slice entry points, as { specifier, exportName }.
34
+ *
35
+ * What defeats `tsc`'s resolution is the INDIRECTION, not the type: TypeScript
36
+ * resolves `import("...")` only when the argument is a string-literal
37
+ * EXPRESSION written at the call site. Reading the specifier from a variable
38
+ * makes it unresolvable regardless of whether that variable's type is `string`
39
+ * or a literal (verified both ways). `string` is used here for readability, not
40
+ * because a literal type would leak.
41
+ */
42
+ const WRITE_ROUTES: ReadonlyArray<{ specifier: string; exportName: string }> = [
43
+ { specifier: "./create.js", exportName: "createPageRoute" },
44
+ { specifier: "./update.js", exportName: "updatePageRoute" },
45
+ { specifier: "./delete.js", exportName: "deletePageRoute" },
46
+ ];
24
47
 
25
48
  export const contentRoutes: FastifyPluginAsync = async (fastify) => {
26
49
  await fastify.register(listTypesRoute);
27
50
  await fastify.register(listPagesRoute);
28
51
  await fastify.register(getPageRoute);
29
- await fastify.register(createPageRoute);
30
- await fastify.register(updatePageRoute);
31
- await fastify.register(deletePageRoute);
52
+
53
+ if (isModuleEnabled("content-write")) {
54
+ for (const { specifier, exportName } of WRITE_ROUTES) {
55
+ const mod = (await import(specifier)) as Record<
56
+ string,
57
+ FastifyPluginAsync | undefined
58
+ >;
59
+ const route = mod[exportName];
60
+ if (!route) {
61
+ throw new Error(
62
+ `content-write slice drift: '${specifier}' does not export '${exportName}'`,
63
+ );
64
+ }
65
+ await fastify.register(route);
66
+ }
67
+ }
32
68
  };
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * PATCH /v1/content/:type/:slug
3
4
  *
@@ -0,0 +1,393 @@
1
+ /**
2
+ * @file index.test.ts
3
+ * Route-level inject tests for GET /v1/deliverables/:grantId (#3961 D5).
4
+ *
5
+ * Mirrors apps/api/src/routes/flows/__tests__/index.test.ts's shape: a bare
6
+ * Fastify instance with only the route under test registered, prisma
7
+ * mocked via __mocks__/prisma.ts.
8
+ */
9
+
10
+ import type { FastifyInstance } from "fastify";
11
+
12
+ // eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts activates the auto-mock (Jest requires explicit call for local files)
13
+ jest.mock("../../../lib/prisma.js");
14
+
15
+ import { deliverableDownloadRoute } from "../index";
16
+ import { prisma } from "../../../lib/prisma.js";
17
+ import { compilePlaybook, loadVendoredPlayLibrary } from "../../../services/playbook-compile.js";
18
+
19
+ const GRANT_ID = "dgrant_00000000-0000-7000-8000-0000000000g1";
20
+ const SITE_ID = "site-abc";
21
+ const ASSET_ID = "dasset_00000000-0000-7000-8000-0000000000a1";
22
+
23
+ function grantRow(overrides: Partial<Record<string, unknown>> = {}) {
24
+ return {
25
+ id: GRANT_ID,
26
+ siteId: SITE_ID,
27
+ assetId: ASSET_ID,
28
+ leadId: "lead_00000000-0000-7000-8000-0000000000e1",
29
+ sessionId: "flsess_00000000-0000-7000-8000-0000000000c1",
30
+ createdAt: new Date(),
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function assetRow(overrides: Partial<Record<string, unknown>> = {}) {
36
+ return {
37
+ id: ASSET_ID,
38
+ siteId: SITE_ID,
39
+ key: "guide-default",
40
+ filename: "guide.pdf",
41
+ contentType: "application/pdf",
42
+ bytes: Buffer.from("PDF-BYTES"),
43
+ size: 9,
44
+ createdAt: new Date(),
45
+ updatedAt: new Date(),
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ describe("GET /v1/deliverables/:grantId", () => {
51
+ let app: FastifyInstance;
52
+
53
+ beforeEach(async () => {
54
+ jest.clearAllMocks();
55
+ process.env.DATABASE_URL = "postgres://stub";
56
+
57
+ const fastify = require("fastify")();
58
+ await fastify.register(deliverableDownloadRoute);
59
+ app = fastify;
60
+ });
61
+
62
+ afterEach(async () => {
63
+ await app.close();
64
+ delete process.env.DATABASE_URL;
65
+ });
66
+
67
+ // ── Happy path ───────────────────────────────────────────────────────────
68
+
69
+ it("returns 200 with the asset's bytes, content-type, and content-disposition filename", async () => {
70
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
71
+ grantRow(),
72
+ );
73
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(
74
+ assetRow(),
75
+ );
76
+
77
+ const res = await app.inject({
78
+ method: "GET",
79
+ url: `/v1/deliverables/${GRANT_ID}`,
80
+ });
81
+
82
+ expect(res.statusCode).toBe(200);
83
+ expect(res.headers["content-type"]).toBe("application/pdf");
84
+ expect(res.headers["content-disposition"]).toBe(
85
+ 'attachment; filename="guide.pdf"',
86
+ );
87
+ expect(res.rawPayload.toString()).toBe("PDF-BYTES");
88
+ });
89
+
90
+ // ── AC1 — X-Content-Type-Options: nosniff (#4002, #3961 security-review) ──
91
+
92
+ it("sets X-Content-Type-Options: nosniff on a 200 download response", async () => {
93
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
94
+ grantRow(),
95
+ );
96
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(
97
+ assetRow(),
98
+ );
99
+
100
+ const res = await app.inject({
101
+ method: "GET",
102
+ url: `/v1/deliverables/${GRANT_ID}`,
103
+ });
104
+
105
+ expect(res.statusCode).toBe(200);
106
+ expect(res.headers["x-content-type-options"]).toBe("nosniff");
107
+ });
108
+
109
+ it("allows repeated downloads on the same grant (no consumption)", async () => {
110
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
111
+ grantRow(),
112
+ );
113
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(
114
+ assetRow(),
115
+ );
116
+
117
+ const first = await app.inject({
118
+ method: "GET",
119
+ url: `/v1/deliverables/${GRANT_ID}`,
120
+ });
121
+ const second = await app.inject({
122
+ method: "GET",
123
+ url: `/v1/deliverables/${GRANT_ID}`,
124
+ });
125
+
126
+ expect(first.statusCode).toBe(200);
127
+ expect(second.statusCode).toBe(200);
128
+ });
129
+
130
+ // ── AC2 — uniform 404 (no enumeration oracle) ───────────────────────────
131
+
132
+ it("returns a uniform 404 for an unknown grant id", async () => {
133
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(null);
134
+
135
+ const res = await app.inject({
136
+ method: "GET",
137
+ url: "/v1/deliverables/dgrant_00000000-0000-7000-8000-999999999999",
138
+ });
139
+
140
+ expect(res.statusCode).toBe(404);
141
+ expect(res.json()).toEqual({
142
+ ok: false,
143
+ error: { code: "not_found", message: expect.any(String) },
144
+ });
145
+ expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it("returns a uniform 404 for a malformed id (identical shape to unknown)", async () => {
149
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(null);
150
+
151
+ const res = await app.inject({
152
+ method: "GET",
153
+ url: "/v1/deliverables/not-a-real-grant-id",
154
+ });
155
+
156
+ expect(res.statusCode).toBe(404);
157
+ expect(res.json()).toEqual({
158
+ ok: false,
159
+ error: { code: "not_found", message: expect.any(String) },
160
+ });
161
+ });
162
+
163
+ it("returns a uniform 404 (identical shape) when the grant exists but its asset row is missing", async () => {
164
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
165
+ grantRow(),
166
+ );
167
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
168
+
169
+ const res = await app.inject({
170
+ method: "GET",
171
+ url: `/v1/deliverables/${GRANT_ID}`,
172
+ });
173
+
174
+ expect(res.statusCode).toBe(404);
175
+ expect(res.json()).toEqual({
176
+ ok: false,
177
+ error: { code: "not_found", message: expect.any(String) },
178
+ });
179
+ });
180
+
181
+ it("returns a uniform 404 when the asset's siteId does not match the grant's (defense in depth)", async () => {
182
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
183
+ grantRow({ siteId: "site-abc" }),
184
+ );
185
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(
186
+ assetRow({ siteId: "site-other" }),
187
+ );
188
+
189
+ const res = await app.inject({
190
+ method: "GET",
191
+ url: `/v1/deliverables/${GRANT_ID}`,
192
+ });
193
+
194
+ expect(res.statusCode).toBe(404);
195
+ expect(res.json()).toEqual({
196
+ ok: false,
197
+ error: { code: "not_found", message: expect.any(String) },
198
+ });
199
+ });
200
+
201
+ // ── No route serves assets by key, id, or filename (structural) ─────────
202
+
203
+ it("has no route accepting a siteId/key/filename combination — only /:grantId", async () => {
204
+ const res = await app.inject({
205
+ method: "GET",
206
+ url: "/v1/deliverables/by-key/guide-default",
207
+ });
208
+
209
+ expect(res.statusCode).toBe(404);
210
+ });
211
+
212
+ // ── Filename sanitization — no header injection from an operator-authored filename ──
213
+
214
+ it("sanitizes CR/LF out of the filename before it reaches Content-Disposition", async () => {
215
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
216
+ grantRow(),
217
+ );
218
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(
219
+ assetRow({ filename: 'evil.pdf"\r\nX-Injected: true' }),
220
+ );
221
+
222
+ const res = await app.inject({
223
+ method: "GET",
224
+ url: `/v1/deliverables/${GRANT_ID}`,
225
+ });
226
+
227
+ expect(res.statusCode).toBe(200);
228
+ const disposition = res.headers["content-disposition"] as string;
229
+ expect(disposition).not.toContain("\r");
230
+ expect(disposition).not.toContain("\n");
231
+ expect(res.headers["x-injected"]).toBeUndefined();
232
+ });
233
+
234
+ it("falls back to a safe default filename when sanitization empties the name", async () => {
235
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
236
+ grantRow(),
237
+ );
238
+ (prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(
239
+ assetRow({ filename: "\r\n\r\n" }),
240
+ );
241
+
242
+ const res = await app.inject({
243
+ method: "GET",
244
+ url: `/v1/deliverables/${GRANT_ID}`,
245
+ });
246
+
247
+ expect(res.statusCode).toBe(200);
248
+ expect(res.headers["content-disposition"]).toContain("download");
249
+ });
250
+
251
+ // ── flows_not_configured-style guard ────────────────────────────────────
252
+
253
+ it("returns 503 when DATABASE_URL is unset (feature not configured; NOT attacker-distinguishing)", async () => {
254
+ delete process.env.DATABASE_URL;
255
+
256
+ const res = await app.inject({
257
+ method: "GET",
258
+ url: `/v1/deliverables/${GRANT_ID}`,
259
+ });
260
+
261
+ expect(res.statusCode).toBe(503);
262
+ expect(prisma.deliverableGrant.findUnique).not.toHaveBeenCalled();
263
+ });
264
+
265
+ // ── FEAT-074 U4b (#4718) D4 — the playbook: virtual asset (AC-1) ─────────
266
+
267
+ const LIBRARY = loadVendoredPlayLibrary();
268
+ const LEAD_ID = "lead_00000000-0000-7000-8000-0000000000e1";
269
+
270
+ function playbookGrantRow(overrides: Partial<Record<string, unknown>> = {}) {
271
+ return grantRow({
272
+ assetId: "playbook:targeting-maturity",
273
+ leadId: LEAD_ID,
274
+ ...overrides,
275
+ });
276
+ }
277
+
278
+ function playbookInstanceRow(level: number) {
279
+ const { source, markdown } = compilePlaybook({
280
+ score: { id: `wlscore_${level}`, tier: `level_${level}`, ruleTrace: [] },
281
+ plays: LIBRARY,
282
+ leadId: LEAD_ID,
283
+ });
284
+ return { level, source, markdown };
285
+ }
286
+
287
+ it("renders and serves a PDF for a playbook: grant, filename playbook-level-<N>.pdf", async () => {
288
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
289
+ playbookGrantRow(),
290
+ );
291
+ (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue(
292
+ playbookInstanceRow(5),
293
+ );
294
+
295
+ const res = await app.inject({
296
+ method: "GET",
297
+ url: `/v1/deliverables/${GRANT_ID}`,
298
+ });
299
+
300
+ expect(res.statusCode).toBe(200);
301
+ expect(res.headers["content-type"]).toBe("application/pdf");
302
+ expect(res.headers["content-disposition"]).toBe(
303
+ 'attachment; filename="playbook-level-5.pdf"',
304
+ );
305
+ expect(res.headers["x-content-type-options"]).toBe("nosniff");
306
+ expect(res.rawPayload.subarray(0, 4).toString("latin1")).toBe("%PDF");
307
+ expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
308
+ });
309
+
310
+ it("labels the Gate (Level 0) filename playbook-gate.pdf, never playbook-level-0.pdf", async () => {
311
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
312
+ playbookGrantRow(),
313
+ );
314
+ (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue(
315
+ playbookInstanceRow(0),
316
+ );
317
+
318
+ const res = await app.inject({
319
+ method: "GET",
320
+ url: `/v1/deliverables/${GRANT_ID}`,
321
+ });
322
+
323
+ expect(res.statusCode).toBe(200);
324
+ expect(res.headers["content-disposition"]).toBe(
325
+ 'attachment; filename="playbook-gate.pdf"',
326
+ );
327
+ });
328
+
329
+ it("scopes the PlaybookInstance read by grant.siteId AND grant.leadId — never a request-supplied id (AC-1 tenancy)", async () => {
330
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
331
+ playbookGrantRow(),
332
+ );
333
+ (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue(
334
+ playbookInstanceRow(1),
335
+ );
336
+
337
+ await app.inject({ method: "GET", url: `/v1/deliverables/${GRANT_ID}` });
338
+
339
+ expect(prisma.playbookInstance.findFirst).toHaveBeenCalledWith(
340
+ expect.objectContaining({
341
+ where: { siteId: SITE_ID, leadId: LEAD_ID, experimentKey: "targeting-maturity" },
342
+ }),
343
+ );
344
+ });
345
+
346
+ it("returns a uniform 404 when the lead has no PlaybookInstance (AC-1)", async () => {
347
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
348
+ playbookGrantRow(),
349
+ );
350
+ (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue(null);
351
+
352
+ const res = await app.inject({
353
+ method: "GET",
354
+ url: `/v1/deliverables/${GRANT_ID}`,
355
+ });
356
+
357
+ expect(res.statusCode).toBe(404);
358
+ expect(res.json()).toEqual({
359
+ ok: false,
360
+ error: { code: "not_found", message: expect.any(String) },
361
+ });
362
+ });
363
+
364
+ it("returns 500 render_failed (never a bare uncaught throw) when the stored source names a play the current library no longer declares", async () => {
365
+ (prisma.deliverableGrant.findUnique as jest.Mock).mockResolvedValue(
366
+ playbookGrantRow(),
367
+ );
368
+ (prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue({
369
+ level: 0,
370
+ source: {
371
+ artifact_type: "playbook",
372
+ scope: "person",
373
+ scope_ref: LEAD_ID,
374
+ method: "targeting-maturity",
375
+ level: 0,
376
+ plays: ["tm-999.999"],
377
+ compiled_from: "wlscore_defect",
378
+ },
379
+ markdown: "",
380
+ });
381
+
382
+ const res = await app.inject({
383
+ method: "GET",
384
+ url: `/v1/deliverables/${GRANT_ID}`,
385
+ });
386
+
387
+ expect(res.statusCode).toBe(500);
388
+ expect(res.json()).toEqual({
389
+ ok: false,
390
+ error: { code: "render_failed", message: expect.any(String) },
391
+ });
392
+ });
393
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Deliverables route group (#3961 D5; extended FEAT-074 U4b, #4718 D4).
3
+ *
4
+ * Routes:
5
+ * GET /v1/deliverables/:grantId — stream the grant's asset bytes
6
+ *
7
+ * The capability-token download route — a lead-facing URL (an email-click
8
+ * download), not an agent-facing business-data endpoint. No MCP twin (the
9
+ * same no-twin class as the identity-bound `/v1/workspaces/*` and
10
+ * `/v1/billing/*` routes — apps/api/CLAUDE.md § Endpoint conventions). No
11
+ * siteId in the URL: the grant id alone resolves both the asset and its
12
+ * site (a stable-id ref, ADR 0009 invariant 1 posture — never a
13
+ * cross-namespace `@relation`).
14
+ *
15
+ * A `grant.assetId` prefixed `playbook:` (FEAT-074 U4b, #4718 D4) is the
16
+ * VIRTUAL asset a `deliverable-fulfillment.ts` playbook grant mints — it
17
+ * skips the `DeliverableAsset` lookup entirely, loads the lead's NEWEST
18
+ * `PlaybookInstance` for `(grant.siteId, grant.leadId, experimentKey)`, and
19
+ * renders it to a PDF on request (`renderPlaybookPdf`, D1 — never a stored
20
+ * blob). Every other `assetId` follows the unchanged operator-uploaded path
21
+ * below.
22
+ *
23
+ * Every miss — unknown id, malformed id, grant-without-asset, a `playbook:`
24
+ * grant with no matching `PlaybookInstance` — is a UNIFORM 404
25
+ * (`{ ok:false, error:{ code:'not_found' } }`): no route distinguishes
26
+ * "wrong token" from "no such asset," so nothing here is an enumeration
27
+ * oracle. Repeated downloads on one grant are allowed — a lead re-opening
28
+ * their email is the normal case, so the grant is never consumed on read.
29
+ *
30
+ * A render failure (`PlaybookRenderError` — the vendored library no longer
31
+ * declares a play the stored instance names, D1's "library defect, not a
32
+ * render branch" posture) is deliberately NOT folded into the uniform-404
33
+ * contract above — it is a genuine internal error, not a token/asset miss.
34
+ * Caught, logged with `{grantId, experimentKey}` (never PII) so an on-call
35
+ * engineer can diagnose a library-drift incident from logs alone, and
36
+ * reported as a `500` (mirrors `waitlist-experiments/respond.ts`'s
37
+ * contract-drift posture).
38
+ */
39
+ import { logger } from "@working-theory/observability";
40
+ import type { FastifyPluginAsync } from "fastify";
41
+
42
+ import { resolveLocale } from "../../lib/locales.js";
43
+ import { prisma } from "../../lib/prisma.js";
44
+ import type { PlaybookSource } from "../../services/playbook-compile.js";
45
+ import { PlaybookRenderError, renderPlaybookPdf } from "../../services/playbook-render.js";
46
+
47
+ type DeliverableGrantRow = {
48
+ id: string;
49
+ siteId: string;
50
+ assetId: string;
51
+ leadId: string;
52
+ };
53
+
54
+ type DeliverableAssetRow = {
55
+ id: string;
56
+ siteId: string;
57
+ filename: string;
58
+ contentType: string;
59
+ bytes: Buffer;
60
+ };
61
+
62
+ type PlaybookInstanceRow = {
63
+ level: number;
64
+ source: unknown;
65
+ markdown: string;
66
+ };
67
+
68
+ const PLAYBOOK_ASSET_PREFIX = "playbook:";
69
+
70
+ const NOT_FOUND = {
71
+ ok: false as const,
72
+ error: { code: "not_found", message: "Not found." },
73
+ };
74
+
75
+ const DELIVERABLES_NOT_CONFIGURED = {
76
+ ok: false as const,
77
+ error: {
78
+ code: "deliverables_not_configured",
79
+ message: "Deliverables are not configured.",
80
+ },
81
+ };
82
+
83
+ const RENDER_FAILED = {
84
+ ok: false as const,
85
+ error: { code: "render_failed", message: "Failed to render the document." },
86
+ };
87
+
88
+ /**
89
+ * Strip CR/LF (header injection — the same class of guard
90
+ * @working-theory/email's `hasCRLF` applies to header-bound strings) and
91
+ * collapse anything outside a safe charset to `_`. `filename` is
92
+ * operator-authored (the `deliverable:upload` CLI's `--filename`, or the
93
+ * uploaded file's own basename) — it MUST NOT reach the
94
+ * `Content-Disposition` header un-sanitized.
95
+ */
96
+ function sanitizeFilename(filename: string): string {
97
+ const stripped = filename.replace(/[\r\n]/g, "");
98
+ const safe = stripped.replace(/[^a-zA-Z0-9._\- ]/g, "_");
99
+ return safe.length > 0 ? safe : "download";
100
+ }
101
+
102
+ /**
103
+ * `playbook-level-<N>.pdf`, `playbook-gate.pdf` for Level 0 (spec §2.1's
104
+ * never-say-Level-0 rule — a download filename is customer-visible; mirrors
105
+ * `renderPlaybookPdf`'s own Gate-vs-Level chrome-label split). Passed
106
+ * through `sanitizeFilename` like every other filename this route serves.
107
+ */
108
+ function playbookFilename(level: number): string {
109
+ return level === 0 ? "playbook-gate.pdf" : `playbook-level-${level}.pdf`;
110
+ }
111
+
112
+ export const deliverableDownloadRoute: FastifyPluginAsync = async (fastify) => {
113
+ fastify.get<{ Params: { grantId: string }; Querystring: { locale?: string } }>(
114
+ "/v1/deliverables/:grantId",
115
+ async (request, reply) => {
116
+ if (!process.env.DATABASE_URL) {
117
+ // Constant across every grantId (not attacker-distinguishing) —
118
+ // this guard sits OUTSIDE the uniform-404 contract, which covers
119
+ // only per-token misses.
120
+ return reply.status(503).send(DELIVERABLES_NOT_CONFIGURED);
121
+ }
122
+
123
+ const { grantId } = request.params;
124
+
125
+ const grant = (await prisma.deliverableGrant.findUnique({
126
+ where: { id: grantId },
127
+ })) as DeliverableGrantRow | null;
128
+
129
+ if (!grant) {
130
+ return reply.status(404).send(NOT_FOUND);
131
+ }
132
+
133
+ // FEAT-074 U4b (#4718) D4 — the `playbook:` virtual asset: skip the
134
+ // DeliverableAsset lookup entirely, resolve the lead's NEWEST
135
+ // PlaybookInstance for (siteId, leadId, experimentKey), and render it
136
+ // on request. Tenancy: scoped by grant.siteId + grant.leadId ALONE —
137
+ // never a request-supplied id (spec §2.2 Tenancy).
138
+ if (grant.assetId.startsWith(PLAYBOOK_ASSET_PREFIX)) {
139
+ const experimentKey = grant.assetId.slice(PLAYBOOK_ASSET_PREFIX.length);
140
+
141
+ const instance = (await prisma.playbookInstance.findFirst({
142
+ where: { siteId: grant.siteId, leadId: grant.leadId, experimentKey },
143
+ orderBy: { createdAt: "desc" },
144
+ select: { level: true, source: true, markdown: true },
145
+ })) as PlaybookInstanceRow | null;
146
+
147
+ if (!instance) {
148
+ return reply.status(404).send(NOT_FOUND);
149
+ }
150
+
151
+ const locale = resolveLocale(request.query.locale);
152
+
153
+ let bytes: Uint8Array;
154
+ try {
155
+ bytes = await renderPlaybookPdf({
156
+ source: instance.source as PlaybookSource,
157
+ markdown: instance.markdown,
158
+ locale,
159
+ });
160
+ } catch (err) {
161
+ if (!(err instanceof PlaybookRenderError)) throw err;
162
+ logger.error(
163
+ { grantId, experimentKey, err: err.message },
164
+ "[deliverables] playbook render failed — the vendored library no longer declares a play this instance names",
165
+ );
166
+ return reply.status(500).send(RENDER_FAILED);
167
+ }
168
+
169
+ const filename = sanitizeFilename(playbookFilename(instance.level));
170
+
171
+ return reply
172
+ .header("Content-Type", "application/pdf")
173
+ .header("Content-Disposition", `attachment; filename="${filename}"`)
174
+ .header("X-Content-Type-Options", "nosniff")
175
+ .send(Buffer.from(bytes));
176
+ }
177
+
178
+ const asset = (await prisma.deliverableAsset.findUnique({
179
+ where: { id: grant.assetId },
180
+ })) as DeliverableAssetRow | null;
181
+
182
+ if (!asset || asset.siteId !== grant.siteId) {
183
+ return reply.status(404).send(NOT_FOUND);
184
+ }
185
+
186
+ const filename = sanitizeFilename(asset.filename);
187
+
188
+ return reply
189
+ .header("Content-Type", asset.contentType)
190
+ .header("Content-Disposition", `attachment; filename="${filename}"`)
191
+ // The operator-supplied contentType is echoed verbatim above; this
192
+ // origin also serves /v1/auth/* cookies, so nosniff stops a
193
+ // mis-typed or malicious contentType from being MIME-sniffed by
194
+ // the browser into an executable context (#4002, #3961
195
+ // security-review — Medium/Low, non-blocking on #3961's Pass).
196
+ .header("X-Content-Type-Options", "nosniff")
197
+ .send(asset.bytes);
198
+ },
199
+ );
200
+ };