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
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Instant AEO score service (#4666 D3/D4).
3
+ *
4
+ * Productizes the existing pure `@working-theory/aeo` scorer behind the
5
+ * SSRF-guarded external-URL crawler at `@working-theory/aeo/fetch`. The
6
+ * rule set, severity table, and `identityMode`/`locale` defaults are
7
+ * declared ONCE, here.
8
+ */
9
+ import {
10
+ runAudit,
11
+ schemaCoverage,
12
+ jsonldValidity,
13
+ entityMarkup,
14
+ faqHowtoIncentive,
15
+ EXAMPLE_SCORE_CRAWL,
16
+ EXAMPLE_SCORE_META,
17
+ EXAMPLE_SCORE_ANNOTATIONS,
18
+ type Rule,
19
+ type AuditReport,
20
+ type PerRuleResult,
21
+ } from "@working-theory/aeo";
22
+ import {
23
+ assertUrlSafe,
24
+ crawlSite,
25
+ buildAuditPayload,
26
+ type CrawlResult,
27
+ type FetchImpl,
28
+ type Resolver,
29
+ type SkippedEntry,
30
+ } from "@working-theory/aeo/fetch";
31
+ import type {
32
+ AeoDimension,
33
+ AeoDimensionId,
34
+ AeoScoreResult,
35
+ } from "@working-theory/validation";
36
+
37
+ /** The service's fixed rule set — the four content rules only (§ 2.1 FR4). */
38
+ export const AEO_SCORE_RULES: Rule[] = [
39
+ schemaCoverage,
40
+ jsonldValidity,
41
+ entityMarkup,
42
+ faqHowtoIncentive,
43
+ ];
44
+
45
+ /** Severity table, verbatim from prospect-audit-site's "Compose the score-led sales report". */
46
+ export const AEO_DIMENSION_SEVERITY: Record<AeoDimensionId, "high" | "medium" | "low"> = {
47
+ "schema-coverage": "high",
48
+ "entity-markup": "high",
49
+ "jsonld-validity": "medium",
50
+ "faq-howto-incentive": "low",
51
+ };
52
+
53
+ const AEO_DIMENSION_LABELS: Record<AeoDimensionId, string> = {
54
+ "schema-coverage": "Schema Coverage",
55
+ "jsonld-validity": "JSON-LD Validity",
56
+ "entity-markup": "Entity Markup",
57
+ "faq-howto-incentive": "FAQ / HowTo Incentive",
58
+ };
59
+
60
+ /** D5 — the route's default + the configured-budget cap, both declared once. */
61
+ export const AEO_SCORE_DEFAULT_BUDGET_MS = 6000;
62
+ export const AEO_SCORE_MAX_BUDGET_MS = 15000;
63
+
64
+ /**
65
+ * Resolve `AEO_SCORE_BUDGET_MS` for the route. A missing/unparseable/
66
+ * non-positive value falls back to the default silently (nothing to warn
67
+ * about — the env var is simply unset in dev). A value above the 15 000 ms
68
+ * cap is logged and falls back to the default (D5) — raising it beyond the
69
+ * crawler's own 15 s per-fetch bound would collide with the handler's 25 s
70
+ * request guard once cold start is added. Re-checked on EVERY request, not
71
+ * once at process startup — a serverless deployment has no persistent
72
+ * startup phase to check this at.
73
+ */
74
+ export function resolveScoreBudgetMs(
75
+ rawEnv: string | undefined,
76
+ log?: { warn: (msg: string) => void },
77
+ ): number {
78
+ if (!rawEnv) return AEO_SCORE_DEFAULT_BUDGET_MS;
79
+ const parsed = Number(rawEnv);
80
+ if (!Number.isFinite(parsed) || parsed <= 0) return AEO_SCORE_DEFAULT_BUDGET_MS;
81
+ if (parsed > AEO_SCORE_MAX_BUDGET_MS) {
82
+ log?.warn(
83
+ `AEO_SCORE_BUDGET_MS=${rawEnv} exceeds the ${AEO_SCORE_MAX_BUDGET_MS}ms cap; falling back to the ${AEO_SCORE_DEFAULT_BUDGET_MS}ms default.`,
84
+ );
85
+ return AEO_SCORE_DEFAULT_BUDGET_MS;
86
+ }
87
+ return parsed;
88
+ }
89
+
90
+ const SEVERITY_RANK: Record<"high" | "medium" | "low", number> = {
91
+ high: 0,
92
+ medium: 1,
93
+ low: 2,
94
+ };
95
+
96
+ /** `lowestTwo` ordering (D4): score asc, then severity high>medium>low, then weight desc. */
97
+ function compareDimensions(a: AeoDimension, b: AeoDimension): number {
98
+ if (a.score !== b.score) return a.score - b.score;
99
+ const severityDiff = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
100
+ if (severityDiff !== 0) return severityDiff;
101
+ return b.weight - a.weight;
102
+ }
103
+
104
+ const FAQ_HOWTO_INCENTIVE_ID: AeoDimensionId = "faq-howto-incentive";
105
+
106
+ /** The fixed opportunity sentence for the incentive dimension (D4) — its per-page results carry no evidence. */
107
+ function faqHowtoOpportunityFinding(slug: string): string {
108
+ return `${slug}: no FAQPage or HowTo structured data (a visibility opportunity, not a defect)`;
109
+ }
110
+
111
+ function buildDimension(ruleId: AeoDimensionId, report: AuditReport): AeoDimension {
112
+ const perRule = report.perRule.find((r) => r.id === ruleId) as PerRuleResult;
113
+ const perPage = perRule.perPage ?? [];
114
+ const fraction = perRule.score ?? (perRule.pass ? 1 : 0);
115
+ const score = Math.round(fraction * 100);
116
+
117
+ let finding: string | null;
118
+ let pagesPassing: number;
119
+ const pagesTotal = perPage.length > 0 ? perPage.length : 1;
120
+
121
+ if (ruleId === FAQ_HOWTO_INCENTIVE_ID) {
122
+ pagesPassing = perPage.filter((p) => p.bonus === true).length;
123
+ const firstMissing = perPage.find((p) => p.bonus !== true);
124
+ finding = firstMissing ? faqHowtoOpportunityFinding(firstMissing.slug) : null;
125
+ } else {
126
+ pagesPassing = perPage.length > 0 ? perPage.filter((p) => p.pass).length : (perRule.pass ? 1 : 0);
127
+ const firstFailing = perPage.find((p) => !p.pass);
128
+ finding = firstFailing ? (firstFailing.evidence ?? perRule.evidence ?? null) : null;
129
+ }
130
+
131
+ return {
132
+ id: ruleId,
133
+ label: AEO_DIMENSION_LABELS[ruleId],
134
+ weight: perRule.weight,
135
+ score,
136
+ severity: AEO_DIMENSION_SEVERITY[ruleId],
137
+ finding,
138
+ pagesPassing,
139
+ pagesTotal,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * `assertUrlSafe`'s per-path rejection text (`packages/aeo/src/fetch/ssrf.ts`)
145
+ * can disclose which internal host/IP a crawl target resolved to — an
146
+ * anonymous-endpoint recon primitive (AILK #5110, mirroring the #4707 fix
147
+ * already applied to `scoreUrl`'s top-level `unavailable`/`ssrf_blocked`
148
+ * check below). `crawlSite` re-checks every path individually (DNS-rebinding
149
+ * defense) and writes the raw `assertUrlSafe` text straight into
150
+ * `SkippedEntry.reason` (`packages/aeo/src/fetch/crawl.ts:125`) — deliberately
151
+ * so, per that file's own test
152
+ * (`packages/aeo/__tests__/fetch/crawl.test.ts:47-49`), so the redaction
153
+ * happens HERE instead, at the boundary where a `ready` result's
154
+ * `meta.skipped` is assembled. Every `assertUrlSafe` rejection message has
155
+ * one of these six fixed prefixes; `crawlSite`'s other `SkippedEntry.reason`
156
+ * producers (`"budget_exceeded"`, a `fetchPage` HTTP/redirect/fetch-failure
157
+ * message) never do.
158
+ */
159
+ const SSRF_GUARD_REASON_PATTERN =
160
+ /^(Not a valid URL:|Only http\(s\) URLs allowed,|Blocked host \(internal\/loopback\/metadata\):|DNS resolution failed for|No DNS addresses resolved for|Host .+ resolves to a blocked address:)/;
161
+
162
+ /**
163
+ * `meta.skipped[]` boundary serializer (#5110). A per-path SSRF-guard
164
+ * rejection is logged server-side (the specific host/IP detail, for operator
165
+ * diagnosis) and replaced on the wire with the same generic `"ssrf_blocked"`
166
+ * sentinel `scoreUrl`'s top-level check already returns (below) — never the
167
+ * raw text. Any other skip reason passes through unchanged.
168
+ */
169
+ function toClientSkippedEntry(entry: SkippedEntry): SkippedEntry {
170
+ if (!SSRF_GUARD_REASON_PATTERN.test(entry.reason)) return entry;
171
+ console.warn("aeo-score: per-path SSRF-guard rejection (redacted on the wire)", {
172
+ path: entry.path,
173
+ url: entry.url,
174
+ reason: entry.reason,
175
+ });
176
+ return { ...entry, reason: "ssrf_blocked" };
177
+ }
178
+
179
+ /** Pure: crawl payload → runAudit → dimensions (D3/D4). Never throws on an empty crawl — the caller checks first. */
180
+ export function scoreCrawl(
181
+ crawl: CrawlResult,
182
+ opts?: { now?: () => Date },
183
+ ): AeoScoreResult {
184
+ if (crawl.pages.length === 0) {
185
+ return {
186
+ status: "unavailable",
187
+ reason: "no_pages_fetched",
188
+ message: "No pages were fetched.",
189
+ };
190
+ }
191
+
192
+ const payload = buildAuditPayload(crawl.pages);
193
+ const report = runAudit(
194
+ { ...payload, locale: "en", identityMode: "organization" },
195
+ AEO_SCORE_RULES,
196
+ );
197
+
198
+ const dimensions = AEO_SCORE_RULES.map((rule) =>
199
+ buildDimension(rule.id as AeoDimensionId, report),
200
+ );
201
+ const lowestTwo = [...dimensions].sort(compareDimensions).slice(0, 2);
202
+ const partial = crawl.skipped.some((s) => s.reason === "budget_exceeded");
203
+ const now = opts?.now ? opts.now() : new Date();
204
+
205
+ return {
206
+ status: "ready",
207
+ score: Math.round(report.fractionalScore ?? report.score),
208
+ dimensions,
209
+ lowestTwo,
210
+ meta: {
211
+ auditedAt: now.toISOString(),
212
+ pagesAudited: crawl.pages.length,
213
+ partial,
214
+ skipped: crawl.skipped.map(toClientSkippedEntry),
215
+ schemaVersion: report.meta.schemaVersion,
216
+ registryVersion: report.meta.registryVersion,
217
+ },
218
+ };
219
+ }
220
+
221
+ export interface ScoreUrlDeps {
222
+ fetchImpl?: FetchImpl;
223
+ resolver?: Resolver;
224
+ budgetMs?: number;
225
+ }
226
+
227
+ /** `assertUrlSafe` → `crawlSite` → `scoreCrawl` (D3). */
228
+ export async function scoreUrl(
229
+ url: string,
230
+ deps: ScoreUrlDeps = {},
231
+ ): Promise<AeoScoreResult> {
232
+ const safe = await assertUrlSafe(url, {
233
+ ...(deps.resolver !== undefined && { resolver: deps.resolver }),
234
+ });
235
+ if (!safe.ok) {
236
+ return { status: "unavailable", reason: "ssrf_blocked", message: safe.reason };
237
+ }
238
+
239
+ const budgetMs = deps.budgetMs ?? AEO_SCORE_DEFAULT_BUDGET_MS;
240
+ try {
241
+ const crawl = await crawlSite(url, {
242
+ ...(deps.fetchImpl !== undefined && { fetchImpl: deps.fetchImpl }),
243
+ ...(deps.resolver !== undefined && { resolver: deps.resolver }),
244
+ deadline: Date.now() + budgetMs,
245
+ });
246
+ if (crawl.pages.length === 0) {
247
+ return {
248
+ status: "unavailable",
249
+ reason: "no_pages_fetched",
250
+ message: "No pages were fetched within the crawl budget.",
251
+ };
252
+ }
253
+ return scoreCrawl(crawl);
254
+ } catch (e) {
255
+ const message = e instanceof Error ? e.message : String(e);
256
+ return { status: "unavailable", reason: "crawl_failed", message };
257
+ }
258
+ }
259
+
260
+ /** The bundled pre-launch example — no runtime `fs` read (D6). */
261
+ export function loadExampleScore(): AeoScoreResult {
262
+ const result = scoreCrawl(EXAMPLE_SCORE_CRAWL);
263
+ if (result.status !== "ready") {
264
+ // The fixture is committed and known-good; this should never happen.
265
+ throw new Error("Example score fixture failed to score.");
266
+ }
267
+ const annotate = (d: AeoDimension): AeoDimension => ({
268
+ ...d,
269
+ annotation: EXAMPLE_SCORE_ANNOTATIONS[d.id],
270
+ });
271
+ return {
272
+ ...result,
273
+ dimensions: result.dimensions.map(annotate),
274
+ lowestTwo: result.lowestTwo.map(annotate),
275
+ meta: {
276
+ ...result.meta,
277
+ example: true,
278
+ sourceUrl: EXAMPLE_SCORE_META.sourceUrl,
279
+ generatedAt: EXAMPLE_SCORE_META.generatedAt,
280
+ // Wire-visible disclosure (acceptance-review finding, #4666): whether
281
+ // the crawl data is a genuine fetch of `sourceUrl` or a hand-authored
282
+ // representative dataset. Sourced from EXAMPLE_SCORE_META itself, not
283
+ // hardcoded here, so regenerating the fixture from a real crawl flips
284
+ // this automatically.
285
+ illustrative: EXAMPLE_SCORE_META.illustrative,
286
+ },
287
+ };
288
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @file answer-distributions.ts
3
+ * @description Shared per-question answer bucketing (#4771 D4).
4
+ *
5
+ * Extracted from `waitlist-dashboard.ts`'s `computeMetricRow` (the original
6
+ * inline `distributions` body) — moved verbatim, not rewritten, so
7
+ * `waitlist-dashboard.test.ts` stays green with NO edits to that file (the
8
+ * AC 7 regression floor).
9
+ *
10
+ * Takes already-flattened answer maps, not rows: `waitlist-dashboard.ts`
11
+ * passes `leads.map((l) => l.payload)`; `flow-aggregate.ts` passes
12
+ * `sessions.map((s) => flattenSessionState(s.state))`. That is what lets one
13
+ * implementation serve two different row types without either service
14
+ * knowing about the other's.
15
+ *
16
+ * Hosted as a THIRD, neutral module rather than inside `flow-aggregate.ts`
17
+ * (the issue's own `## Scope` names only that file) — a deliberate scope
18
+ * extension per `rules/worker-contract.md` § Mid-task scope expansion:
19
+ * hosting the shared helper in `flow-aggregate.ts` would make the operator
20
+ * dashboard import from the flow-aggregate service, an import direction no
21
+ * reader would predict from the names.
22
+ */
23
+
24
+ import { logger } from "@working-theory/observability";
25
+ import type { WaitlistAnswerType } from "@working-theory/validation";
26
+
27
+ /**
28
+ * Defensive upper bound on an aggregation read (Medium finding, PR #4687
29
+ * security review). Caps the raw row count any SINGLE scoped read can
30
+ * return, so a `kind:"all"` aggregation spanning many sites (or a flow with
31
+ * many completed sessions) cannot load an unbounded result set into memory.
32
+ * Generous enough that no real deployment should hit it yet measured — if
33
+ * one does, the affected read undercounts rather than growing unbounded,
34
+ * and `logAggregationCapHit` below leaves a signal to act on (measure, then
35
+ * follow up with a real bound or DB-side aggregation).
36
+ */
37
+ export const MAX_AGGREGATION_ROWS = 20_000;
38
+
39
+ export function logAggregationCapHit(where: string, rowCount: number): void {
40
+ if (rowCount < MAX_AGGREGATION_ROWS) return;
41
+ logger.warn(
42
+ { where, rowCount, cap: MAX_AGGREGATION_ROWS },
43
+ "[answer-distributions] aggregation read hit its row cap — results may undercount",
44
+ );
45
+ }
46
+
47
+ /** A question's declared option set + the two fields the bucketer needs. */
48
+ export type AnswerDistributionQuestion = {
49
+ id: string;
50
+ answerType: WaitlistAnswerType;
51
+ // `| undefined` (not just `?`) — the repo's `exactOptionalPropertyTypes`
52
+ // distinguishes "key absent" from "key present with value undefined", and
53
+ // both `WaitlistQuestion.options` and `WaitlistExperimentConfig`'s own
54
+ // `.optional()`-inferred fields are of the latter, wider shape.
55
+ options?: readonly { value: string }[] | undefined;
56
+ };
57
+
58
+ export type AnswerDistributionOption = {
59
+ value: string;
60
+ count: number;
61
+ share: number;
62
+ };
63
+
64
+ export type AnswerDistribution = {
65
+ questionId: string;
66
+ answerType: WaitlistAnswerType;
67
+ answered: number;
68
+ options: AnswerDistributionOption[];
69
+ };
70
+
71
+ /**
72
+ * Read one question's answer out of an already-flattened answer map — moved
73
+ * verbatim from `waitlist-dashboard.ts`'s (formerly local) `readPayloadAnswer`.
74
+ */
75
+ export function readPayloadAnswer(
76
+ payload: unknown,
77
+ questionId: string,
78
+ ): string | string[] | undefined {
79
+ if (typeof payload !== "object" || payload === null) return undefined;
80
+ const value = (payload as Record<string, unknown>)[questionId];
81
+ if (typeof value === "string" && value.length > 0) return value;
82
+ if (Array.isArray(value)) {
83
+ const strings = value.filter((v): v is string => typeof v === "string");
84
+ return strings.length > 0 ? strings : undefined;
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ /**
90
+ * Bucket every declared question's answers across a set of already-flattened
91
+ * answer maps — moved verbatim from `computeMetricRow`'s inline
92
+ * `distributions` body. A value outside a question's declared options is
93
+ * counted under `"(other)"`; `text` / `email` / `url` questions are never
94
+ * charted (empty `options`, `answered` still populated).
95
+ */
96
+ export function computeAnswerDistributions(
97
+ questions: readonly AnswerDistributionQuestion[],
98
+ answerSets: readonly unknown[],
99
+ ): AnswerDistribution[] {
100
+ return questions.map((q) => {
101
+ const answeredSets = answerSets.filter(
102
+ (payload) => readPayloadAnswer(payload, q.id) !== undefined,
103
+ );
104
+ const answered = answeredSets.length;
105
+ if (
106
+ q.answerType === "text" ||
107
+ q.answerType === "email" ||
108
+ q.answerType === "url"
109
+ ) {
110
+ return {
111
+ questionId: q.id,
112
+ answerType: q.answerType,
113
+ answered,
114
+ options: [],
115
+ };
116
+ }
117
+ const declared = new Set((q.options ?? []).map((o) => o.value));
118
+ const counts = new Map<string, number>();
119
+ for (const payload of answeredSets) {
120
+ const value = readPayloadAnswer(payload, q.id);
121
+ const values = Array.isArray(value)
122
+ ? value
123
+ : value !== undefined
124
+ ? [value]
125
+ : [];
126
+ for (const v of values) {
127
+ const bucket = declared.has(v) ? v : "(other)";
128
+ counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
129
+ }
130
+ }
131
+ const options = [...counts.entries()].map(([value, count]) => ({
132
+ value,
133
+ count,
134
+ share: answered > 0 ? count / answered : 0,
135
+ }));
136
+ return { questionId: q.id, answerType: q.answerType, answered, options };
137
+ });
138
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * @file consent.ts
3
+ * @description The consent-grant writer (#5275 Part B) — `recordConsent`,
4
+ * the one function that turns a person's answers to a site's consent
5
+ * purposes into `ConsentGrant` rows, plus `recordLegacyConsent`, the D11
6
+ * path for a caller that could only say `consent: true`.
7
+ *
8
+ * SURFACE-AGNOSTIC BY DESIGN (D10). A grant's subject is a `subjectType` +
9
+ * `subjectId` pair of plain scalars with no relation into any other table
10
+ * (ADR 0009 invariant 1), so a new surface — the newsletter subscribe path
11
+ * is the named follow-up — adopts consent by adding ONE call here and NO
12
+ * migration. Nothing in this module knows what a project listing is.
13
+ *
14
+ * THE EVIDENCE RULE (spec §5.2): no row is ever written with statement text
15
+ * the system did not actually render to that person. Two consequences:
16
+ *
17
+ * - `recordConsent` resolves each statement from the SAME registry and the
18
+ * SAME `resolveConsentStatement` the renderer used (D7), so the recorded
19
+ * wording and the rendered wording are one string by construction, and a
20
+ * request body never carries wording at all. A purpose id the registry
21
+ * does not know is skipped, not written: no statement was rendered for it.
22
+ * - A grant whose wording cannot be proven — a legacy `consent: true` body,
23
+ * or the migration backfill — carries `UNRECOVERABLE_CONSENT_STATEMENT`,
24
+ * never today's default text. Stamping current wording onto a grant
25
+ * nobody read would fabricate evidence (D11).
26
+ *
27
+ * APPEND-ONLY (D9): every write is a `createMany`; this module never
28
+ * updates or upserts, so a later withdrawal sits beside the earlier grant
29
+ * rather than replacing it. Declines are written as `granted: false` (D8).
30
+ */
31
+
32
+ import { createHash } from "node:crypto";
33
+
34
+ import { logger } from "@working-theory/observability";
35
+ import {
36
+ consentPurposeById,
37
+ resolveConsentStatement,
38
+ SITE_CONSENT_CONFIG_DEFAULTS,
39
+ type SiteConsentConfig,
40
+ } from "@working-theory/validation";
41
+
42
+ import { prisma } from "../lib/prisma.js";
43
+
44
+ /**
45
+ * The one delegate this module writes through — the shape of `prisma` AND of
46
+ * the `tx` client an interactive `$transaction` hands out, so a caller that
47
+ * creates the subject row can write the grant in the SAME transaction (the
48
+ * listing's `start` does): either the row and its evidence both land, or
49
+ * neither does.
50
+ */
51
+ export interface ConsentWriteClient {
52
+ consentGrant: {
53
+ createMany(args: { data: ConsentGrantRow[] }): Promise<{ count: number }>;
54
+ };
55
+ }
56
+
57
+ interface ConsentGrantRow {
58
+ siteId: string;
59
+ subjectType: string;
60
+ subjectId: string;
61
+ purposeId: string;
62
+ granted: boolean;
63
+ locale: string;
64
+ statement: string;
65
+ statementHash: string;
66
+ }
67
+
68
+ /**
69
+ * The most keys a `consents` map may carry on an anonymous request. Any real
70
+ * registry is a handful of purposes; the cap bounds the per-request work an
71
+ * unbounded map could buy (security review, Low 3).
72
+ */
73
+ export const MAX_CONSENT_KEYS = 64;
74
+
75
+ /**
76
+ * The statement sentinel for a grant whose wording cannot be proven — a row
77
+ * carrying it records THAT a person consented (and when), not WHAT they read.
78
+ * Written on exactly two paths: `recordLegacyConsent` (a body carrying only
79
+ * the legacy `consent: true` literal, D11) and the consent_grants migration's
80
+ * backfill of pre-existing listings, which must carry this exact literal.
81
+ * It is never a substitute for real wording: a caller that rendered a
82
+ * statement records it through `recordConsent`.
83
+ */
84
+ export const UNRECOVERABLE_CONSENT_STATEMENT =
85
+ "[unrecoverable: no statement was rendered by this system for this grant]";
86
+
87
+ /** BCP-47 `und` — the locale of a statement nobody read. */
88
+ export const UNRECOVERABLE_CONSENT_LOCALE = "und";
89
+
90
+ /** sha256 hex of a statement — the `statementHash` that groups identical wordings. */
91
+ export function consentStatementHash(statement: string): string {
92
+ return createHash("sha256").update(statement).digest("hex");
93
+ }
94
+
95
+ /**
96
+ * The site's consent registry.
97
+ *
98
+ * There is no per-site consent config ROW yet (spec §4 deliberately adds
99
+ * none), so every site resolves to `SITE_CONSENT_CONFIG_DEFAULTS`. This
100
+ * function is the single seam a `SiteConsentConfig` row wires into later
101
+ * (the `listingConfigFor` shape: read the blob, `consentConfigFor(blob)`).
102
+ * The RENDERER (`packages/ui`) resolves its statements from the same
103
+ * `SITE_CONSENT_CONFIG_DEFAULTS`, which is what keeps the recorded statement
104
+ * identical to the rendered one until a config row exists — at which point
105
+ * both sides must read the same row.
106
+ */
107
+ export async function consentConfigForSite(
108
+ _siteId: string,
109
+ ): Promise<SiteConsentConfig> {
110
+ return SITE_CONSENT_CONFIG_DEFAULTS;
111
+ }
112
+
113
+ /**
114
+ * The id of the first REQUIRED registry purpose `consents` does not GRANT —
115
+ * declined (`false`) or simply absent — or `undefined` when every required
116
+ * purpose is `true`. Absence counts as unmet on purpose (security review,
117
+ * Medium 1): a body carrying `consents: {}` beside `consent: true` would
118
+ * otherwise create the subject with no grant row at all, which is a worse
119
+ * record than the legacy sentinel. A surface whose subject cannot exist
120
+ * without the required purposes (the listing: the row goes on to publish the
121
+ * founder's address) checks this before creating the subject.
122
+ */
123
+ export function requiredConsentUnmet(
124
+ config: SiteConsentConfig,
125
+ consents: Record<string, boolean>,
126
+ ): string | undefined {
127
+ for (const purpose of config.purposes) {
128
+ if (purpose.required && consents[purpose.id] !== true) return purpose.id;
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ export interface RecordConsentInput {
134
+ siteId: string;
135
+ /** e.g. `project_listing`, `subscriber` — a plain scalar, no relation (D10). */
136
+ subjectType: string;
137
+ subjectId: string;
138
+ /** The locale the statements were RENDERED in; the served locale is what gets recorded. */
139
+ locale: string;
140
+ config: SiteConsentConfig;
141
+ /** purposeId → the person's answer. `false` is a stored decline (D8). */
142
+ consents: Record<string, boolean>;
143
+ }
144
+
145
+ /**
146
+ * Write one `ConsentGrant` row per answered purpose in one `createMany`.
147
+ * Returns the number of rows written. Append-only — never update/upsert.
148
+ * `db` defaults to the shared client; pass a `$transaction` client to land
149
+ * the grant atomically with the subject row it belongs to.
150
+ */
151
+ export async function recordConsent(
152
+ input: RecordConsentInput,
153
+ db: ConsentWriteClient = prisma,
154
+ ): Promise<number> {
155
+ const rows: ConsentGrantRow[] = [];
156
+ const unknown: string[] = [];
157
+
158
+ for (const [purposeId, granted] of Object.entries(input.consents)) {
159
+ const purpose = consentPurposeById(input.config, purposeId);
160
+ if (!purpose) {
161
+ // Not in the registry ⇒ no statement was rendered for it ⇒ nothing to
162
+ // record (the evidence rule). Collected and logged ONCE per call below,
163
+ // because a client naming a purpose the site does not declare is a
164
+ // drift someone should see — and a map full of junk keys must not buy
165
+ // a log line each.
166
+ unknown.push(purposeId);
167
+ continue;
168
+ }
169
+ const { statement, locale } = resolveConsentStatement(purpose, input.locale);
170
+ rows.push({
171
+ siteId: input.siteId,
172
+ subjectType: input.subjectType,
173
+ subjectId: input.subjectId,
174
+ purposeId,
175
+ granted,
176
+ locale,
177
+ statement,
178
+ statementHash: consentStatementHash(statement),
179
+ });
180
+ }
181
+
182
+ if (unknown.length > 0) {
183
+ logger.warn(
184
+ {
185
+ siteId: input.siteId,
186
+ subjectType: input.subjectType,
187
+ unknownCount: unknown.length,
188
+ sample: unknown.slice(0, 5),
189
+ },
190
+ "[consent] skipping unknown purpose ids — not in the site's consent registry, no statement was rendered for them",
191
+ );
192
+ }
193
+
194
+ if (rows.length === 0) return 0;
195
+ const result = await db.consentGrant.createMany({ data: rows });
196
+ return result.count;
197
+ }
198
+
199
+ export interface RecordLegacyConsentInput {
200
+ siteId: string;
201
+ subjectType: string;
202
+ subjectId: string;
203
+ config: SiteConsentConfig;
204
+ }
205
+
206
+ /**
207
+ * The D11 path: a body that carried only the legacy `consent: true` literal.
208
+ * Writes ONE grant against the site's first `required` purpose (falling back
209
+ * to its first purpose), `granted: true`, with the statement marked
210
+ * unrecoverable — the caller affirmed, but this system rendered it no
211
+ * wording to affirm.
212
+ */
213
+ export async function recordLegacyConsent(
214
+ input: RecordLegacyConsentInput,
215
+ db: ConsentWriteClient = prisma,
216
+ ): Promise<number> {
217
+ const purpose =
218
+ input.config.purposes.find((p) => p.required) ?? input.config.purposes[0];
219
+ if (!purpose) return 0;
220
+
221
+ const result = await db.consentGrant.createMany({
222
+ data: [
223
+ {
224
+ siteId: input.siteId,
225
+ subjectType: input.subjectType,
226
+ subjectId: input.subjectId,
227
+ purposeId: purpose.id,
228
+ granted: true,
229
+ locale: UNRECOVERABLE_CONSENT_LOCALE,
230
+ statement: UNRECOVERABLE_CONSENT_STATEMENT,
231
+ statementHash: consentStatementHash(UNRECOVERABLE_CONSENT_STATEMENT),
232
+ },
233
+ ],
234
+ });
235
+ return result.count;
236
+ }