create-ailk 0.1.1 → 0.3.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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -31,39 +31,70 @@ async function getBuildFunction(): Promise<typeof buildServer> {
31
31
 
32
32
  // Initialize build function
33
33
  const buildPromise = getBuildFunction();
34
- let serverInstance: FastifyServer | null = null;
35
34
 
36
35
  /**
37
- * Get or create the Fastify server instance
36
+ * The in-flight (or settled) build. Caching the PROMISE rather than the
37
+ * resolved instance is what makes this safe: a warm Vercel container serves
38
+ * concurrent invocations on one Node process, and the old check-then-set
39
+ * (`if (!serverInstance)` followed by two awaits before the assignment) let
40
+ * two requests arriving during a cold start both see null and both construct
41
+ * a complete Fastify server — every plugin registered, every route wired, the
42
+ * Prisma client initialised — with one of the two then discarded. Nothing
43
+ * failed; the only symptoms were cold-start latency and memory (#5332).
38
44
  */
39
- async function getServer() {
40
- if (!serverInstance) {
41
- logger.info('[Vercel Handler] Building server instance...');
42
- const startTime = Date.now();
43
-
44
- try {
45
- const buildTimeout = new Promise<never>((_, reject) => {
46
- setTimeout(() => reject(new Error('Server build timeout after 10s')), 10000);
47
- });
45
+ let serverPromise: Promise<FastifyServer> | null = null;
48
46
 
49
- const build = await Promise.race([buildPromise, buildTimeout]);
50
- logger.info('[Vercel Handler] Build function obtained, creating server...');
47
+ async function buildServerInstance(): Promise<FastifyServer> {
48
+ logger.info('[Vercel Handler] Building server instance...');
49
+ const startTime = Date.now();
51
50
 
52
- const initTimeout = new Promise<never>((_, reject) => {
53
- setTimeout(() => reject(new Error('Server initialization timeout after 10s')), 10000);
54
- });
51
+ try {
52
+ const buildTimeout = new Promise<never>((_, reject) => {
53
+ setTimeout(() => reject(new Error('Server build timeout after 10s')), 10000);
54
+ });
55
55
 
56
- serverInstance = await Promise.race([build(), initTimeout]);
56
+ const build = await Promise.race([buildPromise, buildTimeout]);
57
+ logger.info('[Vercel Handler] Build function obtained, creating server...');
57
58
 
58
- const buildTime = Date.now() - startTime;
59
- logger.info({ buildTime }, '[Vercel Handler] Server built successfully');
60
- } catch (error) {
61
- const buildTime = Date.now() - startTime;
62
- logger.error({ err: error, buildTime }, '[Vercel Handler] Failed to build server');
63
- throw new Error(`Server build failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
64
- }
59
+ const initTimeout = new Promise<never>((_, reject) => {
60
+ setTimeout(() => reject(new Error('Server initialization timeout after 10s')), 10000);
61
+ });
62
+
63
+ const serverInstance = await Promise.race([build(), initTimeout]);
64
+
65
+ const buildTime = Date.now() - startTime;
66
+ logger.info({ buildTime }, '[Vercel Handler] Server built successfully');
67
+ return serverInstance;
68
+ } catch (error) {
69
+ const buildTime = Date.now() - startTime;
70
+ logger.error({ err: error, buildTime }, '[Vercel Handler] Failed to build server');
71
+ throw new Error(`Server build failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
65
72
  }
66
- return serverInstance;
73
+ }
74
+
75
+ /**
76
+ * Get or create the Fastify server instance
77
+ */
78
+ function getServer(): Promise<FastifyServer> {
79
+ if (serverPromise) {
80
+ return serverPromise;
81
+ }
82
+
83
+ const attempt = buildServerInstance();
84
+ serverPromise = attempt;
85
+ // Evict a FAILED build from the cache so the next invocation retries —
86
+ // a rejected promise left in place would serve every later request the
87
+ // same cold-start failure for the life of the container. The guard keeps
88
+ // this from clearing a newer attempt; `attempt` is returned as-is, so the
89
+ // rejection still reaches the caller (this handler only silences the
90
+ // unhandled-rejection warning on its own bookkeeping branch).
91
+ attempt.catch(() => {
92
+ if (serverPromise === attempt) {
93
+ serverPromise = null;
94
+ }
95
+ });
96
+
97
+ return attempt;
67
98
  }
68
99
 
69
100
  /**
@@ -156,9 +187,12 @@ export default async function handler(
156
187
  logger.error({ err: error }, '[Vercel Handler] Error handling request');
157
188
 
158
189
  if (!res.headersSent) {
190
+ // Generic body only (security review on #146): this catch sits above
191
+ // Fastify's error serializer, so error.message could carry hosts,
192
+ // paths, or driver detail. The logger.error above keeps the detail.
159
193
  res.status(500).json({
160
194
  error: 'Internal Server Error',
161
- message: error instanceof Error ? error.message : String(error),
195
+ message: 'Something went wrong on our end.',
162
196
  });
163
197
  }
164
198
  }
@@ -40,6 +40,14 @@ MCP_RATE_LIMIT_WRITE_PER_MIN=10
40
40
  # Example: https://app.yourdomain.com,https://other.yourdomain.com
41
41
  MCP_CORS_WRITE_ALLOWED_ORIGINS=
42
42
 
43
+ # ── Paid modules (D-44, #5368) ──────────────────────────────────────────────────
44
+
45
+ # Comma-separated allowlist of installed paid modules, e.g.
46
+ # "multi-tenant,scheduling". Unset (the default) or "" means NO paid modules
47
+ # — installed modules enable themselves explicitly. Recognized keys: docs,
48
+ # multi-tenant, scheduling, content-write. Must match apps/api's value.
49
+ # AILK_ENABLED_MODULES=
50
+
43
51
  # ── Abuse protection ───────────────────────────────────────────────────────────
44
52
 
45
53
  # Maximum number of capture_lead submissions per IP per day.
@@ -25,8 +25,8 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
25
25
  **Read (9):** `list_pages`, `read_content`, `search_content`, `get_site_info`, `get_business_hours`, `get_locations`, `get_offerings`, `get_pricing`, `get_faq`
26
26
  **Conversion-write (3):** `capture_lead`, `subscribe_newsletter`, `request_callback`
27
27
  **Maintenance (2):** `audit`, `scaffold`
28
- **Flow (2):** `get_flow`, `submit_flow_step`
29
- **Booking (3):** `list_availability`, `get_event_meta`, `create_booking`
28
+ **Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366): identity-blind and free of any `requirePaidTier()` call like every tool in this list, but registered only when the signal-collectors module is present, the same shape Booking below already has
29
+ **Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`)
30
30
 
31
31
  ### Why the catalog reports 23 and the server registers 22
32
32
 
@@ -24,7 +24,28 @@ interface RegisteredTool {
24
24
  description?: string;
25
25
  }
26
26
 
27
+ // The catalog includes module-gated tools (scheduling, content-write,
28
+ // signal-collectors); an unset AILK_ENABLED_MODULES no longer registers them
29
+ // by default (D-44, #5368), so this drift check enables every paid module
30
+ // explicitly — it is comparing the full catalog, not what a default build
31
+ // registers.
32
+ const PAID_MODULES = "scheduling,content-write,signal-collectors";
33
+
27
34
  describe("MCP catalog ↔ server drift", () => {
35
+ const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
36
+
37
+ beforeEach(() => {
38
+ process.env.AILK_ENABLED_MODULES = PAID_MODULES;
39
+ });
40
+
41
+ afterAll(() => {
42
+ if (priorEnabledModules === undefined) {
43
+ delete process.env.AILK_ENABLED_MODULES;
44
+ } else {
45
+ process.env.AILK_ENABLED_MODULES = priorEnabledModules;
46
+ }
47
+ });
48
+
28
49
  it("registered tool names equal catalog tool names", () => {
29
50
  const server = createMcpServer();
30
51
  const registered = (
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `isModuleEnabled()` default-flip regression test (D-44, #5368).
3
+ *
4
+ * Mirrors `apps/api/src/config/__tests__/modules.test.ts` — apps/* may not
5
+ * import apps/*, so this reader (and its test) are duplicated rather than
6
+ * shared. Proves the mechanical claim #5368 exists to make true: an UNSET
7
+ * `AILK_ENABLED_MODULES` now gates every paid module OFF, the same as an
8
+ * explicit empty string — only an explicit allowlist turns modules on, and
9
+ * turns on exactly the modules it names.
10
+ *
11
+ * Reverting `enabledModules()`'s unset branch back to the `undefined`
12
+ * sentinel (S1, #5088's original shape) makes the "unset" cases below fail
13
+ * RED — see #5368 verification notes.
14
+ */
15
+
16
+ import { isModuleEnabled } from "../../src/config/modules.js";
17
+
18
+ const RECOGNIZED_MODULES = [
19
+ "docs",
20
+ "multi-tenant",
21
+ "scheduling",
22
+ "content-write",
23
+ ] as const;
24
+
25
+ describe("isModuleEnabled", () => {
26
+ const originalEnv = process.env.AILK_ENABLED_MODULES;
27
+
28
+ afterEach(() => {
29
+ if (originalEnv === undefined) {
30
+ delete process.env.AILK_ENABLED_MODULES;
31
+ } else {
32
+ process.env.AILK_ENABLED_MODULES = originalEnv;
33
+ }
34
+ });
35
+
36
+ describe("AILK_ENABLED_MODULES unset", () => {
37
+ it.each(RECOGNIZED_MODULES)("returns false for %s", (moduleName) => {
38
+ delete process.env.AILK_ENABLED_MODULES;
39
+ expect(isModuleEnabled(moduleName)).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('AILK_ENABLED_MODULES=""', () => {
44
+ it.each(RECOGNIZED_MODULES)("returns false for %s", (moduleName) => {
45
+ process.env.AILK_ENABLED_MODULES = "";
46
+ expect(isModuleEnabled(moduleName)).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe("AILK_ENABLED_MODULES as an explicit allowlist", () => {
51
+ it.each(RECOGNIZED_MODULES)(
52
+ "enables exactly the listed modules — %s on when named",
53
+ (moduleName) => {
54
+ process.env.AILK_ENABLED_MODULES = moduleName;
55
+ expect(isModuleEnabled(moduleName)).toBe(true);
56
+ for (const other of RECOGNIZED_MODULES) {
57
+ if (other === moduleName) continue;
58
+ expect(isModuleEnabled(other)).toBe(false);
59
+ }
60
+ },
61
+ );
62
+
63
+ it("enables every module named in a comma list, and no others", () => {
64
+ process.env.AILK_ENABLED_MODULES = "multi-tenant,scheduling";
65
+ expect(isModuleEnabled("multi-tenant")).toBe(true);
66
+ expect(isModuleEnabled("scheduling")).toBe(true);
67
+ expect(isModuleEnabled("docs")).toBe(false);
68
+ expect(isModuleEnabled("content-write")).toBe(false);
69
+ });
70
+
71
+ it("trims whitespace and drops empty entries, same as before the flip", () => {
72
+ process.env.AILK_ENABLED_MODULES = " multi-tenant , , scheduling ";
73
+ expect(isModuleEnabled("multi-tenant")).toBe(true);
74
+ expect(isModuleEnabled("scheduling")).toBe(true);
75
+ expect(isModuleEnabled("docs")).toBe(false);
76
+ });
77
+ });
78
+ });
@@ -18,7 +18,7 @@ import { getMcpToolRegistrations } from "@working-theory/metadata";
18
18
 
19
19
  import { createMcpServer, listRegisteredTools } from "../src/server.js";
20
20
 
21
- const PAID_MODULES = ["scheduling", "content-write"] as const;
21
+ const PAID_MODULES = ["scheduling", "content-write", "signal-collectors"] as const;
22
22
 
23
23
  function withEnabledModules<T>(enabled: string, fn: () => T): T {
24
24
  const prior = process.env.AILK_ENABLED_MODULES;
@@ -1,26 +1,29 @@
1
1
  /**
2
2
  * Paid-module presence — the registration-grain signal used by `server.ts`
3
3
  * to decide whether a paid module's tools are wired into this build
4
- * (S1, #5088). Mirrors `apps/api/src/config/modules.ts` apps/* may not
5
- * import apps/*, so this tiny reader is duplicated rather than shared.
4
+ * (S1, #5088; defaulted off by D-44, #5368). Mirrors
5
+ * `apps/api/src/config/modules.ts` apps/* may not import apps/*, so this
6
+ * tiny reader is duplicated rather than shared.
6
7
  *
7
8
  * `AILK_ENABLED_MODULES` is a comma-separated allowlist (e.g.
8
- * `"multi-tenant,scheduling"`). Unset means "every module on"today's
9
- * behavior, unchanged for every existing deployment; an explicit empty
10
- * string (`""`) means "no paid modules" (the OSS-strip build target).
9
+ * `"multi-tenant,scheduling"`). Unset means **no paid modules**installed
10
+ * modules enable themselves explicitly; an explicit empty string (`""`)
11
+ * means the same (the OSS-strip build target); a list enables exactly the
12
+ * modules it names.
11
13
  *
12
- * Recognized module keys: `docs`, `multi-tenant`, `scheduling` (the
13
- * `ModuleName` vocabulary `packages/create-ailk/src/module-architecture.ts`
14
- * already models via `MODULE_SLICES`) plus `content-write` the Pro
14
+ * Recognized module keys: `docs`, `multi-tenant`, `scheduling`,
15
+ * `signal-collectors` (the `ModuleName` vocabulary
16
+ * `packages/create-ailk/src/module-architecture.ts` already models via
17
+ * `MODULE_SLICES`) plus `content-write` — the Pro
15
18
  * content-write surface (`create_page`/`update_page`/`delete_page`).
16
19
  * `content-write` is intentionally NOT a `MODULE_SLICES` entry: it is not
17
20
  * (yet) a create-ailk `--modules` selector, so it stays a local flag here
18
21
  * rather than widening the create-ailk module vocabulary.
19
22
  */
20
23
 
21
- function enabledModules(): Set<string> | undefined {
24
+ function enabledModules(): Set<string> {
22
25
  const raw = process.env.AILK_ENABLED_MODULES;
23
- if (raw === undefined) return undefined; // undefined sentinel = "all enabled"
26
+ if (raw === undefined) return new Set(); // unset = no paid modules
24
27
  return new Set(
25
28
  raw
26
29
  .split(",")
@@ -31,7 +34,5 @@ function enabledModules(): Set<string> | undefined {
31
34
 
32
35
  /** Whether the named paid module's tools should register on this build/boot. */
33
36
  export function isModuleEnabled(moduleName: string): boolean {
34
- const enabled = enabledModules();
35
- if (enabled === undefined) return true;
36
- return enabled.has(moduleName);
37
+ return enabledModules().has(moduleName);
37
38
  }
@@ -20,7 +20,6 @@ import { ailkAudit } from "./tools/audit.js";
20
20
  import { ailkCaptureLead } from "./tools/capture_lead.js";
21
21
  import { ailkGetBusinessHours } from "./tools/get_business_hours.js";
22
22
  import { ailkGetFaq } from "./tools/get_faq.js";
23
- import { ailkGetFlow } from "./tools/get_flow.js";
24
23
  import { ailkGetLocations } from "./tools/get_locations.js";
25
24
  import { ailkGetOfferings } from "./tools/get_offerings.js";
26
25
  import { ailkGetPricing } from "./tools/get_pricing.js";
@@ -31,7 +30,6 @@ import { ailkReadContent } from "./tools/read_content.js";
31
30
  import { ailkRequestCallback } from "./tools/request_callback.js";
32
31
  import { ailkScaffold } from "./tools/scaffold.js";
33
32
  import { ailkSearchContent } from "./tools/search_content.js";
34
- import { ailkSubmitFlowStep } from "./tools/submit_flow_step.js";
35
33
  import { ailkSubscribeNewsletter } from "./tools/subscribe_newsletter.js";
36
34
 
37
35
  /**
@@ -65,8 +63,6 @@ const HANDLERS: Record<string, (input: unknown) => Promise<unknown>> = {
65
63
  subscribe_newsletter: (input) => ailkSubscribeNewsletter(input as never),
66
64
  request_callback: (input) => ailkRequestCallback(input as never),
67
65
  scaffold: (input) => ailkScaffold(input as never),
68
- get_flow: (input) => ailkGetFlow(input as never),
69
- submit_flow_step: (input) => ailkSubmitFlowStep(input as never),
70
66
  };
71
67
 
72
68
  type ToolHandler = (input: unknown) => Promise<unknown>;
@@ -119,6 +115,14 @@ const MODULE_TOOL_ENTRIES: Record<
119
115
  create_page: { specifier: "./tools/create_page.js", exportName: "ailkCreatePage" },
120
116
  update_page: { specifier: "./tools/update_page.js", exportName: "ailkUpdatePage" },
121
117
  delete_page: { specifier: "./tools/delete_page.js", exportName: "ailkDeletePage" },
118
+ // D-48 signal-collectors — the flow engine's MCP twins of GET
119
+ // /v1/flows/:slug and POST /v1/flows/:slug/steps. Anonymous, identity-blind;
120
+ // free-anonymous status is now module-gated rather than unconditional.
121
+ get_flow: { specifier: "./tools/get_flow.js", exportName: "ailkGetFlow" },
122
+ submit_flow_step: {
123
+ specifier: "./tools/submit_flow_step.js",
124
+ exportName: "ailkSubmitFlowStep",
125
+ },
122
126
  };
123
127
 
124
128
  /**
@@ -124,27 +124,12 @@ export {
124
124
  type ScaffoldOutput,
125
125
  } from "./scaffold.js";
126
126
 
127
- export {
128
- getFlowInputSchema,
129
- getFlowOutputSchema,
130
- ailkGetFlow,
131
- type GetFlowInput,
132
- type AilkGetFlowOutput,
133
- } from "./get_flow.js";
134
-
135
- export {
136
- submitFlowStepInputSchemaForTool,
137
- submitFlowStepOutputSchema,
138
- ailkSubmitFlowStep,
139
- type SubmitFlowStepInput,
140
- type AilkSubmitFlowStepOutput,
141
- } from "./submit_flow_step.js";
142
-
143
- // The scheduling tools (list_availability, get_event_meta, create_booking) are
144
- // deliberately NOT re-exported here, and neither are the content-write tools
145
- // (create_page, update_page, delete_page). Both groups are module-conditional
146
- // slice files (#5182): a re-export from this barrel is a static path into a
147
- // file the OSS strip removes, which reds the public tree's type-check. They are
148
- // reached only through `createMcpServer`'s per-module loader in ../server.ts.
127
+ // The scheduling tools (list_availability, get_event_meta, create_booking),
128
+ // the content-write tools (create_page, update_page, delete_page), and the
129
+ // signal-collectors flow tools (get_flow, submit_flow_step) are deliberately
130
+ // NOT re-exported here. All three groups are module-conditional slice files
131
+ // (#5182): a re-export from this barrel is a static path into a file the OSS
132
+ // strip removes, which reds the public tree's type-check. They are reached
133
+ // only through `createMcpServer`'s per-module loader in ../server.ts.
149
134
  // The rule this barrel now follows: it re-exports exactly the tools no module
150
135
  // gate can remove.
@@ -26,6 +26,21 @@ CONTENT_BACKEND=mdx
26
26
  # Same convention as apps/workspace/lib/client-api.ts's identically-named var.
27
27
  # Should be apps/api's origin with NO trailing slash (a single trailing
28
28
  # slash is stripped before joining, but keep this clean regardless).
29
+ #
30
+ # THIS IS THE ONE BROWSER-SIDE NAME (#5397). All three browser readers of the
31
+ # apps/api origin resolve it from this variable: resolveSubmitUrl
32
+ # (packages/ui, the form blocks above), apps/workspace's client-api, and
33
+ # @working-theory/api-client's getApiBase() — which runs in the browser
34
+ # wherever a "use client" component binds it, e.g. the founder application's
35
+ # ListingApplicationClient. getApiBase() previously read NEXT_PUBLIC_API_URL,
36
+ # a name this file never documented, so a site configured exactly per these
37
+ # docs sent its browser traffic to http://localhost:3001. That name is still
38
+ # honoured as a deprecated fallback; do not set it in a new deployment.
39
+ #
40
+ # The server-side counterpart is AILK_API_URL (see the repo-root .env.example)
41
+ # — deliberately NOT NEXT_PUBLIC_, and it still wins over this one when both
42
+ # are set, so an RSC/Server-Action call can target an internal origin the
43
+ # browser never sees.
29
44
  # NEXT_PUBLIC_AILK_API_URL=http://localhost:3002
30
45
 
31
46
  # =============================================================================
@@ -24,14 +24,14 @@ jest.mock("next/headers", () => ({
24
24
 
25
25
  const mockGetSession = jest.fn();
26
26
  // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test of the session-branch can mock without standing up Prisma.
27
- jest.mock("../../../../../auth", () => ({
27
+ jest.mock("../../../../auth", () => ({
28
28
  __esModule: true,
29
29
  auth: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
30
30
  }));
31
31
 
32
32
  const mockGetActiveProviderIds = jest.fn();
33
33
  // eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth-env.ts is a pure env-introspection module with no external boundary to mock instead.
34
- jest.mock("../../../../../lib/auth-env", () => ({
34
+ jest.mock("../../../../lib/auth-env", () => ({
35
35
  __esModule: true,
36
36
  getActiveProviderIds: () => mockGetActiveProviderIds(),
37
37
  }));
@@ -48,8 +48,8 @@ jest.mock("better-auth/react", () => ({
48
48
  createAuthClient: () => ({ signIn: { social: mockSignInSocial } }),
49
49
  }));
50
50
 
51
- import AuthedLayout from "../../layout";
52
- import { SignInGate } from "../../sign-in-gate";
51
+ import AuthedLayout from "../layout";
52
+ import { SignInGate } from "../sign-in-gate";
53
53
 
54
54
  describe("AuthedLayout — the (authed) session guard", () => {
55
55
  beforeEach(() => {
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  "use client";
2
3
 
3
4
  /**
@@ -53,7 +54,7 @@ import {
53
54
  type FlowStepperMessages,
54
55
  type FlowStepSubmitPayload,
55
56
  type FlowStepSubmitResult,
56
- } from "@working-theory/ui";
57
+ } from "@working-theory/ui/slices/signal-collectors";
57
58
  import type { AeoScoreResult, FlowConfig, FlowState } from "@working-theory/validation";
58
59
  import {
59
60
  deriveFlags,
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * Tests for FlowStepperClient's permalink completion redirect on a
3
4
  * `mode:"recommending"` flow (#4770 D1/D7/D12).
@@ -64,7 +65,7 @@ interface MockFlowStepperProps {
64
65
 
65
66
  let captured: MockFlowStepperProps | undefined;
66
67
 
67
- jest.mock("@working-theory/ui", () => ({
68
+ jest.mock("@working-theory/ui/slices/signal-collectors", () => ({
68
69
  FlowStepper: (props: MockFlowStepperProps) => {
69
70
  captured = props;
70
71
  return require("react").createElement("div", {
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * Tests for FlowStepperClient's segment-keyed completion redirect (#3960 D5).
3
4
  *
@@ -68,7 +69,7 @@ const PII_PAYLOAD = {
68
69
 
69
70
  let capturedOnCompleted: MockFlowStepperProps["onCompleted"];
70
71
 
71
- jest.mock("@working-theory/ui", () => ({
72
+ jest.mock("@working-theory/ui/slices/signal-collectors", () => ({
72
73
  FlowStepper: (props: MockFlowStepperProps) => {
73
74
  capturedOnCompleted = props.onCompleted;
74
75
  async function handleSubmit(): Promise<void> {
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * Tests for submitFlowStepAction Server Action (#3957 D8).
3
4
  *
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * #5053 — an interactive tool is two routes.
3
4
  *
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  "use server";
2
3
 
3
4
  /**
@@ -29,7 +30,7 @@
29
30
  */
30
31
 
31
32
  import { flows } from "@working-theory/api-client";
32
- import type { FlowStepSubmitResult } from "@working-theory/ui";
33
+ import type { FlowStepSubmitResult } from "@working-theory/ui/slices/signal-collectors";
33
34
  import {
34
35
  flowSlugSchema,
35
36
  submitFlowStepInputSchema,
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * /[locale]/flows/[slug] — the tool's LANDING route (#5053 D3/D5).
3
4
  *
@@ -1,3 +1,4 @@
1
+ // oss: false
1
2
  /**
2
3
  * /[locale]/flows/[slug]/start — the flow half of the two-route tool
3
4
  * (#5053 D3/D5). Moved here from `../page.tsx`, which is now the indexed
@@ -4,6 +4,7 @@
4
4
  // Chrome extract — Nav + Footer from @working-theory/ui (#1451)
5
5
  // Shell/Chrome composition — Nav + Footer wrap via @working-theory/templates MarketingShell (#2820)
6
6
  // Session-aware Nav actions — Sign in / Account + Sign out from the server-read session (#4984)
7
+ // Site-declared theme — `_site.mdx` `theme:` block → token overlay → CSS vars
7
8
  //
8
9
  // Sits under app/[locale]/ and sets the <html lang> attribute to the
9
10
  // active locale. next-intl's NextIntlClientProvider is wired here so
@@ -48,6 +49,7 @@ import { SignOutTrigger } from "../../components/sign-out-trigger";
48
49
  import { navItems } from "../../lib/nav-config";
49
50
  import { getOgImageUrl } from "../../lib/og";
50
51
  import { SITE_NAME, siteLogo } from "../../lib/site-brand";
52
+ import { loadSiteBrandTokens } from "../../lib/site-theme";
51
53
  import { resolveBaseUrl } from "../../src/lib/routes";
52
54
 
53
55
  // ── Constants ────────────────────────────────────────────────────────────────
@@ -238,11 +240,21 @@ export default async function LocaleLayout({
238
240
  // Shared brand mark injected into both Nav and Footer chrome slots.
239
241
  const brandLogo = siteLogo();
240
242
 
243
+ // The site's own declared colours, read from `content/_site.mdx`'s `theme:`
244
+ // block and translated from that customer-facing vocabulary onto a token
245
+ // overlay (`siteThemeToBrandTokens`). `undefined` when the site declares no
246
+ // theme — which is the regression floor: no `brand` key is passed, the
247
+ // resolver takes its untouched fast path, and the emitted CSS is
248
+ // byte-for-byte what it was before this wiring existed.
249
+ const siteBrandTokens = loadSiteBrandTokens();
250
+
241
251
  // Both theme modes, gated by [data-theme="dark"] (#1532 — sibling fix; the
242
252
  // single-mode call inlined an unconditional :root block that never
243
253
  // responded to a client-side data-theme toggle). themeMode still drives
244
254
  // the initial data-theme attribute below for a no-flash first paint.
245
- const inlineThemeStyle = themeToCssVarsBothModes();
255
+ const inlineThemeStyle = themeToCssVarsBothModes(
256
+ siteBrandTokens ? { brand: siteBrandTokens } : {},
257
+ );
246
258
 
247
259
  return (
248
260
  <html lang={locale} dir={isRtl(locale) ? "rtl" : "ltr"} data-theme={themeMode}>
@@ -4,9 +4,11 @@
4
4
  * Returns a plain-text index of AILK's content surface for LLM consumption,
5
5
  * following the llms.txt convention (https://llmstxt.org).
6
6
  *
7
- * Aggregates two surfaces:
8
- * 1. Section-catalog (MDX seeds in content/) via @working-theory/content-adapters
9
- * 2. Fumadocs blog corpus via blogSource.getPages()
7
+ * Indexes the section catalog (MDX seeds in content/) via
8
+ * @working-theory/content-adapters. Nothing is gated: a Free site has no blog
9
+ * (D-46), so there is no blog corpus to omit — `getBlogPages` defaults to an
10
+ * empty list and the docs + blog module ships its own variant of this route
11
+ * that reads `blogSource` (#5365 D9).
10
12
  *
11
13
  * Format per entry:
12
14
  * - Title: <title>
@@ -14,8 +16,9 @@
14
16
  * Description: <description>
15
17
  *
16
18
  * DI note: `createLlmsHandler` accepts optional `getContentAdapter` and
17
- * `getBlogPages` deps for testing — tests inject stubs to avoid mocking
18
- * internal modules. The exported `GET` uses the real implementations.
19
+ * `getBlogPages` deps — tests inject stubs, and the module's variant of this
20
+ * route supplies the real `blogSource` reader. The exported `GET` is
21
+ * unchanged either way.
19
22
  */
20
23
 
21
24
  import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
@@ -23,7 +26,6 @@ import type { ContentAdapter, SchemaOrgType } from "@working-theory/content-adap
23
26
  import { getContentAdapter as defaultGetContentAdapter } from "@working-theory/page-renderer";
24
27
 
25
28
  import { SITE_NAME } from "../../lib/site-brand";
26
- import { blogSource as defaultBlogSource } from "../../lib/source";
27
29
  import { resolveBaseUrl, slugToPath } from "../../src/lib/routes";
28
30
 
29
31
  export interface LlmsHandlerDeps {
@@ -33,8 +35,9 @@ export interface LlmsHandlerDeps {
33
35
  */
34
36
  getContentAdapter?: () => ContentAdapter;
35
37
  /**
36
- * Blog pages getter. Defaults to the real blogSource.getPages().
37
- * Override in tests to inject stub pages without mocking lib/source.
38
+ * Blog pages getter. Defaults to none Free has no blog (D-46, #5365 D9).
39
+ * The docs + blog module's own copy of this route reads `blogSource`.
40
+ * Override in tests to inject stub pages.
38
41
  */
39
42
  getBlogPages?: () => Array<{ url: string; data: { title: string; description?: string } }>;
40
43
  }
@@ -45,7 +48,7 @@ export interface LlmsHandlerDeps {
45
48
  */
46
49
  export function createLlmsHandler(deps: LlmsHandlerDeps = {}) {
47
50
  const resolveAdapter = deps.getContentAdapter ?? defaultGetContentAdapter;
48
- const resolveBlogPages = deps.getBlogPages ?? (() => defaultBlogSource.getPages() as Array<{ url: string; data: { title: string; description?: string } }>);
51
+ const resolveBlogPages = deps.getBlogPages ?? (() => []);
49
52
 
50
53
  return async function llmsGET(): Promise<Response> {
51
54
  const adapter = resolveAdapter();